@siteboon/claude-code-ui 1.8.2

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.
Files changed (106) hide show
  1. package/.env.example +12 -0
  2. package/.nvmrc +1 -0
  3. package/LICENSE +675 -0
  4. package/README.md +275 -0
  5. package/index.html +48 -0
  6. package/package.json +84 -0
  7. package/postcss.config.js +6 -0
  8. package/public/convert-icons.md +53 -0
  9. package/public/favicon.png +0 -0
  10. package/public/favicon.svg +9 -0
  11. package/public/generate-icons.js +49 -0
  12. package/public/icons/claude-ai-icon.svg +1 -0
  13. package/public/icons/cursor.svg +1 -0
  14. package/public/icons/generate-icons.md +19 -0
  15. package/public/icons/icon-128x128.png +0 -0
  16. package/public/icons/icon-128x128.svg +12 -0
  17. package/public/icons/icon-144x144.png +0 -0
  18. package/public/icons/icon-144x144.svg +12 -0
  19. package/public/icons/icon-152x152.png +0 -0
  20. package/public/icons/icon-152x152.svg +12 -0
  21. package/public/icons/icon-192x192.png +0 -0
  22. package/public/icons/icon-192x192.svg +12 -0
  23. package/public/icons/icon-384x384.png +0 -0
  24. package/public/icons/icon-384x384.svg +12 -0
  25. package/public/icons/icon-512x512.png +0 -0
  26. package/public/icons/icon-512x512.svg +12 -0
  27. package/public/icons/icon-72x72.png +0 -0
  28. package/public/icons/icon-72x72.svg +12 -0
  29. package/public/icons/icon-96x96.png +0 -0
  30. package/public/icons/icon-96x96.svg +12 -0
  31. package/public/icons/icon-template.svg +12 -0
  32. package/public/logo.svg +9 -0
  33. package/public/manifest.json +61 -0
  34. package/public/screenshots/cli-selection.png +0 -0
  35. package/public/screenshots/desktop-main.png +0 -0
  36. package/public/screenshots/mobile-chat.png +0 -0
  37. package/public/screenshots/tools-modal.png +0 -0
  38. package/public/sw.js +49 -0
  39. package/server/claude-cli.js +391 -0
  40. package/server/cursor-cli.js +250 -0
  41. package/server/database/db.js +86 -0
  42. package/server/database/init.sql +16 -0
  43. package/server/index.js +1167 -0
  44. package/server/middleware/auth.js +80 -0
  45. package/server/projects.js +1063 -0
  46. package/server/routes/auth.js +135 -0
  47. package/server/routes/cursor.js +794 -0
  48. package/server/routes/git.js +823 -0
  49. package/server/routes/mcp-utils.js +48 -0
  50. package/server/routes/mcp.js +552 -0
  51. package/server/routes/taskmaster.js +1971 -0
  52. package/server/utils/mcp-detector.js +198 -0
  53. package/server/utils/taskmaster-websocket.js +129 -0
  54. package/src/App.jsx +751 -0
  55. package/src/components/ChatInterface.jsx +3485 -0
  56. package/src/components/ClaudeLogo.jsx +11 -0
  57. package/src/components/ClaudeStatus.jsx +107 -0
  58. package/src/components/CodeEditor.jsx +422 -0
  59. package/src/components/CreateTaskModal.jsx +88 -0
  60. package/src/components/CursorLogo.jsx +9 -0
  61. package/src/components/DarkModeToggle.jsx +35 -0
  62. package/src/components/DiffViewer.jsx +41 -0
  63. package/src/components/ErrorBoundary.jsx +73 -0
  64. package/src/components/FileTree.jsx +480 -0
  65. package/src/components/GitPanel.jsx +1283 -0
  66. package/src/components/ImageViewer.jsx +54 -0
  67. package/src/components/LoginForm.jsx +110 -0
  68. package/src/components/MainContent.jsx +577 -0
  69. package/src/components/MicButton.jsx +272 -0
  70. package/src/components/MobileNav.jsx +88 -0
  71. package/src/components/NextTaskBanner.jsx +695 -0
  72. package/src/components/PRDEditor.jsx +871 -0
  73. package/src/components/ProtectedRoute.jsx +44 -0
  74. package/src/components/QuickSettingsPanel.jsx +262 -0
  75. package/src/components/Settings.jsx +2023 -0
  76. package/src/components/SetupForm.jsx +135 -0
  77. package/src/components/Shell.jsx +663 -0
  78. package/src/components/Sidebar.jsx +1665 -0
  79. package/src/components/StandaloneShell.jsx +106 -0
  80. package/src/components/TaskCard.jsx +210 -0
  81. package/src/components/TaskDetail.jsx +406 -0
  82. package/src/components/TaskIndicator.jsx +108 -0
  83. package/src/components/TaskList.jsx +1054 -0
  84. package/src/components/TaskMasterSetupWizard.jsx +603 -0
  85. package/src/components/TaskMasterStatus.jsx +86 -0
  86. package/src/components/TodoList.jsx +91 -0
  87. package/src/components/Tooltip.jsx +91 -0
  88. package/src/components/ui/badge.jsx +31 -0
  89. package/src/components/ui/button.jsx +46 -0
  90. package/src/components/ui/input.jsx +19 -0
  91. package/src/components/ui/scroll-area.jsx +23 -0
  92. package/src/contexts/AuthContext.jsx +158 -0
  93. package/src/contexts/TaskMasterContext.jsx +324 -0
  94. package/src/contexts/TasksSettingsContext.jsx +95 -0
  95. package/src/contexts/ThemeContext.jsx +94 -0
  96. package/src/contexts/WebSocketContext.jsx +29 -0
  97. package/src/hooks/useAudioRecorder.js +109 -0
  98. package/src/hooks/useVersionCheck.js +39 -0
  99. package/src/index.css +822 -0
  100. package/src/lib/utils.js +6 -0
  101. package/src/main.jsx +10 -0
  102. package/src/utils/api.js +141 -0
  103. package/src/utils/websocket.js +109 -0
  104. package/src/utils/whisper.js +37 -0
  105. package/tailwind.config.js +63 -0
  106. package/vite.config.js +29 -0
@@ -0,0 +1,1971 @@
1
+ /**
2
+ * TASKMASTER API ROUTES
3
+ * ====================
4
+ *
5
+ * This module provides API endpoints for TaskMaster integration including:
6
+ * - .taskmaster folder detection in project directories
7
+ * - MCP server configuration detection
8
+ * - TaskMaster state and metadata management
9
+ */
10
+
11
+ import express from 'express';
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import { promises as fsPromises } from 'fs';
15
+ import { spawn } from 'child_process';
16
+ import { fileURLToPath } from 'url';
17
+ import { dirname } from 'path';
18
+ import os from 'os';
19
+ import { extractProjectDirectory } from '../projects.js';
20
+ import { detectTaskMasterMCPServer } from '../utils/mcp-detector.js';
21
+ import { broadcastTaskMasterProjectUpdate, broadcastTaskMasterTasksUpdate } from '../utils/taskmaster-websocket.js';
22
+
23
+ const __filename = fileURLToPath(import.meta.url);
24
+ const __dirname = dirname(__filename);
25
+
26
+ const router = express.Router();
27
+
28
+ /**
29
+ * Check if TaskMaster CLI is installed globally
30
+ * @returns {Promise<Object>} Installation status result
31
+ */
32
+ async function checkTaskMasterInstallation() {
33
+ return new Promise((resolve) => {
34
+ // Check if task-master command is available
35
+ const child = spawn('which', ['task-master'], {
36
+ stdio: ['ignore', 'pipe', 'pipe'],
37
+ shell: true
38
+ });
39
+
40
+ let output = '';
41
+ let errorOutput = '';
42
+
43
+ child.stdout.on('data', (data) => {
44
+ output += data.toString();
45
+ });
46
+
47
+ child.stderr.on('data', (data) => {
48
+ errorOutput += data.toString();
49
+ });
50
+
51
+ child.on('close', (code) => {
52
+ if (code === 0 && output.trim()) {
53
+ // TaskMaster is installed, get version
54
+ const versionChild = spawn('task-master', ['--version'], {
55
+ stdio: ['ignore', 'pipe', 'pipe'],
56
+ shell: true
57
+ });
58
+
59
+ let versionOutput = '';
60
+
61
+ versionChild.stdout.on('data', (data) => {
62
+ versionOutput += data.toString();
63
+ });
64
+
65
+ versionChild.on('close', (versionCode) => {
66
+ resolve({
67
+ isInstalled: true,
68
+ installPath: output.trim(),
69
+ version: versionCode === 0 ? versionOutput.trim() : 'unknown',
70
+ reason: null
71
+ });
72
+ });
73
+
74
+ versionChild.on('error', () => {
75
+ resolve({
76
+ isInstalled: true,
77
+ installPath: output.trim(),
78
+ version: 'unknown',
79
+ reason: null
80
+ });
81
+ });
82
+ } else {
83
+ resolve({
84
+ isInstalled: false,
85
+ installPath: null,
86
+ version: null,
87
+ reason: 'TaskMaster CLI not found in PATH'
88
+ });
89
+ }
90
+ });
91
+
92
+ child.on('error', (error) => {
93
+ resolve({
94
+ isInstalled: false,
95
+ installPath: null,
96
+ version: null,
97
+ reason: `Error checking installation: ${error.message}`
98
+ });
99
+ });
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Detect .taskmaster folder presence in a given project directory
105
+ * @param {string} projectPath - Absolute path to project directory
106
+ * @returns {Promise<Object>} Detection result with status and metadata
107
+ */
108
+ async function detectTaskMasterFolder(projectPath) {
109
+ try {
110
+ const taskMasterPath = path.join(projectPath, '.taskmaster');
111
+
112
+ // Check if .taskmaster directory exists
113
+ try {
114
+ const stats = await fsPromises.stat(taskMasterPath);
115
+ if (!stats.isDirectory()) {
116
+ return {
117
+ hasTaskmaster: false,
118
+ reason: '.taskmaster exists but is not a directory'
119
+ };
120
+ }
121
+ } catch (error) {
122
+ if (error.code === 'ENOENT') {
123
+ return {
124
+ hasTaskmaster: false,
125
+ reason: '.taskmaster directory not found'
126
+ };
127
+ }
128
+ throw error;
129
+ }
130
+
131
+ // Check for key TaskMaster files
132
+ const keyFiles = [
133
+ 'tasks/tasks.json',
134
+ 'config.json'
135
+ ];
136
+
137
+ const fileStatus = {};
138
+ let hasEssentialFiles = true;
139
+
140
+ for (const file of keyFiles) {
141
+ const filePath = path.join(taskMasterPath, file);
142
+ try {
143
+ await fsPromises.access(filePath, fs.constants.R_OK);
144
+ fileStatus[file] = true;
145
+ } catch (error) {
146
+ fileStatus[file] = false;
147
+ if (file === 'tasks/tasks.json') {
148
+ hasEssentialFiles = false;
149
+ }
150
+ }
151
+ }
152
+
153
+ // Parse tasks.json if it exists for metadata
154
+ let taskMetadata = null;
155
+ if (fileStatus['tasks/tasks.json']) {
156
+ try {
157
+ const tasksPath = path.join(taskMasterPath, 'tasks/tasks.json');
158
+ const tasksContent = await fsPromises.readFile(tasksPath, 'utf8');
159
+ const tasksData = JSON.parse(tasksContent);
160
+
161
+ // Handle both tagged and legacy formats
162
+ let tasks = [];
163
+ if (tasksData.tasks) {
164
+ // Legacy format
165
+ tasks = tasksData.tasks;
166
+ } else {
167
+ // Tagged format - get tasks from all tags
168
+ Object.values(tasksData).forEach(tagData => {
169
+ if (tagData.tasks) {
170
+ tasks = tasks.concat(tagData.tasks);
171
+ }
172
+ });
173
+ }
174
+
175
+ // Calculate task statistics
176
+ const stats = tasks.reduce((acc, task) => {
177
+ acc.total++;
178
+ acc[task.status] = (acc[task.status] || 0) + 1;
179
+
180
+ // Count subtasks
181
+ if (task.subtasks) {
182
+ task.subtasks.forEach(subtask => {
183
+ acc.subtotalTasks++;
184
+ acc.subtasks = acc.subtasks || {};
185
+ acc.subtasks[subtask.status] = (acc.subtasks[subtask.status] || 0) + 1;
186
+ });
187
+ }
188
+
189
+ return acc;
190
+ }, {
191
+ total: 0,
192
+ subtotalTasks: 0,
193
+ pending: 0,
194
+ 'in-progress': 0,
195
+ done: 0,
196
+ review: 0,
197
+ deferred: 0,
198
+ cancelled: 0,
199
+ subtasks: {}
200
+ });
201
+
202
+ taskMetadata = {
203
+ taskCount: stats.total,
204
+ subtaskCount: stats.subtotalTasks,
205
+ completed: stats.done || 0,
206
+ pending: stats.pending || 0,
207
+ inProgress: stats['in-progress'] || 0,
208
+ review: stats.review || 0,
209
+ completionPercentage: stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0,
210
+ lastModified: (await fsPromises.stat(tasksPath)).mtime.toISOString()
211
+ };
212
+ } catch (parseError) {
213
+ console.warn('Failed to parse tasks.json:', parseError.message);
214
+ taskMetadata = { error: 'Failed to parse tasks.json' };
215
+ }
216
+ }
217
+
218
+ return {
219
+ hasTaskmaster: true,
220
+ hasEssentialFiles,
221
+ files: fileStatus,
222
+ metadata: taskMetadata,
223
+ path: taskMasterPath
224
+ };
225
+
226
+ } catch (error) {
227
+ console.error('Error detecting TaskMaster folder:', error);
228
+ return {
229
+ hasTaskmaster: false,
230
+ reason: `Error checking directory: ${error.message}`
231
+ };
232
+ }
233
+ }
234
+
235
+ // MCP detection is now handled by the centralized utility
236
+
237
+ // API Routes
238
+
239
+ /**
240
+ * GET /api/taskmaster/installation-status
241
+ * Check if TaskMaster CLI is installed on the system
242
+ */
243
+ router.get('/installation-status', async (req, res) => {
244
+ try {
245
+ const installationStatus = await checkTaskMasterInstallation();
246
+
247
+ // Also check for MCP server configuration
248
+ const mcpStatus = await detectTaskMasterMCPServer();
249
+
250
+ res.json({
251
+ success: true,
252
+ installation: installationStatus,
253
+ mcpServer: mcpStatus,
254
+ isReady: installationStatus.isInstalled && mcpStatus.hasMCPServer
255
+ });
256
+ } catch (error) {
257
+ console.error('Error checking TaskMaster installation:', error);
258
+ res.status(500).json({
259
+ success: false,
260
+ error: 'Failed to check TaskMaster installation status',
261
+ installation: {
262
+ isInstalled: false,
263
+ reason: `Server error: ${error.message}`
264
+ },
265
+ mcpServer: {
266
+ hasMCPServer: false,
267
+ reason: `Server error: ${error.message}`
268
+ },
269
+ isReady: false
270
+ });
271
+ }
272
+ });
273
+
274
+ /**
275
+ * GET /api/taskmaster/detect/:projectName
276
+ * Detect TaskMaster configuration for a specific project
277
+ */
278
+ router.get('/detect/:projectName', async (req, res) => {
279
+ try {
280
+ const { projectName } = req.params;
281
+
282
+ // Use the existing extractProjectDirectory function to get actual project path
283
+ let projectPath;
284
+ try {
285
+ projectPath = await extractProjectDirectory(projectName);
286
+ } catch (error) {
287
+ console.error('Error extracting project directory:', error);
288
+ return res.status(404).json({
289
+ error: 'Project path not found',
290
+ projectName,
291
+ message: error.message
292
+ });
293
+ }
294
+
295
+ // Verify the project path exists
296
+ try {
297
+ await fsPromises.access(projectPath, fs.constants.R_OK);
298
+ } catch (error) {
299
+ return res.status(404).json({
300
+ error: 'Project path not accessible',
301
+ projectPath,
302
+ projectName,
303
+ message: error.message
304
+ });
305
+ }
306
+
307
+ // Run detection in parallel
308
+ const [taskMasterResult, mcpResult] = await Promise.all([
309
+ detectTaskMasterFolder(projectPath),
310
+ detectTaskMasterMCPServer()
311
+ ]);
312
+
313
+ // Determine overall status
314
+ let status = 'not-configured';
315
+ if (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles) {
316
+ if (mcpResult.hasMCPServer && mcpResult.isConfigured) {
317
+ status = 'fully-configured';
318
+ } else {
319
+ status = 'taskmaster-only';
320
+ }
321
+ } else if (mcpResult.hasMCPServer && mcpResult.isConfigured) {
322
+ status = 'mcp-only';
323
+ }
324
+
325
+ const responseData = {
326
+ projectName,
327
+ projectPath,
328
+ status,
329
+ taskmaster: taskMasterResult,
330
+ mcp: mcpResult,
331
+ timestamp: new Date().toISOString()
332
+ };
333
+
334
+ // Broadcast TaskMaster project update via WebSocket
335
+ if (req.app.locals.wss) {
336
+ broadcastTaskMasterProjectUpdate(
337
+ req.app.locals.wss,
338
+ projectName,
339
+ taskMasterResult
340
+ );
341
+ }
342
+
343
+ res.json(responseData);
344
+
345
+ } catch (error) {
346
+ console.error('TaskMaster detection error:', error);
347
+ res.status(500).json({
348
+ error: 'Failed to detect TaskMaster configuration',
349
+ message: error.message
350
+ });
351
+ }
352
+ });
353
+
354
+ /**
355
+ * GET /api/taskmaster/detect-all
356
+ * Detect TaskMaster configuration for all known projects
357
+ * This endpoint works with the existing projects system
358
+ */
359
+ router.get('/detect-all', async (req, res) => {
360
+ try {
361
+ // Import getProjects from the projects module
362
+ const { getProjects } = await import('../projects.js');
363
+ const projects = await getProjects();
364
+
365
+ // Run detection for all projects in parallel
366
+ const detectionPromises = projects.map(async (project) => {
367
+ try {
368
+ // Use the project's fullPath if available, otherwise extract the directory
369
+ let projectPath;
370
+ if (project.fullPath) {
371
+ projectPath = project.fullPath;
372
+ } else {
373
+ try {
374
+ projectPath = await extractProjectDirectory(project.name);
375
+ } catch (error) {
376
+ throw new Error(`Failed to extract project directory: ${error.message}`);
377
+ }
378
+ }
379
+
380
+ const [taskMasterResult, mcpResult] = await Promise.all([
381
+ detectTaskMasterFolder(projectPath),
382
+ detectTaskMasterMCPServer()
383
+ ]);
384
+
385
+ // Determine status
386
+ let status = 'not-configured';
387
+ if (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles) {
388
+ if (mcpResult.hasMCPServer && mcpResult.isConfigured) {
389
+ status = 'fully-configured';
390
+ } else {
391
+ status = 'taskmaster-only';
392
+ }
393
+ } else if (mcpResult.hasMCPServer && mcpResult.isConfigured) {
394
+ status = 'mcp-only';
395
+ }
396
+
397
+ return {
398
+ projectName: project.name,
399
+ displayName: project.displayName,
400
+ projectPath,
401
+ status,
402
+ taskmaster: taskMasterResult,
403
+ mcp: mcpResult
404
+ };
405
+ } catch (error) {
406
+ return {
407
+ projectName: project.name,
408
+ displayName: project.displayName,
409
+ status: 'error',
410
+ error: error.message
411
+ };
412
+ }
413
+ });
414
+
415
+ const results = await Promise.all(detectionPromises);
416
+
417
+ res.json({
418
+ projects: results,
419
+ summary: {
420
+ total: results.length,
421
+ fullyConfigured: results.filter(p => p.status === 'fully-configured').length,
422
+ taskmasterOnly: results.filter(p => p.status === 'taskmaster-only').length,
423
+ mcpOnly: results.filter(p => p.status === 'mcp-only').length,
424
+ notConfigured: results.filter(p => p.status === 'not-configured').length,
425
+ errors: results.filter(p => p.status === 'error').length
426
+ },
427
+ timestamp: new Date().toISOString()
428
+ });
429
+
430
+ } catch (error) {
431
+ console.error('Bulk TaskMaster detection error:', error);
432
+ res.status(500).json({
433
+ error: 'Failed to detect TaskMaster configuration for projects',
434
+ message: error.message
435
+ });
436
+ }
437
+ });
438
+
439
+ /**
440
+ * POST /api/taskmaster/initialize/:projectName
441
+ * Initialize TaskMaster in a project (placeholder for future CLI integration)
442
+ */
443
+ router.post('/initialize/:projectName', async (req, res) => {
444
+ try {
445
+ const { projectName } = req.params;
446
+ const { rules } = req.body; // Optional rule profiles
447
+
448
+ // This will be implemented in a later subtask with CLI integration
449
+ res.status(501).json({
450
+ error: 'TaskMaster initialization not yet implemented',
451
+ message: 'This endpoint will execute task-master init via CLI in a future update',
452
+ projectName,
453
+ rules
454
+ });
455
+
456
+ } catch (error) {
457
+ console.error('TaskMaster initialization error:', error);
458
+ res.status(500).json({
459
+ error: 'Failed to initialize TaskMaster',
460
+ message: error.message
461
+ });
462
+ }
463
+ });
464
+
465
+ /**
466
+ * GET /api/taskmaster/next/:projectName
467
+ * Get the next recommended task using task-master CLI
468
+ */
469
+ router.get('/next/:projectName', async (req, res) => {
470
+ try {
471
+ const { projectName } = req.params;
472
+
473
+ // Get project path
474
+ let projectPath;
475
+ try {
476
+ projectPath = await extractProjectDirectory(projectName);
477
+ } catch (error) {
478
+ return res.status(404).json({
479
+ error: 'Project not found',
480
+ message: `Project "${projectName}" does not exist`
481
+ });
482
+ }
483
+
484
+ // Try to execute task-master next command
485
+ try {
486
+ const { spawn } = await import('child_process');
487
+
488
+ const nextTaskCommand = spawn('task-master', ['next'], {
489
+ cwd: projectPath,
490
+ stdio: ['pipe', 'pipe', 'pipe']
491
+ });
492
+
493
+ let stdout = '';
494
+ let stderr = '';
495
+
496
+ nextTaskCommand.stdout.on('data', (data) => {
497
+ stdout += data.toString();
498
+ });
499
+
500
+ nextTaskCommand.stderr.on('data', (data) => {
501
+ stderr += data.toString();
502
+ });
503
+
504
+ await new Promise((resolve, reject) => {
505
+ nextTaskCommand.on('close', (code) => {
506
+ if (code === 0) {
507
+ resolve();
508
+ } else {
509
+ reject(new Error(`task-master next failed with code ${code}: ${stderr}`));
510
+ }
511
+ });
512
+
513
+ nextTaskCommand.on('error', (error) => {
514
+ reject(error);
515
+ });
516
+ });
517
+
518
+ // Parse the output - task-master next usually returns JSON
519
+ let nextTaskData = null;
520
+ if (stdout.trim()) {
521
+ try {
522
+ nextTaskData = JSON.parse(stdout);
523
+ } catch (parseError) {
524
+ // If not JSON, treat as plain text
525
+ nextTaskData = { message: stdout.trim() };
526
+ }
527
+ }
528
+
529
+ res.json({
530
+ projectName,
531
+ projectPath,
532
+ nextTask: nextTaskData,
533
+ timestamp: new Date().toISOString()
534
+ });
535
+
536
+ } catch (cliError) {
537
+ console.warn('Failed to execute task-master CLI:', cliError.message);
538
+
539
+ // Fallback to loading tasks and finding next one locally
540
+ const tasksResponse = await fetch(`${req.protocol}://${req.get('host')}/api/taskmaster/tasks/${encodeURIComponent(projectName)}`, {
541
+ headers: {
542
+ 'Authorization': req.headers.authorization
543
+ }
544
+ });
545
+
546
+ if (tasksResponse.ok) {
547
+ const tasksData = await tasksResponse.json();
548
+ const nextTask = tasksData.tasks?.find(task =>
549
+ task.status === 'pending' || task.status === 'in-progress'
550
+ ) || null;
551
+
552
+ res.json({
553
+ projectName,
554
+ projectPath,
555
+ nextTask,
556
+ fallback: true,
557
+ message: 'Used fallback method (CLI not available)',
558
+ timestamp: new Date().toISOString()
559
+ });
560
+ } else {
561
+ throw new Error('Failed to load tasks via fallback method');
562
+ }
563
+ }
564
+
565
+ } catch (error) {
566
+ console.error('TaskMaster next task error:', error);
567
+ res.status(500).json({
568
+ error: 'Failed to get next task',
569
+ message: error.message
570
+ });
571
+ }
572
+ });
573
+
574
+ /**
575
+ * GET /api/taskmaster/tasks/:projectName
576
+ * Load actual tasks from .taskmaster/tasks/tasks.json
577
+ */
578
+ router.get('/tasks/:projectName', async (req, res) => {
579
+ try {
580
+ const { projectName } = req.params;
581
+
582
+ // Get project path
583
+ let projectPath;
584
+ try {
585
+ projectPath = await extractProjectDirectory(projectName);
586
+ } catch (error) {
587
+ return res.status(404).json({
588
+ error: 'Project not found',
589
+ message: `Project "${projectName}" does not exist`
590
+ });
591
+ }
592
+
593
+ const taskMasterPath = path.join(projectPath, '.taskmaster');
594
+ const tasksFilePath = path.join(taskMasterPath, 'tasks', 'tasks.json');
595
+
596
+ // Check if tasks file exists
597
+ try {
598
+ await fsPromises.access(tasksFilePath);
599
+ } catch (error) {
600
+ return res.json({
601
+ projectName,
602
+ tasks: [],
603
+ message: 'No tasks.json file found'
604
+ });
605
+ }
606
+
607
+ // Read and parse tasks file
608
+ try {
609
+ const tasksContent = await fsPromises.readFile(tasksFilePath, 'utf8');
610
+ const tasksData = JSON.parse(tasksContent);
611
+
612
+ let tasks = [];
613
+ let currentTag = 'master';
614
+
615
+ // Handle both tagged and legacy formats
616
+ if (Array.isArray(tasksData)) {
617
+ // Legacy format
618
+ tasks = tasksData;
619
+ } else if (tasksData.tasks) {
620
+ // Simple format with tasks array
621
+ tasks = tasksData.tasks;
622
+ } else {
623
+ // Tagged format - get tasks from current tag or master
624
+ if (tasksData[currentTag] && tasksData[currentTag].tasks) {
625
+ tasks = tasksData[currentTag].tasks;
626
+ } else if (tasksData.master && tasksData.master.tasks) {
627
+ tasks = tasksData.master.tasks;
628
+ } else {
629
+ // Get tasks from first available tag
630
+ const firstTag = Object.keys(tasksData).find(key =>
631
+ tasksData[key].tasks && Array.isArray(tasksData[key].tasks)
632
+ );
633
+ if (firstTag) {
634
+ tasks = tasksData[firstTag].tasks;
635
+ currentTag = firstTag;
636
+ }
637
+ }
638
+ }
639
+
640
+ // Transform tasks to ensure all have required fields
641
+ const transformedTasks = tasks.map(task => ({
642
+ id: task.id,
643
+ title: task.title || 'Untitled Task',
644
+ description: task.description || '',
645
+ status: task.status || 'pending',
646
+ priority: task.priority || 'medium',
647
+ dependencies: task.dependencies || [],
648
+ createdAt: task.createdAt || task.created || new Date().toISOString(),
649
+ updatedAt: task.updatedAt || task.updated || new Date().toISOString(),
650
+ details: task.details || '',
651
+ testStrategy: task.testStrategy || task.test_strategy || '',
652
+ subtasks: task.subtasks || []
653
+ }));
654
+
655
+ res.json({
656
+ projectName,
657
+ projectPath,
658
+ tasks: transformedTasks,
659
+ currentTag,
660
+ totalTasks: transformedTasks.length,
661
+ tasksByStatus: {
662
+ pending: transformedTasks.filter(t => t.status === 'pending').length,
663
+ 'in-progress': transformedTasks.filter(t => t.status === 'in-progress').length,
664
+ done: transformedTasks.filter(t => t.status === 'done').length,
665
+ review: transformedTasks.filter(t => t.status === 'review').length,
666
+ deferred: transformedTasks.filter(t => t.status === 'deferred').length,
667
+ cancelled: transformedTasks.filter(t => t.status === 'cancelled').length
668
+ },
669
+ timestamp: new Date().toISOString()
670
+ });
671
+
672
+ } catch (parseError) {
673
+ console.error('Failed to parse tasks.json:', parseError);
674
+ return res.status(500).json({
675
+ error: 'Failed to parse tasks file',
676
+ message: parseError.message
677
+ });
678
+ }
679
+
680
+ } catch (error) {
681
+ console.error('TaskMaster tasks loading error:', error);
682
+ res.status(500).json({
683
+ error: 'Failed to load TaskMaster tasks',
684
+ message: error.message
685
+ });
686
+ }
687
+ });
688
+
689
+ /**
690
+ * GET /api/taskmaster/prd/:projectName
691
+ * List all PRD files in the project's .taskmaster/docs directory
692
+ */
693
+ router.get('/prd/:projectName', async (req, res) => {
694
+ try {
695
+ const { projectName } = req.params;
696
+
697
+ // Get project path
698
+ let projectPath;
699
+ try {
700
+ projectPath = await extractProjectDirectory(projectName);
701
+ } catch (error) {
702
+ return res.status(404).json({
703
+ error: 'Project not found',
704
+ message: `Project "${projectName}" does not exist`
705
+ });
706
+ }
707
+
708
+ const docsPath = path.join(projectPath, '.taskmaster', 'docs');
709
+
710
+ // Check if docs directory exists
711
+ try {
712
+ await fsPromises.access(docsPath, fs.constants.R_OK);
713
+ } catch (error) {
714
+ return res.json({
715
+ projectName,
716
+ prdFiles: [],
717
+ message: 'No .taskmaster/docs directory found'
718
+ });
719
+ }
720
+
721
+ // Read directory and filter for PRD files
722
+ try {
723
+ const files = await fsPromises.readdir(docsPath);
724
+ const prdFiles = [];
725
+
726
+ for (const file of files) {
727
+ const filePath = path.join(docsPath, file);
728
+ const stats = await fsPromises.stat(filePath);
729
+
730
+ if (stats.isFile() && (file.endsWith('.txt') || file.endsWith('.md'))) {
731
+ prdFiles.push({
732
+ name: file,
733
+ path: path.relative(projectPath, filePath),
734
+ size: stats.size,
735
+ modified: stats.mtime.toISOString(),
736
+ created: stats.birthtime.toISOString()
737
+ });
738
+ }
739
+ }
740
+
741
+ res.json({
742
+ projectName,
743
+ projectPath,
744
+ prdFiles: prdFiles.sort((a, b) => new Date(b.modified) - new Date(a.modified)),
745
+ timestamp: new Date().toISOString()
746
+ });
747
+
748
+ } catch (readError) {
749
+ console.error('Error reading docs directory:', readError);
750
+ return res.status(500).json({
751
+ error: 'Failed to read PRD files',
752
+ message: readError.message
753
+ });
754
+ }
755
+
756
+ } catch (error) {
757
+ console.error('PRD list error:', error);
758
+ res.status(500).json({
759
+ error: 'Failed to list PRD files',
760
+ message: error.message
761
+ });
762
+ }
763
+ });
764
+
765
+ /**
766
+ * POST /api/taskmaster/prd/:projectName
767
+ * Create or update a PRD file in the project's .taskmaster/docs directory
768
+ */
769
+ router.post('/prd/:projectName', async (req, res) => {
770
+ try {
771
+ const { projectName } = req.params;
772
+ const { fileName, content } = req.body;
773
+
774
+ if (!fileName || !content) {
775
+ return res.status(400).json({
776
+ error: 'Missing required fields',
777
+ message: 'fileName and content are required'
778
+ });
779
+ }
780
+
781
+ // Validate filename
782
+ if (!fileName.match(/^[\w\-. ]+\.(txt|md)$/)) {
783
+ return res.status(400).json({
784
+ error: 'Invalid filename',
785
+ message: 'Filename must end with .txt or .md and contain only alphanumeric characters, spaces, dots, and dashes'
786
+ });
787
+ }
788
+
789
+ // Get project path
790
+ let projectPath;
791
+ try {
792
+ projectPath = await extractProjectDirectory(projectName);
793
+ } catch (error) {
794
+ return res.status(404).json({
795
+ error: 'Project not found',
796
+ message: `Project "${projectName}" does not exist`
797
+ });
798
+ }
799
+
800
+ const docsPath = path.join(projectPath, '.taskmaster', 'docs');
801
+ const filePath = path.join(docsPath, fileName);
802
+
803
+ // Ensure docs directory exists
804
+ try {
805
+ await fsPromises.mkdir(docsPath, { recursive: true });
806
+ } catch (error) {
807
+ console.error('Failed to create docs directory:', error);
808
+ return res.status(500).json({
809
+ error: 'Failed to create directory',
810
+ message: error.message
811
+ });
812
+ }
813
+
814
+ // Write the PRD file
815
+ try {
816
+ await fsPromises.writeFile(filePath, content, 'utf8');
817
+
818
+ // Get file stats
819
+ const stats = await fsPromises.stat(filePath);
820
+
821
+ res.json({
822
+ projectName,
823
+ projectPath,
824
+ fileName,
825
+ filePath: path.relative(projectPath, filePath),
826
+ size: stats.size,
827
+ created: stats.birthtime.toISOString(),
828
+ modified: stats.mtime.toISOString(),
829
+ message: 'PRD file saved successfully',
830
+ timestamp: new Date().toISOString()
831
+ });
832
+
833
+ } catch (writeError) {
834
+ console.error('Failed to write PRD file:', writeError);
835
+ return res.status(500).json({
836
+ error: 'Failed to write PRD file',
837
+ message: writeError.message
838
+ });
839
+ }
840
+
841
+ } catch (error) {
842
+ console.error('PRD create/update error:', error);
843
+ res.status(500).json({
844
+ error: 'Failed to create/update PRD file',
845
+ message: error.message
846
+ });
847
+ }
848
+ });
849
+
850
+ /**
851
+ * GET /api/taskmaster/prd/:projectName/:fileName
852
+ * Get content of a specific PRD file
853
+ */
854
+ router.get('/prd/:projectName/:fileName', async (req, res) => {
855
+ try {
856
+ const { projectName, fileName } = req.params;
857
+
858
+ // Get project path
859
+ let projectPath;
860
+ try {
861
+ projectPath = await extractProjectDirectory(projectName);
862
+ } catch (error) {
863
+ return res.status(404).json({
864
+ error: 'Project not found',
865
+ message: `Project "${projectName}" does not exist`
866
+ });
867
+ }
868
+
869
+ const filePath = path.join(projectPath, '.taskmaster', 'docs', fileName);
870
+
871
+ // Check if file exists
872
+ try {
873
+ await fsPromises.access(filePath, fs.constants.R_OK);
874
+ } catch (error) {
875
+ return res.status(404).json({
876
+ error: 'PRD file not found',
877
+ message: `File "${fileName}" does not exist`
878
+ });
879
+ }
880
+
881
+ // Read file content
882
+ try {
883
+ const content = await fsPromises.readFile(filePath, 'utf8');
884
+ const stats = await fsPromises.stat(filePath);
885
+
886
+ res.json({
887
+ projectName,
888
+ projectPath,
889
+ fileName,
890
+ filePath: path.relative(projectPath, filePath),
891
+ content,
892
+ size: stats.size,
893
+ created: stats.birthtime.toISOString(),
894
+ modified: stats.mtime.toISOString(),
895
+ timestamp: new Date().toISOString()
896
+ });
897
+
898
+ } catch (readError) {
899
+ console.error('Failed to read PRD file:', readError);
900
+ return res.status(500).json({
901
+ error: 'Failed to read PRD file',
902
+ message: readError.message
903
+ });
904
+ }
905
+
906
+ } catch (error) {
907
+ console.error('PRD read error:', error);
908
+ res.status(500).json({
909
+ error: 'Failed to read PRD file',
910
+ message: error.message
911
+ });
912
+ }
913
+ });
914
+
915
+ /**
916
+ * DELETE /api/taskmaster/prd/:projectName/:fileName
917
+ * Delete a specific PRD file
918
+ */
919
+ router.delete('/prd/:projectName/:fileName', async (req, res) => {
920
+ try {
921
+ const { projectName, fileName } = req.params;
922
+
923
+ // Get project path
924
+ let projectPath;
925
+ try {
926
+ projectPath = await extractProjectDirectory(projectName);
927
+ } catch (error) {
928
+ return res.status(404).json({
929
+ error: 'Project not found',
930
+ message: `Project "${projectName}" does not exist`
931
+ });
932
+ }
933
+
934
+ const filePath = path.join(projectPath, '.taskmaster', 'docs', fileName);
935
+
936
+ // Check if file exists
937
+ try {
938
+ await fsPromises.access(filePath, fs.constants.F_OK);
939
+ } catch (error) {
940
+ return res.status(404).json({
941
+ error: 'PRD file not found',
942
+ message: `File "${fileName}" does not exist`
943
+ });
944
+ }
945
+
946
+ // Delete the file
947
+ try {
948
+ await fsPromises.unlink(filePath);
949
+
950
+ res.json({
951
+ projectName,
952
+ projectPath,
953
+ fileName,
954
+ message: 'PRD file deleted successfully',
955
+ timestamp: new Date().toISOString()
956
+ });
957
+
958
+ } catch (deleteError) {
959
+ console.error('Failed to delete PRD file:', deleteError);
960
+ return res.status(500).json({
961
+ error: 'Failed to delete PRD file',
962
+ message: deleteError.message
963
+ });
964
+ }
965
+
966
+ } catch (error) {
967
+ console.error('PRD delete error:', error);
968
+ res.status(500).json({
969
+ error: 'Failed to delete PRD file',
970
+ message: error.message
971
+ });
972
+ }
973
+ });
974
+
975
+ /**
976
+ * POST /api/taskmaster/init/:projectName
977
+ * Initialize TaskMaster in a project
978
+ */
979
+ router.post('/init/:projectName', async (req, res) => {
980
+ try {
981
+ const { projectName } = req.params;
982
+
983
+ // Get project path
984
+ let projectPath;
985
+ try {
986
+ projectPath = await extractProjectDirectory(projectName);
987
+ } catch (error) {
988
+ return res.status(404).json({
989
+ error: 'Project not found',
990
+ message: `Project "${projectName}" does not exist`
991
+ });
992
+ }
993
+
994
+ // Check if TaskMaster is already initialized
995
+ const taskMasterPath = path.join(projectPath, '.taskmaster');
996
+ try {
997
+ await fsPromises.access(taskMasterPath, fs.constants.F_OK);
998
+ return res.status(400).json({
999
+ error: 'TaskMaster already initialized',
1000
+ message: 'TaskMaster is already configured for this project'
1001
+ });
1002
+ } catch (error) {
1003
+ // Directory doesn't exist, we can proceed
1004
+ }
1005
+
1006
+ // Run taskmaster init command
1007
+ const initProcess = spawn('npx', ['task-master', 'init'], {
1008
+ cwd: projectPath,
1009
+ stdio: ['pipe', 'pipe', 'pipe']
1010
+ });
1011
+
1012
+ let stdout = '';
1013
+ let stderr = '';
1014
+
1015
+ initProcess.stdout.on('data', (data) => {
1016
+ stdout += data.toString();
1017
+ });
1018
+
1019
+ initProcess.stderr.on('data', (data) => {
1020
+ stderr += data.toString();
1021
+ });
1022
+
1023
+ initProcess.on('close', (code) => {
1024
+ if (code === 0) {
1025
+ // Broadcast TaskMaster project update via WebSocket
1026
+ if (req.app.locals.wss) {
1027
+ broadcastTaskMasterProjectUpdate(
1028
+ req.app.locals.wss,
1029
+ projectName,
1030
+ { hasTaskmaster: true, status: 'initialized' }
1031
+ );
1032
+ }
1033
+
1034
+ res.json({
1035
+ projectName,
1036
+ projectPath,
1037
+ message: 'TaskMaster initialized successfully',
1038
+ output: stdout,
1039
+ timestamp: new Date().toISOString()
1040
+ });
1041
+ } else {
1042
+ console.error('TaskMaster init failed:', stderr);
1043
+ res.status(500).json({
1044
+ error: 'Failed to initialize TaskMaster',
1045
+ message: stderr || stdout,
1046
+ code
1047
+ });
1048
+ }
1049
+ });
1050
+
1051
+ // Send 'yes' responses to automated prompts
1052
+ initProcess.stdin.write('yes\n');
1053
+ initProcess.stdin.end();
1054
+
1055
+ } catch (error) {
1056
+ console.error('TaskMaster init error:', error);
1057
+ res.status(500).json({
1058
+ error: 'Failed to initialize TaskMaster',
1059
+ message: error.message
1060
+ });
1061
+ }
1062
+ });
1063
+
1064
+ /**
1065
+ * POST /api/taskmaster/add-task/:projectName
1066
+ * Add a new task to the project
1067
+ */
1068
+ router.post('/add-task/:projectName', async (req, res) => {
1069
+ try {
1070
+ const { projectName } = req.params;
1071
+ const { prompt, title, description, priority = 'medium', dependencies } = req.body;
1072
+
1073
+ if (!prompt && (!title || !description)) {
1074
+ return res.status(400).json({
1075
+ error: 'Missing required parameters',
1076
+ message: 'Either "prompt" or both "title" and "description" are required'
1077
+ });
1078
+ }
1079
+
1080
+ // Get project path
1081
+ let projectPath;
1082
+ try {
1083
+ projectPath = await extractProjectDirectory(projectName);
1084
+ } catch (error) {
1085
+ return res.status(404).json({
1086
+ error: 'Project not found',
1087
+ message: `Project "${projectName}" does not exist`
1088
+ });
1089
+ }
1090
+
1091
+ // Build the task-master add-task command
1092
+ const args = ['task-master-ai', 'add-task'];
1093
+
1094
+ if (prompt) {
1095
+ args.push('--prompt', prompt);
1096
+ args.push('--research'); // Use research for AI-generated tasks
1097
+ } else {
1098
+ args.push('--prompt', `Create a task titled "${title}" with description: ${description}`);
1099
+ }
1100
+
1101
+ if (priority) {
1102
+ args.push('--priority', priority);
1103
+ }
1104
+
1105
+ if (dependencies) {
1106
+ args.push('--dependencies', dependencies);
1107
+ }
1108
+
1109
+ // Run task-master add-task command
1110
+ const addTaskProcess = spawn('npx', args, {
1111
+ cwd: projectPath,
1112
+ stdio: ['pipe', 'pipe', 'pipe']
1113
+ });
1114
+
1115
+ let stdout = '';
1116
+ let stderr = '';
1117
+
1118
+ addTaskProcess.stdout.on('data', (data) => {
1119
+ stdout += data.toString();
1120
+ });
1121
+
1122
+ addTaskProcess.stderr.on('data', (data) => {
1123
+ stderr += data.toString();
1124
+ });
1125
+
1126
+ addTaskProcess.on('close', (code) => {
1127
+ console.log('Add task process completed with code:', code);
1128
+ console.log('Stdout:', stdout);
1129
+ console.log('Stderr:', stderr);
1130
+
1131
+ if (code === 0) {
1132
+ // Broadcast task update via WebSocket
1133
+ if (req.app.locals.wss) {
1134
+ broadcastTaskMasterTasksUpdate(
1135
+ req.app.locals.wss,
1136
+ projectName
1137
+ );
1138
+ }
1139
+
1140
+ res.json({
1141
+ projectName,
1142
+ projectPath,
1143
+ message: 'Task added successfully',
1144
+ output: stdout,
1145
+ timestamp: new Date().toISOString()
1146
+ });
1147
+ } else {
1148
+ console.error('Add task failed:', stderr);
1149
+ res.status(500).json({
1150
+ error: 'Failed to add task',
1151
+ message: stderr || stdout,
1152
+ code
1153
+ });
1154
+ }
1155
+ });
1156
+
1157
+ addTaskProcess.stdin.end();
1158
+
1159
+ } catch (error) {
1160
+ console.error('Add task error:', error);
1161
+ res.status(500).json({
1162
+ error: 'Failed to add task',
1163
+ message: error.message
1164
+ });
1165
+ }
1166
+ });
1167
+
1168
+ /**
1169
+ * PUT /api/taskmaster/update-task/:projectName/:taskId
1170
+ * Update a specific task using TaskMaster CLI
1171
+ */
1172
+ router.put('/update-task/:projectName/:taskId', async (req, res) => {
1173
+ try {
1174
+ const { projectName, taskId } = req.params;
1175
+ const { title, description, status, priority, details } = req.body;
1176
+
1177
+ // Get project path
1178
+ let projectPath;
1179
+ try {
1180
+ projectPath = await extractProjectDirectory(projectName);
1181
+ } catch (error) {
1182
+ return res.status(404).json({
1183
+ error: 'Project not found',
1184
+ message: `Project "${projectName}" does not exist`
1185
+ });
1186
+ }
1187
+
1188
+ // If only updating status, use set-status command
1189
+ if (status && Object.keys(req.body).length === 1) {
1190
+ const setStatusProcess = spawn('npx', ['task-master-ai', 'set-status', `--id=${taskId}`, `--status=${status}`], {
1191
+ cwd: projectPath,
1192
+ stdio: ['pipe', 'pipe', 'pipe']
1193
+ });
1194
+
1195
+ let stdout = '';
1196
+ let stderr = '';
1197
+
1198
+ setStatusProcess.stdout.on('data', (data) => {
1199
+ stdout += data.toString();
1200
+ });
1201
+
1202
+ setStatusProcess.stderr.on('data', (data) => {
1203
+ stderr += data.toString();
1204
+ });
1205
+
1206
+ setStatusProcess.on('close', (code) => {
1207
+ if (code === 0) {
1208
+ // Broadcast task update via WebSocket
1209
+ if (req.app.locals.wss) {
1210
+ broadcastTaskMasterTasksUpdate(req.app.locals.wss, projectName);
1211
+ }
1212
+
1213
+ res.json({
1214
+ projectName,
1215
+ projectPath,
1216
+ taskId,
1217
+ message: 'Task status updated successfully',
1218
+ output: stdout,
1219
+ timestamp: new Date().toISOString()
1220
+ });
1221
+ } else {
1222
+ console.error('Set task status failed:', stderr);
1223
+ res.status(500).json({
1224
+ error: 'Failed to update task status',
1225
+ message: stderr || stdout,
1226
+ code
1227
+ });
1228
+ }
1229
+ });
1230
+
1231
+ setStatusProcess.stdin.end();
1232
+ } else {
1233
+ // For other updates, use update-task command with a prompt describing the changes
1234
+ const updates = [];
1235
+ if (title) updates.push(`title: "${title}"`);
1236
+ if (description) updates.push(`description: "${description}"`);
1237
+ if (priority) updates.push(`priority: "${priority}"`);
1238
+ if (details) updates.push(`details: "${details}"`);
1239
+
1240
+ const prompt = `Update task with the following changes: ${updates.join(', ')}`;
1241
+
1242
+ const updateProcess = spawn('npx', ['task-master-ai', 'update-task', `--id=${taskId}`, `--prompt=${prompt}`], {
1243
+ cwd: projectPath,
1244
+ stdio: ['pipe', 'pipe', 'pipe']
1245
+ });
1246
+
1247
+ let stdout = '';
1248
+ let stderr = '';
1249
+
1250
+ updateProcess.stdout.on('data', (data) => {
1251
+ stdout += data.toString();
1252
+ });
1253
+
1254
+ updateProcess.stderr.on('data', (data) => {
1255
+ stderr += data.toString();
1256
+ });
1257
+
1258
+ updateProcess.on('close', (code) => {
1259
+ if (code === 0) {
1260
+ // Broadcast task update via WebSocket
1261
+ if (req.app.locals.wss) {
1262
+ broadcastTaskMasterTasksUpdate(req.app.locals.wss, projectName);
1263
+ }
1264
+
1265
+ res.json({
1266
+ projectName,
1267
+ projectPath,
1268
+ taskId,
1269
+ message: 'Task updated successfully',
1270
+ output: stdout,
1271
+ timestamp: new Date().toISOString()
1272
+ });
1273
+ } else {
1274
+ console.error('Update task failed:', stderr);
1275
+ res.status(500).json({
1276
+ error: 'Failed to update task',
1277
+ message: stderr || stdout,
1278
+ code
1279
+ });
1280
+ }
1281
+ });
1282
+
1283
+ updateProcess.stdin.end();
1284
+ }
1285
+
1286
+ } catch (error) {
1287
+ console.error('Update task error:', error);
1288
+ res.status(500).json({
1289
+ error: 'Failed to update task',
1290
+ message: error.message
1291
+ });
1292
+ }
1293
+ });
1294
+
1295
+ /**
1296
+ * POST /api/taskmaster/parse-prd/:projectName
1297
+ * Parse a PRD file to generate tasks
1298
+ */
1299
+ router.post('/parse-prd/:projectName', async (req, res) => {
1300
+ try {
1301
+ const { projectName } = req.params;
1302
+ const { fileName = 'prd.txt', numTasks, append = false } = req.body;
1303
+
1304
+ // Get project path
1305
+ let projectPath;
1306
+ try {
1307
+ projectPath = await extractProjectDirectory(projectName);
1308
+ } catch (error) {
1309
+ return res.status(404).json({
1310
+ error: 'Project not found',
1311
+ message: `Project "${projectName}" does not exist`
1312
+ });
1313
+ }
1314
+
1315
+ const prdPath = path.join(projectPath, '.taskmaster', 'docs', fileName);
1316
+
1317
+ // Check if PRD file exists
1318
+ try {
1319
+ await fsPromises.access(prdPath, fs.constants.F_OK);
1320
+ } catch (error) {
1321
+ return res.status(404).json({
1322
+ error: 'PRD file not found',
1323
+ message: `File "${fileName}" does not exist in .taskmaster/docs/`
1324
+ });
1325
+ }
1326
+
1327
+ // Build the command args
1328
+ const args = ['task-master-ai', 'parse-prd', prdPath];
1329
+
1330
+ if (numTasks) {
1331
+ args.push('--num-tasks', numTasks.toString());
1332
+ }
1333
+
1334
+ if (append) {
1335
+ args.push('--append');
1336
+ }
1337
+
1338
+ args.push('--research'); // Use research for better PRD parsing
1339
+
1340
+ // Run task-master parse-prd command
1341
+ const parsePRDProcess = spawn('npx', args, {
1342
+ cwd: projectPath,
1343
+ stdio: ['pipe', 'pipe', 'pipe']
1344
+ });
1345
+
1346
+ let stdout = '';
1347
+ let stderr = '';
1348
+
1349
+ parsePRDProcess.stdout.on('data', (data) => {
1350
+ stdout += data.toString();
1351
+ });
1352
+
1353
+ parsePRDProcess.stderr.on('data', (data) => {
1354
+ stderr += data.toString();
1355
+ });
1356
+
1357
+ parsePRDProcess.on('close', (code) => {
1358
+ if (code === 0) {
1359
+ // Broadcast task update via WebSocket
1360
+ if (req.app.locals.wss) {
1361
+ broadcastTaskMasterTasksUpdate(
1362
+ req.app.locals.wss,
1363
+ projectName
1364
+ );
1365
+ }
1366
+
1367
+ res.json({
1368
+ projectName,
1369
+ projectPath,
1370
+ prdFile: fileName,
1371
+ message: 'PRD parsed and tasks generated successfully',
1372
+ output: stdout,
1373
+ timestamp: new Date().toISOString()
1374
+ });
1375
+ } else {
1376
+ console.error('Parse PRD failed:', stderr);
1377
+ res.status(500).json({
1378
+ error: 'Failed to parse PRD',
1379
+ message: stderr || stdout,
1380
+ code
1381
+ });
1382
+ }
1383
+ });
1384
+
1385
+ parsePRDProcess.stdin.end();
1386
+
1387
+ } catch (error) {
1388
+ console.error('Parse PRD error:', error);
1389
+ res.status(500).json({
1390
+ error: 'Failed to parse PRD',
1391
+ message: error.message
1392
+ });
1393
+ }
1394
+ });
1395
+
1396
+ /**
1397
+ * GET /api/taskmaster/prd-templates
1398
+ * Get available PRD templates
1399
+ */
1400
+ router.get('/prd-templates', async (req, res) => {
1401
+ try {
1402
+ // Return built-in templates
1403
+ const templates = [
1404
+ {
1405
+ id: 'web-app',
1406
+ name: 'Web Application',
1407
+ description: 'Template for web application projects with frontend and backend components',
1408
+ category: 'web',
1409
+ content: `# Product Requirements Document - Web Application
1410
+
1411
+ ## Overview
1412
+ **Product Name:** [Your App Name]
1413
+ **Version:** 1.0
1414
+ **Date:** ${new Date().toISOString().split('T')[0]}
1415
+ **Author:** [Your Name]
1416
+
1417
+ ## Executive Summary
1418
+ Brief description of what this web application will do and why it's needed.
1419
+
1420
+ ## Product Goals
1421
+ - Goal 1: [Specific measurable goal]
1422
+ - Goal 2: [Specific measurable goal]
1423
+ - Goal 3: [Specific measurable goal]
1424
+
1425
+ ## User Stories
1426
+ ### Core Features
1427
+ 1. **User Registration & Authentication**
1428
+ - As a user, I want to create an account so I can access personalized features
1429
+ - As a user, I want to log in securely so my data is protected
1430
+ - As a user, I want to reset my password if I forget it
1431
+
1432
+ 2. **Main Application Features**
1433
+ - As a user, I want to [core feature 1] so I can [benefit]
1434
+ - As a user, I want to [core feature 2] so I can [benefit]
1435
+ - As a user, I want to [core feature 3] so I can [benefit]
1436
+
1437
+ 3. **User Interface**
1438
+ - As a user, I want a responsive design so I can use the app on any device
1439
+ - As a user, I want intuitive navigation so I can easily find features
1440
+
1441
+ ## Technical Requirements
1442
+ ### Frontend
1443
+ - Framework: React/Vue/Angular or vanilla JavaScript
1444
+ - Styling: CSS framework (Tailwind, Bootstrap, etc.)
1445
+ - State Management: Redux/Vuex/Context API
1446
+ - Build Tools: Webpack/Vite
1447
+ - Testing: Jest/Vitest for unit tests
1448
+
1449
+ ### Backend
1450
+ - Runtime: Node.js/Python/Java
1451
+ - Database: PostgreSQL/MySQL/MongoDB
1452
+ - API: RESTful API or GraphQL
1453
+ - Authentication: JWT tokens
1454
+ - Testing: Integration and unit tests
1455
+
1456
+ ### Infrastructure
1457
+ - Hosting: Cloud provider (AWS, Azure, GCP)
1458
+ - CI/CD: GitHub Actions/GitLab CI
1459
+ - Monitoring: Application monitoring tools
1460
+ - Security: HTTPS, input validation, rate limiting
1461
+
1462
+ ## Success Metrics
1463
+ - User engagement metrics
1464
+ - Performance benchmarks (load time < 2s)
1465
+ - Error rates < 1%
1466
+ - User satisfaction scores
1467
+
1468
+ ## Timeline
1469
+ - Phase 1: Core functionality (4-6 weeks)
1470
+ - Phase 2: Advanced features (2-4 weeks)
1471
+ - Phase 3: Polish and launch (2 weeks)
1472
+
1473
+ ## Constraints & Assumptions
1474
+ - Budget constraints
1475
+ - Technical limitations
1476
+ - Team size and expertise
1477
+ - Timeline constraints`
1478
+ },
1479
+ {
1480
+ id: 'api',
1481
+ name: 'REST API',
1482
+ description: 'Template for REST API development projects',
1483
+ category: 'backend',
1484
+ content: `# Product Requirements Document - REST API
1485
+
1486
+ ## Overview
1487
+ **API Name:** [Your API Name]
1488
+ **Version:** v1.0
1489
+ **Date:** ${new Date().toISOString().split('T')[0]}
1490
+ **Author:** [Your Name]
1491
+
1492
+ ## Executive Summary
1493
+ Description of the API's purpose, target users, and primary use cases.
1494
+
1495
+ ## API Goals
1496
+ - Goal 1: Provide secure data access
1497
+ - Goal 2: Ensure scalable architecture
1498
+ - Goal 3: Maintain high availability (99.9% uptime)
1499
+
1500
+ ## Functional Requirements
1501
+ ### Core Endpoints
1502
+ 1. **Authentication Endpoints**
1503
+ - POST /api/auth/login - User authentication
1504
+ - POST /api/auth/logout - User logout
1505
+ - POST /api/auth/refresh - Token refresh
1506
+ - POST /api/auth/register - User registration
1507
+
1508
+ 2. **Data Management Endpoints**
1509
+ - GET /api/resources - List resources with pagination
1510
+ - GET /api/resources/{id} - Get specific resource
1511
+ - POST /api/resources - Create new resource
1512
+ - PUT /api/resources/{id} - Update existing resource
1513
+ - DELETE /api/resources/{id} - Delete resource
1514
+
1515
+ 3. **Administrative Endpoints**
1516
+ - GET /api/admin/users - Manage users (admin only)
1517
+ - GET /api/admin/analytics - System analytics
1518
+ - POST /api/admin/backup - Trigger system backup
1519
+
1520
+ ## Technical Requirements
1521
+ ### API Design
1522
+ - RESTful architecture following OpenAPI 3.0 specification
1523
+ - JSON request/response format
1524
+ - Consistent error response format
1525
+ - API versioning strategy
1526
+
1527
+ ### Authentication & Security
1528
+ - JWT token-based authentication
1529
+ - Role-based access control (RBAC)
1530
+ - Rate limiting (100 requests/minute per user)
1531
+ - Input validation and sanitization
1532
+ - HTTPS enforcement
1533
+
1534
+ ### Database
1535
+ - Database type: [PostgreSQL/MongoDB/MySQL]
1536
+ - Connection pooling
1537
+ - Database migrations
1538
+ - Backup and recovery procedures
1539
+
1540
+ ### Performance Requirements
1541
+ - Response time: < 200ms for 95% of requests
1542
+ - Throughput: 1000+ requests/second
1543
+ - Concurrent users: 10,000+
1544
+ - Database query optimization
1545
+
1546
+ ### Documentation
1547
+ - Auto-generated API documentation (Swagger/OpenAPI)
1548
+ - Code examples for common use cases
1549
+ - SDK development for major languages
1550
+ - Postman collection for testing
1551
+
1552
+ ## Error Handling
1553
+ - Standardized error codes and messages
1554
+ - Proper HTTP status codes
1555
+ - Detailed error logging
1556
+ - Graceful degradation strategies
1557
+
1558
+ ## Testing Strategy
1559
+ - Unit tests (80%+ coverage)
1560
+ - Integration tests for all endpoints
1561
+ - Load testing and performance testing
1562
+ - Security testing (OWASP compliance)
1563
+
1564
+ ## Monitoring & Logging
1565
+ - Application performance monitoring
1566
+ - Error tracking and alerting
1567
+ - Access logs and audit trails
1568
+ - Health check endpoints
1569
+
1570
+ ## Deployment
1571
+ - Containerized deployment (Docker)
1572
+ - CI/CD pipeline setup
1573
+ - Environment management (dev, staging, prod)
1574
+ - Blue-green deployment strategy
1575
+
1576
+ ## Success Metrics
1577
+ - API uptime > 99.9%
1578
+ - Average response time < 200ms
1579
+ - Zero critical security vulnerabilities
1580
+ - Developer adoption metrics`
1581
+ },
1582
+ {
1583
+ id: 'mobile-app',
1584
+ name: 'Mobile Application',
1585
+ description: 'Template for mobile app development projects (iOS/Android)',
1586
+ category: 'mobile',
1587
+ content: `# Product Requirements Document - Mobile Application
1588
+
1589
+ ## Overview
1590
+ **App Name:** [Your App Name]
1591
+ **Platform:** iOS / Android / Cross-platform
1592
+ **Version:** 1.0
1593
+ **Date:** ${new Date().toISOString().split('T')[0]}
1594
+ **Author:** [Your Name]
1595
+
1596
+ ## Executive Summary
1597
+ Brief description of the mobile app's purpose, target audience, and key value proposition.
1598
+
1599
+ ## Product Goals
1600
+ - Goal 1: [Specific user engagement goal]
1601
+ - Goal 2: [Specific functionality goal]
1602
+ - Goal 3: [Specific performance goal]
1603
+
1604
+ ## User Stories
1605
+ ### Core Features
1606
+ 1. **Onboarding & Authentication**
1607
+ - As a new user, I want a simple onboarding process
1608
+ - As a user, I want to sign up with email or social media
1609
+ - As a user, I want biometric authentication for security
1610
+
1611
+ 2. **Main App Features**
1612
+ - As a user, I want [core feature 1] accessible from home screen
1613
+ - As a user, I want [core feature 2] to work offline
1614
+ - As a user, I want to sync data across devices
1615
+
1616
+ 3. **User Experience**
1617
+ - As a user, I want intuitive navigation patterns
1618
+ - As a user, I want fast loading times
1619
+ - As a user, I want accessibility features
1620
+
1621
+ ## Technical Requirements
1622
+ ### Mobile Development
1623
+ - **Cross-platform:** React Native / Flutter / Xamarin
1624
+ - **Native:** Swift (iOS) / Kotlin (Android)
1625
+ - **State Management:** Redux / MobX / Provider
1626
+ - **Navigation:** React Navigation / Flutter Navigation
1627
+
1628
+ ### Backend Integration
1629
+ - REST API or GraphQL integration
1630
+ - Real-time features (WebSockets/Push notifications)
1631
+ - Offline data synchronization
1632
+ - Background processing
1633
+
1634
+ ### Device Features
1635
+ - Camera and photo library access
1636
+ - GPS location services
1637
+ - Push notifications
1638
+ - Biometric authentication
1639
+ - Device storage
1640
+
1641
+ ### Performance Requirements
1642
+ - App launch time < 3 seconds
1643
+ - Screen transition animations < 300ms
1644
+ - Memory usage optimization
1645
+ - Battery usage optimization
1646
+
1647
+ ## Platform-Specific Considerations
1648
+ ### iOS Requirements
1649
+ - iOS 13.0+ minimum version
1650
+ - App Store guidelines compliance
1651
+ - iOS design guidelines (Human Interface Guidelines)
1652
+ - TestFlight beta testing
1653
+
1654
+ ### Android Requirements
1655
+ - Android 8.0+ (API level 26) minimum
1656
+ - Google Play Store guidelines
1657
+ - Material Design guidelines
1658
+ - Google Play Console testing
1659
+
1660
+ ## User Interface Design
1661
+ - Responsive design for different screen sizes
1662
+ - Dark mode support
1663
+ - Accessibility compliance (WCAG 2.1)
1664
+ - Consistent design system
1665
+
1666
+ ## Security & Privacy
1667
+ - Secure data storage (Keychain/Keystore)
1668
+ - API communication encryption
1669
+ - Privacy policy compliance (GDPR/CCPA)
1670
+ - App security best practices
1671
+
1672
+ ## Testing Strategy
1673
+ - Unit testing (80%+ coverage)
1674
+ - UI/E2E testing (Detox/Appium)
1675
+ - Device testing on multiple screen sizes
1676
+ - Performance testing
1677
+ - Security testing
1678
+
1679
+ ## App Store Deployment
1680
+ - App store optimization (ASO)
1681
+ - App icons and screenshots
1682
+ - Store listing content
1683
+ - Release management strategy
1684
+
1685
+ ## Analytics & Monitoring
1686
+ - User analytics (Firebase/Analytics)
1687
+ - Crash reporting (Crashlytics/Sentry)
1688
+ - Performance monitoring
1689
+ - User feedback collection
1690
+
1691
+ ## Success Metrics
1692
+ - App store ratings > 4.0
1693
+ - User retention rates
1694
+ - Daily/Monthly active users
1695
+ - App performance metrics
1696
+ - Conversion rates`
1697
+ },
1698
+ {
1699
+ id: 'data-analysis',
1700
+ name: 'Data Analysis Project',
1701
+ description: 'Template for data analysis and visualization projects',
1702
+ category: 'data',
1703
+ content: `# Product Requirements Document - Data Analysis Project
1704
+
1705
+ ## Overview
1706
+ **Project Name:** [Your Analysis Project]
1707
+ **Analysis Type:** [Descriptive/Predictive/Prescriptive]
1708
+ **Date:** ${new Date().toISOString().split('T')[0]}
1709
+ **Author:** [Your Name]
1710
+
1711
+ ## Executive Summary
1712
+ Description of the business problem, data sources, and expected insights.
1713
+
1714
+ ## Project Goals
1715
+ - Goal 1: [Specific business question to answer]
1716
+ - Goal 2: [Specific prediction to make]
1717
+ - Goal 3: [Specific recommendation to provide]
1718
+
1719
+ ## Business Requirements
1720
+ ### Key Questions
1721
+ 1. What patterns exist in the current data?
1722
+ 2. What factors influence [target variable]?
1723
+ 3. What predictions can be made for [future outcome]?
1724
+ 4. What recommendations can improve [business metric]?
1725
+
1726
+ ### Success Criteria
1727
+ - Actionable insights for stakeholders
1728
+ - Statistical significance in findings
1729
+ - Reproducible analysis pipeline
1730
+ - Clear visualization and reporting
1731
+
1732
+ ## Data Requirements
1733
+ ### Data Sources
1734
+ 1. **Primary Data**
1735
+ - Source: [Database/API/Files]
1736
+ - Format: [CSV/JSON/SQL]
1737
+ - Size: [Volume estimate]
1738
+ - Update frequency: [Real-time/Daily/Monthly]
1739
+
1740
+ 2. **External Data**
1741
+ - Third-party APIs
1742
+ - Public datasets
1743
+ - Market research data
1744
+
1745
+ ### Data Quality Requirements
1746
+ - Data completeness (< 5% missing values)
1747
+ - Data accuracy validation
1748
+ - Data consistency checks
1749
+ - Historical data availability
1750
+
1751
+ ## Technical Requirements
1752
+ ### Data Pipeline
1753
+ - Data extraction and ingestion
1754
+ - Data cleaning and preprocessing
1755
+ - Data transformation and feature engineering
1756
+ - Data validation and quality checks
1757
+
1758
+ ### Analysis Tools
1759
+ - **Programming:** Python/R/SQL
1760
+ - **Libraries:** pandas, numpy, scikit-learn, matplotlib
1761
+ - **Visualization:** Tableau, PowerBI, or custom dashboards
1762
+ - **Version Control:** Git for code and DVC for data
1763
+
1764
+ ### Computing Resources
1765
+ - Local development environment
1766
+ - Cloud computing (AWS/GCP/Azure) if needed
1767
+ - Database access and permissions
1768
+ - Storage requirements
1769
+
1770
+ ## Analysis Methodology
1771
+ ### Data Exploration
1772
+ 1. Descriptive statistics and data profiling
1773
+ 2. Data visualization and pattern identification
1774
+ 3. Correlation analysis
1775
+ 4. Outlier detection and handling
1776
+
1777
+ ### Statistical Analysis
1778
+ 1. Hypothesis formulation
1779
+ 2. Statistical testing
1780
+ 3. Confidence intervals
1781
+ 4. Effect size calculations
1782
+
1783
+ ### Machine Learning (if applicable)
1784
+ 1. Feature selection and engineering
1785
+ 2. Model selection and training
1786
+ 3. Cross-validation and evaluation
1787
+ 4. Model interpretation and explainability
1788
+
1789
+ ## Deliverables
1790
+ ### Reports
1791
+ - Executive summary for stakeholders
1792
+ - Technical analysis report
1793
+ - Data quality report
1794
+ - Methodology documentation
1795
+
1796
+ ### Visualizations
1797
+ - Interactive dashboards
1798
+ - Static charts and graphs
1799
+ - Data story presentations
1800
+ - Key findings infographics
1801
+
1802
+ ### Code & Documentation
1803
+ - Reproducible analysis scripts
1804
+ - Data pipeline code
1805
+ - Documentation and comments
1806
+ - Testing and validation code
1807
+
1808
+ ## Timeline
1809
+ - Phase 1: Data collection and exploration (2 weeks)
1810
+ - Phase 2: Analysis and modeling (3 weeks)
1811
+ - Phase 3: Reporting and visualization (1 week)
1812
+ - Phase 4: Stakeholder presentation (1 week)
1813
+
1814
+ ## Risks & Assumptions
1815
+ - Data availability and quality risks
1816
+ - Technical complexity assumptions
1817
+ - Resource and timeline constraints
1818
+ - Stakeholder engagement assumptions
1819
+
1820
+ ## Success Metrics
1821
+ - Stakeholder satisfaction with insights
1822
+ - Accuracy of predictions (if applicable)
1823
+ - Business impact of recommendations
1824
+ - Reproducibility of results`
1825
+ }
1826
+ ];
1827
+
1828
+ res.json({
1829
+ templates,
1830
+ timestamp: new Date().toISOString()
1831
+ });
1832
+
1833
+ } catch (error) {
1834
+ console.error('PRD templates error:', error);
1835
+ res.status(500).json({
1836
+ error: 'Failed to get PRD templates',
1837
+ message: error.message
1838
+ });
1839
+ }
1840
+ });
1841
+
1842
+ /**
1843
+ * POST /api/taskmaster/apply-template/:projectName
1844
+ * Apply a PRD template to create a new PRD file
1845
+ */
1846
+ router.post('/apply-template/:projectName', async (req, res) => {
1847
+ try {
1848
+ const { projectName } = req.params;
1849
+ const { templateId, fileName = 'prd.txt', customizations = {} } = req.body;
1850
+
1851
+ if (!templateId) {
1852
+ return res.status(400).json({
1853
+ error: 'Missing required parameter',
1854
+ message: 'templateId is required'
1855
+ });
1856
+ }
1857
+
1858
+ // Get project path
1859
+ let projectPath;
1860
+ try {
1861
+ projectPath = await extractProjectDirectory(projectName);
1862
+ } catch (error) {
1863
+ return res.status(404).json({
1864
+ error: 'Project not found',
1865
+ message: `Project "${projectName}" does not exist`
1866
+ });
1867
+ }
1868
+
1869
+ // Get the template content (this would normally fetch from the templates list)
1870
+ const templates = await getAvailableTemplates();
1871
+ const template = templates.find(t => t.id === templateId);
1872
+
1873
+ if (!template) {
1874
+ return res.status(404).json({
1875
+ error: 'Template not found',
1876
+ message: `Template "${templateId}" does not exist`
1877
+ });
1878
+ }
1879
+
1880
+ // Apply customizations to template content
1881
+ let content = template.content;
1882
+
1883
+ // Replace placeholders with customizations
1884
+ for (const [key, value] of Object.entries(customizations)) {
1885
+ const placeholder = `[${key}]`;
1886
+ content = content.replace(new RegExp(placeholder.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'g'), value);
1887
+ }
1888
+
1889
+ // Ensure .taskmaster/docs directory exists
1890
+ const docsDir = path.join(projectPath, '.taskmaster', 'docs');
1891
+ try {
1892
+ await fsPromises.mkdir(docsDir, { recursive: true });
1893
+ } catch (error) {
1894
+ console.error('Failed to create docs directory:', error);
1895
+ }
1896
+
1897
+ const filePath = path.join(docsDir, fileName);
1898
+
1899
+ // Write the template content to the file
1900
+ try {
1901
+ await fsPromises.writeFile(filePath, content, 'utf8');
1902
+
1903
+ res.json({
1904
+ projectName,
1905
+ projectPath,
1906
+ templateId,
1907
+ templateName: template.name,
1908
+ fileName,
1909
+ filePath: filePath,
1910
+ message: 'PRD template applied successfully',
1911
+ timestamp: new Date().toISOString()
1912
+ });
1913
+
1914
+ } catch (writeError) {
1915
+ console.error('Failed to write PRD template:', writeError);
1916
+ return res.status(500).json({
1917
+ error: 'Failed to write PRD template',
1918
+ message: writeError.message
1919
+ });
1920
+ }
1921
+
1922
+ } catch (error) {
1923
+ console.error('Apply template error:', error);
1924
+ res.status(500).json({
1925
+ error: 'Failed to apply PRD template',
1926
+ message: error.message
1927
+ });
1928
+ }
1929
+ });
1930
+
1931
+ // Helper function to get available templates
1932
+ async function getAvailableTemplates() {
1933
+ // This could be extended to read from files or database
1934
+ return [
1935
+ {
1936
+ id: 'web-app',
1937
+ name: 'Web Application',
1938
+ description: 'Template for web application projects',
1939
+ category: 'web',
1940
+ content: `# Product Requirements Document - Web Application
1941
+
1942
+ ## Overview
1943
+ **Product Name:** [Your App Name]
1944
+ **Version:** 1.0
1945
+ **Date:** ${new Date().toISOString().split('T')[0]}
1946
+ **Author:** [Your Name]
1947
+
1948
+ ## Executive Summary
1949
+ Brief description of what this web application will do and why it's needed.
1950
+
1951
+ ## User Stories
1952
+ 1. As a user, I want [feature] so I can [benefit]
1953
+ 2. As a user, I want [feature] so I can [benefit]
1954
+ 3. As a user, I want [feature] so I can [benefit]
1955
+
1956
+ ## Technical Requirements
1957
+ - Frontend framework
1958
+ - Backend services
1959
+ - Database requirements
1960
+ - Security considerations
1961
+
1962
+ ## Success Metrics
1963
+ - User engagement metrics
1964
+ - Performance benchmarks
1965
+ - Business objectives`
1966
+ },
1967
+ // Add other templates here if needed
1968
+ ];
1969
+ }
1970
+
1971
+ export default router;