kanon-cli 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.
@@ -0,0 +1,260 @@
1
+ import chalk from 'chalk';
2
+ import api from '../lib/api.js';
3
+ import { getToken, loadProjectConfig } from '../lib/config.js';
4
+
5
+ function requireAuth() {
6
+ if (!getToken()) {
7
+ console.error(chalk.red('Not logged in. Run: kanon login'));
8
+ process.exit(1);
9
+ }
10
+ }
11
+
12
+ async function getBoardContext() {
13
+ const config = loadProjectConfig();
14
+ const boardId = config?.board_id;
15
+ if (!boardId) throw new Error('No board configured. Run: kanon init');
16
+ const data = await api.getBoard(boardId, { cardLimit: 0 });
17
+ return data.board || data;
18
+ }
19
+
20
+ function resolveLabel(board, name) {
21
+ const lower = name.toLowerCase();
22
+ const label = (board.labels || []).find(l => l.name.toLowerCase() === lower);
23
+ if (!label) {
24
+ const available = (board.labels || []).map(l => l.name).join(', ');
25
+ throw new Error(`Label "${name}" not found. Available: ${available}`);
26
+ }
27
+ return label;
28
+ }
29
+
30
+ function resolveList(board, name) {
31
+ const lower = name.toLowerCase();
32
+ const list = (board.lists || []).find(l => l.title.toLowerCase() === lower);
33
+ if (!list) {
34
+ const available = (board.lists || []).map(l => l.title).join(', ');
35
+ throw new Error(`List "${name}" not found. Available: ${available}`);
36
+ }
37
+ return list;
38
+ }
39
+
40
+ function resolveMember(board, name) {
41
+ const lower = name.toLowerCase();
42
+ const member = (board.members || []).find(m =>
43
+ (m.name || '').toLowerCase().includes(lower) ||
44
+ (m.email || '').toLowerCase().includes(lower)
45
+ );
46
+ if (!member) {
47
+ const available = (board.members || []).map(m => m.name || m.email).join(', ');
48
+ throw new Error(`Member "${name}" not found. Available: ${available}`);
49
+ }
50
+ return member;
51
+ }
52
+
53
+ // --- kanon card <id> ---
54
+
55
+ export async function showCardCommand(cardId) {
56
+ requireAuth();
57
+ try {
58
+ const data = await api.getCard(cardId);
59
+ const card = data.card || data;
60
+
61
+ console.log(chalk.bold(`\n${card.title}`));
62
+ console.log(chalk.dim(`ID: ${card.id}`));
63
+ if (card.is_done) console.log(chalk.green('Done'));
64
+ if (card.due_date) console.log(chalk.yellow(`Due: ${card.due_date.substring(0, 10)}`));
65
+ if (card.description) console.log(`\n${card.description}`);
66
+
67
+ if (card.labels?.length) {
68
+ console.log(chalk.dim('\nLabels:'));
69
+ for (const l of card.labels) console.log(` ${chalk.hex(l.color || '#888')(l.name)}`);
70
+ }
71
+
72
+ if (card.assignees?.length) {
73
+ console.log(chalk.dim('\nAssignees:'));
74
+ for (const a of card.assignees) console.log(` ${a.name || a.email}`);
75
+ }
76
+
77
+ if (card.checklists?.length) {
78
+ for (const cl of card.checklists) {
79
+ console.log(chalk.dim(`\nChecklist: ${cl.title}`));
80
+ for (const item of cl.items || []) {
81
+ console.log(` ${item.completed ? chalk.green('[x]') : '[ ]'} ${item.text}`);
82
+ }
83
+ }
84
+ }
85
+
86
+ if (card.attachments?.length) {
87
+ console.log(chalk.dim(`\nAttachments (${card.attachments.length}):`));
88
+ for (const a of card.attachments) console.log(` ${a.filename || a.name}`);
89
+ }
90
+
91
+ if (card.comments?.length) {
92
+ console.log(chalk.dim(`\nComments (${card.comments.length}):`));
93
+ for (const c of card.comments.slice(0, 15)) {
94
+ const author = c.user_name || c.user?.name || 'Unknown';
95
+ console.log(` ${chalk.cyan(author)} ${chalk.dim(c.created_at)}:`);
96
+ console.log(` ${c.text}\n`);
97
+ }
98
+ }
99
+ } catch (err) {
100
+ console.error(chalk.red(`Failed to read card: ${err.message}`));
101
+ process.exit(1);
102
+ }
103
+ }
104
+
105
+ // --- kanon card create ---
106
+
107
+ export async function createCardCommand(title, listName, options) {
108
+ requireAuth();
109
+ try {
110
+ const board = await getBoardContext();
111
+ const list = resolveList(board, listName);
112
+
113
+ const data = { title };
114
+ if (options.description) data.description = options.description;
115
+ if (options.due) data.due_date = options.due;
116
+
117
+ const result = await api.createCard(list.id, data);
118
+ const cardId = result.card?.id || result.id;
119
+
120
+ // Apply labels after creation
121
+ if (options.label?.length) {
122
+ for (const name of options.label) {
123
+ const label = resolveLabel(board, name);
124
+ await api.addLabel(cardId, label.id);
125
+ }
126
+ }
127
+
128
+ // Apply assignees after creation
129
+ if (options.assign?.length) {
130
+ for (const name of options.assign) {
131
+ const member = resolveMember(board, name);
132
+ await api.addAssignee(cardId, member.user_id || member.id);
133
+ }
134
+ }
135
+
136
+ console.log(chalk.green(`Card created: "${title}" in "${list.title}" (${cardId})`));
137
+ } catch (err) {
138
+ console.error(chalk.red(`Failed to create card: ${err.message}`));
139
+ process.exit(1);
140
+ }
141
+ }
142
+
143
+ // --- kanon card update ---
144
+
145
+ export async function updateCardCommand(cardId, options) {
146
+ requireAuth();
147
+ try {
148
+ // 1. Simple PUT fields
149
+ const updates = {};
150
+ if (options.title) updates.title = options.title;
151
+ if (options.description) updates.description = options.description;
152
+ if (options.due) updates.due_date = options.due;
153
+ if (options.clearDue) updates.due_date = null;
154
+ if (options.done) updates.is_done = true;
155
+ if (options.undone) updates.is_done = false;
156
+
157
+ if (Object.keys(updates).length) {
158
+ await api.updateCard(cardId, updates);
159
+ }
160
+
161
+ // 2. Labels (need board context)
162
+ const needsBoard = options.addLabel?.length || options.removeLabel?.length ||
163
+ options.move || options.assign?.length || options.unassign?.length;
164
+ let board;
165
+ if (needsBoard) {
166
+ board = await getBoardContext();
167
+ }
168
+
169
+ if (options.addLabel?.length) {
170
+ for (const name of options.addLabel) {
171
+ const label = resolveLabel(board, name);
172
+ await api.addLabel(cardId, label.id);
173
+ }
174
+ }
175
+
176
+ if (options.removeLabel?.length) {
177
+ for (const name of options.removeLabel) {
178
+ const label = resolveLabel(board, name);
179
+ await api.removeLabel(cardId, label.id);
180
+ }
181
+ }
182
+
183
+ // 3. Assignees
184
+ if (options.assign?.length) {
185
+ for (const name of options.assign) {
186
+ const member = resolveMember(board, name);
187
+ await api.addAssignee(cardId, member.user_id || member.id);
188
+ }
189
+ }
190
+
191
+ if (options.unassign?.length) {
192
+ for (const name of options.unassign) {
193
+ const member = resolveMember(board, name);
194
+ await api.removeAssignee(cardId, member.user_id || member.id);
195
+ }
196
+ }
197
+
198
+ // 4. Move
199
+ if (options.move) {
200
+ const list = resolveList(board, options.move);
201
+ await api.moveCard(cardId, list.id);
202
+ }
203
+
204
+ // 5. Comment
205
+ if (options.comment) {
206
+ await api.addComment(cardId, options.comment);
207
+ }
208
+
209
+ console.log(chalk.green(`Card ${cardId} updated.`));
210
+ } catch (err) {
211
+ console.error(chalk.red(`Failed to update card: ${err.message}`));
212
+ process.exit(1);
213
+ }
214
+ }
215
+
216
+ // --- kanon card archive ---
217
+
218
+ export async function archiveCardCommand(cardId) {
219
+ requireAuth();
220
+ try {
221
+ await api.archiveCard(cardId);
222
+ console.log(chalk.green(`Card ${cardId} archived.`));
223
+ } catch (err) {
224
+ console.error(chalk.red(`Failed to archive card: ${err.message}`));
225
+ process.exit(1);
226
+ }
227
+ }
228
+
229
+ // --- kanon board (kept here as it's board-context) ---
230
+
231
+ export async function boardCommand() {
232
+ requireAuth();
233
+ try {
234
+ const board = await getBoardContext();
235
+
236
+ console.log(chalk.bold(`\n${board.title}`));
237
+ console.log(chalk.dim(`ID: ${board.id}`));
238
+
239
+ if (board.lists?.length) {
240
+ console.log(chalk.dim('\nLists:'));
241
+ for (const l of board.lists) {
242
+ const cardCount = l.cards?.length || 0;
243
+ console.log(` ${l.title} ${chalk.dim(`(${cardCount} cards)`)}`);
244
+ }
245
+ }
246
+
247
+ if (board.labels?.length) {
248
+ console.log(chalk.dim('\nLabels:'));
249
+ for (const l of board.labels) console.log(` ${chalk.hex(l.color || '#888')(l.name)}`);
250
+ }
251
+
252
+ if (board.members?.length) {
253
+ console.log(chalk.dim('\nMembers:'));
254
+ for (const m of board.members) console.log(` ${m.name || m.email}`);
255
+ }
256
+ } catch (err) {
257
+ console.error(chalk.red(`Failed to read board: ${err.message}`));
258
+ process.exit(1);
259
+ }
260
+ }
@@ -0,0 +1,79 @@
1
+ import chalk from 'chalk';
2
+ import api from '../lib/api.js';
3
+ import { getToken, loadProjectConfig } from '../lib/config.js';
4
+
5
+ function requireAuth() {
6
+ if (!getToken()) {
7
+ console.error(chalk.red('Not logged in. Run: kanon login'));
8
+ process.exit(1);
9
+ }
10
+ }
11
+
12
+ export async function cardsCommand(options) {
13
+ requireAuth();
14
+
15
+ const config = loadProjectConfig();
16
+ if (!config?.board_id) {
17
+ console.error(chalk.red('No board configured. Run: kanon init'));
18
+ process.exit(1);
19
+ }
20
+
21
+ try {
22
+ const data = await api.getBoard(config.board_id, { cardLimit: 50 });
23
+ const lists = data.board?.lists || [];
24
+
25
+ if (!lists.length) {
26
+ console.log(chalk.yellow('No lists found on this board.'));
27
+ return;
28
+ }
29
+
30
+ let totalCards = 0;
31
+
32
+ for (const list of lists) {
33
+ let cards = list.cards || [];
34
+
35
+ // Filter by list name
36
+ if (options.list) {
37
+ if (!list.title?.toLowerCase().includes(options.list.toLowerCase())) continue;
38
+ }
39
+
40
+ // Filter by label
41
+ if (options.label) {
42
+ cards = cards.filter(c =>
43
+ c.labels?.some(l => l.name?.toLowerCase().includes(options.label.toLowerCase()))
44
+ );
45
+ }
46
+
47
+ // Filter by assignee (mine)
48
+ if (options.mine) {
49
+ const { getUserId } = await import('../lib/config.js');
50
+ const myId = getUserId();
51
+ cards = cards.filter(c =>
52
+ c.assignees?.some(a => a.id === myId || a.user_id === myId)
53
+ );
54
+ }
55
+
56
+ if (!cards.length && !options.all) continue;
57
+
58
+ console.log(chalk.bold(`\n${list.title} (${cards.length})`));
59
+
60
+ for (const card of cards) {
61
+ const labels = card.labels?.map(l => chalk.hex(l.color || '#888')(l.name)).join(' ') || '';
62
+ const assignees = card.assignees?.map(a => a.name || a.user_name).join(', ') || '';
63
+ const due = card.due_date ? chalk.yellow(` due:${card.due_date.substring(0, 10)}`) : '';
64
+ const done = card.is_done ? chalk.green(' done') : '';
65
+
66
+ console.log(` ${chalk.white(card.title)}${done}${due} ${chalk.dim(card.id)}`);
67
+ if (labels) console.log(` ${labels}`);
68
+ if (assignees) console.log(` ${chalk.dim(assignees)}`);
69
+
70
+ totalCards++;
71
+ }
72
+ }
73
+
74
+ console.log(chalk.dim(`\n${totalCards} card(s) shown.\n`));
75
+ } catch (err) {
76
+ console.error(chalk.red(`Failed to fetch cards: ${err.message}`));
77
+ process.exit(1);
78
+ }
79
+ }
@@ -0,0 +1,129 @@
1
+ import chalk from 'chalk';
2
+ import api from '../lib/api.js';
3
+ import { getToken } from '../lib/config.js';
4
+
5
+ function requireAuth() {
6
+ if (!getToken()) {
7
+ console.error(chalk.red('Not logged in. Run: kanon login'));
8
+ process.exit(1);
9
+ }
10
+ }
11
+
12
+ async function getCardChecklists(cardId) {
13
+ const data = await api.getCard(cardId);
14
+ const card = data.card || data;
15
+ return card.checklists || [];
16
+ }
17
+
18
+ function findItem(checklists, text) {
19
+ const lower = text.toLowerCase();
20
+ for (const cl of checklists) {
21
+ for (const item of cl.items || []) {
22
+ if (item.text.toLowerCase().includes(lower)) {
23
+ return item;
24
+ }
25
+ }
26
+ }
27
+ return null;
28
+ }
29
+
30
+ // --- kanon checklist <cardId> create "title" ---
31
+
32
+ export async function checklistCreateCommand(cardId, title) {
33
+ requireAuth();
34
+ try {
35
+ const result = await api.createChecklist(cardId, title);
36
+ const id = result.checklist?.id || result.id;
37
+ console.log(chalk.green(`Checklist "${title}" created on card ${cardId} (${id}).`));
38
+ } catch (err) {
39
+ console.error(chalk.red(`Failed to create checklist: ${err.message}`));
40
+ process.exit(1);
41
+ }
42
+ }
43
+
44
+ // --- kanon checklist <cardId> add "item text" ---
45
+
46
+ export async function checklistAddCommand(cardId, text) {
47
+ requireAuth();
48
+ try {
49
+ let checklists = await getCardChecklists(cardId);
50
+ let checklistId;
51
+
52
+ if (checklists.length === 0) {
53
+ // Create a default checklist, then re-fetch to get its ID
54
+ await api.createChecklist(cardId, 'Checklist');
55
+ checklists = await getCardChecklists(cardId);
56
+ checklistId = checklists[0]?.id;
57
+ if (!checklistId) throw new Error('Failed to create checklist');
58
+ } else {
59
+ checklistId = checklists[0].id;
60
+ }
61
+
62
+ await api.addChecklistItem(checklistId, text);
63
+ console.log(chalk.green(`Item added to checklist on card ${cardId}.`));
64
+ } catch (err) {
65
+ console.error(chalk.red(`Failed to add checklist item: ${err.message}`));
66
+ process.exit(1);
67
+ }
68
+ }
69
+
70
+ // --- kanon checklist <cardId> check "item text" ---
71
+
72
+ export async function checklistCheckCommand(cardId, text) {
73
+ requireAuth();
74
+ try {
75
+ const checklists = await getCardChecklists(cardId);
76
+ const item = findItem(checklists, text);
77
+ if (!item) {
78
+ console.error(chalk.red(`No checklist item matching "${text}" found.`));
79
+ process.exit(1);
80
+ }
81
+ if (!item.completed) {
82
+ await api.toggleChecklistItem(item.id);
83
+ }
84
+ console.log(chalk.green(`Checked: "${item.text}"`));
85
+ } catch (err) {
86
+ console.error(chalk.red(`Failed to check item: ${err.message}`));
87
+ process.exit(1);
88
+ }
89
+ }
90
+
91
+ // --- kanon checklist <cardId> uncheck "item text" ---
92
+
93
+ export async function checklistUncheckCommand(cardId, text) {
94
+ requireAuth();
95
+ try {
96
+ const checklists = await getCardChecklists(cardId);
97
+ const item = findItem(checklists, text);
98
+ if (!item) {
99
+ console.error(chalk.red(`No checklist item matching "${text}" found.`));
100
+ process.exit(1);
101
+ }
102
+ if (item.completed) {
103
+ await api.toggleChecklistItem(item.id);
104
+ }
105
+ console.log(chalk.green(`Unchecked: "${item.text}"`));
106
+ } catch (err) {
107
+ console.error(chalk.red(`Failed to uncheck item: ${err.message}`));
108
+ process.exit(1);
109
+ }
110
+ }
111
+
112
+ // --- kanon checklist <cardId> remove "item text" ---
113
+
114
+ export async function checklistRemoveCommand(cardId, text) {
115
+ requireAuth();
116
+ try {
117
+ const checklists = await getCardChecklists(cardId);
118
+ const item = findItem(checklists, text);
119
+ if (!item) {
120
+ console.error(chalk.red(`No checklist item matching "${text}" found.`));
121
+ process.exit(1);
122
+ }
123
+ await api.deleteChecklistItem(item.id);
124
+ console.log(chalk.green(`Removed: "${item.text}"`));
125
+ } catch (err) {
126
+ console.error(chalk.red(`Failed to remove item: ${err.message}`));
127
+ process.exit(1);
128
+ }
129
+ }
@@ -0,0 +1,24 @@
1
+ import chalk from 'chalk';
2
+ import { createDashboardServer } from '../dashboard/server/index.js';
3
+ import { exec } from 'child_process';
4
+ import { platform } from 'os';
5
+
6
+ export async function dashboardCommand(options) {
7
+ const port = parseInt(options.port) || 3737;
8
+
9
+ console.log(chalk.bold('\nKanon Dashboard\n'));
10
+
11
+ createDashboardServer(port);
12
+
13
+ const url = `http://localhost:${port}`;
14
+
15
+ // Open browser (Commander's --no-browser sets options.browser = false)
16
+ if (options.browser !== false) {
17
+ const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open';
18
+ exec(`${cmd} ${url}`, () => {});
19
+ console.log(chalk.dim(`Opening ${url} in browser...`));
20
+ }
21
+
22
+ console.log(chalk.green(`\nDashboard ready at ${chalk.bold(url)}`));
23
+ console.log(chalk.dim('Press Ctrl+C to stop.\n'));
24
+ }
@@ -0,0 +1,89 @@
1
+ import readline from 'readline';
2
+ import chalk from 'chalk';
3
+ import api from '../lib/api.js';
4
+ import { getToken, loadProjectConfig, saveProjectConfig } from '../lib/config.js';
5
+
6
+ export async function initCommand() {
7
+ if (!getToken()) {
8
+ console.error(chalk.red('Not logged in. Run: kanon login'));
9
+ process.exit(1);
10
+ }
11
+
12
+ const existing = loadProjectConfig();
13
+ if (existing) {
14
+ console.log(chalk.yellow('kanon.config.yaml already exists in this directory.'));
15
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
16
+ const answer = await new Promise(r => rl.question('Overwrite? (y/N) ', r));
17
+ rl.close();
18
+ if (answer.toLowerCase() !== 'y') {
19
+ console.log('Aborted.');
20
+ return;
21
+ }
22
+ }
23
+
24
+ try {
25
+ console.log(chalk.dim('Fetching boards...\n'));
26
+ const data = await api.getTeams();
27
+ const teams = data.teams || data;
28
+
29
+ // Flatten all boards
30
+ const allBoards = [];
31
+ for (const team of teams) {
32
+ for (const board of team.boards || []) {
33
+ allBoards.push({ team: team.name, ...board });
34
+ }
35
+ }
36
+
37
+ if (!allBoards.length) {
38
+ console.log(chalk.red('No boards found.'));
39
+ return;
40
+ }
41
+
42
+ console.log(chalk.bold('Select a board:\n'));
43
+ allBoards.forEach((b, i) => {
44
+ console.log(` ${chalk.cyan(i + 1)} ${b.title || b.name} ${chalk.dim(`(${b.team})`)}`);
45
+ });
46
+
47
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
48
+ const choice = await new Promise(r => rl.question('\nBoard number: ', r));
49
+ rl.close();
50
+
51
+ const idx = parseInt(choice, 10) - 1;
52
+ if (isNaN(idx) || idx < 0 || idx >= allBoards.length) {
53
+ console.error(chalk.red('Invalid selection.'));
54
+ process.exit(1);
55
+ }
56
+
57
+ const board = allBoards[idx];
58
+
59
+ const config = {
60
+ board_id: board.id,
61
+ watch: {
62
+ cooldown_seconds: 30,
63
+ ignore_own_events: true,
64
+ },
65
+ rules: {
66
+ labels: [],
67
+ events: { agent_assigned: { enabled: true } },
68
+ },
69
+ claude: {
70
+ command: 'claude',
71
+ max_concurrent: 1,
72
+ },
73
+ admin: {
74
+ check_interval_seconds: 60,
75
+ max_session_minutes: 30,
76
+ stuck_timeout_minutes: 5,
77
+ auto_restart_stuck: true,
78
+ queue_max_size: 20,
79
+ },
80
+ };
81
+
82
+ saveProjectConfig(config);
83
+ console.log(chalk.green(`\nCreated kanon.config.yaml for board "${board.title || board.name}"`));
84
+ console.log(chalk.dim('Run: kanon watch --dry-run to test'));
85
+ } catch (err) {
86
+ console.error(chalk.red(`Init failed: ${err.message}`));
87
+ process.exit(1);
88
+ }
89
+ }
@@ -0,0 +1,61 @@
1
+ import chalk from 'chalk';
2
+ import api from '../lib/api.js';
3
+ import { getToken, loadProjectConfig } from '../lib/config.js';
4
+
5
+ function requireAuth() {
6
+ if (!getToken()) {
7
+ console.error(chalk.red('Not logged in. Run: kanon login'));
8
+ process.exit(1);
9
+ }
10
+ }
11
+
12
+ function getBoardId() {
13
+ const config = loadProjectConfig();
14
+ const boardId = config?.board_id;
15
+ if (!boardId) throw new Error('No board configured. Run: kanon init');
16
+ return boardId;
17
+ }
18
+
19
+ // --- kanon label create "name" [--color "#hex"] ---
20
+
21
+ export async function labelCreateCommand(name, options) {
22
+ requireAuth();
23
+ try {
24
+ const boardId = getBoardId();
25
+ const color = options.color || '#888888';
26
+ const result = await api.createBoardLabel(boardId, name, color);
27
+ const id = result.label?.id || result.id;
28
+ console.log(chalk.green(`Label "${name}" created (${id}).`));
29
+ } catch (err) {
30
+ console.error(chalk.red(`Failed to create label: ${err.message}`));
31
+ process.exit(1);
32
+ }
33
+ }
34
+
35
+ // --- kanon label delete "name" ---
36
+
37
+ export async function labelDeleteCommand(name) {
38
+ requireAuth();
39
+ try {
40
+ const boardId = getBoardId();
41
+ const data = await api.getBoard(boardId, { cardLimit: 0 });
42
+ const board = data.board || data;
43
+ const lower = name.toLowerCase();
44
+ const label = (board.labels || []).find(l => l.name.toLowerCase() === lower);
45
+ if (!label) {
46
+ const available = (board.labels || []).map(l => l.name).join(', ');
47
+ console.error(chalk.red(`Label "${name}" not found. Available: ${available}`));
48
+ process.exit(1);
49
+ }
50
+ if (typeof api.deleteBoardLabel === 'function') {
51
+ await api.deleteBoardLabel(boardId, label.id);
52
+ console.log(chalk.green(`Label "${name}" deleted.`));
53
+ } else {
54
+ console.error(chalk.red('Label deletion not supported by current API.'));
55
+ process.exit(1);
56
+ }
57
+ } catch (err) {
58
+ console.error(chalk.red(`Failed to delete label: ${err.message}`));
59
+ process.exit(1);
60
+ }
61
+ }