roadmap-skill 0.2.3 → 0.2.5

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.
@@ -69,6 +69,7 @@ var init_file_helpers = __esm({
69
69
  init_esm_shims();
70
70
  import express from "express";
71
71
  import * as path4 from "path";
72
+ import { fileURLToPath as fileURLToPath2 } from "url";
72
73
 
73
74
  // src/storage/index.ts
74
75
  init_esm_shims();
@@ -268,10 +269,662 @@ var ProjectStorage = class {
268
269
  }
269
270
  return results;
270
271
  }
272
+ async exportAllData() {
273
+ await this.ensureDirectory();
274
+ const fs2 = await import("fs/promises");
275
+ const files = await fs2.readdir(this.storageDir);
276
+ const jsonFiles = files.filter((f) => f.endsWith(".json"));
277
+ const projects = [];
278
+ for (const file of jsonFiles) {
279
+ try {
280
+ const filePath = path3.join(this.storageDir, file);
281
+ const data = await readJsonFile(filePath);
282
+ projects.push(data);
283
+ } catch {
284
+ continue;
285
+ }
286
+ }
287
+ return {
288
+ version: 1,
289
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
290
+ projects
291
+ };
292
+ }
293
+ async importAllData(data) {
294
+ await this.ensureDirectory();
295
+ let imported = 0;
296
+ let errors = 0;
297
+ const errorDetails = [];
298
+ if (!data.projects || !Array.isArray(data.projects)) {
299
+ throw new Error("Invalid backup data: projects array is required");
300
+ }
301
+ for (const projectData of data.projects) {
302
+ try {
303
+ if (!projectData.project || !projectData.project.id) {
304
+ errors++;
305
+ errorDetails.push("Skipping invalid project: missing project or id");
306
+ continue;
307
+ }
308
+ const filePath = this.getFilePath(projectData.project.id);
309
+ await writeJsonFile(filePath, projectData);
310
+ imported++;
311
+ } catch (error) {
312
+ errors++;
313
+ const errorMessage = error instanceof Error ? error.message : String(error);
314
+ errorDetails.push(`Failed to import project ${projectData.project?.id || "unknown"}: ${errorMessage}`);
315
+ }
316
+ }
317
+ return {
318
+ success: errors === 0,
319
+ imported,
320
+ errors,
321
+ errorDetails
322
+ };
323
+ }
271
324
  };
272
325
  var storage = new ProjectStorage();
273
326
 
327
+ // src/services/index.ts
328
+ init_esm_shims();
329
+
330
+ // src/services/tag-service.ts
331
+ init_esm_shims();
332
+ function generateTagId() {
333
+ return `tag_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
334
+ }
335
+ var TagService = class {
336
+ storage;
337
+ constructor(storage2) {
338
+ this.storage = storage2;
339
+ }
340
+ /**
341
+ * Create a new tag in a project
342
+ * @param projectId - The project ID
343
+ * @param data - Tag creation data
344
+ * @returns The created tag or error
345
+ */
346
+ async create(projectId, data) {
347
+ try {
348
+ const projectData = await this.storage.readProject(projectId);
349
+ if (!projectData) {
350
+ return {
351
+ success: false,
352
+ error: `Project with ID '${projectId}' not found`,
353
+ code: "NOT_FOUND"
354
+ };
355
+ }
356
+ const existingTag = projectData.tags.find(
357
+ (t) => t.name.toLowerCase() === data.name.toLowerCase()
358
+ );
359
+ if (existingTag) {
360
+ return {
361
+ success: false,
362
+ error: `Tag with name '${data.name}' already exists in this project`,
363
+ code: "DUPLICATE_ERROR"
364
+ };
365
+ }
366
+ const now = (/* @__PURE__ */ new Date()).toISOString();
367
+ const tag = {
368
+ id: generateTagId(),
369
+ name: data.name,
370
+ color: data.color,
371
+ description: data.description || "",
372
+ createdAt: now
373
+ };
374
+ projectData.tags.push(tag);
375
+ projectData.project.updatedAt = now;
376
+ await this.saveProjectData(projectId, projectData);
377
+ return {
378
+ success: true,
379
+ data: tag
380
+ };
381
+ } catch (error) {
382
+ return {
383
+ success: false,
384
+ error: error instanceof Error ? error.message : "Failed to create tag",
385
+ code: "INTERNAL_ERROR"
386
+ };
387
+ }
388
+ }
389
+ /**
390
+ * List all tags in a project
391
+ * @param projectId - The project ID
392
+ * @returns Array of tags or error
393
+ */
394
+ async list(projectId) {
395
+ try {
396
+ const projectData = await this.storage.readProject(projectId);
397
+ if (!projectData) {
398
+ return {
399
+ success: false,
400
+ error: `Project with ID '${projectId}' not found`,
401
+ code: "NOT_FOUND"
402
+ };
403
+ }
404
+ return {
405
+ success: true,
406
+ data: projectData.tags
407
+ };
408
+ } catch (error) {
409
+ return {
410
+ success: false,
411
+ error: error instanceof Error ? error.message : "Failed to list tags",
412
+ code: "INTERNAL_ERROR"
413
+ };
414
+ }
415
+ }
416
+ /**
417
+ * Update an existing tag
418
+ * @param projectId - The project ID
419
+ * @param tagId - The tag ID
420
+ * @param data - Tag update data
421
+ * @returns The updated tag or error
422
+ */
423
+ async update(projectId, tagId, data) {
424
+ try {
425
+ const projectData = await this.storage.readProject(projectId);
426
+ if (!projectData) {
427
+ return {
428
+ success: false,
429
+ error: `Project with ID '${projectId}' not found`,
430
+ code: "NOT_FOUND"
431
+ };
432
+ }
433
+ const tagIndex = projectData.tags.findIndex((t) => t.id === tagId);
434
+ if (tagIndex === -1) {
435
+ return {
436
+ success: false,
437
+ error: `Tag with ID '${tagId}' not found in project '${projectId}'`,
438
+ code: "NOT_FOUND"
439
+ };
440
+ }
441
+ if (Object.keys(data).length === 0) {
442
+ return {
443
+ success: false,
444
+ error: "At least one field to update is required",
445
+ code: "VALIDATION_ERROR"
446
+ };
447
+ }
448
+ if (data.name) {
449
+ const existingTag2 = projectData.tags.find(
450
+ (t) => t.name.toLowerCase() === data.name.toLowerCase() && t.id !== tagId
451
+ );
452
+ if (existingTag2) {
453
+ return {
454
+ success: false,
455
+ error: `Tag with name '${data.name}' already exists in this project`,
456
+ code: "DUPLICATE_ERROR"
457
+ };
458
+ }
459
+ }
460
+ const now = (/* @__PURE__ */ new Date()).toISOString();
461
+ const existingTag = projectData.tags[tagIndex];
462
+ const updatedTag = {
463
+ ...existingTag,
464
+ ...data,
465
+ id: existingTag.id,
466
+ createdAt: existingTag.createdAt
467
+ };
468
+ projectData.tags[tagIndex] = updatedTag;
469
+ projectData.project.updatedAt = now;
470
+ await this.saveProjectData(projectId, projectData);
471
+ return {
472
+ success: true,
473
+ data: updatedTag
474
+ };
475
+ } catch (error) {
476
+ return {
477
+ success: false,
478
+ error: error instanceof Error ? error.message : "Failed to update tag",
479
+ code: "INTERNAL_ERROR"
480
+ };
481
+ }
482
+ }
483
+ /**
484
+ * Delete a tag by ID
485
+ * Also removes the tag from all tasks that use it
486
+ * @param projectId - The project ID
487
+ * @param tagId - The tag ID
488
+ * @returns Delete result with tag info and count of updated tasks
489
+ */
490
+ async delete(projectId, tagId) {
491
+ try {
492
+ const projectData = await this.storage.readProject(projectId);
493
+ if (!projectData) {
494
+ return {
495
+ success: false,
496
+ error: `Project with ID '${projectId}' not found`,
497
+ code: "NOT_FOUND"
498
+ };
499
+ }
500
+ const tagIndex = projectData.tags.findIndex((t) => t.id === tagId);
501
+ if (tagIndex === -1) {
502
+ return {
503
+ success: false,
504
+ error: `Tag with ID '${tagId}' not found in project '${projectId}'`,
505
+ code: "NOT_FOUND"
506
+ };
507
+ }
508
+ const tag = projectData.tags[tagIndex];
509
+ const now = (/* @__PURE__ */ new Date()).toISOString();
510
+ let tasksUpdated = 0;
511
+ for (const task of projectData.tasks) {
512
+ const tagIndexInTask = task.tags.indexOf(tagId);
513
+ if (tagIndexInTask !== -1) {
514
+ task.tags.splice(tagIndexInTask, 1);
515
+ task.updatedAt = now;
516
+ tasksUpdated++;
517
+ }
518
+ }
519
+ projectData.tags.splice(tagIndex, 1);
520
+ projectData.project.updatedAt = now;
521
+ await this.saveProjectData(projectId, projectData);
522
+ return {
523
+ success: true,
524
+ data: {
525
+ deleted: true,
526
+ tag,
527
+ tasksUpdated
528
+ }
529
+ };
530
+ } catch (error) {
531
+ return {
532
+ success: false,
533
+ error: error instanceof Error ? error.message : "Failed to delete tag",
534
+ code: "INTERNAL_ERROR"
535
+ };
536
+ }
537
+ }
538
+ /**
539
+ * Get all tasks that have a specific tag by tag name
540
+ * @param projectId - The project ID
541
+ * @param tagName - The tag name
542
+ * @returns Tag info and matching tasks
543
+ */
544
+ async getTasksByTag(projectId, tagName) {
545
+ try {
546
+ const projectData = await this.storage.readProject(projectId);
547
+ if (!projectData) {
548
+ return {
549
+ success: false,
550
+ error: `Project with ID '${projectId}' not found`,
551
+ code: "NOT_FOUND"
552
+ };
553
+ }
554
+ const tag = projectData.tags.find(
555
+ (t) => t.name.toLowerCase() === tagName.toLowerCase()
556
+ );
557
+ if (!tag) {
558
+ return {
559
+ success: false,
560
+ error: `Tag with name '${tagName}' not found in project '${projectId}'`,
561
+ code: "NOT_FOUND"
562
+ };
563
+ }
564
+ const tasks = projectData.tasks.filter((t) => t.tags.includes(tag.id));
565
+ return {
566
+ success: true,
567
+ data: {
568
+ tag,
569
+ tasks,
570
+ count: tasks.length
571
+ }
572
+ };
573
+ } catch (error) {
574
+ return {
575
+ success: false,
576
+ error: error instanceof Error ? error.message : "Failed to get tasks by tag",
577
+ code: "INTERNAL_ERROR"
578
+ };
579
+ }
580
+ }
581
+ /**
582
+ * Helper method to save project data
583
+ * @param projectId - The project ID
584
+ * @param projectData - The project data to save
585
+ */
586
+ async saveProjectData(projectId, projectData) {
587
+ const filePath = this.storage.getFilePath(projectId);
588
+ const { writeJsonFile: writeJsonFile2 } = await Promise.resolve().then(() => (init_file_helpers(), file_helpers_exports));
589
+ await writeJsonFile2(filePath, projectData);
590
+ }
591
+ };
592
+
593
+ // src/services/task-service.ts
594
+ init_esm_shims();
595
+ init_file_helpers();
596
+ function generateTaskId() {
597
+ return `task_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
598
+ }
599
+ function calculateCompletedAt(currentStatus, newStatus, existingCompletedAt, now) {
600
+ if (!newStatus) {
601
+ return existingCompletedAt;
602
+ }
603
+ if (newStatus === "done" && currentStatus !== "done") {
604
+ return now;
605
+ }
606
+ if (currentStatus === "done" && newStatus !== "done") {
607
+ return null;
608
+ }
609
+ return existingCompletedAt;
610
+ }
611
+ var TaskService = {
612
+ /**
613
+ * Create a new task in a project
614
+ * @param projectId - The project ID
615
+ * @param data - Task creation data
616
+ * @returns The created task or error
617
+ */
618
+ async create(projectId, data) {
619
+ try {
620
+ const projectData = await storage.readProject(projectId);
621
+ if (!projectData) {
622
+ return {
623
+ success: false,
624
+ error: `Project with ID '${projectId}' not found`,
625
+ code: "NOT_FOUND"
626
+ };
627
+ }
628
+ const now = (/* @__PURE__ */ new Date()).toISOString();
629
+ const task = {
630
+ id: generateTaskId(),
631
+ projectId,
632
+ title: data.title,
633
+ description: data.description,
634
+ status: "todo",
635
+ priority: data.priority ?? "medium",
636
+ tags: data.tags ?? [],
637
+ dueDate: data.dueDate ?? null,
638
+ assignee: data.assignee ?? null,
639
+ createdAt: now,
640
+ updatedAt: now,
641
+ completedAt: null
642
+ };
643
+ projectData.tasks.push(task);
644
+ projectData.project.updatedAt = now;
645
+ const filePath = storage.getFilePath(projectId);
646
+ await writeJsonFile(filePath, projectData);
647
+ return {
648
+ success: true,
649
+ data: task
650
+ };
651
+ } catch (error) {
652
+ return {
653
+ success: false,
654
+ error: error instanceof Error ? error.message : "Failed to create task",
655
+ code: "INTERNAL_ERROR"
656
+ };
657
+ }
658
+ },
659
+ /**
660
+ * Get a specific task by project ID and task ID
661
+ * @param projectId - The project ID
662
+ * @param taskId - The task ID
663
+ * @returns The task or error
664
+ */
665
+ async get(projectId, taskId) {
666
+ try {
667
+ const projectData = await storage.readProject(projectId);
668
+ if (!projectData) {
669
+ return {
670
+ success: false,
671
+ error: `Project with ID '${projectId}' not found`,
672
+ code: "NOT_FOUND"
673
+ };
674
+ }
675
+ const task = projectData.tasks.find((t) => t.id === taskId);
676
+ if (!task) {
677
+ return {
678
+ success: false,
679
+ error: `Task with ID '${taskId}' not found in project '${projectId}'`,
680
+ code: "NOT_FOUND"
681
+ };
682
+ }
683
+ return {
684
+ success: true,
685
+ data: task
686
+ };
687
+ } catch (error) {
688
+ return {
689
+ success: false,
690
+ error: error instanceof Error ? error.message : "Failed to get task",
691
+ code: "INTERNAL_ERROR"
692
+ };
693
+ }
694
+ },
695
+ /**
696
+ * Update an existing task
697
+ * Handles completedAt automatically based on status changes
698
+ * @param projectId - The project ID
699
+ * @param taskId - The task ID
700
+ * @param data - Task update data
701
+ * @returns The updated task or error
702
+ */
703
+ async update(projectId, taskId, data) {
704
+ try {
705
+ const projectData = await storage.readProject(projectId);
706
+ if (!projectData) {
707
+ return {
708
+ success: false,
709
+ error: `Project with ID '${projectId}' not found`,
710
+ code: "NOT_FOUND"
711
+ };
712
+ }
713
+ const taskIndex = projectData.tasks.findIndex((t) => t.id === taskId);
714
+ if (taskIndex === -1) {
715
+ return {
716
+ success: false,
717
+ error: `Task with ID '${taskId}' not found in project '${projectId}'`,
718
+ code: "NOT_FOUND"
719
+ };
720
+ }
721
+ const updateKeys = Object.keys(data);
722
+ if (updateKeys.length === 0) {
723
+ return {
724
+ success: false,
725
+ error: "At least one field to update is required",
726
+ code: "VALIDATION_ERROR"
727
+ };
728
+ }
729
+ const now = (/* @__PURE__ */ new Date()).toISOString();
730
+ const existingTask = projectData.tasks[taskIndex];
731
+ const completedAt = calculateCompletedAt(
732
+ existingTask.status,
733
+ data.status,
734
+ existingTask.completedAt,
735
+ now
736
+ );
737
+ const updatedTask = {
738
+ ...existingTask,
739
+ ...data,
740
+ id: existingTask.id,
741
+ projectId: existingTask.projectId,
742
+ createdAt: existingTask.createdAt,
743
+ updatedAt: now,
744
+ completedAt
745
+ };
746
+ projectData.tasks[taskIndex] = updatedTask;
747
+ projectData.project.updatedAt = now;
748
+ const filePath = storage.getFilePath(projectId);
749
+ await writeJsonFile(filePath, projectData);
750
+ return {
751
+ success: true,
752
+ data: updatedTask
753
+ };
754
+ } catch (error) {
755
+ return {
756
+ success: false,
757
+ error: error instanceof Error ? error.message : "Failed to update task",
758
+ code: "INTERNAL_ERROR"
759
+ };
760
+ }
761
+ },
762
+ /**
763
+ * Delete a task by project ID and task ID
764
+ * @param projectId - The project ID
765
+ * @param taskId - The task ID
766
+ * @returns Void or error
767
+ */
768
+ async delete(projectId, taskId) {
769
+ try {
770
+ const projectData = await storage.readProject(projectId);
771
+ if (!projectData) {
772
+ return {
773
+ success: false,
774
+ error: `Project with ID '${projectId}' not found`,
775
+ code: "NOT_FOUND"
776
+ };
777
+ }
778
+ const taskIndex = projectData.tasks.findIndex((t) => t.id === taskId);
779
+ if (taskIndex === -1) {
780
+ return {
781
+ success: false,
782
+ error: `Task with ID '${taskId}' not found in project '${projectId}'`,
783
+ code: "NOT_FOUND"
784
+ };
785
+ }
786
+ const now = (/* @__PURE__ */ new Date()).toISOString();
787
+ projectData.tasks.splice(taskIndex, 1);
788
+ projectData.project.updatedAt = now;
789
+ const filePath = storage.getFilePath(projectId);
790
+ await writeJsonFile(filePath, projectData);
791
+ return {
792
+ success: true,
793
+ data: void 0
794
+ };
795
+ } catch (error) {
796
+ return {
797
+ success: false,
798
+ error: error instanceof Error ? error.message : "Failed to delete task",
799
+ code: "INTERNAL_ERROR"
800
+ };
801
+ }
802
+ },
803
+ /**
804
+ * List tasks with optional filters
805
+ * @param filters - Optional filters for the search
806
+ * @returns Array of tasks or error
807
+ */
808
+ async list(filters) {
809
+ try {
810
+ const results = await storage.searchTasks({
811
+ projectId: filters?.projectId,
812
+ status: filters?.status,
813
+ priority: filters?.priority,
814
+ tags: filters?.tags,
815
+ assignee: filters?.assignee,
816
+ dueBefore: filters?.dueBefore,
817
+ dueAfter: filters?.dueAfter,
818
+ includeCompleted: filters?.includeCompleted
819
+ });
820
+ const tasks = results.map((r) => r.task);
821
+ return {
822
+ success: true,
823
+ data: tasks
824
+ };
825
+ } catch (error) {
826
+ return {
827
+ success: false,
828
+ error: error instanceof Error ? error.message : "Failed to list tasks",
829
+ code: "INTERNAL_ERROR"
830
+ };
831
+ }
832
+ },
833
+ /**
834
+ * Update multiple tasks at once
835
+ * @param projectId - The project ID
836
+ * @param taskIds - Array of task IDs to update
837
+ * @param data - Batch update data
838
+ * @returns Batch update result or error
839
+ */
840
+ async batchUpdate(projectId, taskIds, data) {
841
+ try {
842
+ const projectData = await storage.readProject(projectId);
843
+ if (!projectData) {
844
+ return {
845
+ success: false,
846
+ error: `Project with ID '${projectId}' not found`,
847
+ code: "NOT_FOUND"
848
+ };
849
+ }
850
+ const now = (/* @__PURE__ */ new Date()).toISOString();
851
+ const updatedTasks = [];
852
+ const notFoundIds = [];
853
+ for (const taskId of taskIds) {
854
+ const taskIndex = projectData.tasks.findIndex((t) => t.id === taskId);
855
+ if (taskIndex === -1) {
856
+ notFoundIds.push(taskId);
857
+ continue;
858
+ }
859
+ const existingTask = projectData.tasks[taskIndex];
860
+ let updatedTags = existingTask.tags;
861
+ if (data.tags && data.tags.length > 0) {
862
+ const existingTags = existingTask.tags || [];
863
+ switch (data.tagOperation) {
864
+ case "add":
865
+ updatedTags = [.../* @__PURE__ */ new Set([...existingTags, ...data.tags])];
866
+ break;
867
+ case "remove":
868
+ updatedTags = existingTags.filter((tag) => !data.tags.includes(tag));
869
+ break;
870
+ case "replace":
871
+ default:
872
+ updatedTags = data.tags;
873
+ break;
874
+ }
875
+ }
876
+ const completedAt = calculateCompletedAt(
877
+ existingTask.status,
878
+ data.status,
879
+ existingTask.completedAt,
880
+ now
881
+ );
882
+ const updatedTask = {
883
+ ...existingTask,
884
+ ...data.status && { status: data.status },
885
+ ...data.priority && { priority: data.priority },
886
+ tags: updatedTags,
887
+ updatedAt: now,
888
+ completedAt
889
+ };
890
+ projectData.tasks[taskIndex] = updatedTask;
891
+ updatedTasks.push(updatedTask);
892
+ }
893
+ if (updatedTasks.length === 0) {
894
+ return {
895
+ success: false,
896
+ error: "No tasks were found to update",
897
+ code: "NOT_FOUND"
898
+ };
899
+ }
900
+ projectData.project.updatedAt = now;
901
+ const filePath = storage.getFilePath(projectId);
902
+ await writeJsonFile(filePath, projectData);
903
+ return {
904
+ success: true,
905
+ data: {
906
+ updatedTasks,
907
+ updatedCount: updatedTasks.length,
908
+ notFoundIds: notFoundIds.length > 0 ? notFoundIds : void 0
909
+ }
910
+ };
911
+ } catch (error) {
912
+ return {
913
+ success: false,
914
+ error: error instanceof Error ? error.message : "Failed to batch update tasks",
915
+ code: "INTERNAL_ERROR"
916
+ };
917
+ }
918
+ }
919
+ };
920
+
921
+ // src/services/types.ts
922
+ init_esm_shims();
923
+
274
924
  // src/web/server.ts
925
+ var __filename2 = fileURLToPath2(import.meta.url);
926
+ var __dirname2 = path4.dirname(__filename2);
927
+ var tagService = new TagService(storage);
275
928
  function createServer(port = 7860) {
276
929
  return new Promise((resolve, reject) => {
277
930
  const app = express();
@@ -337,31 +990,13 @@ function createServer(port = 7860) {
337
990
  app.post("/api/tasks", async (req, res) => {
338
991
  try {
339
992
  const { projectId, ...taskData } = req.body;
340
- const projectData = await storage.readProject(projectId);
341
- if (!projectData) {
342
- res.status(404).json({ error: "Project not found" });
993
+ const result = await TaskService.create(projectId, taskData);
994
+ if (!result.success) {
995
+ const statusCode = result.code === "NOT_FOUND" ? 404 : 400;
996
+ res.status(statusCode).json({ error: result.error });
343
997
  return;
344
998
  }
345
- const now = (/* @__PURE__ */ new Date()).toISOString();
346
- const task = {
347
- id: `task_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
348
- projectId,
349
- ...taskData,
350
- status: taskData.status || "todo",
351
- priority: taskData.priority || "medium",
352
- tags: taskData.tags || [],
353
- dueDate: taskData.dueDate || null,
354
- assignee: taskData.assignee || null,
355
- createdAt: now,
356
- updatedAt: now,
357
- completedAt: null
358
- };
359
- projectData.tasks.push(task);
360
- projectData.project.updatedAt = now;
361
- const filePath = storage.getFilePath(projectId);
362
- const { writeJsonFile: writeJsonFile2 } = await Promise.resolve().then(() => (init_file_helpers(), file_helpers_exports));
363
- await writeJsonFile2(filePath, projectData);
364
- res.json({ success: true, data: task });
999
+ res.json({ success: true, data: result.data });
365
1000
  } catch (error) {
366
1001
  res.status(500).json({ error: error.message });
367
1002
  }
@@ -369,33 +1004,13 @@ function createServer(port = 7860) {
369
1004
  app.put("/api/tasks", async (req, res) => {
370
1005
  try {
371
1006
  const { projectId, taskId, ...updateData } = req.body;
372
- const projectData = await storage.readProject(projectId);
373
- if (!projectData) {
374
- res.status(404).json({ error: "Project not found" });
1007
+ const result = await TaskService.update(projectId, taskId, updateData);
1008
+ if (!result.success) {
1009
+ const statusCode = result.code === "NOT_FOUND" ? 404 : 400;
1010
+ res.status(statusCode).json({ error: result.error });
375
1011
  return;
376
1012
  }
377
- const taskIndex = projectData.tasks.findIndex((t) => t.id === taskId);
378
- if (taskIndex === -1) {
379
- res.status(404).json({ error: "Task not found" });
380
- return;
381
- }
382
- const now = (/* @__PURE__ */ new Date()).toISOString();
383
- const existingTask = projectData.tasks[taskIndex];
384
- const updatedTask = {
385
- ...existingTask,
386
- ...updateData,
387
- id: existingTask.id,
388
- projectId: existingTask.projectId,
389
- createdAt: existingTask.createdAt,
390
- updatedAt: now,
391
- completedAt: updateData.status === "done" && existingTask.status !== "done" ? now : updateData.status && updateData.status !== "done" ? null : existingTask.completedAt
392
- };
393
- projectData.tasks[taskIndex] = updatedTask;
394
- projectData.project.updatedAt = now;
395
- const filePath = storage.getFilePath(projectId);
396
- const { writeJsonFile: writeJsonFile2 } = await Promise.resolve().then(() => (init_file_helpers(), file_helpers_exports));
397
- await writeJsonFile2(filePath, projectData);
398
- res.json({ success: true, data: updatedTask });
1013
+ res.json({ success: true, data: result.data });
399
1014
  } catch (error) {
400
1015
  res.status(500).json({ error: error.message });
401
1016
  }
@@ -403,27 +1018,96 @@ function createServer(port = 7860) {
403
1018
  app.delete("/api/tasks", async (req, res) => {
404
1019
  try {
405
1020
  const { projectId, taskId } = req.query;
406
- const projectData = await storage.readProject(projectId);
407
- if (!projectData) {
408
- res.status(404).json({ error: "Project not found" });
1021
+ const result = await TaskService.delete(projectId, taskId);
1022
+ if (!result.success) {
1023
+ const statusCode = result.code === "NOT_FOUND" ? 404 : 400;
1024
+ res.status(statusCode).json({ error: result.error });
409
1025
  return;
410
1026
  }
411
- const taskIndex = projectData.tasks.findIndex((t) => t.id === taskId);
412
- if (taskIndex === -1) {
413
- res.status(404).json({ error: "Task not found" });
1027
+ res.json({ success: true });
1028
+ } catch (error) {
1029
+ res.status(500).json({ error: error.message });
1030
+ }
1031
+ });
1032
+ app.post("/api/projects/:projectId/tags", async (req, res) => {
1033
+ try {
1034
+ const { projectId } = req.params;
1035
+ const result = await tagService.create(projectId, req.body);
1036
+ if (!result.success) {
1037
+ const statusCode = result.code === "NOT_FOUND" ? 404 : 400;
1038
+ res.status(statusCode).json({ error: result.error });
414
1039
  return;
415
1040
  }
416
- projectData.tasks.splice(taskIndex, 1);
417
- projectData.project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
418
- const filePath = storage.getFilePath(projectId);
419
- const { writeJsonFile: writeJsonFile2 } = await Promise.resolve().then(() => (init_file_helpers(), file_helpers_exports));
420
- await writeJsonFile2(filePath, projectData);
421
- res.json({ success: true });
1041
+ res.json({ success: true, data: result.data });
422
1042
  } catch (error) {
423
1043
  res.status(500).json({ error: error.message });
424
1044
  }
425
1045
  });
426
- const distPath = path4.join(process.cwd(), "dist", "web", "app");
1046
+ app.get("/api/projects/:projectId/tags", async (req, res) => {
1047
+ try {
1048
+ const { projectId } = req.params;
1049
+ const result = await tagService.list(projectId);
1050
+ if (!result.success) {
1051
+ const statusCode = result.code === "NOT_FOUND" ? 404 : 400;
1052
+ res.status(statusCode).json({ error: result.error });
1053
+ return;
1054
+ }
1055
+ res.json({ success: true, data: result.data });
1056
+ } catch (error) {
1057
+ res.status(500).json({ error: error.message });
1058
+ }
1059
+ });
1060
+ app.put("/api/projects/:projectId/tags/:tagId", async (req, res) => {
1061
+ try {
1062
+ const { projectId, tagId } = req.params;
1063
+ const result = await tagService.update(projectId, tagId, req.body);
1064
+ if (!result.success) {
1065
+ const statusCode = result.code === "NOT_FOUND" ? 404 : 400;
1066
+ res.status(statusCode).json({ error: result.error });
1067
+ return;
1068
+ }
1069
+ res.json({ success: true, data: result.data });
1070
+ } catch (error) {
1071
+ res.status(500).json({ error: error.message });
1072
+ }
1073
+ });
1074
+ app.delete("/api/projects/:projectId/tags/:tagId", async (req, res) => {
1075
+ try {
1076
+ const { projectId, tagId } = req.params;
1077
+ const result = await tagService.delete(projectId, tagId);
1078
+ if (!result.success) {
1079
+ const statusCode = result.code === "NOT_FOUND" ? 404 : 400;
1080
+ res.status(statusCode).json({ error: result.error });
1081
+ return;
1082
+ }
1083
+ res.json({ success: true, data: result.data });
1084
+ } catch (error) {
1085
+ res.status(500).json({ error: error.message });
1086
+ }
1087
+ });
1088
+ app.get("/api/backup", async (_req, res) => {
1089
+ try {
1090
+ const backup = await storage.exportAllData();
1091
+ const filename = `roadmap-skill-backup-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`;
1092
+ res.setHeader("Content-Type", "application/json");
1093
+ res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
1094
+ res.json(backup);
1095
+ } catch (error) {
1096
+ res.status(500).json({ error: error.message });
1097
+ }
1098
+ });
1099
+ app.post("/api/backup", async (req, res) => {
1100
+ try {
1101
+ const result = await storage.importAllData(req.body);
1102
+ res.json(result);
1103
+ } catch (error) {
1104
+ res.status(400).json({
1105
+ success: false,
1106
+ error: error.message
1107
+ });
1108
+ }
1109
+ });
1110
+ const distPath = path4.join(__dirname2, "app");
427
1111
  app.use(express.static(distPath));
428
1112
  app.get("*", (req, res) => {
429
1113
  if (req.path.startsWith("/api")) {
@@ -446,6 +1130,13 @@ function createServer(port = 7860) {
446
1130
  });
447
1131
  });
448
1132
  }
1133
+ if (process.argv[1]?.endsWith("server.js")) {
1134
+ const port = parseInt(process.argv[2] || "7860", 10);
1135
+ createServer(port).catch((err) => {
1136
+ console.error("Failed to start server:", err);
1137
+ process.exit(1);
1138
+ });
1139
+ }
449
1140
  export {
450
1141
  createServer
451
1142
  };