@howaboua/opencode-roadmap-plugin 0.1.4 → 0.1.6
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/README.md +85 -14
- package/dist/src/descriptions/createroadmap.txt +20 -16
- package/dist/src/descriptions/loader.js +11 -4
- package/dist/src/descriptions/readroadmap.txt +19 -2
- package/dist/src/descriptions/updateroadmap.txt +8 -6
- package/dist/src/errors/loader.js +11 -3
- package/dist/src/index.js +1 -1
- package/dist/src/storage.d.ts +13 -12
- package/dist/src/storage.js +83 -52
- package/dist/src/tools/createroadmap.d.ts +4 -0
- package/dist/src/tools/createroadmap.js +19 -8
- package/dist/src/tools/readroadmap.d.ts +4 -0
- package/dist/src/tools/readroadmap.js +8 -5
- package/dist/src/tools/updateroadmap.d.ts +4 -0
- package/dist/src/tools/updateroadmap.js +20 -10
- package/dist/src/types.d.ts +21 -21
- package/dist/src/types.js +1 -1
- package/package.json +10 -4
- package/dist/descriptions/index.d.ts +0 -1
- package/dist/descriptions/index.js +0 -1
- package/dist/descriptions/loader.d.ts +0 -1
- package/dist/descriptions/loader.js +0 -17
- package/dist/errors/loader.d.ts +0 -2
- package/dist/errors/loader.js +0 -24
- package/dist/storage.d.ts +0 -25
- package/dist/storage.js +0 -214
- package/dist/tools/createroadmap.d.ts +0 -2
- package/dist/tools/createroadmap.js +0 -135
- package/dist/tools/readroadmap.d.ts +0 -2
- package/dist/tools/readroadmap.js +0 -90
- package/dist/tools/updateroadmap.d.ts +0 -2
- package/dist/tools/updateroadmap.js +0 -107
- package/dist/types.d.ts +0 -151
- package/dist/types.js +0 -18
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool for reading roadmap state, progress, and details.
|
|
3
|
+
* Supports filtering by feature or action number.
|
|
4
|
+
*/
|
|
1
5
|
import { tool } from "@opencode-ai/plugin";
|
|
2
6
|
import { FileStorage, RoadmapValidator } from "../storage.js";
|
|
3
7
|
import { loadDescription } from "../descriptions/index.js";
|
|
4
|
-
import { getErrorMessage } from "../errors/loader.js";
|
|
5
8
|
export async function createReadRoadmapTool(directory) {
|
|
6
9
|
const description = await loadDescription("readroadmap.txt");
|
|
7
10
|
return tool({
|
|
@@ -19,17 +22,17 @@ export async function createReadRoadmapTool(directory) {
|
|
|
19
22
|
async execute(args) {
|
|
20
23
|
const storage = new FileStorage(directory);
|
|
21
24
|
if (!(await storage.exists())) {
|
|
22
|
-
throw new Error(
|
|
25
|
+
throw new Error("Roadmap not found. Use CreateRoadmap to create one.");
|
|
23
26
|
}
|
|
24
27
|
const roadmap = await storage.read();
|
|
25
28
|
if (!roadmap) {
|
|
26
|
-
throw new Error(
|
|
29
|
+
throw new Error("Roadmap file is corrupted. Please fix manually.");
|
|
27
30
|
}
|
|
28
31
|
if (args.actionNumber && args.featureNumber) {
|
|
29
32
|
throw new Error("Cannot specify both actionNumber and featureNumber. Use one or the other, or neither for full roadmap.");
|
|
30
33
|
}
|
|
31
34
|
if (args.actionNumber) {
|
|
32
|
-
const actionNumberError =
|
|
35
|
+
const actionNumberError = RoadmapValidator.validateActionNumber(args.actionNumber);
|
|
33
36
|
if (actionNumberError) {
|
|
34
37
|
throw new Error(`${actionNumberError.message} Use ReadRoadmap to see valid action numbers.`);
|
|
35
38
|
}
|
|
@@ -46,7 +49,7 @@ export async function createReadRoadmapTool(directory) {
|
|
|
46
49
|
throw new Error(`Action "${args.actionNumber}" not found. Use ReadRoadmap with no arguments to see all available actions.`);
|
|
47
50
|
}
|
|
48
51
|
if (args.featureNumber) {
|
|
49
|
-
const featureNumberError =
|
|
52
|
+
const featureNumberError = RoadmapValidator.validateFeatureNumber(args.featureNumber);
|
|
50
53
|
if (featureNumberError) {
|
|
51
54
|
throw new Error(`${featureNumberError.message} Use ReadRoadmap to see valid feature numbers.`);
|
|
52
55
|
}
|
|
@@ -1,2 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool for updating action status and descriptions in a roadmap.
|
|
3
|
+
* Enforces forward-only status progression and archives when complete.
|
|
4
|
+
*/
|
|
1
5
|
import { type ToolDefinition } from "@opencode-ai/plugin";
|
|
2
6
|
export declare function createUpdateRoadmapTool(directory: string): Promise<ToolDefinition>;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool for updating action status and descriptions in a roadmap.
|
|
3
|
+
* Enforces forward-only status progression and archives when complete.
|
|
4
|
+
*/
|
|
1
5
|
import { tool } from "@opencode-ai/plugin";
|
|
2
6
|
import { FileStorage, RoadmapValidator } from "../storage.js";
|
|
3
7
|
import { loadDescription } from "../descriptions/index.js";
|
|
4
|
-
import { getErrorMessage } from "../errors/loader.js";
|
|
5
8
|
export async function createUpdateRoadmapTool(directory) {
|
|
6
9
|
const description = await loadDescription("updateroadmap.txt");
|
|
7
10
|
return tool({
|
|
@@ -13,20 +16,20 @@ export async function createUpdateRoadmapTool(directory) {
|
|
|
13
16
|
.optional()
|
|
14
17
|
.describe("New action description (full overwrite). If not provided, only status is updated."),
|
|
15
18
|
status: tool.schema
|
|
16
|
-
.enum(["pending", "in_progress", "completed"])
|
|
19
|
+
.enum(["pending", "in_progress", "completed", "cancelled"])
|
|
17
20
|
.optional()
|
|
18
|
-
.describe("New action status
|
|
21
|
+
.describe("New action status. Flexible transitions allowed except from cancelled."),
|
|
19
22
|
},
|
|
20
23
|
async execute(args) {
|
|
21
24
|
const storage = new FileStorage(directory);
|
|
22
25
|
if (!(await storage.exists())) {
|
|
23
|
-
throw new Error(
|
|
26
|
+
throw new Error("Roadmap not found. Use CreateRoadmap to create one.");
|
|
24
27
|
}
|
|
25
28
|
const roadmap = await storage.read();
|
|
26
29
|
if (!roadmap) {
|
|
27
|
-
throw new Error(
|
|
30
|
+
throw new Error("Roadmap file is corrupted. Please fix manually.");
|
|
28
31
|
}
|
|
29
|
-
const actionNumberError =
|
|
32
|
+
const actionNumberError = RoadmapValidator.validateActionNumber(args.actionNumber);
|
|
30
33
|
if (actionNumberError) {
|
|
31
34
|
throw new Error(`${actionNumberError.message} Use ReadRoadmap to see valid action numbers.`);
|
|
32
35
|
}
|
|
@@ -43,17 +46,21 @@ export async function createUpdateRoadmapTool(directory) {
|
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
if (!actionFound) {
|
|
46
|
-
throw new Error(
|
|
49
|
+
throw new Error(`Action "${args.actionNumber}" not found. Use ReadRoadmap to see valid action numbers.`);
|
|
50
|
+
}
|
|
51
|
+
// TypeScript: we know targetAction and targetFeature are not null here
|
|
52
|
+
if (!targetAction || !targetFeature) {
|
|
53
|
+
throw new Error("Internal error: target action not found.");
|
|
47
54
|
}
|
|
48
55
|
// Validate that at least one field is being updated
|
|
49
56
|
if (args.description === undefined && args.status === undefined) {
|
|
50
|
-
throw new Error(
|
|
57
|
+
throw new Error("No changes specified. Please provide description and/or status.");
|
|
51
58
|
}
|
|
52
59
|
const oldStatus = targetAction.status;
|
|
53
60
|
const oldDescription = targetAction.description;
|
|
54
61
|
// Validate description if provided
|
|
55
62
|
if (args.description !== undefined) {
|
|
56
|
-
const descError =
|
|
63
|
+
const descError = RoadmapValidator.validateDescription(args.description, "action");
|
|
57
64
|
if (descError) {
|
|
58
65
|
throw new Error(`${descError.message}`);
|
|
59
66
|
}
|
|
@@ -61,7 +68,7 @@ export async function createUpdateRoadmapTool(directory) {
|
|
|
61
68
|
}
|
|
62
69
|
// Validate and update status if provided
|
|
63
70
|
if (args.status !== undefined) {
|
|
64
|
-
const statusTransitionError =
|
|
71
|
+
const statusTransitionError = RoadmapValidator.validateStatusProgression(targetAction.status, args.status);
|
|
65
72
|
if (statusTransitionError) {
|
|
66
73
|
throw new Error(`${statusTransitionError.message} Current status: "${targetAction.status}", requested: "${args.status}"`);
|
|
67
74
|
}
|
|
@@ -75,6 +82,9 @@ export async function createUpdateRoadmapTool(directory) {
|
|
|
75
82
|
if (args.status !== undefined && oldStatus !== args.status) {
|
|
76
83
|
changes.push(`status: "${oldStatus}" → "${args.status}"`);
|
|
77
84
|
}
|
|
85
|
+
if (changes.length === 0) {
|
|
86
|
+
return `Action ${args.actionNumber} unchanged. Provided values match current state.`;
|
|
87
|
+
}
|
|
78
88
|
// Check if all actions are completed
|
|
79
89
|
let allCompleted = true;
|
|
80
90
|
for (const feature of roadmap.features) {
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
export declare const ActionStatus: z.ZodEnum<["pending", "in_progress", "completed"]>;
|
|
2
|
+
export declare const ActionStatus: z.ZodEnum<["pending", "in_progress", "completed", "cancelled"]>;
|
|
3
3
|
export type ActionStatus = z.infer<typeof ActionStatus>;
|
|
4
4
|
export declare const Action: z.ZodObject<{
|
|
5
5
|
number: z.ZodString;
|
|
6
6
|
description: z.ZodString;
|
|
7
|
-
status: z.ZodEnum<["pending", "in_progress", "completed"]>;
|
|
7
|
+
status: z.ZodEnum<["pending", "in_progress", "completed", "cancelled"]>;
|
|
8
8
|
}, "strip", z.ZodTypeAny, {
|
|
9
9
|
number: string;
|
|
10
|
-
status: "pending" | "in_progress" | "completed";
|
|
10
|
+
status: "pending" | "in_progress" | "completed" | "cancelled";
|
|
11
11
|
description: string;
|
|
12
12
|
}, {
|
|
13
13
|
number: string;
|
|
14
|
-
status: "pending" | "in_progress" | "completed";
|
|
14
|
+
status: "pending" | "in_progress" | "completed" | "cancelled";
|
|
15
15
|
description: string;
|
|
16
16
|
}>;
|
|
17
17
|
export type Action = z.infer<typeof Action>;
|
|
@@ -22,14 +22,14 @@ export declare const Feature: z.ZodObject<{
|
|
|
22
22
|
actions: z.ZodArray<z.ZodObject<{
|
|
23
23
|
number: z.ZodString;
|
|
24
24
|
description: z.ZodString;
|
|
25
|
-
status: z.ZodEnum<["pending", "in_progress", "completed"]>;
|
|
25
|
+
status: z.ZodEnum<["pending", "in_progress", "completed", "cancelled"]>;
|
|
26
26
|
}, "strip", z.ZodTypeAny, {
|
|
27
27
|
number: string;
|
|
28
|
-
status: "pending" | "in_progress" | "completed";
|
|
28
|
+
status: "pending" | "in_progress" | "completed" | "cancelled";
|
|
29
29
|
description: string;
|
|
30
30
|
}, {
|
|
31
31
|
number: string;
|
|
32
|
-
status: "pending" | "in_progress" | "completed";
|
|
32
|
+
status: "pending" | "in_progress" | "completed" | "cancelled";
|
|
33
33
|
description: string;
|
|
34
34
|
}>, "many">;
|
|
35
35
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -38,7 +38,7 @@ export declare const Feature: z.ZodObject<{
|
|
|
38
38
|
title: string;
|
|
39
39
|
actions: {
|
|
40
40
|
number: string;
|
|
41
|
-
status: "pending" | "in_progress" | "completed";
|
|
41
|
+
status: "pending" | "in_progress" | "completed" | "cancelled";
|
|
42
42
|
description: string;
|
|
43
43
|
}[];
|
|
44
44
|
}, {
|
|
@@ -47,7 +47,7 @@ export declare const Feature: z.ZodObject<{
|
|
|
47
47
|
title: string;
|
|
48
48
|
actions: {
|
|
49
49
|
number: string;
|
|
50
|
-
status: "pending" | "in_progress" | "completed";
|
|
50
|
+
status: "pending" | "in_progress" | "completed" | "cancelled";
|
|
51
51
|
description: string;
|
|
52
52
|
}[];
|
|
53
53
|
}>;
|
|
@@ -60,14 +60,14 @@ export declare const Roadmap: z.ZodObject<{
|
|
|
60
60
|
actions: z.ZodArray<z.ZodObject<{
|
|
61
61
|
number: z.ZodString;
|
|
62
62
|
description: z.ZodString;
|
|
63
|
-
status: z.ZodEnum<["pending", "in_progress", "completed"]>;
|
|
63
|
+
status: z.ZodEnum<["pending", "in_progress", "completed", "cancelled"]>;
|
|
64
64
|
}, "strip", z.ZodTypeAny, {
|
|
65
65
|
number: string;
|
|
66
|
-
status: "pending" | "in_progress" | "completed";
|
|
66
|
+
status: "pending" | "in_progress" | "completed" | "cancelled";
|
|
67
67
|
description: string;
|
|
68
68
|
}, {
|
|
69
69
|
number: string;
|
|
70
|
-
status: "pending" | "in_progress" | "completed";
|
|
70
|
+
status: "pending" | "in_progress" | "completed" | "cancelled";
|
|
71
71
|
description: string;
|
|
72
72
|
}>, "many">;
|
|
73
73
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -76,7 +76,7 @@ export declare const Roadmap: z.ZodObject<{
|
|
|
76
76
|
title: string;
|
|
77
77
|
actions: {
|
|
78
78
|
number: string;
|
|
79
|
-
status: "pending" | "in_progress" | "completed";
|
|
79
|
+
status: "pending" | "in_progress" | "completed" | "cancelled";
|
|
80
80
|
description: string;
|
|
81
81
|
}[];
|
|
82
82
|
}, {
|
|
@@ -85,7 +85,7 @@ export declare const Roadmap: z.ZodObject<{
|
|
|
85
85
|
title: string;
|
|
86
86
|
actions: {
|
|
87
87
|
number: string;
|
|
88
|
-
status: "pending" | "in_progress" | "completed";
|
|
88
|
+
status: "pending" | "in_progress" | "completed" | "cancelled";
|
|
89
89
|
description: string;
|
|
90
90
|
}[];
|
|
91
91
|
}>, "many">;
|
|
@@ -96,7 +96,7 @@ export declare const Roadmap: z.ZodObject<{
|
|
|
96
96
|
title: string;
|
|
97
97
|
actions: {
|
|
98
98
|
number: string;
|
|
99
|
-
status: "pending" | "in_progress" | "completed";
|
|
99
|
+
status: "pending" | "in_progress" | "completed" | "cancelled";
|
|
100
100
|
description: string;
|
|
101
101
|
}[];
|
|
102
102
|
}[];
|
|
@@ -107,7 +107,7 @@ export declare const Roadmap: z.ZodObject<{
|
|
|
107
107
|
title: string;
|
|
108
108
|
actions: {
|
|
109
109
|
number: string;
|
|
110
|
-
status: "pending" | "in_progress" | "completed";
|
|
110
|
+
status: "pending" | "in_progress" | "completed" | "cancelled";
|
|
111
111
|
description: string;
|
|
112
112
|
}[];
|
|
113
113
|
}[];
|
|
@@ -129,21 +129,21 @@ export interface ValidationResult {
|
|
|
129
129
|
errors: ValidationError[];
|
|
130
130
|
}
|
|
131
131
|
export interface CreateRoadmapInput {
|
|
132
|
-
features:
|
|
132
|
+
features: {
|
|
133
133
|
number: string;
|
|
134
134
|
title: string;
|
|
135
135
|
description: string;
|
|
136
|
-
actions:
|
|
136
|
+
actions: {
|
|
137
137
|
number: string;
|
|
138
138
|
description: string;
|
|
139
139
|
status: "pending";
|
|
140
|
-
}
|
|
141
|
-
}
|
|
140
|
+
}[];
|
|
141
|
+
}[];
|
|
142
142
|
}
|
|
143
143
|
export interface UpdateRoadmapInput {
|
|
144
144
|
actionNumber: string;
|
|
145
145
|
description?: string;
|
|
146
|
-
status
|
|
146
|
+
status?: ActionStatus;
|
|
147
147
|
}
|
|
148
148
|
export interface ReadRoadmapInput {
|
|
149
149
|
actionNumber?: string;
|
package/dist/src/types.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@howaboua/opencode-roadmap-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Strategic roadmap planning and multi-agent coordination for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -25,18 +25,24 @@
|
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@opencode-ai/plugin": "^1.0.0",
|
|
27
27
|
"@types/node": "^20.0.0",
|
|
28
|
+
"@typescript-eslint/eslint-plugin": "^8.48.1",
|
|
29
|
+
"@typescript-eslint/parser": "^8.48.1",
|
|
30
|
+
"eslint": "^9.39.1",
|
|
31
|
+
"eslint-config-prettier": "^10.1.8",
|
|
28
32
|
"typescript": "^5.0.0"
|
|
29
33
|
},
|
|
30
34
|
"dependencies": {
|
|
31
|
-
"zod": "^3.22.0"
|
|
32
|
-
"typescript": "^5.0.0"
|
|
35
|
+
"zod": "^3.22.0"
|
|
33
36
|
},
|
|
34
|
-
|
|
35
37
|
"scripts": {
|
|
36
38
|
"build": "tsc -p tsconfig.json && npm run copy-assets",
|
|
37
39
|
"copy-assets": "mkdir -p dist/src/descriptions dist/src/errors && cp src/descriptions/*.txt dist/src/descriptions/ && cp src/errors/*.txt dist/src/errors/",
|
|
40
|
+
"lint": "eslint . --ext .ts",
|
|
38
41
|
"prepublishOnly": "npm run build"
|
|
39
42
|
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
40
46
|
"opencode": {
|
|
41
47
|
"type": "plugin",
|
|
42
48
|
"tools": [
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { loadDescription } from "./loader";
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { loadDescription } from "./loader.js";
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function loadDescription(filename: string): Promise<string>;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
export async function loadDescription(filename) {
|
|
4
|
-
// In the compiled output, __dirname is .../dist/descriptions, but the assets are in .../src/descriptions.
|
|
5
|
-
// This path adjustment ensures the assets are found regardless of the build process.
|
|
6
|
-
const descriptionsDir = join(__dirname, "..", "..", "src", "descriptions");
|
|
7
|
-
const filePath = join(descriptionsDir, filename);
|
|
8
|
-
try {
|
|
9
|
-
return await fs.readFile(filePath, "utf-8");
|
|
10
|
-
}
|
|
11
|
-
catch (error) {
|
|
12
|
-
if (error.code === "ENOENT") {
|
|
13
|
-
throw new Error(`Description file not found: ${filename}. Looked in: ${filePath}. Please ensure asset files are correctly located.`);
|
|
14
|
-
}
|
|
15
|
-
throw error;
|
|
16
|
-
}
|
|
17
|
-
}
|
package/dist/errors/loader.d.ts
DELETED
package/dist/errors/loader.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
const ERROR_CACHE = {};
|
|
4
|
-
export async function loadErrorTemplate(filename) {
|
|
5
|
-
if (ERROR_CACHE[filename])
|
|
6
|
-
return ERROR_CACHE[filename];
|
|
7
|
-
const errorsDir = join(__dirname, "..", "..", "src", "errors");
|
|
8
|
-
const filePath = join(errorsDir, filename + ".txt");
|
|
9
|
-
try {
|
|
10
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
11
|
-
ERROR_CACHE[filename] = content.trim();
|
|
12
|
-
return ERROR_CACHE[filename];
|
|
13
|
-
}
|
|
14
|
-
catch (error) {
|
|
15
|
-
if (error.code === "ENOENT") {
|
|
16
|
-
throw new Error(`Error template not found: ${filename} at ${filePath}`);
|
|
17
|
-
}
|
|
18
|
-
throw error;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
export async function getErrorMessage(filename, params = {}) {
|
|
22
|
-
const template = await loadErrorTemplate(filename);
|
|
23
|
-
return template.replace(/\{(\w+)\}/g, (_, key) => params[key] || `{${key}}`);
|
|
24
|
-
}
|
package/dist/storage.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { Roadmap, RoadmapStorage, ValidationError } from "./types";
|
|
2
|
-
export declare class FileStorage implements RoadmapStorage {
|
|
3
|
-
private directory;
|
|
4
|
-
constructor(directory: string);
|
|
5
|
-
exists(): Promise<boolean>;
|
|
6
|
-
read(): Promise<Roadmap | null>;
|
|
7
|
-
write(roadmap: Roadmap): Promise<void>;
|
|
8
|
-
archive(): Promise<string>;
|
|
9
|
-
}
|
|
10
|
-
export declare class RoadmapValidator {
|
|
11
|
-
static validateFeatureNumber(number: string): Promise<ValidationError | null>;
|
|
12
|
-
static validateActionNumber(number: string): Promise<ValidationError | null>;
|
|
13
|
-
static validateActionSequence(actions: Array<{
|
|
14
|
-
number: string;
|
|
15
|
-
}>, globalSeenNumbers?: Set<string>, featureNumber?: string): Promise<ValidationError[]>;
|
|
16
|
-
static validateFeatureSequence(features: Array<{
|
|
17
|
-
number: string;
|
|
18
|
-
actions: Array<{
|
|
19
|
-
number: string;
|
|
20
|
-
}>;
|
|
21
|
-
}>): Promise<ValidationError[]>;
|
|
22
|
-
static validateTitle(title: string, fieldType: "feature" | "action"): Promise<ValidationError | null>;
|
|
23
|
-
static validateDescription(description: string, fieldType: "feature" | "action"): Promise<ValidationError | null>;
|
|
24
|
-
static validateStatusProgression(currentStatus: string, newStatus: string): Promise<ValidationError | null>;
|
|
25
|
-
}
|
package/dist/storage.js
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import { getErrorMessage } from "./errors/loader.js";
|
|
4
|
-
const ROADMAP_FILE = "roadmap.json";
|
|
5
|
-
export class FileStorage {
|
|
6
|
-
directory;
|
|
7
|
-
constructor(directory) {
|
|
8
|
-
this.directory = directory;
|
|
9
|
-
}
|
|
10
|
-
async exists() {
|
|
11
|
-
try {
|
|
12
|
-
await fs.access(join(this.directory, ROADMAP_FILE));
|
|
13
|
-
return true;
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
async read() {
|
|
20
|
-
try {
|
|
21
|
-
const filePath = join(this.directory, ROADMAP_FILE);
|
|
22
|
-
const data = await fs.readFile(filePath, "utf-8");
|
|
23
|
-
if (!data.trim()) {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
const parsed = JSON.parse(data);
|
|
27
|
-
if (!parsed || typeof parsed !== "object") {
|
|
28
|
-
throw new Error("Invalid roadmap format: not an object");
|
|
29
|
-
}
|
|
30
|
-
if (!Array.isArray(parsed.features)) {
|
|
31
|
-
throw new Error("Invalid roadmap format: missing or invalid features array");
|
|
32
|
-
}
|
|
33
|
-
return parsed;
|
|
34
|
-
}
|
|
35
|
-
catch (error) {
|
|
36
|
-
if (error instanceof SyntaxError) {
|
|
37
|
-
throw new Error("Roadmap file contains invalid JSON. File may be corrupted.");
|
|
38
|
-
}
|
|
39
|
-
if (error instanceof Error && error.message.includes("ENOENT")) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
throw error;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
async write(roadmap) {
|
|
46
|
-
const filePath = join(this.directory, ROADMAP_FILE);
|
|
47
|
-
const tempPath = join(this.directory, `${ROADMAP_FILE}.tmp.${Date.now()}`);
|
|
48
|
-
try {
|
|
49
|
-
const data = JSON.stringify(roadmap, null, 2);
|
|
50
|
-
await fs.writeFile(tempPath, data, "utf-8");
|
|
51
|
-
await fs.rename(tempPath, filePath);
|
|
52
|
-
}
|
|
53
|
-
catch (error) {
|
|
54
|
-
try {
|
|
55
|
-
await fs.unlink(tempPath);
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
// Ignore cleanup errors
|
|
59
|
-
}
|
|
60
|
-
throw error;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
async archive() {
|
|
64
|
-
const filePath = join(this.directory, ROADMAP_FILE);
|
|
65
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
66
|
-
const archiveFilename = `roadmap.archive.${timestamp}.json`;
|
|
67
|
-
const archivePath = join(this.directory, archiveFilename);
|
|
68
|
-
await fs.rename(filePath, archivePath);
|
|
69
|
-
return archiveFilename;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
export class RoadmapValidator {
|
|
73
|
-
static async validateFeatureNumber(number) {
|
|
74
|
-
if (!number || typeof number !== "string") {
|
|
75
|
-
return {
|
|
76
|
-
code: "INVALID_FEATURE_NUMBER",
|
|
77
|
-
message: await getErrorMessage("invalid_feature_id", { id: "undefined" }),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
if (!/^\d+$/.test(number)) {
|
|
81
|
-
return {
|
|
82
|
-
code: "INVALID_FEATURE_NUMBER_FORMAT",
|
|
83
|
-
message: await getErrorMessage("invalid_feature_id", { id: number }),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
static async validateActionNumber(number) {
|
|
89
|
-
if (!number || typeof number !== "string") {
|
|
90
|
-
return {
|
|
91
|
-
code: "INVALID_ACTION_NUMBER",
|
|
92
|
-
message: await getErrorMessage("invalid_action_id", { id: "undefined" }),
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
if (!/^\d+\.\d{2}$/.test(number)) {
|
|
96
|
-
return {
|
|
97
|
-
code: "INVALID_ACTION_NUMBER_FORMAT",
|
|
98
|
-
message: await getErrorMessage("invalid_action_id", { id: number }),
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
static async validateActionSequence(actions, globalSeenNumbers, featureNumber) {
|
|
104
|
-
const errors = [];
|
|
105
|
-
const seenNumbers = new Set();
|
|
106
|
-
for (let i = 0; i < actions.length; i++) {
|
|
107
|
-
const action = actions[i];
|
|
108
|
-
const numberError = await this.validateActionNumber(action.number);
|
|
109
|
-
if (numberError) {
|
|
110
|
-
errors.push(numberError);
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
// Check action-feature mismatch
|
|
114
|
-
if (featureNumber) {
|
|
115
|
-
const actionFeaturePrefix = action.number.split('.')[0];
|
|
116
|
-
if (actionFeaturePrefix !== featureNumber) {
|
|
117
|
-
errors.push({
|
|
118
|
-
code: "ACTION_FEATURE_MISMATCH",
|
|
119
|
-
message: await getErrorMessage("action_mismatch", { action: action.number, feature: featureNumber }),
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
// Check for duplicates within this feature
|
|
124
|
-
if (seenNumbers.has(action.number)) {
|
|
125
|
-
errors.push({
|
|
126
|
-
code: "DUPLICATE_ACTION_NUMBER",
|
|
127
|
-
message: `Duplicate action ID "${action.number}".`,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
// Check for global duplicates
|
|
131
|
-
if (globalSeenNumbers?.has(action.number)) {
|
|
132
|
-
errors.push({
|
|
133
|
-
code: "DUPLICATE_ACTION_NUMBER_GLOBAL",
|
|
134
|
-
message: `Duplicate action ID "${action.number}" (exists in another feature).`,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
seenNumbers.add(action.number);
|
|
138
|
-
globalSeenNumbers?.add(action.number);
|
|
139
|
-
}
|
|
140
|
-
return errors;
|
|
141
|
-
}
|
|
142
|
-
static async validateFeatureSequence(features) {
|
|
143
|
-
const errors = [];
|
|
144
|
-
const seenNumbers = new Set();
|
|
145
|
-
const seenActionNumbers = new Set();
|
|
146
|
-
for (let i = 0; i < features.length; i++) {
|
|
147
|
-
const feature = features[i];
|
|
148
|
-
const numberError = await this.validateFeatureNumber(feature.number);
|
|
149
|
-
if (numberError) {
|
|
150
|
-
errors.push(numberError);
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
if (seenNumbers.has(feature.number)) {
|
|
154
|
-
errors.push({
|
|
155
|
-
code: "DUPLICATE_FEATURE_NUMBER",
|
|
156
|
-
message: `Duplicate feature ID "${feature.number}".`,
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
seenNumbers.add(feature.number);
|
|
160
|
-
const actionErrors = await this.validateActionSequence(feature.actions, seenActionNumbers, feature.number);
|
|
161
|
-
errors.push(...actionErrors);
|
|
162
|
-
}
|
|
163
|
-
return errors;
|
|
164
|
-
}
|
|
165
|
-
static async validateTitle(title, fieldType) {
|
|
166
|
-
if (!title || typeof title !== "string") {
|
|
167
|
-
return {
|
|
168
|
-
code: "INVALID_TITLE",
|
|
169
|
-
message: `Invalid ${fieldType} title. Must be non-empty string.`,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
if (title.trim() === "") {
|
|
173
|
-
return {
|
|
174
|
-
code: "EMPTY_TITLE",
|
|
175
|
-
message: `${fieldType.charAt(0).toUpperCase() + fieldType.slice(1)} title cannot be empty.`,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
static async validateDescription(description, fieldType) {
|
|
181
|
-
if (!description || typeof description !== "string") {
|
|
182
|
-
return {
|
|
183
|
-
code: "INVALID_DESCRIPTION",
|
|
184
|
-
message: `Invalid ${fieldType} description. Must be non-empty string.`,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
if (description.trim() === "") {
|
|
188
|
-
return {
|
|
189
|
-
code: "EMPTY_DESCRIPTION",
|
|
190
|
-
message: `${fieldType.charAt(0).toUpperCase() + fieldType.slice(1)} description cannot be empty.`,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
static async validateStatusProgression(currentStatus, newStatus) {
|
|
196
|
-
const statusFlow = {
|
|
197
|
-
pending: ["in_progress", "completed"],
|
|
198
|
-
in_progress: ["completed"],
|
|
199
|
-
completed: [],
|
|
200
|
-
};
|
|
201
|
-
const allowedTransitions = statusFlow[currentStatus] || [];
|
|
202
|
-
if (!allowedTransitions.includes(newStatus)) {
|
|
203
|
-
return {
|
|
204
|
-
code: "INVALID_STATUS_TRANSITION",
|
|
205
|
-
message: await getErrorMessage("invalid_transition", {
|
|
206
|
-
from: currentStatus,
|
|
207
|
-
to: newStatus,
|
|
208
|
-
allowed: allowedTransitions.length > 0 ? allowedTransitions.join(", ") : "None (terminal state)"
|
|
209
|
-
}),
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
}
|