@howaboua/opencode-roadmap-plugin 0.1.4 → 0.1.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.
@@ -7,9 +7,10 @@ export async function loadDescription(filename) {
7
7
  return await fs.readFile(filePath, "utf-8");
8
8
  }
9
9
  catch (error) {
10
- if (error.code === "ENOENT") {
10
+ const err = error;
11
+ if (err?.code === "ENOENT") {
11
12
  throw new Error(`Description file not found: ${filename}. Looked in: ${filePath}. Please ensure asset files are correctly located.`);
12
13
  }
13
- throw error;
14
+ throw err ?? new Error("Unknown error while loading description");
14
15
  }
15
16
  }
@@ -11,10 +11,11 @@ export async function loadErrorTemplate(filename) {
11
11
  return ERROR_CACHE[filename];
12
12
  }
13
13
  catch (error) {
14
- if (error.code === "ENOENT") {
14
+ const err = error;
15
+ if (err?.code === "ENOENT") {
15
16
  throw new Error(`Error template not found: ${filename} at ${filePath}`);
16
17
  }
17
- throw error;
18
+ throw err ?? new Error("Unknown error loading error template");
18
19
  }
19
20
  }
20
21
  export async function getErrorMessage(filename, params = {}) {
package/dist/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createCreateRoadmapTool } from "./tools/createroadmap.js";
2
2
  import { createUpdateRoadmapTool } from "./tools/updateroadmap.js";
3
3
  import { createReadRoadmapTool } from "./tools/readroadmap.js";
4
- export const RoadmapPlugin = async ({ project, directory, worktree, $ }) => {
4
+ export const RoadmapPlugin = async ({ directory }) => {
5
5
  return {
6
6
  tool: {
7
7
  createroadmap: await createCreateRoadmapTool(directory),
@@ -1,6 +1,6 @@
1
1
  import type { Roadmap, RoadmapStorage, ValidationError } from "./types.js";
2
2
  export declare class FileStorage implements RoadmapStorage {
3
- private directory;
3
+ private readonly directory;
4
4
  constructor(directory: string);
5
5
  exists(): Promise<boolean>;
6
6
  read(): Promise<Roadmap | null>;
@@ -8,18 +8,18 @@ export declare class FileStorage implements RoadmapStorage {
8
8
  archive(): Promise<string>;
9
9
  }
10
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<{
11
+ static validateFeatureNumber(number: string): ValidationError | null;
12
+ static validateActionNumber(number: string): ValidationError | null;
13
+ static validateActionSequence(actions: {
14
14
  number: string;
15
- }>, globalSeenNumbers?: Set<string>, featureNumber?: string): Promise<ValidationError[]>;
16
- static validateFeatureSequence(features: Array<{
15
+ }[], globalSeenNumbers?: Set<string>, featureNumber?: string): ValidationError[];
16
+ static validateFeatureSequence(features: {
17
17
  number: string;
18
- actions: Array<{
18
+ actions: {
19
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>;
20
+ }[];
21
+ }[]): ValidationError[];
22
+ static validateTitle(title: string, fieldType: "feature" | "action"): ValidationError | null;
23
+ static validateDescription(description: string, fieldType: "feature" | "action"): ValidationError | null;
24
+ static validateStatusProgression(currentStatus: string, newStatus: string): ValidationError | null;
25
25
  }
@@ -1,6 +1,6 @@
1
1
  import { promises as fs } from "fs";
2
2
  import { join } from "path";
3
- import { getErrorMessage } from "./errors/loader.js";
3
+ import { Roadmap as RoadmapSchema } from "./types.js";
4
4
  const ROADMAP_FILE = "roadmap.json";
5
5
  export class FileStorage {
6
6
  directory;
@@ -24,13 +24,8 @@ export class FileStorage {
24
24
  return null;
25
25
  }
26
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;
27
+ const validated = RoadmapSchema.parse(parsed);
28
+ return validated;
34
29
  }
35
30
  catch (error) {
36
31
  if (error instanceof SyntaxError) {
@@ -39,7 +34,10 @@ export class FileStorage {
39
34
  if (error instanceof Error && error.message.includes("ENOENT")) {
40
35
  return null;
41
36
  }
42
- throw error;
37
+ if (error instanceof Error) {
38
+ throw error;
39
+ }
40
+ throw new Error("Unknown error while reading roadmap");
43
41
  }
44
42
  }
45
43
  async write(roadmap) {
@@ -57,7 +55,10 @@ export class FileStorage {
57
55
  catch {
58
56
  // Ignore cleanup errors
59
57
  }
60
- throw error;
58
+ if (error instanceof Error) {
59
+ throw error;
60
+ }
61
+ throw new Error("Unknown error while writing roadmap");
61
62
  }
62
63
  }
63
64
  async archive() {
@@ -70,42 +71,41 @@ export class FileStorage {
70
71
  }
71
72
  }
72
73
  export class RoadmapValidator {
73
- static async validateFeatureNumber(number) {
74
+ static validateFeatureNumber(number) {
74
75
  if (!number || typeof number !== "string") {
75
76
  return {
76
77
  code: "INVALID_FEATURE_NUMBER",
77
- message: await getErrorMessage("invalid_feature_id", { id: "undefined" }),
78
+ message: "Invalid feature ID: must be a string.",
78
79
  };
79
80
  }
80
81
  if (!/^\d+$/.test(number)) {
81
82
  return {
82
83
  code: "INVALID_FEATURE_NUMBER_FORMAT",
83
- message: await getErrorMessage("invalid_feature_id", { id: number }),
84
+ message: "Invalid feature ID format: must be a simple number.",
84
85
  };
85
86
  }
86
87
  return null;
87
88
  }
88
- static async validateActionNumber(number) {
89
+ static validateActionNumber(number) {
89
90
  if (!number || typeof number !== "string") {
90
91
  return {
91
92
  code: "INVALID_ACTION_NUMBER",
92
- message: await getErrorMessage("invalid_action_id", { id: "undefined" }),
93
+ message: "Invalid action ID: must be a string.",
93
94
  };
94
95
  }
95
96
  if (!/^\d+\.\d{2}$/.test(number)) {
96
97
  return {
97
98
  code: "INVALID_ACTION_NUMBER_FORMAT",
98
- message: await getErrorMessage("invalid_action_id", { id: number }),
99
+ message: "Invalid action ID format: must be X.YY (e.g., 1.01).",
99
100
  };
100
101
  }
101
102
  return null;
102
103
  }
103
- static async validateActionSequence(actions, globalSeenNumbers, featureNumber) {
104
+ static validateActionSequence(actions, globalSeenNumbers, featureNumber) {
104
105
  const errors = [];
105
106
  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);
107
+ for (const action of actions) {
108
+ const numberError = this.validateActionNumber(action.number);
109
109
  if (numberError) {
110
110
  errors.push(numberError);
111
111
  continue;
@@ -116,7 +116,7 @@ export class RoadmapValidator {
116
116
  if (actionFeaturePrefix !== featureNumber) {
117
117
  errors.push({
118
118
  code: "ACTION_FEATURE_MISMATCH",
119
- message: await getErrorMessage("action_mismatch", { action: action.number, feature: featureNumber }),
119
+ message: `Action "${action.number}" does not belong to feature "${featureNumber}".`,
120
120
  });
121
121
  }
122
122
  }
@@ -139,13 +139,12 @@ export class RoadmapValidator {
139
139
  }
140
140
  return errors;
141
141
  }
142
- static async validateFeatureSequence(features) {
142
+ static validateFeatureSequence(features) {
143
143
  const errors = [];
144
144
  const seenNumbers = new Set();
145
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);
146
+ for (const feature of features) {
147
+ const numberError = this.validateFeatureNumber(feature.number);
149
148
  if (numberError) {
150
149
  errors.push(numberError);
151
150
  continue;
@@ -157,12 +156,12 @@ export class RoadmapValidator {
157
156
  });
158
157
  }
159
158
  seenNumbers.add(feature.number);
160
- const actionErrors = await this.validateActionSequence(feature.actions, seenActionNumbers, feature.number);
159
+ const actionErrors = this.validateActionSequence(feature.actions, seenActionNumbers, feature.number);
161
160
  errors.push(...actionErrors);
162
161
  }
163
162
  return errors;
164
163
  }
165
- static async validateTitle(title, fieldType) {
164
+ static validateTitle(title, fieldType) {
166
165
  if (!title || typeof title !== "string") {
167
166
  return {
168
167
  code: "INVALID_TITLE",
@@ -177,7 +176,7 @@ export class RoadmapValidator {
177
176
  }
178
177
  return null;
179
178
  }
180
- static async validateDescription(description, fieldType) {
179
+ static validateDescription(description, fieldType) {
181
180
  if (!description || typeof description !== "string") {
182
181
  return {
183
182
  code: "INVALID_DESCRIPTION",
@@ -192,7 +191,7 @@ export class RoadmapValidator {
192
191
  }
193
192
  return null;
194
193
  }
195
- static async validateStatusProgression(currentStatus, newStatus) {
194
+ static validateStatusProgression(currentStatus, newStatus) {
196
195
  const statusFlow = {
197
196
  pending: ["in_progress", "completed"],
198
197
  in_progress: ["completed"],
@@ -202,11 +201,7 @@ export class RoadmapValidator {
202
201
  if (!allowedTransitions.includes(newStatus)) {
203
202
  return {
204
203
  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
- }),
204
+ message: `Invalid transition from "${currentStatus}" to "${newStatus}". Allowed: ${allowedTransitions.length > 0 ? allowedTransitions.join(", ") : "None (terminal state)"}`,
210
205
  };
211
206
  }
212
207
  return null;
@@ -48,20 +48,20 @@ export async function createCreateRoadmapTool(directory) {
48
48
  if (!feature.actions || feature.actions.length === 0) {
49
49
  throw new Error(`Feature "${feature.number}" must have at least one action. Each feature needs at least one action to be valid.`);
50
50
  }
51
- const titleError = await RoadmapValidator.validateTitle(feature.title, "feature");
51
+ const titleError = RoadmapValidator.validateTitle(feature.title, "feature");
52
52
  if (titleError)
53
53
  validationErrors.push(titleError);
54
- const descError = await RoadmapValidator.validateDescription(feature.description, "feature");
54
+ const descError = RoadmapValidator.validateDescription(feature.description, "feature");
55
55
  if (descError)
56
56
  validationErrors.push(descError);
57
57
  for (const action of feature.actions) {
58
- const actionTitleError = await RoadmapValidator.validateTitle(action.description, "action");
58
+ const actionTitleError = RoadmapValidator.validateTitle(action.description, "action");
59
59
  if (actionTitleError)
60
60
  validationErrors.push(actionTitleError);
61
61
  }
62
62
  }
63
63
  // Validate sequence consistency of input (internal consistency)
64
- const sequenceErrors = await RoadmapValidator.validateFeatureSequence(args.features);
64
+ const sequenceErrors = RoadmapValidator.validateFeatureSequence(args.features);
65
65
  validationErrors.push(...sequenceErrors);
66
66
  if (validationErrors.length > 0) {
67
67
  const errorMessages = validationErrors.map((err) => err.message).join("\n");
@@ -118,7 +118,7 @@ export async function createCreateRoadmapTool(directory) {
118
118
  // Final Sort of Features
119
119
  roadmap.features.sort((a, b) => parseInt(a.number) - parseInt(b.number));
120
120
  // Final Validation of the Merged Roadmap
121
- const finalErrors = await RoadmapValidator.validateFeatureSequence(roadmap.features);
121
+ const finalErrors = RoadmapValidator.validateFeatureSequence(roadmap.features);
122
122
  if (finalErrors.length > 0) {
123
123
  throw new Error(`Resulting roadmap would be invalid:\n${finalErrors.map(e => e.message).join("\n")}`);
124
124
  }
@@ -1,7 +1,6 @@
1
1
  import { tool } from "@opencode-ai/plugin";
2
2
  import { FileStorage, RoadmapValidator } from "../storage.js";
3
3
  import { loadDescription } from "../descriptions/index.js";
4
- import { getErrorMessage } from "../errors/loader.js";
5
4
  export async function createReadRoadmapTool(directory) {
6
5
  const description = await loadDescription("readroadmap.txt");
7
6
  return tool({
@@ -19,17 +18,17 @@ export async function createReadRoadmapTool(directory) {
19
18
  async execute(args) {
20
19
  const storage = new FileStorage(directory);
21
20
  if (!(await storage.exists())) {
22
- throw new Error(await getErrorMessage("roadmap_not_found"));
21
+ throw new Error("Roadmap not found. Use CreateRoadmap to create one.");
23
22
  }
24
23
  const roadmap = await storage.read();
25
24
  if (!roadmap) {
26
- throw new Error(await getErrorMessage("roadmap_corrupted"));
25
+ throw new Error("Roadmap file is corrupted. Please fix manually.");
27
26
  }
28
27
  if (args.actionNumber && args.featureNumber) {
29
28
  throw new Error("Cannot specify both actionNumber and featureNumber. Use one or the other, or neither for full roadmap.");
30
29
  }
31
30
  if (args.actionNumber) {
32
- const actionNumberError = await RoadmapValidator.validateActionNumber(args.actionNumber);
31
+ const actionNumberError = RoadmapValidator.validateActionNumber(args.actionNumber);
33
32
  if (actionNumberError) {
34
33
  throw new Error(`${actionNumberError.message} Use ReadRoadmap to see valid action numbers.`);
35
34
  }
@@ -46,7 +45,7 @@ export async function createReadRoadmapTool(directory) {
46
45
  throw new Error(`Action "${args.actionNumber}" not found. Use ReadRoadmap with no arguments to see all available actions.`);
47
46
  }
48
47
  if (args.featureNumber) {
49
- const featureNumberError = await RoadmapValidator.validateFeatureNumber(args.featureNumber);
48
+ const featureNumberError = RoadmapValidator.validateFeatureNumber(args.featureNumber);
50
49
  if (featureNumberError) {
51
50
  throw new Error(`${featureNumberError.message} Use ReadRoadmap to see valid feature numbers.`);
52
51
  }
@@ -1,7 +1,6 @@
1
1
  import { tool } from "@opencode-ai/plugin";
2
2
  import { FileStorage, RoadmapValidator } from "../storage.js";
3
3
  import { loadDescription } from "../descriptions/index.js";
4
- import { getErrorMessage } from "../errors/loader.js";
5
4
  export async function createUpdateRoadmapTool(directory) {
6
5
  const description = await loadDescription("updateroadmap.txt");
7
6
  return tool({
@@ -20,13 +19,13 @@ export async function createUpdateRoadmapTool(directory) {
20
19
  async execute(args) {
21
20
  const storage = new FileStorage(directory);
22
21
  if (!(await storage.exists())) {
23
- throw new Error(await getErrorMessage("roadmap_not_found"));
22
+ throw new Error("Roadmap not found. Use CreateRoadmap to create one.");
24
23
  }
25
24
  const roadmap = await storage.read();
26
25
  if (!roadmap) {
27
- throw new Error(await getErrorMessage("roadmap_corrupted"));
26
+ throw new Error("Roadmap file is corrupted. Please fix manually.");
28
27
  }
29
- const actionNumberError = await RoadmapValidator.validateActionNumber(args.actionNumber);
28
+ const actionNumberError = RoadmapValidator.validateActionNumber(args.actionNumber);
30
29
  if (actionNumberError) {
31
30
  throw new Error(`${actionNumberError.message} Use ReadRoadmap to see valid action numbers.`);
32
31
  }
@@ -43,17 +42,21 @@ export async function createUpdateRoadmapTool(directory) {
43
42
  }
44
43
  }
45
44
  if (!actionFound) {
46
- throw new Error(await getErrorMessage("action_not_found", { id: args.actionNumber }));
45
+ throw new Error(`Action "${args.actionNumber}" not found. Use ReadRoadmap to see valid action numbers.`);
46
+ }
47
+ // TypeScript: we know targetAction and targetFeature are not null here
48
+ if (!targetAction || !targetFeature) {
49
+ throw new Error("Internal error: target action not found.");
47
50
  }
48
51
  // Validate that at least one field is being updated
49
52
  if (args.description === undefined && args.status === undefined) {
50
- throw new Error(await getErrorMessage("no_changes_specified"));
53
+ throw new Error("No changes specified. Please provide description and/or status.");
51
54
  }
52
55
  const oldStatus = targetAction.status;
53
56
  const oldDescription = targetAction.description;
54
57
  // Validate description if provided
55
58
  if (args.description !== undefined) {
56
- const descError = await RoadmapValidator.validateDescription(args.description, "action");
59
+ const descError = RoadmapValidator.validateDescription(args.description, "action");
57
60
  if (descError) {
58
61
  throw new Error(`${descError.message}`);
59
62
  }
@@ -61,7 +64,7 @@ export async function createUpdateRoadmapTool(directory) {
61
64
  }
62
65
  // Validate and update status if provided
63
66
  if (args.status !== undefined) {
64
- const statusTransitionError = await RoadmapValidator.validateStatusProgression(targetAction.status, args.status);
67
+ const statusTransitionError = RoadmapValidator.validateStatusProgression(targetAction.status, args.status);
65
68
  if (statusTransitionError) {
66
69
  throw new Error(`${statusTransitionError.message} Current status: "${targetAction.status}", requested: "${args.status}"`);
67
70
  }
@@ -129,21 +129,21 @@ export interface ValidationResult {
129
129
  errors: ValidationError[];
130
130
  }
131
131
  export interface CreateRoadmapInput {
132
- features: Array<{
132
+ features: {
133
133
  number: string;
134
134
  title: string;
135
135
  description: string;
136
- actions: Array<{
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: ActionStatus;
146
+ status?: ActionStatus;
147
147
  }
148
148
  export interface ReadRoadmapInput {
149
149
  actionNumber?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/opencode-roadmap-plugin",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Strategic roadmap planning and multi-agent coordination for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,16 +25,20 @@
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
+ "typescript": "^5.0.0",
36
+ "zod": "^3.22.0"
33
37
  },
34
-
35
38
  "scripts": {
36
39
  "build": "tsc -p tsconfig.json && npm run copy-assets",
37
40
  "copy-assets": "mkdir -p dist/src/descriptions dist/src/errors && cp src/descriptions/*.txt dist/src/descriptions/ && cp src/errors/*.txt dist/src/errors/",
41
+ "lint": "eslint . --ext .ts",
38
42
  "prepublishOnly": "npm run build"
39
43
  },
40
44
  "opencode": {