@gzmagyari/kanbanboard 1.0.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/.env.example +48 -0
- package/API.md +1256 -0
- package/README.md +138 -0
- package/bin/cli.mjs +437 -0
- package/cron-sync.mjs +9 -0
- package/db.mjs +378 -0
- package/docs/project-manager-chat.md +202 -0
- package/kanban-mcp-server.mjs +377 -0
- package/kanban.mjs +127 -0
- package/lib/paths.mjs +136 -0
- package/llm.mjs +307 -0
- package/package.json +52 -0
- package/public/index.html +4747 -0
- package/repo-grounding.mjs +417 -0
- package/server.mjs +8607 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Kanban MCP Server — exposes KanbanDashboard REST API as MCP tools.
|
|
4
|
+
*
|
|
5
|
+
* Protocol: JSON-RPC 2.0 over stdio (one JSON message per line).
|
|
6
|
+
* No external dependencies — uses only Node.js built-ins.
|
|
7
|
+
*
|
|
8
|
+
* Env vars:
|
|
9
|
+
* KANBAN_BASE_URL — base URL for the Kanban API (default: http://localhost:3000)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createInterface } from 'node:readline';
|
|
13
|
+
import http from 'node:http';
|
|
14
|
+
|
|
15
|
+
const BASE_URL = process.env.KANBAN_BASE_URL || 'http://localhost:3000';
|
|
16
|
+
|
|
17
|
+
// ─── HTTP helper ────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function apiRequest(method, path, body) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const url = new URL(path, BASE_URL);
|
|
22
|
+
const payload = body ? JSON.stringify(body) : null;
|
|
23
|
+
const opts = {
|
|
24
|
+
hostname: url.hostname,
|
|
25
|
+
port: url.port,
|
|
26
|
+
path: url.pathname + url.search,
|
|
27
|
+
method,
|
|
28
|
+
headers: { 'Accept': 'application/json' }
|
|
29
|
+
};
|
|
30
|
+
if (payload) {
|
|
31
|
+
opts.headers['Content-Type'] = 'application/json';
|
|
32
|
+
opts.headers['Content-Length'] = Buffer.byteLength(payload);
|
|
33
|
+
}
|
|
34
|
+
const req = http.request(opts, (res) => {
|
|
35
|
+
const chunks = [];
|
|
36
|
+
res.on('data', c => chunks.push(c));
|
|
37
|
+
res.on('end', () => {
|
|
38
|
+
const text = Buffer.concat(chunks).toString();
|
|
39
|
+
let json;
|
|
40
|
+
try { json = JSON.parse(text); } catch { json = { raw: text }; }
|
|
41
|
+
if (res.statusCode >= 400) {
|
|
42
|
+
const err = new Error(json.error || `HTTP ${res.statusCode}`);
|
|
43
|
+
err.statusCode = res.statusCode;
|
|
44
|
+
err.body = json;
|
|
45
|
+
return reject(err);
|
|
46
|
+
}
|
|
47
|
+
resolve(json);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
req.on('error', reject);
|
|
51
|
+
if (payload) req.write(payload);
|
|
52
|
+
req.end();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Tool definitions ───────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
const TOOLS = [
|
|
59
|
+
{
|
|
60
|
+
name: 'get_task',
|
|
61
|
+
description: 'Get task details by ID, including comments and progress updates',
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
task_id: { type: 'string', description: 'The task ID' }
|
|
66
|
+
},
|
|
67
|
+
required: ['task_id']
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'list_tasks',
|
|
72
|
+
description: 'List tasks, optionally filtered by project, status, or parent',
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
project_id: { type: 'string', description: 'Filter by project ID' },
|
|
77
|
+
status: { type: 'string', description: 'Filter by status (ideas, todo, in_progress, review, testing, done)' },
|
|
78
|
+
parent_id: { type: 'string', description: 'Filter by parent task ID. Use "null" for root tasks.' }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'update_task_status',
|
|
84
|
+
description: 'Move a task to a new status',
|
|
85
|
+
inputSchema: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
task_id: { type: 'string', description: 'The task ID' },
|
|
89
|
+
status: { type: 'string', description: 'New status (ideas, todo, in_progress, review, testing, done)' }
|
|
90
|
+
},
|
|
91
|
+
required: ['task_id', 'status']
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'update_task_fields',
|
|
96
|
+
description: 'Update task fields (title, description, detail_text, assigned_agent_id)',
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
task_id: { type: 'string', description: 'The task ID' },
|
|
101
|
+
title: { type: 'string', description: 'New title' },
|
|
102
|
+
description: { type: 'string', description: 'New description' },
|
|
103
|
+
detail_text: { type: 'string', description: 'New detail text' },
|
|
104
|
+
assigned_agent_id: { type: ['string', 'null'], description: 'Agent ID to assign, or null to unassign' }
|
|
105
|
+
},
|
|
106
|
+
required: ['task_id']
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'create_task',
|
|
111
|
+
description: 'Create a new task in a project',
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: {
|
|
115
|
+
project_id: { type: 'string', description: 'Project ID' },
|
|
116
|
+
title: { type: 'string', description: 'Task title' },
|
|
117
|
+
description: { type: 'string', description: 'Task description' },
|
|
118
|
+
status: { type: 'string', description: 'Initial status (default: todo)' },
|
|
119
|
+
parent_id: { type: 'string', description: 'Parent task ID (optional)' },
|
|
120
|
+
milestone_id: { type: 'string', description: 'Milestone ID (optional)' }
|
|
121
|
+
},
|
|
122
|
+
required: ['project_id', 'title']
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'add_comment',
|
|
127
|
+
description: 'Add a comment to a task',
|
|
128
|
+
inputSchema: {
|
|
129
|
+
type: 'object',
|
|
130
|
+
properties: {
|
|
131
|
+
task_id: { type: 'string', description: 'The task ID' },
|
|
132
|
+
text: { type: 'string', description: 'Comment text' },
|
|
133
|
+
author: { type: 'string', description: 'Author name (default: Agent)' }
|
|
134
|
+
},
|
|
135
|
+
required: ['task_id', 'text']
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'delete_comment',
|
|
140
|
+
description: 'Delete a comment by its ID',
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
comment_id: { type: 'number', description: 'The comment ID to delete' }
|
|
145
|
+
},
|
|
146
|
+
required: ['comment_id']
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'add_progress_update',
|
|
151
|
+
description: 'Add a progress update to a task',
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
task_id: { type: 'string', description: 'The task ID' },
|
|
156
|
+
text: { type: 'string', description: 'Progress update text' },
|
|
157
|
+
author: { type: 'string', description: 'Author name (default: Agent)' }
|
|
158
|
+
},
|
|
159
|
+
required: ['task_id', 'text']
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: 'get_project',
|
|
164
|
+
description: 'Get project details',
|
|
165
|
+
inputSchema: {
|
|
166
|
+
type: 'object',
|
|
167
|
+
properties: {
|
|
168
|
+
project_id: { type: 'string', description: 'The project ID' }
|
|
169
|
+
},
|
|
170
|
+
required: ['project_id']
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'list_agents',
|
|
175
|
+
description: 'List agents for a project',
|
|
176
|
+
inputSchema: {
|
|
177
|
+
type: 'object',
|
|
178
|
+
properties: {
|
|
179
|
+
project_id: { type: 'string', description: 'The project ID' }
|
|
180
|
+
},
|
|
181
|
+
required: ['project_id']
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'get_agent_by_role',
|
|
186
|
+
description: 'Find an agent by role within a project',
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
properties: {
|
|
190
|
+
project_id: { type: 'string', description: 'The project ID' },
|
|
191
|
+
role: { type: 'string', description: 'Agent role (e.g. qa_engineer, developer)' }
|
|
192
|
+
},
|
|
193
|
+
required: ['project_id', 'role']
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'run_agent_on_ticket',
|
|
198
|
+
description: 'Trigger an agent run against a specific ticket. Returns job_id for polling.',
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
agent_id: { type: 'string', description: 'Agent ID' },
|
|
203
|
+
task_id: { type: 'string', description: 'Task/ticket ID' },
|
|
204
|
+
fresh_chrome: { type: 'boolean', description: 'Restart Chrome before running (default: false)' },
|
|
205
|
+
new_session: { type: 'boolean', description: 'Start fresh Claude session (default: false)' }
|
|
206
|
+
},
|
|
207
|
+
required: ['agent_id', 'task_id']
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
// ─── Tool handlers ──────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
async function handleToolCall(name, args) {
|
|
215
|
+
switch (name) {
|
|
216
|
+
case 'get_task': {
|
|
217
|
+
const data = await apiRequest('GET', `/api/tasks/${args.task_id}`);
|
|
218
|
+
return JSON.stringify(data, null, 2);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
case 'list_tasks': {
|
|
222
|
+
const params = new URLSearchParams();
|
|
223
|
+
if (args.project_id) params.set('project_id', args.project_id);
|
|
224
|
+
if (args.status) params.set('status', args.status);
|
|
225
|
+
if (args.parent_id !== undefined) params.set('parent_id', args.parent_id);
|
|
226
|
+
const data = await apiRequest('GET', `/api/tasks?${params}`);
|
|
227
|
+
return JSON.stringify(data, null, 2);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
case 'update_task_status': {
|
|
231
|
+
const data = await apiRequest('PATCH', `/api/tasks/${args.task_id}`, { status: args.status });
|
|
232
|
+
return JSON.stringify(data, null, 2);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
case 'update_task_fields': {
|
|
236
|
+
const body = {};
|
|
237
|
+
if (args.title !== undefined) body.title = args.title;
|
|
238
|
+
if (args.description !== undefined) body.description = args.description;
|
|
239
|
+
if (args.detail_text !== undefined) body.detail_text = args.detail_text;
|
|
240
|
+
if (args.assigned_agent_id !== undefined) body.assigned_agent_id = args.assigned_agent_id;
|
|
241
|
+
const data = await apiRequest('PATCH', `/api/tasks/${args.task_id}`, body);
|
|
242
|
+
return JSON.stringify(data, null, 2);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
case 'create_task': {
|
|
246
|
+
const body = {
|
|
247
|
+
title: args.title,
|
|
248
|
+
project_id: args.project_id,
|
|
249
|
+
status: args.status || 'todo'
|
|
250
|
+
};
|
|
251
|
+
if (args.description) body.description = args.description;
|
|
252
|
+
if (args.parent_id) body.parent_id = args.parent_id;
|
|
253
|
+
if (args.milestone_id) body.milestone_id = args.milestone_id;
|
|
254
|
+
const data = await apiRequest('POST', '/api/tasks', body);
|
|
255
|
+
return JSON.stringify(data, null, 2);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
case 'add_comment': {
|
|
259
|
+
const body = { text: args.text };
|
|
260
|
+
if (args.author) body.author = args.author;
|
|
261
|
+
const data = await apiRequest('POST', `/api/tasks/${args.task_id}/comments`, body);
|
|
262
|
+
return JSON.stringify(data, null, 2);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
case 'delete_comment': {
|
|
266
|
+
const data = await apiRequest('DELETE', `/api/comments/${args.comment_id}`);
|
|
267
|
+
return JSON.stringify(data, null, 2);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
case 'add_progress_update': {
|
|
271
|
+
const body = { text: args.text };
|
|
272
|
+
if (args.author) body.author = args.author;
|
|
273
|
+
const data = await apiRequest('POST', `/api/tasks/${args.task_id}/updates`, body);
|
|
274
|
+
return JSON.stringify(data, null, 2);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
case 'get_project': {
|
|
278
|
+
const data = await apiRequest('GET', `/api/projects/${args.project_id}`);
|
|
279
|
+
return JSON.stringify(data, null, 2);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
case 'list_agents': {
|
|
283
|
+
const data = await apiRequest('GET', `/api/projects/${args.project_id}/agents`);
|
|
284
|
+
return JSON.stringify(data, null, 2);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
case 'get_agent_by_role': {
|
|
288
|
+
const data = await apiRequest('GET', `/api/projects/${args.project_id}/agents/by-role/${args.role}`);
|
|
289
|
+
return JSON.stringify(data, null, 2);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
case 'run_agent_on_ticket': {
|
|
293
|
+
const body = { task_id: args.task_id };
|
|
294
|
+
if (args.fresh_chrome) body.fresh_chrome = true;
|
|
295
|
+
if (args.new_session) body.new_session = true;
|
|
296
|
+
const data = await apiRequest('POST', `/api/agents/${args.agent_id}/run-on-ticket`, body);
|
|
297
|
+
return JSON.stringify(data, null, 2);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
default:
|
|
301
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ─── JSON-RPC / MCP protocol ───────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
function send(msg) {
|
|
308
|
+
process.stdout.write(JSON.stringify(msg) + '\n');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function makeResult(id, result) {
|
|
312
|
+
return { jsonrpc: '2.0', id, result };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function makeError(id, code, message) {
|
|
316
|
+
return { jsonrpc: '2.0', id, error: { code, message } };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function handleMessage(msg) {
|
|
320
|
+
const { id, method, params } = msg;
|
|
321
|
+
|
|
322
|
+
switch (method) {
|
|
323
|
+
case 'initialize':
|
|
324
|
+
send(makeResult(id, {
|
|
325
|
+
protocolVersion: '2024-11-05',
|
|
326
|
+
capabilities: { tools: {} },
|
|
327
|
+
serverInfo: { name: 'kanban', version: '1.0.0' }
|
|
328
|
+
}));
|
|
329
|
+
break;
|
|
330
|
+
|
|
331
|
+
case 'notifications/initialized':
|
|
332
|
+
// No response needed for notifications
|
|
333
|
+
break;
|
|
334
|
+
|
|
335
|
+
case 'tools/list':
|
|
336
|
+
send(makeResult(id, { tools: TOOLS }));
|
|
337
|
+
break;
|
|
338
|
+
|
|
339
|
+
case 'tools/call': {
|
|
340
|
+
const toolName = params?.name;
|
|
341
|
+
const args = params?.arguments || {};
|
|
342
|
+
try {
|
|
343
|
+
const text = await handleToolCall(toolName, args);
|
|
344
|
+
send(makeResult(id, {
|
|
345
|
+
content: [{ type: 'text', text }]
|
|
346
|
+
}));
|
|
347
|
+
} catch (e) {
|
|
348
|
+
send(makeResult(id, {
|
|
349
|
+
content: [{ type: 'text', text: `Error: ${e.message}` }],
|
|
350
|
+
isError: true
|
|
351
|
+
}));
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
default:
|
|
357
|
+
if (id !== undefined) {
|
|
358
|
+
send(makeError(id, -32601, `Method not found: ${method}`));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
const rl = createInterface({ input: process.stdin });
|
|
366
|
+
rl.on('line', async (line) => {
|
|
367
|
+
if (!line.trim()) return;
|
|
368
|
+
try {
|
|
369
|
+
const msg = JSON.parse(line);
|
|
370
|
+
await handleMessage(msg);
|
|
371
|
+
} catch (e) {
|
|
372
|
+
// Parse error
|
|
373
|
+
send(makeError(null, -32700, `Parse error: ${e.message}`));
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
process.stderr.write(`[kanban-mcp] Server started, base URL: ${BASE_URL}\n`);
|
package/kanban.mjs
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* KanbanDashboard server manager CLI.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node kanban.mjs start — Start the server in the background
|
|
7
|
+
* node kanban.mjs stop — Stop the running server
|
|
8
|
+
* node kanban.mjs restart — Stop then start
|
|
9
|
+
* node kanban.mjs status — Show whether the server is running
|
|
10
|
+
*
|
|
11
|
+
* The server PID is stored in .kanban.pid so we can target exactly the
|
|
12
|
+
* right process without killing unrelated node processes.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawn, execSync } from 'child_process';
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import { getPidFile, getLogFile, getServerScript, getHome } from './lib/paths.mjs';
|
|
18
|
+
|
|
19
|
+
const PID_FILE = getPidFile();
|
|
20
|
+
const LOG_FILE = getLogFile();
|
|
21
|
+
const SERVER_SCRIPT = getServerScript();
|
|
22
|
+
|
|
23
|
+
function readPid() {
|
|
24
|
+
try {
|
|
25
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
|
|
26
|
+
return Number.isFinite(pid) ? pid : null;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isProcessRunning(pid) {
|
|
33
|
+
try {
|
|
34
|
+
// On Windows, tasklist filtered by PID. Exit code 0 = found.
|
|
35
|
+
const out = execSync(`tasklist /FI "PID eq ${pid}" /NH`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
36
|
+
return out.includes(String(pid));
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stopServer() {
|
|
43
|
+
const pid = readPid();
|
|
44
|
+
if (!pid) {
|
|
45
|
+
console.log('No PID file found — server is not running (or was not started with this tool).');
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (!isProcessRunning(pid)) {
|
|
49
|
+
console.log(`PID ${pid} is not running. Cleaning up stale PID file.`);
|
|
50
|
+
try { fs.unlinkSync(PID_FILE); } catch {}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
execSync(`taskkill /PID ${pid} /T /F`, { stdio: 'pipe' });
|
|
55
|
+
console.log(`Stopped server (PID ${pid}).`);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.error(`Failed to stop PID ${pid}: ${e.message}`);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
try { fs.unlinkSync(PID_FILE); } catch {}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function startServer() {
|
|
65
|
+
const existingPid = readPid();
|
|
66
|
+
if (existingPid && isProcessRunning(existingPid)) {
|
|
67
|
+
console.log(`Server is already running (PID ${existingPid}). Use 'restart' to bounce it.`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Clean up stale PID file
|
|
72
|
+
if (existingPid) {
|
|
73
|
+
try { fs.unlinkSync(PID_FILE); } catch {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const out = fs.openSync(LOG_FILE, 'a');
|
|
77
|
+
const err = fs.openSync(LOG_FILE, 'a');
|
|
78
|
+
|
|
79
|
+
const child = spawn('node', [SERVER_SCRIPT], {
|
|
80
|
+
cwd: getHome(),
|
|
81
|
+
detached: true,
|
|
82
|
+
stdio: ['ignore', out, err],
|
|
83
|
+
env: { ...process.env }
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
child.unref();
|
|
87
|
+
fs.writeFileSync(PID_FILE, String(child.pid), 'utf8');
|
|
88
|
+
console.log(`Server started (PID ${child.pid}). Logs: ${LOG_FILE}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function showStatus() {
|
|
92
|
+
const pid = readPid();
|
|
93
|
+
if (!pid) {
|
|
94
|
+
console.log('Server is NOT running (no PID file).');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (isProcessRunning(pid)) {
|
|
98
|
+
console.log(`Server is RUNNING (PID ${pid}).`);
|
|
99
|
+
} else {
|
|
100
|
+
console.log(`Server is NOT running (stale PID ${pid}). Cleaning up.`);
|
|
101
|
+
try { fs.unlinkSync(PID_FILE); } catch {}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── CLI ──
|
|
106
|
+
const command = (process.argv[2] || '').toLowerCase();
|
|
107
|
+
|
|
108
|
+
switch (command) {
|
|
109
|
+
case 'start':
|
|
110
|
+
startServer();
|
|
111
|
+
break;
|
|
112
|
+
case 'stop':
|
|
113
|
+
stopServer();
|
|
114
|
+
break;
|
|
115
|
+
case 'restart':
|
|
116
|
+
stopServer();
|
|
117
|
+
// Brief pause to let the port free up
|
|
118
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
119
|
+
startServer();
|
|
120
|
+
break;
|
|
121
|
+
case 'status':
|
|
122
|
+
showStatus();
|
|
123
|
+
break;
|
|
124
|
+
default:
|
|
125
|
+
console.log(`Usage: node kanban.mjs <start|stop|restart|status>`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
package/lib/paths.mjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized path resolution for KanbanBoard.
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* 1. Development — CWD is the repo directory (detected automatically)
|
|
6
|
+
* 2. Installed — uses ~/.kanbanboard/ for all runtime data
|
|
7
|
+
*
|
|
8
|
+
* Override with KANBANBOARD_HOME env var.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
|
|
19
|
+
// Package root is one level up from lib/
|
|
20
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Detect whether we are running from the source repo directory.
|
|
24
|
+
* Check: CWD has a package.json with our package name AND server.mjs exists.
|
|
25
|
+
*/
|
|
26
|
+
function isDevMode() {
|
|
27
|
+
try {
|
|
28
|
+
const cwdPkg = path.join(process.cwd(), 'package.json');
|
|
29
|
+
const raw = fs.readFileSync(cwdPkg, 'utf8');
|
|
30
|
+
const pkg = JSON.parse(raw);
|
|
31
|
+
if (pkg.name === '@gzmagyari/kanbanboard' || pkg.name === 'kanbanboard' || pkg.name === 'kanban-dashboard') {
|
|
32
|
+
return fs.existsSync(path.join(process.cwd(), 'server.mjs'));
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// No package.json or not parseable — not dev mode
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the "home" directory where all runtime data lives.
|
|
42
|
+
*
|
|
43
|
+
* Priority:
|
|
44
|
+
* 1. KANBANBOARD_HOME env var (explicit override)
|
|
45
|
+
* 2. Dev mode (CWD is repo) → CWD
|
|
46
|
+
* 3. Installed mode → ~/.kanbanboard/
|
|
47
|
+
*/
|
|
48
|
+
function resolveHome() {
|
|
49
|
+
const envHome = process.env.KANBANBOARD_HOME;
|
|
50
|
+
if (envHome) return path.resolve(envHome);
|
|
51
|
+
if (isDevMode()) return process.cwd();
|
|
52
|
+
return path.join(os.homedir(), '.kanbanboard');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Computed once at import time
|
|
56
|
+
const _home = resolveHome();
|
|
57
|
+
const _isDev = isDevMode();
|
|
58
|
+
|
|
59
|
+
export function getHome() { return _home; }
|
|
60
|
+
export function isDevelopment() { return _isDev; }
|
|
61
|
+
export function getPackageRoot() { return PACKAGE_ROOT; }
|
|
62
|
+
|
|
63
|
+
// ── Specific path accessors ──
|
|
64
|
+
|
|
65
|
+
/** Directory containing kanban.db */
|
|
66
|
+
export function getDataDir() { return path.join(_home, 'data'); }
|
|
67
|
+
|
|
68
|
+
/** Full path to kanban.db */
|
|
69
|
+
export function getDbPath() { return path.join(getDataDir(), 'kanban.db'); }
|
|
70
|
+
|
|
71
|
+
/** Directory for Chrome pid, profile, etc. */
|
|
72
|
+
export function getRuntimeDir() { return path.join(_home, 'runtime'); }
|
|
73
|
+
|
|
74
|
+
/** Path to .env file */
|
|
75
|
+
export function getEnvPath() { return path.join(_home, '.env'); }
|
|
76
|
+
|
|
77
|
+
/** Path to .mcp.json */
|
|
78
|
+
export function getMcpJsonPath() { return path.join(_home, '.mcp.json'); }
|
|
79
|
+
|
|
80
|
+
/** Path to PID file */
|
|
81
|
+
export function getPidFile() { return path.join(_home, '.kanban.pid'); }
|
|
82
|
+
|
|
83
|
+
/** Path to log file */
|
|
84
|
+
export function getLogFile() { return path.join(_home, 'kanban.log'); }
|
|
85
|
+
|
|
86
|
+
/** Path to agent queue log */
|
|
87
|
+
export function getQueueLogPath() { return path.join(getDataDir(), 'agent-queue.log'); }
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The public/ directory containing index.html.
|
|
91
|
+
* Always from the PACKAGE root (ships with the package, not user data).
|
|
92
|
+
*/
|
|
93
|
+
export function getPublicDir() { return path.join(PACKAGE_ROOT, 'public'); }
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The server.mjs script path (for process manager to spawn).
|
|
97
|
+
* Always from the package root.
|
|
98
|
+
*/
|
|
99
|
+
export function getServerScript() { return path.join(PACKAGE_ROOT, 'server.mjs'); }
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Load .env from the resolved home directory.
|
|
103
|
+
* Must be called BEFORE any module reads process.env for app config.
|
|
104
|
+
*/
|
|
105
|
+
export async function loadEnv() {
|
|
106
|
+
const envPath = getEnvPath();
|
|
107
|
+
if (fs.existsSync(envPath)) {
|
|
108
|
+
const dotenv = await import('dotenv');
|
|
109
|
+
dotenv.config({ path: envPath });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Ensure the home directory structure exists.
|
|
115
|
+
* Called by `kanbanboard init` and lazily by the server on startup.
|
|
116
|
+
*/
|
|
117
|
+
export function ensureDirectories() {
|
|
118
|
+
fs.mkdirSync(getDataDir(), { recursive: true });
|
|
119
|
+
fs.mkdirSync(getRuntimeDir(), { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Print a summary of resolved paths (for debugging / doctor).
|
|
124
|
+
*/
|
|
125
|
+
export function printPathSummary() {
|
|
126
|
+
console.log(`Mode: ${_isDev ? 'development (repo)' : 'installed'}`);
|
|
127
|
+
console.log(`Home: ${_home}`);
|
|
128
|
+
console.log(`Package: ${PACKAGE_ROOT}`);
|
|
129
|
+
console.log(`Data dir: ${getDataDir()}`);
|
|
130
|
+
console.log(`Database: ${getDbPath()}`);
|
|
131
|
+
console.log(`Runtime dir: ${getRuntimeDir()}`);
|
|
132
|
+
console.log(`Env file: ${getEnvPath()}`);
|
|
133
|
+
console.log(`Public dir: ${getPublicDir()}`);
|
|
134
|
+
console.log(`PID file: ${getPidFile()}`);
|
|
135
|
+
console.log(`Log file: ${getLogFile()}`);
|
|
136
|
+
}
|