project-roadmap-tracking 0.1.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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +559 -0
  3. package/bin/dev.cmd +3 -0
  4. package/bin/dev.js +5 -0
  5. package/bin/run.cmd +3 -0
  6. package/bin/run.js +5 -0
  7. package/dist/commands/add.d.ts +16 -0
  8. package/dist/commands/add.js +76 -0
  9. package/dist/commands/complete.d.ts +12 -0
  10. package/dist/commands/complete.js +35 -0
  11. package/dist/commands/init.d.ts +26 -0
  12. package/dist/commands/init.js +111 -0
  13. package/dist/commands/list.d.ts +13 -0
  14. package/dist/commands/list.js +101 -0
  15. package/dist/commands/pass-test.d.ts +10 -0
  16. package/dist/commands/pass-test.js +28 -0
  17. package/dist/commands/show.d.ts +10 -0
  18. package/dist/commands/show.js +73 -0
  19. package/dist/commands/update.d.ts +16 -0
  20. package/dist/commands/update.js +78 -0
  21. package/dist/commands/validate.d.ts +8 -0
  22. package/dist/commands/validate.js +49 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1 -0
  25. package/dist/util/read-config.d.ts +2 -0
  26. package/dist/util/read-config.js +5 -0
  27. package/dist/util/read-roadmap.d.ts +2 -0
  28. package/dist/util/read-roadmap.js +5 -0
  29. package/dist/util/types.d.ts +66 -0
  30. package/dist/util/types.js +36 -0
  31. package/dist/util/update-task.d.ts +4 -0
  32. package/dist/util/update-task.js +16 -0
  33. package/dist/util/validate-task-id.d.ts +2 -0
  34. package/dist/util/validate-task-id.js +5 -0
  35. package/dist/util/validate-task.d.ts +4 -0
  36. package/dist/util/validate-task.js +17 -0
  37. package/dist/util/write-roadmap.d.ts +2 -0
  38. package/dist/util/write-roadmap.js +5 -0
  39. package/oclif.manifest.json +426 -0
  40. package/package.json +73 -0
@@ -0,0 +1,35 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { readConfigFile } from '../util/read-config.js';
3
+ import { readRoadmapFile } from '../util/read-roadmap.js';
4
+ import { STATUS } from '../util/types.js';
5
+ import { updateTaskInRoadmap } from '../util/update-task.js';
6
+ import { writeRoadmapFile } from '../util/write-roadmap.js';
7
+ export default class Complete extends Command {
8
+ static args = {
9
+ taskID: Args.string({ description: 'ID of the task to complete', required: true }),
10
+ };
11
+ static description = 'Mark a task as completed';
12
+ static examples = ['<%= config.bin %> <%= command.id %> F-001 --tests'];
13
+ static flags = {
14
+ // flag with no value (-f, --force)
15
+ // force: Flags.boolean({char: 'f'}),
16
+ // flag with a value (-n, --name=VALUE)
17
+ // name: Flags.string({char: 'n', description: 'name to print'}),
18
+ tests: Flags.boolean({ char: 't', description: 'mark task as passes-tests' }),
19
+ };
20
+ async run() {
21
+ const { args, flags } = await this.parse(Complete);
22
+ const config = await readConfigFile();
23
+ const roadmap = await readRoadmapFile(config.path);
24
+ const updatedRoadmap = await updateTaskInRoadmap(roadmap, args.taskID, flags.tests
25
+ ? {
26
+ 'passes-tests': true,
27
+ status: STATUS.Completed,
28
+ }
29
+ : {
30
+ status: STATUS.Completed,
31
+ });
32
+ await writeRoadmapFile(config.path, updatedRoadmap);
33
+ this.log(`Task ${args.taskID} marked as completed.`);
34
+ }
35
+ }
@@ -0,0 +1,26 @@
1
+ import { Command } from '@oclif/core';
2
+ import { Config, Roadmap } from '../util/types.js';
3
+ export default class Init extends Command {
4
+ static args: {
5
+ folder: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
6
+ };
7
+ static description: string;
8
+ static examples: string[];
9
+ static flags: {
10
+ description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ withSampleTasks: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ };
15
+ buildBlankRoadmap({ description, name, withSampleTasks, }: {
16
+ description: string;
17
+ name: string;
18
+ withSampleTasks: boolean;
19
+ }): Roadmap;
20
+ buildConfig({ description, name, path }: {
21
+ description: string;
22
+ name: string;
23
+ path: string;
24
+ }): Config;
25
+ run(): Promise<void>;
26
+ }
@@ -0,0 +1,111 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { mkdir, readdir, writeFile } from 'node:fs/promises';
3
+ import { PRIORITY, STATUS, TASK_TYPE } from '../util/types.js';
4
+ export default class Init extends Command {
5
+ static args = {
6
+ folder: Args.string({ description: 'folder to initialize the project roadmap in' }),
7
+ };
8
+ static description = 'initialize a new project roadmap (prt.json and prt.config.json)';
9
+ static examples = ['<%= config.bin %> <%= command.id %> [path/to/directory]'];
10
+ static flags = {
11
+ // flag with no value (-f, --force)
12
+ description: Flags.string({ char: 'd', description: 'description to print' }),
13
+ force: Flags.boolean({ char: 'f', description: 'force initialization even if files already exist' }),
14
+ // flag with a value (-n, --name=VALUE)
15
+ name: Flags.string({ char: 'n', description: 'name to print' }),
16
+ withSampleTasks: Flags.boolean({ description: 'include sample tasks in the initialized roadmap' }),
17
+ };
18
+ buildBlankRoadmap({ description, name, withSampleTasks, }) {
19
+ return {
20
+ $schema: 'https://raw.githubusercontent.com/ZacharyEggert/project-roadmap-tracking/refs/heads/master/schemas/roadmap/v1.json',
21
+ metadata: {
22
+ createdAt: new Date().toISOString(),
23
+ createdBy: `project-roadmap-tracking CLI`,
24
+ description: `${description}`,
25
+ name: `${name}`,
26
+ },
27
+ tasks: withSampleTasks
28
+ ? [
29
+ {
30
+ assignedTo: null,
31
+ blocks: [],
32
+ createdAt: new Date().toISOString(),
33
+ 'depends-on': [],
34
+ details: 'This is a sample task to get you started.',
35
+ dueDate: null,
36
+ id: 'F-001',
37
+ 'passes-tests': true,
38
+ priority: PRIORITY.Medium,
39
+ status: STATUS.NotStarted,
40
+ tags: ['sample'],
41
+ title: 'Sample Task',
42
+ type: TASK_TYPE.Feature,
43
+ updatedAt: new Date().toISOString(),
44
+ },
45
+ ]
46
+ : [],
47
+ };
48
+ }
49
+ buildConfig({ description, name, path }) {
50
+ return {
51
+ $schema: 'https://raw.githubusercontent.com/ZacharyEggert/project-roadmap-tracking/refs/heads/master/schemas/config/v1.json',
52
+ metadata: {
53
+ description: `${description}`,
54
+ name: `${name}`,
55
+ },
56
+ path: `${path}/prt.json`,
57
+ };
58
+ }
59
+ async run() {
60
+ 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}`);
80
+ }
81
+ }
82
+ });
83
+ }
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
+ });
110
+ }
111
+ }
@@ -0,0 +1,13 @@
1
+ import { /* Args, */ Command } from '@oclif/core';
2
+ export default class List extends Command {
3
+ static args: {};
4
+ static description: string;
5
+ static examples: string[];
6
+ static flags: {
7
+ incomplete: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ priority: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ sort: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,101 @@
1
+ import { /* Args, */ Command, Flags } from '@oclif/core';
2
+ import { readConfigFile } from '../util/read-config.js';
3
+ import { readRoadmapFile } from '../util/read-roadmap.js';
4
+ import { STATUS } from '../util/types.js';
5
+ export default class List extends Command {
6
+ static args = {
7
+ // file: Args.string({description: 'file to read'}),
8
+ };
9
+ static description = 'list tasks in the project roadmap';
10
+ static examples = ['<%= config.bin %> <%= command.id %> -p=h --incomplete --sort=createdAt'];
11
+ static flags = {
12
+ // flag with no value (-f, --force)
13
+ // force: Flags.boolean({char: 'f'}),
14
+ // flag with a value (-n, --name=VALUE)
15
+ // name: Flags.string({char: 'n', description: 'name to print'}),
16
+ incomplete: Flags.boolean({ char: 'i', description: 'filter tasks to show in-progress and not-started only' }),
17
+ priority: Flags.string({
18
+ char: 'p',
19
+ description: 'filter tasks by priority (high, medium, low)',
20
+ options: ['high', 'medium', 'low', 'h', 'm', 'l'],
21
+ }),
22
+ sort: Flags.string({
23
+ char: 'o',
24
+ description: 'sort tasks by field (dueDate, priority, createdAt)',
25
+ options: ['dueDate', 'priority', 'createdAt'],
26
+ }),
27
+ status: Flags.string({
28
+ char: 's',
29
+ description: 'filter tasks by status (completed, in-progress, not-started)',
30
+ options: ['completed', 'in-progress', 'not-started'],
31
+ }),
32
+ };
33
+ async run() {
34
+ 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;
67
+ }
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];
75
+ }
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;
80
+ }
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();
99
+ }
100
+ }
101
+ }
@@ -0,0 +1,10 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class PassTest extends Command {
3
+ static args: {
4
+ taskID: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {};
9
+ run(): Promise<void>;
10
+ }
@@ -0,0 +1,28 @@
1
+ import { Args, Command } from '@oclif/core';
2
+ import { readConfigFile } from '../util/read-config.js';
3
+ import { readRoadmapFile } from '../util/read-roadmap.js';
4
+ import { updateTaskInRoadmap } from '../util/update-task.js';
5
+ import { writeRoadmapFile } from '../util/write-roadmap.js';
6
+ export default class PassTest extends Command {
7
+ static args = {
8
+ taskID: Args.string({ description: 'ID of the task to mark as passing tests', required: true }),
9
+ };
10
+ static description = 'Mark a task as passes-tests';
11
+ static examples = ['<%= config.bin %> <%= command.id %> F-001'];
12
+ 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'}),
17
+ };
18
+ 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.`);
27
+ }
28
+ }
@@ -0,0 +1,10 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Show extends Command {
3
+ static args: {
4
+ task: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {};
9
+ run(): Promise<void>;
10
+ }
@@ -0,0 +1,73 @@
1
+ import { Args, Command /* , Flags */ } from '@oclif/core';
2
+ import { readConfigFile } from '../util/read-config.js';
3
+ import { readRoadmapFile } from '../util/read-roadmap.js';
4
+ export default class Show extends Command {
5
+ static args = {
6
+ task: Args.string({ description: 'task ID to show', required: true }),
7
+ };
8
+ static description = 'show details of a specific task in the project roadmap';
9
+ static examples = ['<%= config.bin %> <%= command.id %> F-001'];
10
+ 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
+ };
16
+ 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'`);
26
+ }
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
+ }
73
+ }
@@ -0,0 +1,16 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Update extends Command {
3
+ static args: {
4
+ taskID: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ 'clear-notes': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ deps: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ notes: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ tested: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ };
15
+ run(): Promise<void>;
16
+ }
@@ -0,0 +1,78 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { readConfigFile } from '../util/read-config.js';
3
+ import { readRoadmapFile } from '../util/read-roadmap.js';
4
+ import { STATUS } from '../util/types.js';
5
+ import { updateTaskInRoadmap } from '../util/update-task.js';
6
+ import { validateTaskID } from '../util/validate-task-id.js';
7
+ import { writeRoadmapFile } from '../util/write-roadmap.js';
8
+ export default class Update extends Command {
9
+ static args = {
10
+ taskID: Args.string({ description: 'ID of the task to update', required: true }),
11
+ };
12
+ static description = 'Update a task in place';
13
+ static examples = [
14
+ '<%= config.bin %> <%= command.id %> F-001 --status=completed --tested=true --notes="Fixed all bugs"',
15
+ '<%= config.bin %> <%= command.id %> F-002 --deps="F-001" --clear-notes',
16
+ ];
17
+ static flags = {
18
+ 'clear-notes': Flags.boolean({ description: 'clear all notes from the task' }),
19
+ deps: Flags.string({
20
+ char: 'd',
21
+ description: 'update the dependencies of the task (comma-separated list of task IDs)',
22
+ }),
23
+ notes: Flags.string({ char: 'n', description: 'append notes to the task' }),
24
+ status: Flags.string({
25
+ char: 's',
26
+ description: 'set the status of the task (completed, in-progress, not-started)',
27
+ options: [STATUS.Completed, STATUS.InProgress, STATUS.NotStarted],
28
+ }),
29
+ tested: Flags.string({
30
+ char: 't',
31
+ description: 'update whether the task passes tests',
32
+ options: ['true', 'false'],
33
+ }),
34
+ // flag with no value (-f, --force)
35
+ // force: Flags.boolean({char: 'f'}),
36
+ // flag with a value (-n, --name=VALUE)
37
+ // name: Flags.string({char: 'n', description: 'name to print'}),
38
+ };
39
+ async run() {
40
+ 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' : '';
52
+ }
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);
67
+ }
68
+ catch {
69
+ this.error(`Invalid task ID in dependencies: ${dep}`);
70
+ }
71
+ }
72
+ updateObject['depends-on'] = depsArray;
73
+ }
74
+ const updatedRoadmap = await updateTaskInRoadmap(roadmap, args.taskID, updateObject);
75
+ await writeRoadmapFile(config.path, updatedRoadmap);
76
+ this.log(`Task ${args.taskID} updated.`);
77
+ }
78
+ }
@@ -0,0 +1,8 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Validate extends Command {
3
+ static args: {};
4
+ static description: string;
5
+ static examples: string[];
6
+ static flags: {};
7
+ run(): Promise<void>;
8
+ }
@@ -0,0 +1,49 @@
1
+ import { Command } from '@oclif/core';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { readConfigFile } from '../util/read-config.js';
4
+ import { validateTask } from '../util/validate-task.js';
5
+ export default class Validate extends Command {
6
+ static args = {
7
+ // file: Args.string({description: 'file to read'}),
8
+ };
9
+ static description = 'describe the command here';
10
+ static examples = ['<%= config.bin %> <%= command.id %>'];
11
+ static flags = {
12
+ // flag with no value (-f, --force)
13
+ // force: Flags.boolean({char: 'f'}),
14
+ // flag with a value (-n, --name=VALUE)
15
+ // name: Flags.string({char: 'n', description: 'name to print'}),
16
+ };
17
+ async run() {
18
+ await this.parse(Validate);
19
+ this.log(`validating roadmap...`);
20
+ const config = await readConfigFile().catch((error) => {
21
+ this.error(`failed to read config file: ${error ? error.message : String(error)}`);
22
+ });
23
+ const roadmapPath = config.path;
24
+ const roadmapData = await readFile(roadmapPath, 'utf8');
25
+ let roadmap;
26
+ try {
27
+ roadmap = JSON.parse(roadmapData);
28
+ this.log(`roadmap at ${roadmapPath} is valid JSON`);
29
+ }
30
+ catch (error) {
31
+ this.error(`roadmap at ${roadmapPath} is not valid JSON: ${error ? error.message : String(error)}`);
32
+ }
33
+ if (roadmap.tasks.length === 0) {
34
+ this.log('roadmap contains no tasks to validate');
35
+ this.log('roadmap validation complete');
36
+ return;
37
+ }
38
+ for (const task of roadmap.tasks) {
39
+ try {
40
+ validateTask(task);
41
+ }
42
+ catch (error) {
43
+ this.error(`task ID ${task.id} is invalid: ${error ? error.message : String(error)}`);
44
+ }
45
+ }
46
+ this.log(roadmap.tasks.length > 1 ? `all ${roadmap.tasks.length} tasks are valid` : `1 task is valid`);
47
+ this.log(`roadmap validation complete`);
48
+ }
49
+ }
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
@@ -0,0 +1,2 @@
1
+ import { Config } from './types.js';
2
+ export declare function readConfigFile(): Promise<Config>;
@@ -0,0 +1,5 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ export async function readConfigFile() {
3
+ const data = await readFile('.prtrc.json', 'utf8');
4
+ return JSON.parse(data);
5
+ }
@@ -0,0 +1,2 @@
1
+ import { Roadmap } from './types.js';
2
+ export declare function readRoadmapFile(path: string): Promise<Roadmap>;
@@ -0,0 +1,5 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ export async function readRoadmapFile(path) {
3
+ const data = await readFile(path, 'utf8');
4
+ return JSON.parse(data);
5
+ }