claude-queue 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/dist/cli.js ADDED
@@ -0,0 +1,558 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { program } from "commander";
5
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
6
+ import { fileURLToPath } from "url";
7
+ import { dirname, join as join5 } from "path";
8
+
9
+ // src/constants.ts
10
+ import { join } from "path";
11
+ import { homedir } from "os";
12
+ var DEFAULT_PORT = 3333;
13
+ var KANBAN_DIR = join(homedir(), ".claude-queue");
14
+ var PID_FILE = join(KANBAN_DIR, "server.pid");
15
+ var LOG_FILE = join(KANBAN_DIR, "server.log");
16
+ var CLAUDE_DIR = join(homedir(), ".claude");
17
+ var SKILLS_DIR = join(CLAUDE_DIR, "skills");
18
+
19
+ // src/server.ts
20
+ import { spawn } from "child_process";
21
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
22
+ import { join as join2 } from "path";
23
+ function ensureKanbanDir() {
24
+ if (!existsSync(KANBAN_DIR)) {
25
+ mkdirSync(KANBAN_DIR, { recursive: true });
26
+ }
27
+ }
28
+ async function isServerRunning(port) {
29
+ try {
30
+ const response = await fetch(`http://localhost:${port}/api/health`);
31
+ return response.ok;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+ function getRunningPid() {
37
+ if (!existsSync(PID_FILE)) {
38
+ return null;
39
+ }
40
+ try {
41
+ const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim());
42
+ try {
43
+ process.kill(pid, 0);
44
+ return pid;
45
+ } catch {
46
+ return null;
47
+ }
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+ function savePid(pid) {
53
+ ensureKanbanDir();
54
+ writeFileSync(PID_FILE, pid.toString());
55
+ }
56
+ async function clearPid() {
57
+ if (existsSync(PID_FILE)) {
58
+ const fs = await import("fs/promises");
59
+ await fs.unlink(PID_FILE).catch(() => {
60
+ });
61
+ }
62
+ }
63
+ function getServerPath() {
64
+ const npmServerPath = join2(import.meta.dirname, "server", "index.js");
65
+ if (existsSync(npmServerPath)) {
66
+ return npmServerPath;
67
+ }
68
+ const devServerPath = join2(import.meta.dirname, "..", "..", "server", "dist", "index.js");
69
+ if (existsSync(devServerPath)) {
70
+ return devServerPath;
71
+ }
72
+ throw new Error("Server not found. Run 'pnpm build' first.");
73
+ }
74
+ async function startServer(port, detach, verbose) {
75
+ const serverPath = getServerPath();
76
+ const env = {
77
+ ...process.env,
78
+ PORT: port.toString(),
79
+ NODE_ENV: "production"
80
+ };
81
+ if (detach) {
82
+ ensureKanbanDir();
83
+ const logStream = await import("fs").then(
84
+ (fs) => fs.createWriteStream(LOG_FILE, { flags: "a" })
85
+ );
86
+ const child2 = spawn("node", [serverPath], {
87
+ env,
88
+ detached: true,
89
+ stdio: ["ignore", logStream, logStream]
90
+ });
91
+ child2.unref();
92
+ if (child2.pid) {
93
+ savePid(child2.pid);
94
+ }
95
+ if (verbose) {
96
+ console.log(`Server started in background (PID: ${child2.pid})`);
97
+ }
98
+ return null;
99
+ }
100
+ const child = spawn("node", [serverPath], {
101
+ env,
102
+ stdio: verbose ? "inherit" : "pipe"
103
+ });
104
+ return child;
105
+ }
106
+ async function waitForServer(port, maxWait = 1e4) {
107
+ const start = Date.now();
108
+ while (Date.now() - start < maxWait) {
109
+ if (await isServerRunning(port)) {
110
+ return true;
111
+ }
112
+ await new Promise((r) => setTimeout(r, 200));
113
+ }
114
+ return false;
115
+ }
116
+
117
+ // src/project.ts
118
+ import { basename, resolve } from "path";
119
+ import { customAlphabet } from "nanoid";
120
+ var generateId = customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789", 4);
121
+ function generateProjectId() {
122
+ return `kbn-${generateId()}`;
123
+ }
124
+ async function registerProject(port, projectPath, verbose) {
125
+ const absolutePath = resolve(projectPath);
126
+ const projectName = basename(absolutePath);
127
+ const response = await fetch(`http://localhost:${port}/api/projects`);
128
+ const projects = await response.json();
129
+ const existing = projects.find((p) => p.path === absolutePath);
130
+ if (existing) {
131
+ if (verbose) {
132
+ console.log(`Project already registered: ${existing.id}`);
133
+ }
134
+ return existing.id;
135
+ }
136
+ const createResponse = await fetch(`http://localhost:${port}/api/projects`, {
137
+ method: "POST",
138
+ headers: { "Content-Type": "application/json" },
139
+ body: JSON.stringify({
140
+ id: generateProjectId(),
141
+ name: projectName,
142
+ path: absolutePath
143
+ })
144
+ });
145
+ if (!createResponse.ok) {
146
+ throw new Error(`Failed to register project: ${await createResponse.text()}`);
147
+ }
148
+ const project = await createResponse.json();
149
+ if (verbose) {
150
+ console.log(`Registered new project: ${project.id}`);
151
+ }
152
+ return project.id;
153
+ }
154
+
155
+ // src/skills.ts
156
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, rmSync } from "fs";
157
+ import { join as join3 } from "path";
158
+ async function configureMcp() {
159
+ const settingsPath = join3(CLAUDE_DIR, "settings.json");
160
+ if (!existsSync2(CLAUDE_DIR)) {
161
+ mkdirSync2(CLAUDE_DIR, { recursive: true });
162
+ }
163
+ let settings = {};
164
+ if (existsSync2(settingsPath)) {
165
+ try {
166
+ settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
167
+ } catch {
168
+ console.log("Warning: Could not parse existing settings.json");
169
+ }
170
+ }
171
+ const mcpServers = settings.mcpServers || {};
172
+ if (!mcpServers["claude-queue"]) {
173
+ mcpServers["claude-queue"] = {
174
+ command: "npx",
175
+ args: ["-y", "-p", "claude-queue", "claude-queue-mcp"],
176
+ env: {
177
+ KANBAN_SERVER_URL: `http://localhost:${DEFAULT_PORT}`
178
+ }
179
+ };
180
+ settings.mcpServers = mcpServers;
181
+ writeFileSync2(settingsPath, JSON.stringify(settings, null, 2));
182
+ console.log("\u2713 Configured MCP server in ~/.claude/settings.json");
183
+ }
184
+ }
185
+ function getSkillPath() {
186
+ const npmSkillPath = join3(import.meta.dirname, "skills", "queue", "SKILL.md");
187
+ if (existsSync2(npmSkillPath)) {
188
+ return npmSkillPath;
189
+ }
190
+ const devSkillPath = join3(import.meta.dirname, "..", "..", "skills", "queue", "SKILL.md");
191
+ if (existsSync2(devSkillPath)) {
192
+ return devSkillPath;
193
+ }
194
+ throw new Error("Skill file not found. Run 'pnpm build' first.");
195
+ }
196
+ function installSkills() {
197
+ const queueSkillDir = join3(SKILLS_DIR, "queue");
198
+ const queueSkillFile = join3(queueSkillDir, "SKILL.md");
199
+ if (existsSync2(queueSkillFile)) {
200
+ return;
201
+ }
202
+ if (!existsSync2(SKILLS_DIR)) {
203
+ mkdirSync2(SKILLS_DIR, { recursive: true });
204
+ }
205
+ if (!existsSync2(queueSkillDir)) {
206
+ mkdirSync2(queueSkillDir, { recursive: true });
207
+ }
208
+ const skillSourcePath = getSkillPath();
209
+ const skillContent = readFileSync2(skillSourcePath, "utf-8");
210
+ writeFileSync2(queueSkillFile, skillContent);
211
+ console.log("\u2713 Installed /queue skill to ~/.claude/skills/queue/");
212
+ }
213
+ function removeMcp() {
214
+ const settingsPath = join3(CLAUDE_DIR, "settings.json");
215
+ if (!existsSync2(settingsPath)) {
216
+ return false;
217
+ }
218
+ let settings = {};
219
+ try {
220
+ settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
221
+ } catch {
222
+ return false;
223
+ }
224
+ const mcpServers = settings.mcpServers || {};
225
+ if (!mcpServers["claude-queue"]) {
226
+ return false;
227
+ }
228
+ delete mcpServers["claude-queue"];
229
+ settings.mcpServers = mcpServers;
230
+ writeFileSync2(settingsPath, JSON.stringify(settings, null, 2));
231
+ return true;
232
+ }
233
+ function removeSkills() {
234
+ const queueSkillDir = join3(SKILLS_DIR, "queue");
235
+ if (!existsSync2(queueSkillDir)) {
236
+ return false;
237
+ }
238
+ rmSync(queueSkillDir, { recursive: true });
239
+ return true;
240
+ }
241
+
242
+ // src/doctor.ts
243
+ import { existsSync as existsSync3, readFileSync as readFileSync3, mkdirSync as mkdirSync3 } from "fs";
244
+ import { join as join4 } from "path";
245
+ async function runDoctor(port, fix) {
246
+ console.log("\n\u{1FA7A} Claude Queue Doctor\n");
247
+ console.log("Checking your setup...\n");
248
+ let issues = 0;
249
+ let warnings = 0;
250
+ const claudeDirExists = existsSync3(CLAUDE_DIR);
251
+ if (claudeDirExists) {
252
+ console.log("\u2705 Claude directory exists (~/.claude)");
253
+ } else {
254
+ console.log("\u274C Claude directory not found (~/.claude)");
255
+ issues++;
256
+ if (fix) {
257
+ mkdirSync3(CLAUDE_DIR, { recursive: true });
258
+ console.log(" \u2192 Created ~/.claude");
259
+ }
260
+ }
261
+ const settingsPath = join4(CLAUDE_DIR, "settings.json");
262
+ let mcpConfigured = false;
263
+ if (existsSync3(settingsPath)) {
264
+ try {
265
+ const settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
266
+ const mcpServers = settings.mcpServers || {};
267
+ mcpConfigured = !!mcpServers["claude-queue"];
268
+ if (mcpConfigured) {
269
+ console.log("\u2705 MCP server configured in settings.json");
270
+ } else {
271
+ console.log("\u274C MCP server not configured");
272
+ issues++;
273
+ if (fix) {
274
+ await configureMcp();
275
+ console.log(" \u2192 Configured MCP server");
276
+ }
277
+ }
278
+ } catch {
279
+ console.log("\u26A0\uFE0F Could not parse settings.json");
280
+ warnings++;
281
+ }
282
+ } else {
283
+ console.log("\u274C settings.json not found");
284
+ issues++;
285
+ if (fix) {
286
+ await configureMcp();
287
+ console.log(" \u2192 Created settings.json with MCP config");
288
+ }
289
+ }
290
+ const skillPath = join4(SKILLS_DIR, "kanban", "SKILL.md");
291
+ if (existsSync3(skillPath)) {
292
+ console.log("\u2705 /queue skill installed");
293
+ } else {
294
+ console.log("\u274C /queue skill not installed");
295
+ issues++;
296
+ if (fix) {
297
+ installSkills();
298
+ console.log(" \u2192 Installed /queue skill");
299
+ }
300
+ }
301
+ if (existsSync3(KANBAN_DIR)) {
302
+ console.log("\u2705 Queue data directory exists (~/.claude-queue)");
303
+ } else {
304
+ console.log("\u26A0\uFE0F Queue data directory not created yet");
305
+ warnings++;
306
+ }
307
+ const dbPath = join4(KANBAN_DIR, "kanban.db");
308
+ if (existsSync3(dbPath)) {
309
+ const stats = await import("fs/promises").then((fs) => fs.stat(dbPath));
310
+ const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
311
+ console.log(`\u2705 Database exists (${sizeMB} MB)`);
312
+ } else {
313
+ console.log("\u26A0\uFE0F Database not created yet (will be created on first run)");
314
+ warnings++;
315
+ }
316
+ const serverRunning = await isServerRunning(port);
317
+ if (serverRunning) {
318
+ console.log(`\u2705 Server running on port ${port}`);
319
+ try {
320
+ const response = await fetch(`http://localhost:${port}/api/maintenance/stats`);
321
+ if (response.ok) {
322
+ const stats = await response.json();
323
+ console.log(` \u2514\u2500\u2500 ${stats.projects} projects, ${stats.tasks.total} tasks`);
324
+ }
325
+ } catch {
326
+ }
327
+ } else {
328
+ console.log(`\u26A0\uFE0F Server not running on port ${port}`);
329
+ warnings++;
330
+ }
331
+ const pid = getRunningPid();
332
+ if (pid) {
333
+ console.log(`\u2705 PID file valid (${pid})`);
334
+ } else if (existsSync3(join4(KANBAN_DIR, "server.pid"))) {
335
+ console.log("\u26A0\uFE0F Stale PID file found");
336
+ warnings++;
337
+ if (fix) {
338
+ await clearPid();
339
+ console.log(" \u2192 Removed stale PID file");
340
+ }
341
+ }
342
+ console.log("\n" + "\u2500".repeat(40));
343
+ if (issues === 0 && warnings === 0) {
344
+ console.log("\u2705 All checks passed! Your setup looks good.\n");
345
+ } else if (issues === 0) {
346
+ console.log(`\u26A0\uFE0F ${warnings} warning(s), but no critical issues.
347
+ `);
348
+ } else {
349
+ console.log(`\u274C ${issues} issue(s) found, ${warnings} warning(s).
350
+ `);
351
+ if (!fix) {
352
+ console.log("Run with --fix to attempt automatic fixes:");
353
+ console.log(" npx claude-queue doctor --fix\n");
354
+ }
355
+ }
356
+ console.log("Quick reference:");
357
+ console.log(" Start server: npx claude-queue");
358
+ console.log(" View board: http://localhost:" + port);
359
+ console.log(" Run skill: /queue <project-id>");
360
+ console.log(" View logs: npx claude-queue logs -f");
361
+ console.log("");
362
+ }
363
+
364
+ // src/index.ts
365
+ var __dirname = dirname(fileURLToPath(import.meta.url));
366
+ var pkg = JSON.parse(readFileSync4(join5(__dirname, "..", "package.json"), "utf-8"));
367
+ program.name("claude-queue").description("Local queue board for managing Claude Code projects").version(pkg.version);
368
+ program.command("start", { isDefault: true }).description("Start the queue server and register current directory as a project").option("-p, --port <port>", "Server port", DEFAULT_PORT.toString()).option("-d, --detach", "Run server in background").option("-v, --verbose", "Verbose output").action(async (options) => {
369
+ const port = parseInt(options.port);
370
+ const detach = options.detach || false;
371
+ const verbose = options.verbose || false;
372
+ ensureKanbanDir();
373
+ await configureMcp();
374
+ installSkills();
375
+ const running = await isServerRunning(port);
376
+ if (!running) {
377
+ if (verbose) {
378
+ console.log("Starting queue server...");
379
+ }
380
+ const child = await startServer(port, detach, verbose);
381
+ const serverReady = await waitForServer(port);
382
+ if (!serverReady) {
383
+ console.error("Server failed to start");
384
+ process.exit(1);
385
+ }
386
+ if (child && !detach) {
387
+ const projectId = await registerProject(port, process.cwd(), verbose);
388
+ console.log(`Kanban board: http://localhost:${port}?project=${projectId}`);
389
+ console.log("Press Ctrl+C to stop");
390
+ process.on("SIGINT", () => {
391
+ child.kill();
392
+ process.exit(0);
393
+ });
394
+ await new Promise(() => {
395
+ });
396
+ }
397
+ } else {
398
+ if (verbose) {
399
+ console.log("Server already running");
400
+ }
401
+ }
402
+ if (running || detach) {
403
+ const projectId = await registerProject(port, process.cwd(), verbose);
404
+ console.log(`Kanban board: http://localhost:${port}?project=${projectId}`);
405
+ }
406
+ });
407
+ program.command("list").description("List all registered projects").option("-p, --port <port>", "Server port", DEFAULT_PORT.toString()).action(async (options) => {
408
+ const port = parseInt(options.port);
409
+ if (!await isServerRunning(port)) {
410
+ console.error("Server is not running. Start it with: claude-queue");
411
+ process.exit(1);
412
+ }
413
+ const response = await fetch(`http://localhost:${port}/api/projects`);
414
+ const projects = await response.json();
415
+ if (projects.length === 0) {
416
+ console.log("No projects registered");
417
+ return;
418
+ }
419
+ console.log("Registered projects:");
420
+ for (const project of projects) {
421
+ console.log(` ${project.id} ${project.name}`);
422
+ console.log(` ${project.path}`);
423
+ }
424
+ });
425
+ program.command("delete <projectId>").description("Delete a project from the queue").option("-p, --port <port>", "Server port", DEFAULT_PORT.toString()).action(async (projectId, options) => {
426
+ const port = parseInt(options.port);
427
+ if (!await isServerRunning(port)) {
428
+ console.error("Server is not running. Start it with: claude-queue");
429
+ process.exit(1);
430
+ }
431
+ const response = await fetch(`http://localhost:${port}/api/projects/${projectId}`, {
432
+ method: "DELETE"
433
+ });
434
+ if (!response.ok) {
435
+ console.error(`Failed to delete project: ${await response.text()}`);
436
+ process.exit(1);
437
+ }
438
+ console.log(`Deleted project: ${projectId}`);
439
+ });
440
+ program.command("status").description("Check server status").option("-p, --port <port>", "Server port", DEFAULT_PORT.toString()).action(async (options) => {
441
+ const port = parseInt(options.port);
442
+ const running = await isServerRunning(port);
443
+ const pid = getRunningPid();
444
+ if (running) {
445
+ console.log(`Server is running on port ${port}`);
446
+ if (pid) {
447
+ console.log(`PID: ${pid}`);
448
+ }
449
+ } else {
450
+ console.log("Server is not running");
451
+ }
452
+ });
453
+ program.command("stop").description("Stop the background server").action(async () => {
454
+ const pid = getRunningPid();
455
+ if (!pid) {
456
+ console.log("No background server running");
457
+ return;
458
+ }
459
+ try {
460
+ process.kill(pid, "SIGTERM");
461
+ console.log(`Stopped server (PID: ${pid})`);
462
+ clearPid();
463
+ } catch (error) {
464
+ console.error(`Failed to stop server: ${error}`);
465
+ }
466
+ });
467
+ program.command("logs").description("View server logs").option("-f, --follow", "Follow log output").option("-n, --lines <lines>", "Number of lines to show", "50").action(async (options) => {
468
+ if (!existsSync4(LOG_FILE)) {
469
+ console.log("No logs found");
470
+ return;
471
+ }
472
+ if (options.follow) {
473
+ const { spawn: spawn2 } = await import("child_process");
474
+ const tail = spawn2("tail", ["-f", LOG_FILE], { stdio: "inherit" });
475
+ process.on("SIGINT", () => {
476
+ tail.kill();
477
+ process.exit(0);
478
+ });
479
+ } else {
480
+ const { spawn: spawn2 } = await import("child_process");
481
+ spawn2("tail", ["-n", options.lines, LOG_FILE], { stdio: "inherit" });
482
+ }
483
+ });
484
+ program.command("clean").description("Clean up log files and temporary data").option("--logs", "Remove log files only").option("--all", "Remove all data (logs, database, PID file)").action(async (options) => {
485
+ const fs = await import("fs/promises");
486
+ const { join: join6 } = await import("path");
487
+ const removed = [];
488
+ if (options.logs || !options.all) {
489
+ if (existsSync4(LOG_FILE)) {
490
+ await fs.unlink(LOG_FILE);
491
+ removed.push("server.log");
492
+ }
493
+ }
494
+ if (options.all) {
495
+ const dbFile = join6(KANBAN_DIR, "queue.db");
496
+ if (existsSync4(LOG_FILE)) {
497
+ await fs.unlink(LOG_FILE);
498
+ removed.push("server.log");
499
+ }
500
+ if (existsSync4(PID_FILE)) {
501
+ await fs.unlink(PID_FILE);
502
+ removed.push("server.pid");
503
+ }
504
+ if (existsSync4(dbFile)) {
505
+ await fs.unlink(dbFile);
506
+ removed.push("queue.db");
507
+ }
508
+ }
509
+ if (removed.length === 0) {
510
+ console.log("No files to clean");
511
+ } else {
512
+ console.log(`Cleaned: ${removed.join(", ")}`);
513
+ }
514
+ });
515
+ program.command("doctor").description("Check system configuration and diagnose common issues").option("-p, --port <port>", "Server port", DEFAULT_PORT.toString()).option("--fix", "Attempt to fix issues automatically").action(async (options) => {
516
+ const port = parseInt(options.port);
517
+ const fix = options.fix || false;
518
+ await runDoctor(port, fix);
519
+ });
520
+ program.command("uninstall").description("Remove MCP server and skill from Claude Code configuration").option("--all", "Also remove all data (database, logs)").action(async (options) => {
521
+ const fs = await import("fs/promises");
522
+ const { join: join6 } = await import("path");
523
+ const removed = [];
524
+ const mcpRemoved = removeMcp();
525
+ if (mcpRemoved) {
526
+ removed.push("MCP server config");
527
+ }
528
+ const skillsRemoved = removeSkills();
529
+ if (skillsRemoved) {
530
+ removed.push("/queue skill");
531
+ }
532
+ if (options.all) {
533
+ const dbFile = join6(KANBAN_DIR, "queue.db");
534
+ if (existsSync4(LOG_FILE)) {
535
+ await fs.unlink(LOG_FILE);
536
+ removed.push("server.log");
537
+ }
538
+ if (existsSync4(PID_FILE)) {
539
+ await fs.unlink(PID_FILE);
540
+ removed.push("server.pid");
541
+ }
542
+ if (existsSync4(dbFile)) {
543
+ await fs.unlink(dbFile);
544
+ removed.push("queue.db");
545
+ }
546
+ if (existsSync4(KANBAN_DIR)) {
547
+ await fs.rmdir(KANBAN_DIR).catch(() => {
548
+ });
549
+ }
550
+ }
551
+ if (removed.length === 0) {
552
+ console.log("Nothing to uninstall");
553
+ } else {
554
+ console.log(`\u2713 Removed: ${removed.join(", ")}`);
555
+ console.log("\nRestart Claude Code to complete the uninstall.");
556
+ }
557
+ });
558
+ program.parse();
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node