project-roadmap-tracking 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +291 -24
  2. package/dist/commands/add.d.ts +2 -0
  3. package/dist/commands/add.js +39 -31
  4. package/dist/commands/complete.d.ts +2 -0
  5. package/dist/commands/complete.js +35 -12
  6. package/dist/commands/init.d.ts +1 -0
  7. package/dist/commands/init.js +63 -46
  8. package/dist/commands/list.d.ts +3 -0
  9. package/dist/commands/list.js +65 -62
  10. package/dist/commands/pass-test.d.ts +4 -1
  11. package/dist/commands/pass-test.js +36 -13
  12. package/dist/commands/show.d.ts +4 -1
  13. package/dist/commands/show.js +38 -59
  14. package/dist/commands/update.d.ts +2 -0
  15. package/dist/commands/update.js +54 -31
  16. package/dist/commands/validate.d.ts +4 -1
  17. package/dist/commands/validate.js +74 -32
  18. package/dist/errors/base.error.d.ts +21 -0
  19. package/dist/errors/base.error.js +35 -0
  20. package/dist/errors/circular-dependency.error.d.ts +8 -0
  21. package/dist/errors/circular-dependency.error.js +13 -0
  22. package/dist/errors/config-not-found.error.d.ts +7 -0
  23. package/dist/errors/config-not-found.error.js +12 -0
  24. package/dist/errors/index.d.ts +16 -0
  25. package/dist/errors/index.js +26 -0
  26. package/dist/errors/invalid-task.error.d.ts +7 -0
  27. package/dist/errors/invalid-task.error.js +12 -0
  28. package/dist/errors/roadmap-not-found.error.d.ts +7 -0
  29. package/dist/errors/roadmap-not-found.error.js +12 -0
  30. package/dist/errors/task-not-found.error.d.ts +7 -0
  31. package/dist/errors/task-not-found.error.js +12 -0
  32. package/dist/errors/validation.error.d.ts +16 -0
  33. package/dist/errors/validation.error.js +16 -0
  34. package/dist/repositories/config.repository.d.ts +76 -0
  35. package/dist/repositories/config.repository.js +282 -0
  36. package/dist/repositories/index.d.ts +2 -0
  37. package/dist/repositories/index.js +2 -0
  38. package/dist/repositories/roadmap.repository.d.ts +82 -0
  39. package/dist/repositories/roadmap.repository.js +201 -0
  40. package/dist/services/display.service.d.ts +182 -0
  41. package/dist/services/display.service.js +320 -0
  42. package/dist/services/error-handler.service.d.ts +114 -0
  43. package/dist/services/error-handler.service.js +169 -0
  44. package/dist/services/roadmap.service.d.ts +142 -0
  45. package/dist/services/roadmap.service.js +269 -0
  46. package/dist/services/task-dependency.service.d.ts +210 -0
  47. package/dist/services/task-dependency.service.js +371 -0
  48. package/dist/services/task-query.service.d.ts +123 -0
  49. package/dist/services/task-query.service.js +259 -0
  50. package/dist/services/task.service.d.ts +132 -0
  51. package/dist/services/task.service.js +173 -0
  52. package/dist/util/read-config.js +12 -2
  53. package/dist/util/read-roadmap.js +12 -2
  54. package/dist/util/types.d.ts +5 -0
  55. package/dist/util/update-task.js +2 -1
  56. package/dist/util/validate-task.js +6 -5
  57. package/oclif.manifest.json +114 -5
  58. package/package.json +19 -3
@@ -1,5 +1,6 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
2
  import { mkdir, readdir, writeFile } from 'node:fs/promises';
3
+ import errorHandlerService from '../services/error-handler.service.js';
3
4
  import { PRIORITY, STATUS, TASK_TYPE } from '../util/types.js';
4
5
  export default class Init extends Command {
5
6
  static args = {
@@ -13,6 +14,11 @@ export default class Init extends Command {
13
14
  force: Flags.boolean({ char: 'f', description: 'force initialization even if files already exist' }),
14
15
  // flag with a value (-n, --name=VALUE)
15
16
  name: Flags.string({ char: 'n', description: 'name to print' }),
17
+ verbose: Flags.boolean({
18
+ char: 'v',
19
+ default: false,
20
+ description: 'show detailed error information including stack traces',
21
+ }),
16
22
  withSampleTasks: Flags.boolean({ description: 'include sample tasks in the initialized roadmap' }),
17
23
  };
18
24
  buildBlankRoadmap({ description, name, withSampleTasks, }) {
@@ -48,7 +54,12 @@ export default class Init extends Command {
48
54
  }
49
55
  buildConfig({ description, name, path }) {
50
56
  return {
51
- $schema: 'https://raw.githubusercontent.com/ZacharyEggert/project-roadmap-tracking/refs/heads/master/schemas/config/v1.json',
57
+ $schema: 'https://raw.githubusercontent.com/ZacharyEggert/project-roadmap-tracking/refs/heads/master/schemas/config/v1.1.json',
58
+ cache: {
59
+ enabled: true,
60
+ maxSize: 10,
61
+ watchFiles: true,
62
+ },
52
63
  metadata: {
53
64
  description: `${description}`,
54
65
  name: `${name}`,
@@ -58,54 +69,60 @@ export default class Init extends Command {
58
69
  }
59
70
  async run() {
60
71
  const { args, flags } = await this.parse(Init);
61
- const path = args.folder ?? '.';
62
- this.log(`creating project roadmap in${path === '.' ? ' current directory' : ': ' + args.folder}`);
63
- const name = flags.name ?? 'My Project Roadmap';
64
- const description = flags.description ?? 'A project roadmap managed by Project Roadmap Tracking';
65
- // create prt.json and prt.config.json files here
66
- const config = this.buildConfig({ description, name, path });
67
- const roadmap = this.buildBlankRoadmap({ description, name, withSampleTasks: flags.withSampleTasks ?? false });
68
- if (path !== '.') {
69
- // check if target directory exists
70
- // if it does not, create it
71
- await readdir(path).catch(async (error) => {
72
- if (error) {
73
- this.log(`target directory does not exist, creating: ${path}`);
74
- try {
75
- // create the directory
76
- await mkdir(path, { recursive: true });
77
- }
78
- catch (error) {
79
- this.error(`failed to create target directory: ${error.message}`);
72
+ try {
73
+ const path = args.folder ?? '.';
74
+ this.log(`creating project roadmap in${path === '.' ? ' current directory' : ': ' + args.folder}`);
75
+ const name = flags.name ?? 'My Project Roadmap';
76
+ const description = flags.description ?? 'A project roadmap managed by Project Roadmap Tracking';
77
+ // create prt.json and prt.config.json files here
78
+ const config = this.buildConfig({ description, name, path });
79
+ const roadmap = this.buildBlankRoadmap({ description, name, withSampleTasks: flags.withSampleTasks ?? false });
80
+ if (path !== '.') {
81
+ // check if target directory exists
82
+ // if it does not, create it
83
+ await readdir(path).catch(async (error) => {
84
+ if (error) {
85
+ this.log(`target directory does not exist, creating: ${path}`);
86
+ try {
87
+ // create the directory
88
+ await mkdir(path, { recursive: true });
89
+ }
90
+ catch (error) {
91
+ this.error(`failed to create target directory: ${error.message}`);
92
+ }
80
93
  }
94
+ });
95
+ }
96
+ // check if config already exists in the target directory
97
+ // if it does, and --force is not set, throw an error
98
+ // if it does, and --force is set, overwrite the files
99
+ await readdir('.')
100
+ .then(async (files) => {
101
+ if (!flags.force && (files.includes('prt.json') || files.includes('prt.config.json'))) {
102
+ this.error('prt.config.json already exist in the current directory. Use --force to overwrite.');
103
+ }
104
+ await writeFile(`./.prtrc.json`, JSON.stringify(config, null, 2));
105
+ this.log('project roadmap config initialized');
106
+ })
107
+ .catch((error) => {
108
+ this.error(`failed to read current directory: ${error.message}`);
109
+ });
110
+ // create prt.json and prt.config.json files in specified directory
111
+ await readdir(path)
112
+ .then(async (files) => {
113
+ if (!flags.force && files.includes('prt.json')) {
114
+ this.error('prt.json already exists in the target directory. Use --force to overwrite.');
81
115
  }
116
+ await writeFile(`${path}/prt.json`, JSON.stringify(roadmap, null, 2));
117
+ this.log('project roadmap initialized');
118
+ })
119
+ .catch((error) => {
120
+ this.error(`failed to read directory: ${error.message}`);
82
121
  });
83
122
  }
84
- // check if config already exists in the target directory
85
- // if it does, and --force is not set, throw an error
86
- // if it does, and --force is set, overwrite the files
87
- await readdir('.')
88
- .then(async (files) => {
89
- if (!flags.force && (files.includes('prt.json') || files.includes('prt.config.json'))) {
90
- this.error('prt.config.json already exist in the current directory. Use --force to overwrite.');
91
- }
92
- await writeFile(`./.prtrc.json`, JSON.stringify(config, null, 2));
93
- this.log('project roadmap config initialized');
94
- })
95
- .catch((error) => {
96
- this.error(`failed to read current directory: ${error.message}`);
97
- });
98
- // create prt.json and prt.config.json files in specified directory
99
- await readdir(path)
100
- .then(async (files) => {
101
- if (!flags.force && files.includes('prt.json')) {
102
- this.error('prt.json already exists in the target directory. Use --force to overwrite.');
103
- }
104
- await writeFile(`${path}/prt.json`, JSON.stringify(roadmap, null, 2));
105
- this.log('project roadmap initialized');
106
- })
107
- .catch((error) => {
108
- this.error(`failed to read directory: ${error.message}`);
109
- });
123
+ catch (error) {
124
+ const exitCode = errorHandlerService.handleError(error);
125
+ this.error(errorHandlerService.formatErrorMessage(error, flags.verbose), { exit: exitCode });
126
+ }
110
127
  }
111
128
  }
@@ -5,9 +5,12 @@ export default class List extends Command {
5
5
  static examples: string[];
6
6
  static flags: {
7
7
  incomplete: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ 'no-repo': import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
9
  priority: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
10
  sort: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
13
  };
14
+ private static readonly priorityMap;
12
15
  run(): Promise<void>;
13
16
  }
@@ -1,7 +1,12 @@
1
1
  import { /* Args, */ Command, Flags } from '@oclif/core';
2
+ import { getDefaultConfigRepository } from '../repositories/config.repository.js';
3
+ import { RoadmapRepository } from '../repositories/roadmap.repository.js';
4
+ import displayService from '../services/display.service.js';
5
+ import errorHandlerService from '../services/error-handler.service.js';
6
+ import taskQueryService, { SortOrder } from '../services/task-query.service.js';
2
7
  import { readConfigFile } from '../util/read-config.js';
3
8
  import { readRoadmapFile } from '../util/read-roadmap.js';
4
- import { STATUS } from '../util/types.js';
9
+ import { PRIORITY } from '../util/types.js';
5
10
  export default class List extends Command {
6
11
  static args = {
7
12
  // file: Args.string({description: 'file to read'}),
@@ -14,6 +19,10 @@ export default class List extends Command {
14
19
  // flag with a value (-n, --name=VALUE)
15
20
  // name: Flags.string({char: 'n', description: 'name to print'}),
16
21
  incomplete: Flags.boolean({ char: 'i', description: 'filter tasks to show in-progress and not-started only' }),
22
+ 'no-repo': Flags.boolean({
23
+ default: false,
24
+ description: 'use legacy direct file I/O instead of repository pattern',
25
+ }),
17
26
  priority: Flags.string({
18
27
  char: 'p',
19
28
  description: 'filter tasks by priority (high, medium, low)',
@@ -29,73 +38,67 @@ export default class List extends Command {
29
38
  description: 'filter tasks by status (completed, in-progress, not-started)',
30
39
  options: ['completed', 'in-progress', 'not-started'],
31
40
  }),
41
+ verbose: Flags.boolean({
42
+ char: 'v',
43
+ default: false,
44
+ description: 'show detailed error information including stack traces',
45
+ }),
46
+ };
47
+ static priorityMap = {
48
+ h: PRIORITY.High,
49
+ high: PRIORITY.High,
50
+ l: PRIORITY.Low,
51
+ low: PRIORITY.Low,
52
+ m: PRIORITY.Medium,
53
+ medium: PRIORITY.Medium,
32
54
  };
33
55
  async run() {
34
56
  const { flags } = await this.parse(List);
35
- const incompleteOnly = flags.incomplete ?? false;
36
- const priority = (flags.priority ?? null);
37
- const sortBy = (flags.sort ?? null);
38
- const statusFilter = (flags.status ?? null);
39
- const priorityFilter = (priority
40
- ? priority === 'high' || priority === 'h'
41
- ? 'high'
42
- : priority === 'medium' || priority === 'm'
43
- ? 'medium'
44
- : priority === 'low' || priority === 'l'
45
- ? 'low'
46
- : null
47
- : null);
48
- // if statusFilter is set, it overrides incompleteOnly
49
- const effectiveStatusFilter = statusFilter
50
- ? [statusFilter]
51
- : incompleteOnly
52
- ? ['in-progress', 'not-started']
53
- : ['completed', 'in-progress', 'not-started'];
54
- const config = await readConfigFile().catch((error) => {
55
- this.error(`failed to read config file: ${error ? error.message : String(error)}`);
56
- });
57
- const roadmapPath = config.path;
58
- const roadmap = await readRoadmapFile(roadmapPath);
59
- const tasks = roadmap.tasks
60
- .filter((task) => effectiveStatusFilter.includes(task.status))
61
- .filter((task) => priorityFilter === null || task.priority === priorityFilter)
62
- .sort((a, b) => {
63
- if (sortBy === 'dueDate') {
64
- const dateA = a.dueDate ? new Date(a.dueDate).getTime() : Infinity;
65
- const dateB = b.dueDate ? new Date(b.dueDate).getTime() : Infinity;
66
- return dateA - dateB;
57
+ try {
58
+ const incompleteOnly = flags.incomplete ?? false;
59
+ const priority = (flags.priority ?? null);
60
+ const sortBy = (flags.sort ?? null);
61
+ const statusFilter = (flags.status ?? null);
62
+ const priorityFilter = (priority ? List.priorityMap[priority] : null);
63
+ // if statusFilter is set, it overrides incompleteOnly
64
+ const effectiveStatusFilter = statusFilter
65
+ ? [statusFilter]
66
+ : incompleteOnly
67
+ ? ['in-progress', 'not-started']
68
+ : ['completed', 'in-progress', 'not-started'];
69
+ // Use repository pattern by default, unless --no-repo flag is set
70
+ const config = flags['no-repo'] ? await readConfigFile() : await getDefaultConfigRepository().load();
71
+ const roadmapPath = config.path;
72
+ const roadmap = flags['no-repo']
73
+ ? await readRoadmapFile(roadmapPath)
74
+ : await RoadmapRepository.fromConfig(config).load(roadmapPath);
75
+ // Build filter criteria
76
+ const filterCriteria = {};
77
+ if (effectiveStatusFilter.length > 0) {
78
+ filterCriteria.status =
79
+ effectiveStatusFilter.length === 3
80
+ ? undefined // All statuses, no filter needed
81
+ : effectiveStatusFilter;
67
82
  }
68
- if (sortBy === 'priority') {
69
- const priorityOrder = {
70
- high: 1,
71
- low: 3,
72
- medium: 2,
73
- };
74
- return priorityOrder[a.priority] - priorityOrder[b.priority];
83
+ if (priorityFilter) {
84
+ filterCriteria.priority = priorityFilter;
75
85
  }
76
- if (sortBy === 'createdAt') {
77
- const createdA = a.createdAt ? new Date(a.createdAt).getTime() : Infinity;
78
- const createdB = b.createdAt ? new Date(b.createdAt).getTime() : Infinity;
79
- return createdA - createdB;
86
+ // Apply filtering and sorting using TaskQueryService
87
+ const hasFilters = Object.keys(filterCriteria).length > 0;
88
+ const filtered = hasFilters
89
+ ? // eslint-disable-next-line unicorn/no-array-callback-reference, unicorn/no-array-method-this-argument
90
+ taskQueryService.filter(roadmap.tasks, filterCriteria)
91
+ : roadmap.tasks;
92
+ const tasks = sortBy ? taskQueryService.sort(filtered, sortBy, SortOrder.Ascending) : filtered;
93
+ // Display using DisplayService
94
+ const lines = displayService.formatTaskList(tasks);
95
+ for (const line of lines) {
96
+ console.log(line);
80
97
  }
81
- return 0;
82
- });
83
- // Display
84
- console.log(`\nTasks (${tasks.length} total):\n`);
85
- for (const task of tasks) {
86
- const status = task.status === STATUS.Completed ? '✓' : task.status === STATUS.InProgress ? '~' : '○';
87
- const tests = task['passes-tests'] ? '✓' : '✗';
88
- const prioritySymbol = {
89
- high: 'H',
90
- low: 'L',
91
- medium: 'M',
92
- }[task.priority];
93
- console.log(`${status} [${prioritySymbol}] [${task.id}] ${task.title}`);
94
- console.log(` Type: ${task.type} | Tests: ${tests} | Deps: ${task['depends-on'].length}`);
95
- if (task['depends-on'].length > 0) {
96
- console.log(` Depends on: ${task['depends-on'].join(', ')}`);
97
- }
98
- console.log();
98
+ }
99
+ catch (error) {
100
+ const exitCode = errorHandlerService.handleError(error);
101
+ this.error(errorHandlerService.formatErrorMessage(error, flags.verbose), { exit: exitCode });
99
102
  }
100
103
  }
101
104
  }
@@ -5,6 +5,9 @@ export default class PassTest extends Command {
5
5
  };
6
6
  static description: string;
7
7
  static examples: string[];
8
- static flags: {};
8
+ static flags: {
9
+ 'no-repo': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
9
12
  run(): Promise<void>;
10
13
  }
@@ -1,4 +1,7 @@
1
- import { Args, Command } from '@oclif/core';
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { getDefaultConfigRepository } from '../repositories/config.repository.js';
3
+ import { RoadmapRepository } from '../repositories/roadmap.repository.js';
4
+ import errorHandlerService from '../services/error-handler.service.js';
2
5
  import { readConfigFile } from '../util/read-config.js';
3
6
  import { readRoadmapFile } from '../util/read-roadmap.js';
4
7
  import { updateTaskInRoadmap } from '../util/update-task.js';
@@ -10,19 +13,39 @@ export default class PassTest extends Command {
10
13
  static description = 'Mark a task as passes-tests';
11
14
  static examples = ['<%= config.bin %> <%= command.id %> F-001'];
12
15
  static flags = {
13
- // flag with no value (-f, --force)
14
- // force: Flags.boolean({char: 'f'}),
15
- // flag with a value (-n, --name=VALUE)
16
- // name: Flags.string({char: 'n', description: 'name to print'}),
16
+ 'no-repo': Flags.boolean({
17
+ default: false,
18
+ description: 'use legacy direct file I/O instead of repository pattern',
19
+ }),
20
+ // flag with no value (-f, --force)
21
+ // force: Flags.boolean({char: 'f'}),
22
+ // flag with a value (-n, --name=VALUE)
23
+ // name: Flags.string({char: 'n', description: 'name to print'}),
24
+ verbose: Flags.boolean({
25
+ char: 'v',
26
+ default: false,
27
+ description: 'show detailed error information including stack traces',
28
+ }),
17
29
  };
18
30
  async run() {
19
- const { args } = await this.parse(PassTest);
20
- const config = await readConfigFile();
21
- const roadmap = await readRoadmapFile(config.path);
22
- const updatedRoadmap = await updateTaskInRoadmap(roadmap, args.taskID, {
23
- 'passes-tests': true,
24
- });
25
- await writeRoadmapFile(config.path, updatedRoadmap);
26
- this.log(`Task ${args.taskID} marked as passing tests.`);
31
+ const { args, flags } = await this.parse(PassTest);
32
+ try {
33
+ // Use repository pattern by default, unless --no-repo flag is set
34
+ const config = flags['no-repo'] ? await readConfigFile() : await getDefaultConfigRepository().load();
35
+ const roadmap = flags['no-repo']
36
+ ? await readRoadmapFile(config.path)
37
+ : await RoadmapRepository.fromConfig(config).load(config.path);
38
+ const updatedRoadmap = await updateTaskInRoadmap(roadmap, args.taskID, {
39
+ 'passes-tests': true,
40
+ });
41
+ await (flags['no-repo']
42
+ ? writeRoadmapFile(config.path, updatedRoadmap)
43
+ : RoadmapRepository.fromConfig(config).save(config.path, updatedRoadmap));
44
+ this.log(`Task ${args.taskID} marked as passing tests.`);
45
+ }
46
+ catch (error) {
47
+ const exitCode = errorHandlerService.handleError(error);
48
+ this.error(errorHandlerService.formatErrorMessage(error, flags.verbose), { exit: exitCode });
49
+ }
27
50
  }
28
51
  }
@@ -5,6 +5,9 @@ export default class Show extends Command {
5
5
  };
6
6
  static description: string;
7
7
  static examples: string[];
8
- static flags: {};
8
+ static flags: {
9
+ 'no-repo': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
9
12
  run(): Promise<void>;
10
13
  }
@@ -1,4 +1,8 @@
1
- import { Args, Command /* , Flags */ } from '@oclif/core';
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { getDefaultConfigRepository } from '../repositories/config.repository.js';
3
+ import { RoadmapRepository } from '../repositories/roadmap.repository.js';
4
+ import displayService from '../services/display.service.js';
5
+ import errorHandlerService from '../services/error-handler.service.js';
2
6
  import { readConfigFile } from '../util/read-config.js';
3
7
  import { readRoadmapFile } from '../util/read-roadmap.js';
4
8
  export default class Show extends Command {
@@ -8,66 +12,41 @@ export default class Show extends Command {
8
12
  static description = 'show details of a specific task in the project roadmap';
9
13
  static examples = ['<%= config.bin %> <%= command.id %> F-001'];
10
14
  static flags = {
11
- // flag with no value (-f, --force)
12
- // force: Flags.boolean({char: 'f'}),
13
- // flag with a value (-n, --name=VALUE)
14
- // name: Flags.string({char: 'n', description: 'name to print'}),
15
+ 'no-repo': Flags.boolean({
16
+ default: false,
17
+ description: 'use legacy direct file I/O instead of repository pattern',
18
+ }),
19
+ // flag with no value (-f, --force)
20
+ // force: Flags.boolean({char: 'f'}),
21
+ // flag with a value (-n, --name=VALUE)
22
+ // name: Flags.string({char: 'n', description: 'name to print'}),
23
+ verbose: Flags.boolean({
24
+ char: 'v',
25
+ default: false,
26
+ description: 'show detailed error information including stack traces',
27
+ }),
15
28
  };
16
29
  async run() {
17
- const { args } = await this.parse(Show);
18
- const config = await readConfigFile().catch((error) => {
19
- this.error(`failed to read config file: ${error ? error.message : String(error)}`);
20
- });
21
- const roadmapPath = config.path;
22
- const roadmap = await readRoadmapFile(roadmapPath);
23
- const task = roadmap.tasks.find((t) => t.id === args.task);
24
- if (!task) {
25
- this.error(`task with ID ${args.task} not found in roadmap \n see list of tasks with: 'prt list'`);
30
+ const { args, flags } = await this.parse(Show);
31
+ try {
32
+ // Use repository pattern by default, unless --no-repo flag is set
33
+ const config = flags['no-repo'] ? await readConfigFile() : await getDefaultConfigRepository().load();
34
+ const roadmapPath = config.path;
35
+ const roadmap = flags['no-repo']
36
+ ? await readRoadmapFile(roadmapPath)
37
+ : await RoadmapRepository.fromConfig(config).load(roadmapPath);
38
+ const task = roadmap.tasks.find((t) => t.id === args.task);
39
+ if (!task) {
40
+ this.error(`task with ID ${args.task} not found in roadmap \n see list of tasks with: 'prt list'`);
41
+ }
42
+ const lines = displayService.formatTaskDetails(task);
43
+ for (const line of lines) {
44
+ console.log(line);
45
+ }
46
+ }
47
+ catch (error) {
48
+ const exitCode = errorHandlerService.handleError(error);
49
+ this.error(errorHandlerService.formatErrorMessage(error, flags.verbose), { exit: exitCode });
26
50
  }
27
- const completeStatus = task.status === 'completed' ? '✓' : task.status === 'in-progress' ? '~' : '○';
28
- const testStatus = task['passes-tests'] ? '✓' : '✗';
29
- const priorityLabel = {
30
- high: 'High',
31
- low: 'Low',
32
- medium: 'Medium',
33
- }[task.priority];
34
- console.log(`\nTask: ${task.id}\n`);
35
- console.log(`Title: ${task.title}`);
36
- console.log(`Type: ${task.type}`);
37
- console.log(`Priority: ${priorityLabel}`);
38
- console.log(`Status: ${completeStatus} ${task.status
39
- .replaceAll('-', ' ')
40
- .split(' ')
41
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
42
- .join(' ')} | ${testStatus} Tests Passing`);
43
- console.log(`\nDetails:\n${task.details}`);
44
- // Dependencies
45
- if (task['depends-on'].length > 0) {
46
- console.log(`\nDepends On: ${task['depends-on'].join(', ')}`);
47
- }
48
- else {
49
- console.log(`\nDepends On: None`);
50
- }
51
- // Blocks (optional)
52
- if (task.blocks && task.blocks.length > 0) {
53
- console.log(`Blocks: ${task.blocks.join(', ')}`);
54
- }
55
- // Timestamps
56
- console.log(`\nCreated: ${task.createdAt}`);
57
- console.log(`Updated: ${task.updatedAt}`);
58
- // Optional fields - only display if present
59
- if (task.tags && task.tags.length > 0) {
60
- console.log(`\nTags: ${task.tags.join(', ')}`);
61
- }
62
- if (task.effort !== undefined) {
63
- console.log(`Effort: ${task.effort}`);
64
- }
65
- if (task['github-refs'] && task['github-refs'].length > 0) {
66
- console.log(`GitHub Refs: ${task['github-refs'].join(', ')}`);
67
- }
68
- if (task.notes) {
69
- console.log(`\nNotes:\n${task.notes}`);
70
- }
71
- console.log('');
72
51
  }
73
52
  }
@@ -8,9 +8,11 @@ export default class Update extends Command {
8
8
  static flags: {
9
9
  'clear-notes': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
10
  deps: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'no-repo': import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
12
  notes: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
13
  status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
14
  tested: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
16
  };
15
17
  run(): Promise<void>;
16
18
  }
@@ -1,4 +1,7 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
+ import { getDefaultConfigRepository } from '../repositories/config.repository.js';
3
+ import { RoadmapRepository } from '../repositories/roadmap.repository.js';
4
+ import errorHandlerService from '../services/error-handler.service.js';
2
5
  import { readConfigFile } from '../util/read-config.js';
3
6
  import { readRoadmapFile } from '../util/read-roadmap.js';
4
7
  import { STATUS } from '../util/types.js';
@@ -20,6 +23,10 @@ export default class Update extends Command {
20
23
  char: 'd',
21
24
  description: 'update the dependencies of the task (comma-separated list of task IDs)',
22
25
  }),
26
+ 'no-repo': Flags.boolean({
27
+ default: false,
28
+ description: 'use legacy direct file I/O instead of repository pattern',
29
+ }),
23
30
  notes: Flags.string({ char: 'n', description: 'append notes to the task' }),
24
31
  status: Flags.string({
25
32
  char: 's',
@@ -31,6 +38,11 @@ export default class Update extends Command {
31
38
  description: 'update whether the task passes tests',
32
39
  options: ['true', 'false'],
33
40
  }),
41
+ verbose: Flags.boolean({
42
+ char: 'v',
43
+ default: false,
44
+ description: 'show detailed error information including stack traces',
45
+ }),
34
46
  // flag with no value (-f, --force)
35
47
  // force: Flags.boolean({char: 'f'}),
36
48
  // flag with a value (-n, --name=VALUE)
@@ -38,41 +50,52 @@ export default class Update extends Command {
38
50
  };
39
51
  async run() {
40
52
  const { args, flags } = await this.parse(Update);
41
- const config = await readConfigFile();
42
- const roadmap = await readRoadmapFile(config.path);
43
- const updateObject = {};
44
- if (flags['clear-notes']) {
45
- updateObject.notes = '';
46
- }
47
- if (flags.notes) {
48
- let existingNotes = '';
49
- if (!flags['clear-notes']) {
50
- const existingTask = roadmap.tasks.find((task) => task.id === args.taskID);
51
- existingNotes = existingTask?.notes ? existingTask.notes + '\n' : '';
53
+ try {
54
+ // Use repository pattern by default, unless --no-repo flag is set
55
+ const config = flags['no-repo'] ? await readConfigFile() : await getDefaultConfigRepository().load();
56
+ const roadmap = flags['no-repo']
57
+ ? await readRoadmapFile(config.path)
58
+ : await RoadmapRepository.fromConfig(config).load(config.path);
59
+ const updateObject = {};
60
+ if (flags['clear-notes']) {
61
+ updateObject.notes = '';
52
62
  }
53
- updateObject.notes = existingNotes + flags.notes;
54
- }
55
- if (flags.status) {
56
- updateObject.status = flags.status;
57
- }
58
- if (flags.tested) {
59
- updateObject['passes-tests'] = flags.tested === 'true';
60
- }
61
- if (flags.deps) {
62
- const depsArray = flags.deps.split(',').map((dep) => dep.trim());
63
- // checking that each depArray item matches TaskID format
64
- for (const dep of depsArray) {
65
- try {
66
- validateTaskID(dep);
63
+ if (flags.notes) {
64
+ let existingNotes = '';
65
+ if (!flags['clear-notes']) {
66
+ const existingTask = roadmap.tasks.find((task) => task.id === args.taskID);
67
+ existingNotes = existingTask?.notes ? existingTask.notes + '\n' : '';
67
68
  }
68
- catch {
69
- this.error(`Invalid task ID in dependencies: ${dep}`);
69
+ updateObject.notes = existingNotes + flags.notes;
70
+ }
71
+ if (flags.status) {
72
+ updateObject.status = flags.status;
73
+ }
74
+ if (flags.tested) {
75
+ updateObject['passes-tests'] = flags.tested === 'true';
76
+ }
77
+ if (flags.deps) {
78
+ const depsArray = flags.deps.split(',').map((dep) => dep.trim());
79
+ // checking that each depArray item matches TaskID format
80
+ for (const dep of depsArray) {
81
+ try {
82
+ validateTaskID(dep);
83
+ }
84
+ catch {
85
+ this.error(`Invalid task ID in dependencies: ${dep}`);
86
+ }
70
87
  }
88
+ updateObject['depends-on'] = depsArray;
71
89
  }
72
- updateObject['depends-on'] = depsArray;
90
+ const updatedRoadmap = await updateTaskInRoadmap(roadmap, args.taskID, updateObject);
91
+ await (flags['no-repo']
92
+ ? writeRoadmapFile(config.path, updatedRoadmap)
93
+ : RoadmapRepository.fromConfig(config).save(config.path, updatedRoadmap));
94
+ this.log(`Task ${args.taskID} has been updated.`);
95
+ }
96
+ catch (error) {
97
+ const exitCode = errorHandlerService.handleError(error);
98
+ this.error(errorHandlerService.formatErrorMessage(error, flags.verbose), { exit: exitCode });
73
99
  }
74
- const updatedRoadmap = await updateTaskInRoadmap(roadmap, args.taskID, updateObject);
75
- await writeRoadmapFile(config.path, updatedRoadmap);
76
- this.log(`Task ${args.taskID} updated.`);
77
100
  }
78
101
  }