kandown 0.1.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/LICENSE +21 -0
- package/README.md +595 -0
- package/bin/kandown.js +539 -0
- package/bin/tui.js +1257 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/favicon.svg +7 -0
- package/dist/index.html +6456 -0
- package/dist/manifest.json +17 -0
- package/package.json +73 -0
- package/templates/AGENT.md +102 -0
- package/templates/AGENT_KANDOWN.md +146 -0
- package/templates/AGENT_KANDOWN_COMPACT.md +65 -0
- package/templates/README.md +48 -0
- package/templates/kandown.json +34 -0
- package/templates/tasks/t1.md +30 -0
- package/templates/tasks/t2.md +32 -0
- package/templates/tasks/t3.md +13 -0
package/bin/tui.js
ADDED
|
@@ -0,0 +1,1257 @@
|
|
|
1
|
+
// src/cli/tui.tsx
|
|
2
|
+
import { render } from "ink";
|
|
3
|
+
|
|
4
|
+
// src/cli/app.tsx
|
|
5
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
6
|
+
|
|
7
|
+
// src/cli/screens/settings.tsx
|
|
8
|
+
import { useState, useEffect, useCallback } from "react";
|
|
9
|
+
import { Box, Text, useInput, useApp } from "ink";
|
|
10
|
+
|
|
11
|
+
// src/cli/lib/config.ts
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
var DEFAULT_CONFIG = {
|
|
15
|
+
ui: { language: "en", theme: "auto", skin: "kandown", font: "inter" },
|
|
16
|
+
agent: { suggestFollowUp: false, maxSuggestions: 3 },
|
|
17
|
+
board: {
|
|
18
|
+
columns: ["Backlog", "Todo", "In Progress", "Review", "Done"],
|
|
19
|
+
defaultPriority: "P3",
|
|
20
|
+
defaultOwnerType: "human"
|
|
21
|
+
},
|
|
22
|
+
fields: {
|
|
23
|
+
priority: false,
|
|
24
|
+
assignee: false,
|
|
25
|
+
tags: false,
|
|
26
|
+
dueDate: false,
|
|
27
|
+
ownerType: false,
|
|
28
|
+
tools: false
|
|
29
|
+
},
|
|
30
|
+
notifications: {
|
|
31
|
+
browser: false,
|
|
32
|
+
sound: false,
|
|
33
|
+
soundId: "soft",
|
|
34
|
+
statusChanges: true,
|
|
35
|
+
taskEdits: true,
|
|
36
|
+
subtaskCompletions: true,
|
|
37
|
+
editDebounceMs: 2e3
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function loadConfig(kandownDir) {
|
|
41
|
+
const configPath = join(kandownDir, "kandown.json");
|
|
42
|
+
if (!existsSync(configPath)) return structuredClone(DEFAULT_CONFIG);
|
|
43
|
+
try {
|
|
44
|
+
const raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
45
|
+
const merged = {
|
|
46
|
+
ui: { ...DEFAULT_CONFIG.ui, ...raw.ui },
|
|
47
|
+
agent: { ...DEFAULT_CONFIG.agent, ...raw.agent },
|
|
48
|
+
board: {
|
|
49
|
+
...DEFAULT_CONFIG.board,
|
|
50
|
+
...raw.board,
|
|
51
|
+
columns: Array.isArray(raw.board?.columns) && raw.board.columns.length > 0 ? raw.board.columns.filter((name) => typeof name === "string" && name.trim().length > 0) : DEFAULT_CONFIG.board.columns
|
|
52
|
+
},
|
|
53
|
+
fields: { ...DEFAULT_CONFIG.fields, ...raw.fields },
|
|
54
|
+
notifications: { ...DEFAULT_CONFIG.notifications, ...raw.notifications }
|
|
55
|
+
};
|
|
56
|
+
if (raw.agents) merged.agents = raw.agents;
|
|
57
|
+
return merged;
|
|
58
|
+
} catch {
|
|
59
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function saveConfig(kandownDir, config) {
|
|
63
|
+
const configPath = join(kandownDir, "kandown.json");
|
|
64
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
65
|
+
}
|
|
66
|
+
function getConfigValue(config, path) {
|
|
67
|
+
const parts = path.split(".");
|
|
68
|
+
let current = config;
|
|
69
|
+
for (const part of parts) {
|
|
70
|
+
if (current === null || current === void 0) return void 0;
|
|
71
|
+
current = current[part];
|
|
72
|
+
}
|
|
73
|
+
return current;
|
|
74
|
+
}
|
|
75
|
+
function setConfigValue(config, path, value) {
|
|
76
|
+
const result = structuredClone(config);
|
|
77
|
+
const parts = path.split(".");
|
|
78
|
+
let current = result;
|
|
79
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
80
|
+
current = current[parts[i]];
|
|
81
|
+
}
|
|
82
|
+
current[parts[parts.length - 1]] = value;
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/cli/screens/settings.tsx
|
|
87
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
88
|
+
var SETTINGS = [
|
|
89
|
+
// UI
|
|
90
|
+
{
|
|
91
|
+
key: "ui.language",
|
|
92
|
+
label: "Language",
|
|
93
|
+
section: "Appearance",
|
|
94
|
+
type: "select",
|
|
95
|
+
options: ["en", "fr", "es", "de", "pt", "ja", "zh", "ko", "it", "nl", "ru"]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
key: "ui.theme",
|
|
99
|
+
label: "Mode",
|
|
100
|
+
section: "Appearance",
|
|
101
|
+
type: "select",
|
|
102
|
+
options: ["auto", "light", "dark"]
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
key: "ui.skin",
|
|
106
|
+
label: "Skin",
|
|
107
|
+
section: "Appearance",
|
|
108
|
+
type: "select",
|
|
109
|
+
options: ["kandown", "graphite", "sage", "cobalt", "rose"]
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
key: "ui.font",
|
|
113
|
+
label: "Font",
|
|
114
|
+
section: "Appearance",
|
|
115
|
+
type: "select",
|
|
116
|
+
options: ["inter", "system", "serif", "mono", "rounded"]
|
|
117
|
+
},
|
|
118
|
+
// Agent
|
|
119
|
+
{
|
|
120
|
+
key: "agent.suggestFollowUp",
|
|
121
|
+
label: "Suggest follow-up tasks",
|
|
122
|
+
section: "Agent",
|
|
123
|
+
type: "toggle"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
key: "agent.maxSuggestions",
|
|
127
|
+
label: "Max suggestions",
|
|
128
|
+
section: "Agent",
|
|
129
|
+
type: "number",
|
|
130
|
+
min: 1,
|
|
131
|
+
max: 5
|
|
132
|
+
},
|
|
133
|
+
// Board
|
|
134
|
+
{
|
|
135
|
+
key: "board.defaultPriority",
|
|
136
|
+
label: "Default priority",
|
|
137
|
+
section: "Fields",
|
|
138
|
+
type: "select",
|
|
139
|
+
options: ["P1", "P2", "P3", "P4"]
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
key: "board.defaultOwnerType",
|
|
143
|
+
label: "Default owner",
|
|
144
|
+
section: "Fields",
|
|
145
|
+
type: "select",
|
|
146
|
+
options: ["human", "ai"]
|
|
147
|
+
},
|
|
148
|
+
// Fields
|
|
149
|
+
{ key: "fields.priority", label: "Priority", section: "Fields", type: "toggle" },
|
|
150
|
+
{ key: "fields.assignee", label: "Assignee", section: "Fields", type: "toggle" },
|
|
151
|
+
{ key: "fields.tags", label: "Tags", section: "Fields", type: "toggle" },
|
|
152
|
+
{ key: "fields.dueDate", label: "Due date", section: "Fields", type: "toggle" },
|
|
153
|
+
{ key: "fields.ownerType", label: "Owner type", section: "Fields", type: "toggle" },
|
|
154
|
+
{ key: "fields.tools", label: "Tools", section: "Fields", type: "toggle" },
|
|
155
|
+
// Notifications
|
|
156
|
+
{ key: "notifications.browser", label: "Browser notifications", section: "Notifications", type: "toggle" },
|
|
157
|
+
{ key: "notifications.statusChanges", label: "Status changes", section: "Notifications", type: "toggle" },
|
|
158
|
+
{ key: "notifications.taskEdits", label: "Task edits", section: "Notifications", type: "toggle" },
|
|
159
|
+
{ key: "notifications.subtaskCompletions", label: "Subtask completions", section: "Notifications", type: "toggle" },
|
|
160
|
+
{ key: "notifications.sound", label: "Play sound", section: "Notifications", type: "toggle" },
|
|
161
|
+
{
|
|
162
|
+
key: "notifications.soundId",
|
|
163
|
+
label: "Sound",
|
|
164
|
+
section: "Notifications",
|
|
165
|
+
type: "select",
|
|
166
|
+
options: ["soft", "chime", "ping", "pop"]
|
|
167
|
+
}
|
|
168
|
+
];
|
|
169
|
+
var SECTIONS = ["Appearance", "Agent", "Board", "Fields", "Notifications"];
|
|
170
|
+
var LABEL_WIDTH = 30;
|
|
171
|
+
var VALUE_WIDTH = 20;
|
|
172
|
+
var SECTION_ICONS = {
|
|
173
|
+
Appearance: "\u{1F3A8}",
|
|
174
|
+
Agent: "\u{1F916}",
|
|
175
|
+
Board: "\u{1F4CB}",
|
|
176
|
+
Fields: "\u{1F4DD}",
|
|
177
|
+
Notifications: "\u{1F514}"
|
|
178
|
+
};
|
|
179
|
+
function Settings({ kandownDir }) {
|
|
180
|
+
const { exit } = useApp();
|
|
181
|
+
const [config, setConfig] = useState(null);
|
|
182
|
+
const [focusIndex, setFocusIndex] = useState(0);
|
|
183
|
+
const [savedAt, setSavedAt] = useState(null);
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
const loaded = loadConfig(kandownDir);
|
|
186
|
+
setConfig(loaded);
|
|
187
|
+
}, [kandownDir]);
|
|
188
|
+
const persistConfig = useCallback(
|
|
189
|
+
(newConfig) => {
|
|
190
|
+
setConfig(newConfig);
|
|
191
|
+
saveConfig(kandownDir, newConfig);
|
|
192
|
+
setSavedAt(Date.now());
|
|
193
|
+
},
|
|
194
|
+
[kandownDir]
|
|
195
|
+
);
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (savedAt === null) return;
|
|
198
|
+
const timer = setTimeout(() => setSavedAt(null), 2e3);
|
|
199
|
+
return () => clearTimeout(timer);
|
|
200
|
+
}, [savedAt]);
|
|
201
|
+
useInput((input, key) => {
|
|
202
|
+
if (!config) return;
|
|
203
|
+
if (key.escape || input === "q") {
|
|
204
|
+
exit();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (key.upArrow) {
|
|
208
|
+
setFocusIndex((i) => Math.max(0, i - 1));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (key.downArrow) {
|
|
212
|
+
setFocusIndex((i) => Math.min(SETTINGS.length - 1, i + 1));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const setting = SETTINGS[focusIndex];
|
|
216
|
+
if (!setting) return;
|
|
217
|
+
const currentValue = getConfigValue(config, setting.key);
|
|
218
|
+
if (setting.type === "toggle" && (input === " " || key.return)) {
|
|
219
|
+
persistConfig(setConfigValue(config, setting.key, !currentValue));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (setting.type === "select" && setting.options) {
|
|
223
|
+
const options = setting.options;
|
|
224
|
+
const currentIdx = options.indexOf(String(currentValue));
|
|
225
|
+
let newIdx = currentIdx;
|
|
226
|
+
if (key.leftArrow) {
|
|
227
|
+
newIdx = Math.max(0, currentIdx - 1);
|
|
228
|
+
} else if (key.rightArrow) {
|
|
229
|
+
newIdx = Math.min(options.length - 1, currentIdx + 1);
|
|
230
|
+
} else if (input === " " || key.return) {
|
|
231
|
+
newIdx = (currentIdx + 1) % options.length;
|
|
232
|
+
} else {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const newValue = options[newIdx];
|
|
236
|
+
persistConfig(setConfigValue(config, setting.key, newValue));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (setting.type === "number") {
|
|
240
|
+
const num = Number(currentValue);
|
|
241
|
+
const min = setting.min ?? 0;
|
|
242
|
+
const max = setting.max ?? 99;
|
|
243
|
+
if (key.leftArrow) {
|
|
244
|
+
persistConfig(setConfigValue(config, setting.key, Math.max(min, num - 1)));
|
|
245
|
+
} else if (key.rightArrow) {
|
|
246
|
+
persistConfig(setConfigValue(config, setting.key, Math.min(max, num + 1)));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
if (!config) {
|
|
251
|
+
return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading\u2026" }) });
|
|
252
|
+
}
|
|
253
|
+
const showSaved = savedAt !== null;
|
|
254
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
|
|
255
|
+
/* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", marginBottom: 1, children: [
|
|
256
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
257
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: " \u256D\u2500 " }),
|
|
258
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "white", children: "KANDOWN SETTINGS" }),
|
|
259
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: " \u2500\u256E" })
|
|
260
|
+
] }),
|
|
261
|
+
/* @__PURE__ */ jsx(Box, { children: showSaved && /* @__PURE__ */ jsxs(Text, { color: "green", bold: true, children: [
|
|
262
|
+
"\u2713 saved",
|
|
263
|
+
" "
|
|
264
|
+
] }) })
|
|
265
|
+
] }),
|
|
266
|
+
SECTIONS.map((section) => {
|
|
267
|
+
const items = SETTINGS.filter((s) => s.section === section);
|
|
268
|
+
const icon = SECTION_ICONS[section] ?? "\u2022";
|
|
269
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
|
|
270
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
|
|
271
|
+
" ",
|
|
272
|
+
icon,
|
|
273
|
+
" ",
|
|
274
|
+
section
|
|
275
|
+
] }) }),
|
|
276
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " + "\u2500".repeat(LABEL_WIDTH + VALUE_WIDTH + 4) }) }),
|
|
277
|
+
items.map((setting) => {
|
|
278
|
+
const globalIdx = SETTINGS.indexOf(setting);
|
|
279
|
+
const focused = globalIdx === focusIndex;
|
|
280
|
+
const value = getConfigValue(config, setting.key);
|
|
281
|
+
return /* @__PURE__ */ jsx(
|
|
282
|
+
SettingRow,
|
|
283
|
+
{
|
|
284
|
+
setting,
|
|
285
|
+
value,
|
|
286
|
+
focused
|
|
287
|
+
},
|
|
288
|
+
setting.key
|
|
289
|
+
);
|
|
290
|
+
})
|
|
291
|
+
] }, section);
|
|
292
|
+
}),
|
|
293
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2191\u2193 navigate Space toggle \u2190\u2192 change Q quit" }) })
|
|
294
|
+
] });
|
|
295
|
+
}
|
|
296
|
+
function SettingRow({ setting, value, focused }) {
|
|
297
|
+
const marker = focused ? "\u203A" : " ";
|
|
298
|
+
const markerColor = focused ? "yellow" : void 0;
|
|
299
|
+
const labelColor = focused ? "white" : "gray";
|
|
300
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
301
|
+
/* @__PURE__ */ jsxs(Text, { color: markerColor, bold: focused, children: [
|
|
302
|
+
" ",
|
|
303
|
+
marker,
|
|
304
|
+
" "
|
|
305
|
+
] }),
|
|
306
|
+
/* @__PURE__ */ jsx(Box, { width: LABEL_WIDTH, children: /* @__PURE__ */ jsx(Text, { color: labelColor, bold: focused, children: setting.label }) }),
|
|
307
|
+
/* @__PURE__ */ jsx(Box, { width: VALUE_WIDTH, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(ValueDisplay, { setting, value, focused }) })
|
|
308
|
+
] });
|
|
309
|
+
}
|
|
310
|
+
function ValueDisplay({ setting, value, focused }) {
|
|
311
|
+
if (setting.type === "toggle") {
|
|
312
|
+
const on = Boolean(value);
|
|
313
|
+
return /* @__PURE__ */ jsx(Text, { color: on ? "green" : "gray", bold: on, children: on ? "\u25CF ON" : "\u25CB OFF" });
|
|
314
|
+
}
|
|
315
|
+
if (setting.type === "select" && setting.options) {
|
|
316
|
+
const options = setting.options;
|
|
317
|
+
const idx = options.indexOf(String(value));
|
|
318
|
+
const atStart = idx <= 0;
|
|
319
|
+
const atEnd = idx >= options.length - 1;
|
|
320
|
+
const displayValue = String(value);
|
|
321
|
+
if (focused) {
|
|
322
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
323
|
+
/* @__PURE__ */ jsx(Text, { color: atStart ? "gray" : "cyan", children: "\u25C2 " }),
|
|
324
|
+
/* @__PURE__ */ jsx(Text, { color: "white", bold: true, children: displayValue }),
|
|
325
|
+
/* @__PURE__ */ jsx(Text, { color: atEnd ? "gray" : "cyan", children: " \u25B8" })
|
|
326
|
+
] });
|
|
327
|
+
}
|
|
328
|
+
return /* @__PURE__ */ jsx(Text, { dimColor: true, children: displayValue });
|
|
329
|
+
}
|
|
330
|
+
if (setting.type === "number") {
|
|
331
|
+
const num = Number(value);
|
|
332
|
+
const atMin = num <= (setting.min ?? 0);
|
|
333
|
+
const atMax = num >= (setting.max ?? 99);
|
|
334
|
+
if (focused) {
|
|
335
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
336
|
+
/* @__PURE__ */ jsx(Text, { color: atMin ? "gray" : "cyan", children: "\u25C2 " }),
|
|
337
|
+
/* @__PURE__ */ jsx(Text, { color: "white", bold: true, children: num }),
|
|
338
|
+
/* @__PURE__ */ jsx(Text, { color: atMax ? "gray" : "cyan", children: " \u25B8" })
|
|
339
|
+
] });
|
|
340
|
+
}
|
|
341
|
+
return /* @__PURE__ */ jsx(Text, { dimColor: true, children: num });
|
|
342
|
+
}
|
|
343
|
+
return /* @__PURE__ */ jsx(Text, { children: String(value) });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/cli/screens/board.tsx
|
|
347
|
+
import { useState as useState3, useEffect as useEffect2, useCallback as useCallback2 } from "react";
|
|
348
|
+
import { Box as Box3, Text as Text3, useInput as useInput3, useApp as useApp2 } from "ink";
|
|
349
|
+
|
|
350
|
+
// src/cli/lib/board-reader.ts
|
|
351
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
352
|
+
import { dirname, join as join2 } from "path";
|
|
353
|
+
|
|
354
|
+
// src/lib/types.ts
|
|
355
|
+
var DEFAULT_COLUMNS = ["Backlog", "Todo", "In Progress", "Review", "Done"];
|
|
356
|
+
|
|
357
|
+
// src/lib/parser.ts
|
|
358
|
+
function parseSimpleYaml(yaml) {
|
|
359
|
+
const obj = {};
|
|
360
|
+
if (!yaml || typeof yaml !== "string") return obj;
|
|
361
|
+
const lines = yaml.split("\n");
|
|
362
|
+
for (let i = 0; i < lines.length; i++) {
|
|
363
|
+
const line = lines[i] ?? "";
|
|
364
|
+
const m = line.match(/^([a-zA-Z_][\w-]*)\s*:\s*(.*)$/);
|
|
365
|
+
if (!m) continue;
|
|
366
|
+
const key = m[1];
|
|
367
|
+
if (!key) continue;
|
|
368
|
+
let val = m[2]?.trim() ?? "";
|
|
369
|
+
if (val === "|") {
|
|
370
|
+
const block = [];
|
|
371
|
+
i++;
|
|
372
|
+
while (i < lines.length && (/^\s+/.test(lines[i] ?? "") || (lines[i] ?? "") === "")) {
|
|
373
|
+
block.push((lines[i] ?? "").replace(/^ /, ""));
|
|
374
|
+
i++;
|
|
375
|
+
}
|
|
376
|
+
i--;
|
|
377
|
+
obj[key] = block.join("\n").trimEnd();
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (typeof val !== "string") val = "";
|
|
381
|
+
if (val.startsWith("[") && val.endsWith("]")) {
|
|
382
|
+
const arr = val.slice(1, -1).split(",").map((s) => s && typeof s === "string" ? s.trim().replace(/^["']|["']$/g, "") : "").filter(Boolean);
|
|
383
|
+
obj[key] = arr;
|
|
384
|
+
} else {
|
|
385
|
+
obj[key] = typeof val === "string" ? val.replace(/^["']|["']$/g, "") : val;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return obj;
|
|
389
|
+
}
|
|
390
|
+
function parseTaskFile(md) {
|
|
391
|
+
if (!md || typeof md !== "string") {
|
|
392
|
+
return { frontmatter: { id: "", title: "" }, body: "" };
|
|
393
|
+
}
|
|
394
|
+
const lines = md.split("\n");
|
|
395
|
+
if (lines[0] && lines[0].trim() === "---") {
|
|
396
|
+
const fmLines = [];
|
|
397
|
+
let i = 1;
|
|
398
|
+
while (i < lines.length && lines[i].trim() !== "---") {
|
|
399
|
+
fmLines.push(lines[i]);
|
|
400
|
+
i++;
|
|
401
|
+
}
|
|
402
|
+
const body = lines.slice(i + 1).join("\n").trimStart();
|
|
403
|
+
const fm = parseSimpleYaml(fmLines.join("\n"));
|
|
404
|
+
return { frontmatter: fm, body };
|
|
405
|
+
}
|
|
406
|
+
return { frontmatter: { id: "", title: "" }, body: md };
|
|
407
|
+
}
|
|
408
|
+
function normalizeStatus(status) {
|
|
409
|
+
const value = typeof status === "string" ? status.trim() : "";
|
|
410
|
+
return value || "Backlog";
|
|
411
|
+
}
|
|
412
|
+
function normalizePriority(priority) {
|
|
413
|
+
if (typeof priority !== "string") return null;
|
|
414
|
+
const value = priority.toUpperCase();
|
|
415
|
+
return /^(P1|P2|P3|P4)$/.test(value) ? value : null;
|
|
416
|
+
}
|
|
417
|
+
function normalizeOwnerType(ownerType) {
|
|
418
|
+
if (typeof ownerType !== "string") return "";
|
|
419
|
+
const value = ownerType.toLowerCase();
|
|
420
|
+
return value === "human" || value === "ai" ? value : "";
|
|
421
|
+
}
|
|
422
|
+
function taskOrder(task) {
|
|
423
|
+
const value = task.frontmatter.order;
|
|
424
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
425
|
+
if (typeof value === "string" && value.trim()) {
|
|
426
|
+
const parsed = Number(value);
|
|
427
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
428
|
+
}
|
|
429
|
+
return Number.MAX_SAFE_INTEGER;
|
|
430
|
+
}
|
|
431
|
+
function taskToBoardTask(task) {
|
|
432
|
+
const { frontmatter, body } = task;
|
|
433
|
+
const { subtasks } = extractSubtasks(body);
|
|
434
|
+
const done = subtasks.filter((s) => s.done).length;
|
|
435
|
+
const total = subtasks.length;
|
|
436
|
+
const status = normalizeStatus(frontmatter.status);
|
|
437
|
+
const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0) : [];
|
|
438
|
+
return {
|
|
439
|
+
id: frontmatter.id || "",
|
|
440
|
+
title: frontmatter.title || frontmatter.id || "Untitled task",
|
|
441
|
+
checked: /done|termin|closed|complet/i.test(status),
|
|
442
|
+
tags,
|
|
443
|
+
assignee: typeof frontmatter.assignee === "string" && frontmatter.assignee ? frontmatter.assignee : null,
|
|
444
|
+
priority: normalizePriority(frontmatter.priority),
|
|
445
|
+
ownerType: normalizeOwnerType(frontmatter.ownerType),
|
|
446
|
+
progress: total > 0 ? { done, total } : null
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function buildColumnsFromTasks(tasks, configuredColumns = DEFAULT_COLUMNS) {
|
|
450
|
+
const columnNames = configuredColumns.length > 0 ? configuredColumns : DEFAULT_COLUMNS;
|
|
451
|
+
const columnsByName = /* @__PURE__ */ new Map();
|
|
452
|
+
const configured = columnNames.map((name) => ({ name, tasks: [] }));
|
|
453
|
+
for (const column of configured) columnsByName.set(column.name.toLowerCase(), column);
|
|
454
|
+
const unknownColumns = [];
|
|
455
|
+
const sortedTasks = [...tasks].filter((task) => Boolean(task.frontmatter.id)).sort((a, b) => {
|
|
456
|
+
const byOrder = taskOrder(a) - taskOrder(b);
|
|
457
|
+
if (byOrder !== 0) return byOrder;
|
|
458
|
+
return a.frontmatter.id.localeCompare(b.frontmatter.id, void 0, { numeric: true });
|
|
459
|
+
});
|
|
460
|
+
for (const task of sortedTasks) {
|
|
461
|
+
const status = normalizeStatus(task.frontmatter.status);
|
|
462
|
+
let column = columnsByName.get(status.toLowerCase());
|
|
463
|
+
if (!column) {
|
|
464
|
+
column = { name: status, tasks: [] };
|
|
465
|
+
columnsByName.set(status.toLowerCase(), column);
|
|
466
|
+
unknownColumns.push(column);
|
|
467
|
+
}
|
|
468
|
+
column.tasks.push(taskToBoardTask(task));
|
|
469
|
+
}
|
|
470
|
+
return [...unknownColumns, ...configured];
|
|
471
|
+
}
|
|
472
|
+
function extractSubtasks(body) {
|
|
473
|
+
const subtasks = [];
|
|
474
|
+
if (!body || typeof body !== "string") return { subtasks, bodyWithoutSubtasks: body ?? "" };
|
|
475
|
+
const lines = body.split("\n");
|
|
476
|
+
const kept = [];
|
|
477
|
+
let inSubtaskSection = false;
|
|
478
|
+
for (const line of lines) {
|
|
479
|
+
if (/^#{1,6}\s+(subtasks?|sous[- ]t[âa]ches?|crit[èe]res?)/i.test(line)) {
|
|
480
|
+
inSubtaskSection = true;
|
|
481
|
+
kept.push(line);
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (/^#{1,6}\s+/.test(line) && inSubtaskSection) {
|
|
485
|
+
inSubtaskSection = false;
|
|
486
|
+
kept.push(line);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const m = line.match(/^\s*-\s+\[([ xX])\]\s+(.+)$/);
|
|
490
|
+
if (m && inSubtaskSection) {
|
|
491
|
+
const text = m[2]?.trim() ?? "";
|
|
492
|
+
subtasks.push({ done: (m[1]?.toLowerCase() ?? "") === "x", text });
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const descMatch = line.match(/^\s*\[DESC\]\s*(.*)$/);
|
|
496
|
+
if (descMatch && subtasks.length > 0) {
|
|
497
|
+
subtasks[subtasks.length - 1].description = descMatch[1];
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
const reportMatch = line.match(/^\s*\[REPORT\]\s*(.*)$/);
|
|
501
|
+
if (reportMatch && subtasks.length > 0) {
|
|
502
|
+
subtasks[subtasks.length - 1].report = reportMatch[1];
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
kept.push(line);
|
|
506
|
+
}
|
|
507
|
+
return { subtasks, bodyWithoutSubtasks: kept.join("\n") };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/lib/serializer.ts
|
|
511
|
+
function serializeTaskFile(frontmatter, body) {
|
|
512
|
+
const lines = ["---"];
|
|
513
|
+
if (frontmatter && typeof frontmatter === "object") {
|
|
514
|
+
for (const [k, v] of Object.entries(frontmatter)) {
|
|
515
|
+
if (v === null || v === void 0 || v === "") continue;
|
|
516
|
+
if (Array.isArray(v)) {
|
|
517
|
+
if (v.length === 0) continue;
|
|
518
|
+
lines.push(`${k}: [${v.join(", ")}]`);
|
|
519
|
+
} else if (typeof v === "string" && v.includes("\n")) {
|
|
520
|
+
lines.push(`${k}: |`);
|
|
521
|
+
lines.push(...v.split("\n").map((line) => ` ${line}`));
|
|
522
|
+
} else if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
523
|
+
lines.push(`${k}: ${v}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
lines.push("---");
|
|
528
|
+
lines.push("");
|
|
529
|
+
lines.push((body ?? "").trim());
|
|
530
|
+
lines.push("");
|
|
531
|
+
return lines.join("\n");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/cli/lib/board-reader.ts
|
|
535
|
+
function getProjectRoot(kandownDir) {
|
|
536
|
+
return dirname(kandownDir);
|
|
537
|
+
}
|
|
538
|
+
function listTaskIds(kandownDir) {
|
|
539
|
+
const tasksDir = join2(kandownDir, "tasks");
|
|
540
|
+
if (!existsSync2(tasksDir)) return [];
|
|
541
|
+
return readdirSync(tasksDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -3)).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
|
|
542
|
+
}
|
|
543
|
+
function readBoard(kandownDir) {
|
|
544
|
+
const config = loadConfig(kandownDir);
|
|
545
|
+
const tasks = listTaskIds(kandownDir).map((id) => {
|
|
546
|
+
const task = readTask(kandownDir, id);
|
|
547
|
+
return {
|
|
548
|
+
...task,
|
|
549
|
+
frontmatter: {
|
|
550
|
+
...task.frontmatter,
|
|
551
|
+
id: task.frontmatter.id || id,
|
|
552
|
+
status: task.frontmatter.status || "Backlog"
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
});
|
|
556
|
+
return {
|
|
557
|
+
frontmatter: null,
|
|
558
|
+
title: "Project Kanban",
|
|
559
|
+
columns: buildColumnsFromTasks(tasks, config.board.columns)
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function readTask(kandownDir, taskId) {
|
|
563
|
+
const taskPath = join2(kandownDir, "tasks", `${taskId}.md`);
|
|
564
|
+
if (!existsSync2(taskPath)) {
|
|
565
|
+
return {
|
|
566
|
+
frontmatter: { id: taskId, title: `Task ${taskId}`, status: "Backlog" },
|
|
567
|
+
body: ""
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const content = readFileSync2(taskPath, "utf8");
|
|
571
|
+
const parsed = parseTaskFile(content);
|
|
572
|
+
return {
|
|
573
|
+
...parsed,
|
|
574
|
+
frontmatter: {
|
|
575
|
+
...parsed.frontmatter,
|
|
576
|
+
id: parsed.frontmatter.id || taskId,
|
|
577
|
+
status: parsed.frontmatter.status || "Backlog"
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function readAgentDoc(kandownDir) {
|
|
582
|
+
const root = getProjectRoot(kandownDir);
|
|
583
|
+
const candidates = [
|
|
584
|
+
join2(root, "AGENT_KANDOWN_COMPACT.md"),
|
|
585
|
+
join2(root, "AGENT_KANDOWN.md"),
|
|
586
|
+
join2(kandownDir, "AGENT.md")
|
|
587
|
+
];
|
|
588
|
+
for (const candidate of candidates) {
|
|
589
|
+
if (existsSync2(candidate)) {
|
|
590
|
+
return readFileSync2(candidate, "utf8");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return "";
|
|
594
|
+
}
|
|
595
|
+
function moveTaskToColumn(kandownDir, taskId, targetColumn) {
|
|
596
|
+
const taskPath = join2(kandownDir, "tasks", `${taskId}.md`);
|
|
597
|
+
if (!existsSync2(taskPath)) return false;
|
|
598
|
+
const parsed = readTask(kandownDir, taskId);
|
|
599
|
+
writeFileSync2(taskPath, serializeTaskFile({
|
|
600
|
+
...parsed.frontmatter,
|
|
601
|
+
id: taskId,
|
|
602
|
+
status: targetColumn
|
|
603
|
+
}, parsed.body), "utf8");
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/cli/lib/agents.ts
|
|
608
|
+
import { execFileSync } from "child_process";
|
|
609
|
+
var AGENTS = [
|
|
610
|
+
{
|
|
611
|
+
id: "claude",
|
|
612
|
+
name: "Claude Code",
|
|
613
|
+
bin: "claude",
|
|
614
|
+
description: "Anthropic Claude (interactive session)",
|
|
615
|
+
interactive: true,
|
|
616
|
+
buildCommand: ({ systemPrompt, taskPrompt }) => {
|
|
617
|
+
const combined = `${systemPrompt}
|
|
618
|
+
|
|
619
|
+
---
|
|
620
|
+
|
|
621
|
+
${taskPrompt}`;
|
|
622
|
+
return ["claude", combined];
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
id: "codex",
|
|
627
|
+
name: "OpenAI Codex",
|
|
628
|
+
bin: "codex",
|
|
629
|
+
description: "OpenAI Codex CLI",
|
|
630
|
+
interactive: true,
|
|
631
|
+
buildCommand: ({ systemPrompt, taskPrompt }) => {
|
|
632
|
+
const combined = `${systemPrompt}
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
${taskPrompt}`;
|
|
637
|
+
return ["codex", combined];
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
id: "gemini",
|
|
642
|
+
name: "Gemini CLI",
|
|
643
|
+
bin: "gemini",
|
|
644
|
+
description: "Google Gemini CLI",
|
|
645
|
+
interactive: true,
|
|
646
|
+
buildCommand: ({ systemPrompt, taskPrompt }) => {
|
|
647
|
+
const combined = `${systemPrompt}
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
${taskPrompt}`;
|
|
652
|
+
return ["gemini", "-p", combined];
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
id: "goose",
|
|
657
|
+
name: "Goose",
|
|
658
|
+
bin: "goose",
|
|
659
|
+
description: "Block open-source AI agent",
|
|
660
|
+
interactive: false,
|
|
661
|
+
buildCommand: ({ systemPrompt, taskPrompt }) => {
|
|
662
|
+
const combined = `${systemPrompt}
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
${taskPrompt}`;
|
|
667
|
+
return ["goose", "run", "--text", combined];
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
id: "aider",
|
|
672
|
+
name: "Aider",
|
|
673
|
+
bin: "aider",
|
|
674
|
+
description: "Git-aware AI pair programmer",
|
|
675
|
+
interactive: true,
|
|
676
|
+
buildCommand: ({ systemPrompt, taskPrompt }) => {
|
|
677
|
+
const combined = `${systemPrompt}
|
|
678
|
+
|
|
679
|
+
---
|
|
680
|
+
|
|
681
|
+
${taskPrompt}`;
|
|
682
|
+
return ["aider", "--message", combined];
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
id: "opencode",
|
|
687
|
+
name: "OpenCode",
|
|
688
|
+
bin: "opencode",
|
|
689
|
+
description: "SST AI coding TUI",
|
|
690
|
+
interactive: true,
|
|
691
|
+
buildCommand: ({ taskPrompt }) => {
|
|
692
|
+
return ["opencode"];
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
];
|
|
696
|
+
var installCache = /* @__PURE__ */ new Map();
|
|
697
|
+
function isAgentInstalled(bin) {
|
|
698
|
+
if (installCache.has(bin)) return installCache.get(bin);
|
|
699
|
+
try {
|
|
700
|
+
execFileSync("which", [bin], { stdio: "ignore" });
|
|
701
|
+
installCache.set(bin, true);
|
|
702
|
+
return true;
|
|
703
|
+
} catch {
|
|
704
|
+
installCache.set(bin, false);
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
function detectInstalledAgents() {
|
|
709
|
+
return AGENTS.filter((agent) => isAgentInstalled(agent.bin));
|
|
710
|
+
}
|
|
711
|
+
function getAgentById(id) {
|
|
712
|
+
return AGENTS.find((a) => a.id === id);
|
|
713
|
+
}
|
|
714
|
+
function buildPrompt(agentDoc, taskContent, taskId, kandownDir) {
|
|
715
|
+
const systemPrompt = agentDoc.trim();
|
|
716
|
+
const taskPrompt = [
|
|
717
|
+
`## Your Task: ${taskId}`,
|
|
718
|
+
"",
|
|
719
|
+
taskContent.trim(),
|
|
720
|
+
"",
|
|
721
|
+
"---",
|
|
722
|
+
"",
|
|
723
|
+
`**Start working on task ${taskId} now.**`,
|
|
724
|
+
"",
|
|
725
|
+
`The kandown directory is at: \`${kandownDir}\``,
|
|
726
|
+
"",
|
|
727
|
+
"Before anything else:",
|
|
728
|
+
`1. Set task ${taskId} frontmatter status to "In Progress" (it may already be there \u2014 that's fine)`,
|
|
729
|
+
"2. Work through each subtask, checking them off and adding reports as you go",
|
|
730
|
+
'3. When done, write the completion report and set the task status to "Done"'
|
|
731
|
+
].join("\n");
|
|
732
|
+
return { systemPrompt, taskPrompt };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// src/cli/lib/launcher.ts
|
|
736
|
+
import { execSync, spawn } from "child_process";
|
|
737
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
738
|
+
import { join as join3 } from "path";
|
|
739
|
+
import { tmpdir } from "os";
|
|
740
|
+
function isInTmux() {
|
|
741
|
+
return !!process.env.TMUX;
|
|
742
|
+
}
|
|
743
|
+
function launchAgent(opts) {
|
|
744
|
+
const { taskId, agentId, kandownDir, onBeforeExec } = opts;
|
|
745
|
+
const agentDef = getAgentById(agentId);
|
|
746
|
+
if (!agentDef) {
|
|
747
|
+
throw new Error(`Unknown agent: ${agentId}`);
|
|
748
|
+
}
|
|
749
|
+
const task = readTask(kandownDir, taskId);
|
|
750
|
+
const agentDoc = readAgentDoc(kandownDir);
|
|
751
|
+
const taskFileContent = [
|
|
752
|
+
`---`,
|
|
753
|
+
`id: ${task.frontmatter.id}`,
|
|
754
|
+
`title: ${task.frontmatter.title}`,
|
|
755
|
+
`status: ${task.frontmatter.status ?? "unknown"}`,
|
|
756
|
+
`---`,
|
|
757
|
+
"",
|
|
758
|
+
task.body.trim()
|
|
759
|
+
].join("\n");
|
|
760
|
+
const { systemPrompt, taskPrompt } = buildPrompt(agentDoc, taskFileContent, taskId, kandownDir);
|
|
761
|
+
moveTaskToColumn(kandownDir, taskId, "In Progress");
|
|
762
|
+
const contextFile = join3(tmpdir(), `kandown-${taskId}-context.md`);
|
|
763
|
+
writeFileSync3(contextFile, `${systemPrompt}
|
|
764
|
+
|
|
765
|
+
---
|
|
766
|
+
|
|
767
|
+
${taskPrompt}`, "utf8");
|
|
768
|
+
const launchOpts = { systemPrompt, taskPrompt, kandownDir, taskId };
|
|
769
|
+
const [binary, ...args] = agentDef.buildCommand(launchOpts);
|
|
770
|
+
if (!binary) {
|
|
771
|
+
throw new Error(`Agent ${agentId} returned an empty command`);
|
|
772
|
+
}
|
|
773
|
+
if (isInTmux()) {
|
|
774
|
+
const shellCmd = buildShellCmd(binary, args);
|
|
775
|
+
execSync(`tmux split-window -h -p 50 ${shellescape(shellCmd)}`, {
|
|
776
|
+
stdio: "inherit"
|
|
777
|
+
});
|
|
778
|
+
} else {
|
|
779
|
+
onBeforeExec?.();
|
|
780
|
+
const child = spawn(binary, args, {
|
|
781
|
+
stdio: "inherit",
|
|
782
|
+
env: {
|
|
783
|
+
...process.env,
|
|
784
|
+
// 📖 Expose the context file path so agents that support env vars can use it
|
|
785
|
+
KANDOWN_CONTEXT_FILE: contextFile,
|
|
786
|
+
KANDOWN_TASK_ID: taskId,
|
|
787
|
+
KANDOWN_DIR: kandownDir
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
child.on("exit", (code) => {
|
|
791
|
+
process.exit(code ?? 0);
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
function buildShellCmd(binary, args) {
|
|
796
|
+
const parts = [binary, ...args].map(shellescape);
|
|
797
|
+
return parts.join(" ");
|
|
798
|
+
}
|
|
799
|
+
function shellescape(str) {
|
|
800
|
+
return `'${str.replace(/'/g, "'\\''")}'`;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/cli/screens/agent-picker.tsx
|
|
804
|
+
import { useState as useState2 } from "react";
|
|
805
|
+
import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
|
|
806
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
807
|
+
function AgentPicker({ agents, taskId, onSelect, onCancel }) {
|
|
808
|
+
const [cursor, setCursor] = useState2(0);
|
|
809
|
+
useInput2((input, key) => {
|
|
810
|
+
if (key.escape || input === "q") {
|
|
811
|
+
onCancel();
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (key.downArrow || input === "j") {
|
|
815
|
+
setCursor((c) => Math.min(c + 1, agents.length - 1));
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (key.upArrow || input === "k") {
|
|
819
|
+
setCursor((c) => Math.max(c - 1, 0));
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (key.return) {
|
|
823
|
+
const agent = agents[cursor];
|
|
824
|
+
if (agent) onSelect(agent.id);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const num = parseInt(input, 10);
|
|
828
|
+
if (!isNaN(num) && num >= 1 && num <= agents.length) {
|
|
829
|
+
const agent = agents[num - 1];
|
|
830
|
+
if (agent) onSelect(agent.id);
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
const maxNameLen = Math.max(...agents.map((a) => a.name.length));
|
|
834
|
+
const boxWidth = Math.min(60, Math.max(40, maxNameLen + 30));
|
|
835
|
+
return /* @__PURE__ */ jsxs2(
|
|
836
|
+
Box2,
|
|
837
|
+
{
|
|
838
|
+
flexDirection: "column",
|
|
839
|
+
borderStyle: "round",
|
|
840
|
+
borderColor: "cyan",
|
|
841
|
+
paddingX: 2,
|
|
842
|
+
paddingY: 1,
|
|
843
|
+
width: boxWidth,
|
|
844
|
+
children: [
|
|
845
|
+
/* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, children: [
|
|
846
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: "SELECT AGENT" }),
|
|
847
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
|
|
848
|
+
" ",
|
|
849
|
+
"for ",
|
|
850
|
+
/* @__PURE__ */ jsx2(Text2, { color: "yellow", children: taskId })
|
|
851
|
+
] })
|
|
852
|
+
] }),
|
|
853
|
+
agents.map((agent, idx) => {
|
|
854
|
+
const isFocused = idx === cursor;
|
|
855
|
+
const numHint = idx < 9 ? `${idx + 1} ` : " ";
|
|
856
|
+
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
857
|
+
/* @__PURE__ */ jsx2(Text2, { color: "gray", dimColor: true, children: numHint }),
|
|
858
|
+
/* @__PURE__ */ jsxs2(Text2, { color: isFocused ? "black" : void 0, backgroundColor: isFocused ? "cyan" : void 0, children: [
|
|
859
|
+
isFocused ? "\u203A" : " ",
|
|
860
|
+
" ",
|
|
861
|
+
/* @__PURE__ */ jsx2(Text2, { bold: isFocused, children: agent.name }),
|
|
862
|
+
" ",
|
|
863
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: !isFocused, children: agent.description })
|
|
864
|
+
] })
|
|
865
|
+
] }, agent.id);
|
|
866
|
+
}),
|
|
867
|
+
/* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { color: "gray", dimColor: true, children: [
|
|
868
|
+
"\u2191\u2193 or 1\u2013",
|
|
869
|
+
agents.length,
|
|
870
|
+
" select Enter launch Esc cancel"
|
|
871
|
+
] }) })
|
|
872
|
+
]
|
|
873
|
+
}
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// src/cli/screens/board.tsx
|
|
878
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
879
|
+
function truncate(str, maxLen) {
|
|
880
|
+
if (str.length <= maxLen) return str;
|
|
881
|
+
return str.slice(0, maxLen - 1) + "\u2026";
|
|
882
|
+
}
|
|
883
|
+
function pad(str, len) {
|
|
884
|
+
const t = truncate(str, len);
|
|
885
|
+
return t + " ".repeat(Math.max(0, len - t.length));
|
|
886
|
+
}
|
|
887
|
+
function termWidth() {
|
|
888
|
+
return process.stdout.columns || 80;
|
|
889
|
+
}
|
|
890
|
+
function calcColWidth(numCols) {
|
|
891
|
+
const available = termWidth() - (numCols - 1);
|
|
892
|
+
return Math.max(12, Math.floor(available / numCols));
|
|
893
|
+
}
|
|
894
|
+
var RE_HEADER = /^#{1,3}\s/;
|
|
895
|
+
var RE_SUBTASK = /^\s*-\s+\[([ xX])\]/;
|
|
896
|
+
var RE_DONE_SUBTASK = /^\s*-\s+\[x\]/i;
|
|
897
|
+
function TaskRow({
|
|
898
|
+
task,
|
|
899
|
+
focused,
|
|
900
|
+
colWidth
|
|
901
|
+
}) {
|
|
902
|
+
const cursor = focused ? "\u25B8" : " ";
|
|
903
|
+
const check = task.checked ? "\u2713" : "\u25CB";
|
|
904
|
+
const idStr = task.id;
|
|
905
|
+
const available = colWidth - 4 - idStr.length - 1;
|
|
906
|
+
const titleStr = truncate(task.title, Math.max(4, available));
|
|
907
|
+
return /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
908
|
+
/* @__PURE__ */ jsxs3(Text3, { color: focused ? "cyan" : void 0, bold: focused, children: [
|
|
909
|
+
cursor,
|
|
910
|
+
" "
|
|
911
|
+
] }),
|
|
912
|
+
/* @__PURE__ */ jsxs3(Text3, { color: task.checked ? "green" : focused ? "white" : "gray", children: [
|
|
913
|
+
check,
|
|
914
|
+
" "
|
|
915
|
+
] }),
|
|
916
|
+
/* @__PURE__ */ jsx3(Text3, { color: focused ? "cyan" : "yellow", bold: focused, children: idStr }),
|
|
917
|
+
/* @__PURE__ */ jsxs3(Text3, { color: focused ? "white" : "gray", children: [
|
|
918
|
+
" ",
|
|
919
|
+
titleStr
|
|
920
|
+
] })
|
|
921
|
+
] });
|
|
922
|
+
}
|
|
923
|
+
function KanbanColumn({
|
|
924
|
+
name,
|
|
925
|
+
tasks,
|
|
926
|
+
focusedRow,
|
|
927
|
+
isFocused,
|
|
928
|
+
colWidth
|
|
929
|
+
}) {
|
|
930
|
+
const headerBg = isFocused ? "cyan" : void 0;
|
|
931
|
+
const headerColor = isFocused ? "black" : "cyan";
|
|
932
|
+
const countStr = tasks.length > 0 ? ` (${tasks.length})` : "";
|
|
933
|
+
const headerText = truncate(`${name}${countStr}`, colWidth);
|
|
934
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width: colWidth, marginRight: 1, children: [
|
|
935
|
+
/* @__PURE__ */ jsx3(Box3, { backgroundColor: headerBg, children: /* @__PURE__ */ jsx3(Text3, { color: headerColor, bold: true, children: pad(headerText, colWidth) }) }),
|
|
936
|
+
/* @__PURE__ */ jsx3(Text3, { color: isFocused ? "cyan" : "gray", children: "\u2500".repeat(colWidth) }),
|
|
937
|
+
tasks.length === 0 ? /* @__PURE__ */ jsxs3(Text3, { color: "gray", dimColor: true, children: [
|
|
938
|
+
" ".repeat(2),
|
|
939
|
+
"(empty)"
|
|
940
|
+
] }) : tasks.map((task, idx) => /* @__PURE__ */ jsx3(
|
|
941
|
+
TaskRow,
|
|
942
|
+
{
|
|
943
|
+
task,
|
|
944
|
+
focused: isFocused && idx === focusedRow,
|
|
945
|
+
colWidth
|
|
946
|
+
},
|
|
947
|
+
task.id
|
|
948
|
+
))
|
|
949
|
+
] });
|
|
950
|
+
}
|
|
951
|
+
function BoardHeader({ title, inTmux }) {
|
|
952
|
+
const tmuxHint = inTmux ? " tmux" : "";
|
|
953
|
+
return /* @__PURE__ */ jsxs3(Box3, { marginBottom: 1, justifyContent: "space-between", children: [
|
|
954
|
+
/* @__PURE__ */ jsxs3(Text3, { bold: true, color: "cyan", children: [
|
|
955
|
+
" ",
|
|
956
|
+
"KANDOWN",
|
|
957
|
+
tmuxHint,
|
|
958
|
+
" ",
|
|
959
|
+
title
|
|
960
|
+
] }),
|
|
961
|
+
/* @__PURE__ */ jsx3(Text3, { color: "gray", dimColor: true, children: "h/l cols j/k tasks Enter detail a agent r reload q quit" })
|
|
962
|
+
] });
|
|
963
|
+
}
|
|
964
|
+
function StatusBar({ message, task }) {
|
|
965
|
+
if (message) {
|
|
966
|
+
return /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: message }) });
|
|
967
|
+
}
|
|
968
|
+
if (!task) return /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { color: "gray", children: " " }) });
|
|
969
|
+
return /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
|
|
970
|
+
task.id.replace(/^t/, ""),
|
|
971
|
+
task.progress ? ` (${task.progress.done}/${task.progress.total})` : "",
|
|
972
|
+
" ",
|
|
973
|
+
task.checked ? "\u2713 done" : "\u25CB open"
|
|
974
|
+
] }) });
|
|
975
|
+
}
|
|
976
|
+
function TaskDetail({
|
|
977
|
+
task,
|
|
978
|
+
taskId,
|
|
979
|
+
scrollOffset
|
|
980
|
+
}) {
|
|
981
|
+
const fm = task.frontmatter;
|
|
982
|
+
const bodyLines = task.body.split("\n");
|
|
983
|
+
const maxVisible = (process.stdout.rows || 24) - 10;
|
|
984
|
+
const visibleLines = bodyLines.slice(scrollOffset, scrollOffset + maxVisible);
|
|
985
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 2, children: [
|
|
986
|
+
/* @__PURE__ */ jsxs3(Box3, { marginBottom: 1, children: [
|
|
987
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: taskId }),
|
|
988
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "white", bold: true, children: [
|
|
989
|
+
" ",
|
|
990
|
+
fm.title
|
|
991
|
+
] })
|
|
992
|
+
] }),
|
|
993
|
+
/* @__PURE__ */ jsx3(Box3, { marginBottom: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
|
|
994
|
+
"status: ",
|
|
995
|
+
/* @__PURE__ */ jsx3(Text3, { color: "yellow", children: fm.status ?? "\u2014" }),
|
|
996
|
+
fm.priority ? ` priority: ${fm.priority}` : "",
|
|
997
|
+
fm.assignee ? ` assignee: ${fm.assignee}` : "",
|
|
998
|
+
fm.due ? ` due: ${fm.due}` : ""
|
|
999
|
+
] }) }),
|
|
1000
|
+
/* @__PURE__ */ jsx3(Text3, { color: "gray", children: "\u2500".repeat(termWidth() - 4) }),
|
|
1001
|
+
visibleLines.map((line, idx) => {
|
|
1002
|
+
const isHeader = RE_HEADER.test(line);
|
|
1003
|
+
const isSubtask = RE_SUBTASK.test(line);
|
|
1004
|
+
const isDone = RE_DONE_SUBTASK.test(line);
|
|
1005
|
+
return /* @__PURE__ */ jsx3(
|
|
1006
|
+
Text3,
|
|
1007
|
+
{
|
|
1008
|
+
color: isHeader ? "cyan" : isDone ? "green" : isSubtask ? "white" : "gray",
|
|
1009
|
+
bold: isHeader,
|
|
1010
|
+
children: line || " "
|
|
1011
|
+
},
|
|
1012
|
+
scrollOffset + idx
|
|
1013
|
+
);
|
|
1014
|
+
}),
|
|
1015
|
+
bodyLines.length > maxVisible && /* @__PURE__ */ jsxs3(Text3, { color: "gray", dimColor: true, children: [
|
|
1016
|
+
" ",
|
|
1017
|
+
"\u2191\u2193 scroll (",
|
|
1018
|
+
scrollOffset + 1,
|
|
1019
|
+
"\u2013",
|
|
1020
|
+
Math.min(scrollOffset + maxVisible, bodyLines.length),
|
|
1021
|
+
"/",
|
|
1022
|
+
bodyLines.length,
|
|
1023
|
+
" lines)"
|
|
1024
|
+
] })
|
|
1025
|
+
] });
|
|
1026
|
+
}
|
|
1027
|
+
function Board({ kandownDir }) {
|
|
1028
|
+
const { exit } = useApp2();
|
|
1029
|
+
const [board, setBoard] = useState3(null);
|
|
1030
|
+
const [colIndex, setColIndex] = useState3(0);
|
|
1031
|
+
const [rowIndex, setRowIndex] = useState3(0);
|
|
1032
|
+
const [mode, setMode] = useState3("browse");
|
|
1033
|
+
const [detailTask, setDetailTask] = useState3(null);
|
|
1034
|
+
const [detailTaskId, setDetailTaskId] = useState3("");
|
|
1035
|
+
const [detailScroll, setDetailScroll] = useState3(0);
|
|
1036
|
+
const [installedAgents, setInstalledAgents] = useState3([]);
|
|
1037
|
+
const [statusMsg, setStatusMsg] = useState3("");
|
|
1038
|
+
const inTmux = isInTmux();
|
|
1039
|
+
useEffect2(() => {
|
|
1040
|
+
const loaded = readBoard(kandownDir);
|
|
1041
|
+
setBoard(loaded);
|
|
1042
|
+
setInstalledAgents(detectInstalledAgents());
|
|
1043
|
+
}, [kandownDir]);
|
|
1044
|
+
const reloadBoard = useCallback2(() => {
|
|
1045
|
+
const loaded = readBoard(kandownDir);
|
|
1046
|
+
setBoard(loaded);
|
|
1047
|
+
setStatusMsg("Board reloaded");
|
|
1048
|
+
setTimeout(() => setStatusMsg(""), 1500);
|
|
1049
|
+
}, [kandownDir]);
|
|
1050
|
+
const getFocusedTask = useCallback2(() => {
|
|
1051
|
+
if (!board) return null;
|
|
1052
|
+
const col = board.columns[colIndex];
|
|
1053
|
+
if (!col || col.tasks.length === 0) return null;
|
|
1054
|
+
return col.tasks[Math.min(rowIndex, col.tasks.length - 1)] ?? null;
|
|
1055
|
+
}, [board, colIndex, rowIndex]);
|
|
1056
|
+
const openDetail = useCallback2((taskId) => {
|
|
1057
|
+
const task = readTask(kandownDir, taskId);
|
|
1058
|
+
setDetailTask(task);
|
|
1059
|
+
setDetailTaskId(taskId);
|
|
1060
|
+
setDetailScroll(0);
|
|
1061
|
+
setMode("detail");
|
|
1062
|
+
}, [kandownDir]);
|
|
1063
|
+
const handleAgentSelect = useCallback2((agentId) => {
|
|
1064
|
+
const task = getFocusedTask();
|
|
1065
|
+
const taskId = mode === "detail" ? detailTaskId : task?.id;
|
|
1066
|
+
if (!taskId) return;
|
|
1067
|
+
setMode("browse");
|
|
1068
|
+
setStatusMsg(`Launching ${agentId} for ${taskId}\u2026`);
|
|
1069
|
+
setTimeout(() => {
|
|
1070
|
+
try {
|
|
1071
|
+
launchAgent({
|
|
1072
|
+
taskId,
|
|
1073
|
+
agentId,
|
|
1074
|
+
kandownDir,
|
|
1075
|
+
onBeforeExec: () => exit()
|
|
1076
|
+
});
|
|
1077
|
+
reloadBoard();
|
|
1078
|
+
setStatusMsg(`${agentId} launched in tmux pane`);
|
|
1079
|
+
setTimeout(() => setStatusMsg(""), 3e3);
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
setStatusMsg(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1082
|
+
setTimeout(() => setStatusMsg(""), 4e3);
|
|
1083
|
+
}
|
|
1084
|
+
}, 50);
|
|
1085
|
+
}, [mode, detailTaskId, getFocusedTask, kandownDir, exit, reloadBoard]);
|
|
1086
|
+
useInput3((input, key) => {
|
|
1087
|
+
if (mode === "browse") {
|
|
1088
|
+
if (input === "q" || key.escape) {
|
|
1089
|
+
exit();
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if (input === "r") {
|
|
1093
|
+
reloadBoard();
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
if (input === "l" || key.rightArrow) {
|
|
1097
|
+
const maxCol = (board?.columns.length ?? 1) - 1;
|
|
1098
|
+
setColIndex((c) => Math.min(c + 1, maxCol));
|
|
1099
|
+
setRowIndex(0);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (input === "h" || key.leftArrow) {
|
|
1103
|
+
setColIndex((c) => Math.max(c - 1, 0));
|
|
1104
|
+
setRowIndex(0);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (input === "j" || key.downArrow) {
|
|
1108
|
+
const col = board?.columns[colIndex];
|
|
1109
|
+
const max = Math.max(0, (col?.tasks.length ?? 1) - 1);
|
|
1110
|
+
setRowIndex((r) => Math.min(r + 1, max));
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (input === "k" || key.upArrow) {
|
|
1114
|
+
setRowIndex((r) => Math.max(r - 1, 0));
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
if (key.return) {
|
|
1118
|
+
const task = getFocusedTask();
|
|
1119
|
+
if (task) openDetail(task.id);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
if (input === "a") {
|
|
1123
|
+
if (installedAgents.length === 0) {
|
|
1124
|
+
setStatusMsg("No AI agents found in PATH (install claude, codex, aider, goose\u2026)");
|
|
1125
|
+
setTimeout(() => setStatusMsg(""), 3e3);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
const task = getFocusedTask();
|
|
1129
|
+
if (!task) return;
|
|
1130
|
+
setMode("agent-picker");
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
if (mode === "detail") {
|
|
1135
|
+
if (key.escape || input === "q") {
|
|
1136
|
+
setMode("browse");
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (input === "j" || key.downArrow) {
|
|
1140
|
+
setDetailScroll((s) => s + 1);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
if (input === "k" || key.upArrow) {
|
|
1144
|
+
setDetailScroll((s) => Math.max(0, s - 1));
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
if (input === "a") {
|
|
1148
|
+
if (installedAgents.length === 0) {
|
|
1149
|
+
setStatusMsg("No AI agents found in PATH");
|
|
1150
|
+
setTimeout(() => setStatusMsg(""), 3e3);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
setMode("agent-picker");
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
});
|
|
1158
|
+
if (!board) {
|
|
1159
|
+
return /* @__PURE__ */ jsx3(Box3, { padding: 2, children: /* @__PURE__ */ jsx3(Text3, { color: "gray", children: "Loading board\u2026" }) });
|
|
1160
|
+
}
|
|
1161
|
+
if (board.columns.length === 0) {
|
|
1162
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", padding: 2, children: [
|
|
1163
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "red", bold: true, children: [
|
|
1164
|
+
"No board found at ",
|
|
1165
|
+
kandownDir
|
|
1166
|
+
] }),
|
|
1167
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
|
|
1168
|
+
"Run ",
|
|
1169
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "kandown init" }),
|
|
1170
|
+
" to set up kandown in this project."
|
|
1171
|
+
] })
|
|
1172
|
+
] });
|
|
1173
|
+
}
|
|
1174
|
+
const colWidth = calcColWidth(board.columns.length);
|
|
1175
|
+
const focusedTask = getFocusedTask();
|
|
1176
|
+
if (mode === "agent-picker") {
|
|
1177
|
+
const taskId = detailTaskId || focusedTask?.id || "";
|
|
1178
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
1179
|
+
/* @__PURE__ */ jsx3(BoardHeader, { title: board.title, inTmux }),
|
|
1180
|
+
/* @__PURE__ */ jsx3(
|
|
1181
|
+
AgentPicker,
|
|
1182
|
+
{
|
|
1183
|
+
agents: installedAgents,
|
|
1184
|
+
taskId,
|
|
1185
|
+
onSelect: handleAgentSelect,
|
|
1186
|
+
onCancel: () => setMode(detailTaskId ? "detail" : "browse")
|
|
1187
|
+
}
|
|
1188
|
+
)
|
|
1189
|
+
] });
|
|
1190
|
+
}
|
|
1191
|
+
if (mode === "detail" && detailTask) {
|
|
1192
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
1193
|
+
/* @__PURE__ */ jsxs3(Box3, { marginBottom: 1, justifyContent: "space-between", children: [
|
|
1194
|
+
/* @__PURE__ */ jsx3(Text3, { color: "gray", children: "Esc back a launch agent j/k scroll" }),
|
|
1195
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "gray", dimColor: true, children: [
|
|
1196
|
+
"KANDOWN ",
|
|
1197
|
+
board.title
|
|
1198
|
+
] })
|
|
1199
|
+
] }),
|
|
1200
|
+
/* @__PURE__ */ jsx3(TaskDetail, { task: detailTask, taskId: detailTaskId, scrollOffset: detailScroll }),
|
|
1201
|
+
statusMsg && /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: statusMsg }) })
|
|
1202
|
+
] });
|
|
1203
|
+
}
|
|
1204
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
1205
|
+
/* @__PURE__ */ jsx3(BoardHeader, { title: board.title, inTmux }),
|
|
1206
|
+
/* @__PURE__ */ jsx3(Box3, { flexDirection: "row", children: board.columns.map((col, cIdx) => /* @__PURE__ */ jsx3(
|
|
1207
|
+
KanbanColumn,
|
|
1208
|
+
{
|
|
1209
|
+
name: col.name,
|
|
1210
|
+
tasks: col.tasks,
|
|
1211
|
+
focusedRow: cIdx === colIndex ? rowIndex : -1,
|
|
1212
|
+
isFocused: cIdx === colIndex,
|
|
1213
|
+
colWidth
|
|
1214
|
+
},
|
|
1215
|
+
col.name
|
|
1216
|
+
)) }),
|
|
1217
|
+
/* @__PURE__ */ jsx3(StatusBar, { message: statusMsg, task: focusedTask })
|
|
1218
|
+
] });
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// src/cli/app.tsx
|
|
1222
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1223
|
+
function App({ screen, kandownDir }) {
|
|
1224
|
+
switch (screen) {
|
|
1225
|
+
case "settings":
|
|
1226
|
+
return /* @__PURE__ */ jsx4(Settings, { kandownDir });
|
|
1227
|
+
case "board":
|
|
1228
|
+
return /* @__PURE__ */ jsx4(Board, { kandownDir });
|
|
1229
|
+
default:
|
|
1230
|
+
return /* @__PURE__ */ jsx4(Box4, { padding: 2, children: /* @__PURE__ */ jsxs4(Text4, { color: "red", bold: true, children: [
|
|
1231
|
+
"Unknown screen: ",
|
|
1232
|
+
screen
|
|
1233
|
+
] }) });
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// src/cli/tui.tsx
|
|
1238
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
1239
|
+
async function run(screen, kandownDir) {
|
|
1240
|
+
if (!process.stdin.isTTY) {
|
|
1241
|
+
throw new Error(
|
|
1242
|
+
"kandown TUI requires an interactive terminal. Run this command directly in your terminal."
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
process.stdout.write("\x1B[?1049h\x1B[H");
|
|
1246
|
+
const instance = render(/* @__PURE__ */ jsx5(App, { screen, kandownDir }), {
|
|
1247
|
+
exitOnCtrlC: true
|
|
1248
|
+
});
|
|
1249
|
+
try {
|
|
1250
|
+
await instance.waitUntilExit();
|
|
1251
|
+
} finally {
|
|
1252
|
+
process.stdout.write("\x1B[?1049l");
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
export {
|
|
1256
|
+
run
|
|
1257
|
+
};
|