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.
- package/dist/index.js +765 -426
- package/dist/index.js.map +1 -1
- package/dist/web/app/assets/main-BJPGJG4y.js +9 -0
- package/dist/web/app/index.html +1 -1
- package/dist/web/server.js +752 -61
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
- package/dist/web/app/assets/main-DZvNWN99.js +0 -9
package/dist/web/server.js
CHANGED
|
@@ -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
|
|
341
|
-
if (!
|
|
342
|
-
|
|
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
|
-
|
|
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
|
|
373
|
-
if (!
|
|
374
|
-
|
|
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
|
-
|
|
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
|
|
407
|
-
if (!
|
|
408
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|