@howaboua/opencode-roadmap-plugin 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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
- }
@@ -1,2 +0,0 @@
1
- import { type ToolDefinition } from "@opencode-ai/plugin";
2
- export declare function createCreateRoadmapTool(directory: string): Promise<ToolDefinition>;
@@ -1,135 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin";
2
- import { FileStorage, RoadmapValidator } from "../storage.js";
3
- import { loadDescription } from "../descriptions/index.js";
4
- import { getErrorMessage } from "../errors/loader.js";
5
- export async function createCreateRoadmapTool(directory) {
6
- const description = await loadDescription("createroadmap.txt");
7
- return tool({
8
- description,
9
- args: {
10
- features: tool.schema
11
- .array(tool.schema.object({
12
- number: tool.schema.string().describe('Feature number as string ("1", "2", "3...")'),
13
- title: tool.schema.string().describe("Feature title"),
14
- description: tool.schema.string().describe("Brief description of what this feature accomplishes"),
15
- actions: tool.schema
16
- .array(tool.schema.object({
17
- number: tool.schema
18
- .string()
19
- .describe('Action number as string with two decimals ("1.01", "1.02", etc.)'),
20
- description: tool.schema.string().describe("Action description"),
21
- status: tool.schema.enum(["pending"]).describe('Initial action status (must be "pending")'),
22
- }))
23
- .describe("List of actions for this feature in order"),
24
- }))
25
- .describe("Array of features for roadmap"),
26
- },
27
- async execute(args) {
28
- const storage = new FileStorage(directory);
29
- let roadmap;
30
- let isUpdate = false;
31
- if (await storage.exists()) {
32
- const existing = await storage.read();
33
- if (!existing) {
34
- throw new Error("Existing roadmap file is corrupted. Please fix manually.");
35
- }
36
- roadmap = existing;
37
- isUpdate = true;
38
- }
39
- else {
40
- roadmap = { features: [] };
41
- }
42
- if (!args.features || args.features.length === 0) {
43
- throw new Error('Roadmap must have at least one feature with at least one action. Example: {"features": [{"number": "1", "title": "Feature 1", "description": "Description", "actions": [{"number": "1.01", "description": "Action 1", "status": "pending"}]}]}');
44
- }
45
- const validationErrors = [];
46
- // First pass: structural validation of input
47
- for (const feature of args.features) {
48
- if (!feature.actions || feature.actions.length === 0) {
49
- throw new Error(`Feature "${feature.number}" must have at least one action. Each feature needs at least one action to be valid.`);
50
- }
51
- const titleError = await RoadmapValidator.validateTitle(feature.title, "feature");
52
- if (titleError)
53
- validationErrors.push(titleError);
54
- const descError = await RoadmapValidator.validateDescription(feature.description, "feature");
55
- if (descError)
56
- validationErrors.push(descError);
57
- for (const action of feature.actions) {
58
- const actionTitleError = await RoadmapValidator.validateTitle(action.description, "action");
59
- if (actionTitleError)
60
- validationErrors.push(actionTitleError);
61
- }
62
- }
63
- // Validate sequence consistency of input (internal consistency)
64
- const sequenceErrors = await RoadmapValidator.validateFeatureSequence(args.features);
65
- validationErrors.push(...sequenceErrors);
66
- if (validationErrors.length > 0) {
67
- const errorMessages = validationErrors.map((err) => err.message).join("\n");
68
- throw new Error(`Validation errors:\n${errorMessages}\n\nPlease fix these issues and try again.`);
69
- }
70
- // Merge Logic
71
- for (const inputFeature of args.features) {
72
- const existingFeature = roadmap.features.find((f) => f.number === inputFeature.number);
73
- if (existingFeature) {
74
- // Feature exists: Validate Immutability
75
- if (existingFeature.title !== inputFeature.title || existingFeature.description !== inputFeature.description) {
76
- const msg = await getErrorMessage("immutable_feature", {
77
- id: inputFeature.number,
78
- oldTitle: existingFeature.title,
79
- oldDesc: existingFeature.description,
80
- newTitle: inputFeature.title,
81
- newDesc: inputFeature.description
82
- });
83
- throw new Error(msg);
84
- }
85
- // Process Actions
86
- for (const inputAction of inputFeature.actions) {
87
- const existingAction = existingFeature.actions.find((a) => a.number === inputAction.number);
88
- if (existingAction) {
89
- // Action exists: skip (immutable)
90
- continue;
91
- }
92
- else {
93
- // New Action: Append
94
- existingFeature.actions.push({
95
- number: inputAction.number,
96
- description: inputAction.description,
97
- status: inputAction.status,
98
- });
99
- // Sort actions to ensure order
100
- existingFeature.actions.sort((a, b) => parseFloat(a.number) - parseFloat(b.number));
101
- }
102
- }
103
- }
104
- else {
105
- // New Feature: Append
106
- roadmap.features.push({
107
- number: inputFeature.number,
108
- title: inputFeature.title,
109
- description: inputFeature.description,
110
- actions: inputFeature.actions.map((a) => ({
111
- number: a.number,
112
- description: a.description,
113
- status: a.status,
114
- })),
115
- });
116
- }
117
- }
118
- // Final Sort of Features
119
- roadmap.features.sort((a, b) => parseInt(a.number) - parseInt(b.number));
120
- // Final Validation of the Merged Roadmap
121
- const finalErrors = await RoadmapValidator.validateFeatureSequence(roadmap.features);
122
- if (finalErrors.length > 0) {
123
- throw new Error(`Resulting roadmap would be invalid:\n${finalErrors.map(e => e.message).join("\n")}`);
124
- }
125
- await storage.write(roadmap);
126
- const totalActions = roadmap.features.reduce((sum, feature) => sum + feature.actions.length, 0);
127
- const action = isUpdate ? "Updated" : "Created";
128
- const summary = `${action} roadmap with ${roadmap.features.length} features and ${totalActions} actions:\n` +
129
- roadmap.features
130
- .map((feature) => ` Feature ${feature.number}: ${feature.title} (${feature.actions.length} actions)`)
131
- .join("\n");
132
- return summary;
133
- },
134
- });
135
- }
@@ -1,2 +0,0 @@
1
- import { type ToolDefinition } from "@opencode-ai/plugin";
2
- export declare function createReadRoadmapTool(directory: string): Promise<ToolDefinition>;
@@ -1,90 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin";
2
- import { FileStorage, RoadmapValidator } from "../storage.js";
3
- import { loadDescription } from "../descriptions/index.js";
4
- import { getErrorMessage } from "../errors/loader.js";
5
- export async function createReadRoadmapTool(directory) {
6
- const description = await loadDescription("readroadmap.txt");
7
- return tool({
8
- description,
9
- args: {
10
- actionNumber: tool.schema
11
- .string()
12
- .optional()
13
- .describe('Specific action to read ("1.01", "1.02", etc.). If not provided, reads entire roadmap.'),
14
- featureNumber: tool.schema
15
- .string()
16
- .optional()
17
- .describe('Specific feature to read ("1", "2", etc.). Use only if not providing actionNumber.'),
18
- },
19
- async execute(args) {
20
- const storage = new FileStorage(directory);
21
- if (!(await storage.exists())) {
22
- throw new Error(await getErrorMessage("roadmap_not_found"));
23
- }
24
- const roadmap = await storage.read();
25
- if (!roadmap) {
26
- throw new Error(await getErrorMessage("roadmap_corrupted"));
27
- }
28
- if (args.actionNumber && args.featureNumber) {
29
- throw new Error("Cannot specify both actionNumber and featureNumber. Use one or the other, or neither for full roadmap.");
30
- }
31
- if (args.actionNumber) {
32
- const actionNumberError = await RoadmapValidator.validateActionNumber(args.actionNumber);
33
- if (actionNumberError) {
34
- throw new Error(`${actionNumberError.message} Use ReadRoadmap to see valid action numbers.`);
35
- }
36
- for (const feature of roadmap.features) {
37
- const action = feature.actions.find((a) => a.number === args.actionNumber);
38
- if (action) {
39
- return (`Action ${args.actionNumber} from Feature "${feature.title}":\n` +
40
- `Description: ${action.description}\n` +
41
- `Status: ${action.status}\n` +
42
- `Feature: ${feature.number} - ${feature.title}\n` +
43
- `Feature Description: ${feature.description}`);
44
- }
45
- }
46
- throw new Error(`Action "${args.actionNumber}" not found. Use ReadRoadmap with no arguments to see all available actions.`);
47
- }
48
- if (args.featureNumber) {
49
- const featureNumberError = await RoadmapValidator.validateFeatureNumber(args.featureNumber);
50
- if (featureNumberError) {
51
- throw new Error(`${featureNumberError.message} Use ReadRoadmap to see valid feature numbers.`);
52
- }
53
- const feature = roadmap.features.find((f) => f.number === args.featureNumber);
54
- if (!feature) {
55
- throw new Error(`Feature "${args.featureNumber}" not found. Use ReadRoadmap with no arguments to see all available features.`);
56
- }
57
- const actionList = feature.actions
58
- .map((action) => ` ${action.number}: ${action.description} [${action.status}]`)
59
- .join("\n");
60
- const completedCount = feature.actions.filter((a) => a.status === "completed").length;
61
- const totalCount = feature.actions.length;
62
- return (`Feature ${feature.number}: ${feature.title}\n` +
63
- `Description: ${feature.description}\n` +
64
- `Progress: ${completedCount}/${totalCount} actions completed\n` +
65
- `Actions:\n${actionList}`);
66
- }
67
- const totalActions = roadmap.features.reduce((sum, feature) => sum + feature.actions.length, 0);
68
- const completedActions = roadmap.features.reduce((sum, feature) => sum + feature.actions.filter((a) => a.status === "completed").length, 0);
69
- const inProgressActions = roadmap.features.reduce((sum, feature) => sum + feature.actions.filter((a) => a.status === "in_progress").length, 0);
70
- const pendingActions = totalActions - completedActions - inProgressActions;
71
- let output = `Project Roadmap Overview\n` +
72
- `========================\n` +
73
- `Features: ${roadmap.features.length}\n` +
74
- `Total Actions: ${totalActions}\n` +
75
- `Progress: ${completedActions} completed, ${inProgressActions} in progress, ${pendingActions} pending\n\n`;
76
- for (const feature of roadmap.features) {
77
- const featureCompleted = feature.actions.filter((a) => a.status === "completed").length;
78
- const featureTotal = feature.actions.length;
79
- output += `Feature ${feature.number}: ${feature.title} (${featureCompleted}/${featureTotal} complete)\n`;
80
- output += ` Description: ${feature.description}\n`;
81
- for (const action of feature.actions) {
82
- const statusIcon = action.status === "completed" ? "✓" : action.status === "in_progress" ? "→" : "○";
83
- output += ` ${action.number} ${statusIcon} ${action.description} [${action.status}]\n`;
84
- }
85
- output += "\n";
86
- }
87
- return output.trim();
88
- },
89
- });
90
- }
@@ -1,2 +0,0 @@
1
- import { type ToolDefinition } from "@opencode-ai/plugin";
2
- export declare function createUpdateRoadmapTool(directory: string): Promise<ToolDefinition>;
@@ -1,107 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin";
2
- import { FileStorage, RoadmapValidator } from "../storage.js";
3
- import { loadDescription } from "../descriptions/index.js";
4
- import { getErrorMessage } from "../errors/loader.js";
5
- export async function createUpdateRoadmapTool(directory) {
6
- const description = await loadDescription("updateroadmap.txt");
7
- return tool({
8
- description,
9
- args: {
10
- actionNumber: tool.schema.string().describe('Action number to update ("1.01", "1.02", "2.01", etc.) - required'),
11
- description: tool.schema
12
- .string()
13
- .optional()
14
- .describe("New action description (full overwrite). If not provided, only status is updated."),
15
- status: tool.schema
16
- .enum(["pending", "in_progress", "completed"])
17
- .optional()
18
- .describe("New action status - optional if only updating description"),
19
- },
20
- async execute(args) {
21
- const storage = new FileStorage(directory);
22
- if (!(await storage.exists())) {
23
- throw new Error(await getErrorMessage("roadmap_not_found"));
24
- }
25
- const roadmap = await storage.read();
26
- if (!roadmap) {
27
- throw new Error(await getErrorMessage("roadmap_corrupted"));
28
- }
29
- const actionNumberError = await RoadmapValidator.validateActionNumber(args.actionNumber);
30
- if (actionNumberError) {
31
- throw new Error(`${actionNumberError.message} Use ReadRoadmap to see valid action numbers.`);
32
- }
33
- let targetAction = null;
34
- let targetFeature = null;
35
- let actionFound = false;
36
- for (const feature of roadmap.features) {
37
- const action = feature.actions.find((a) => a.number === args.actionNumber);
38
- if (action) {
39
- targetAction = action;
40
- targetFeature = feature;
41
- actionFound = true;
42
- break;
43
- }
44
- }
45
- if (!actionFound) {
46
- throw new Error(await getErrorMessage("action_not_found", { id: args.actionNumber }));
47
- }
48
- // Validate that at least one field is being updated
49
- if (args.description === undefined && args.status === undefined) {
50
- throw new Error(await getErrorMessage("no_changes_specified"));
51
- }
52
- const oldStatus = targetAction.status;
53
- const oldDescription = targetAction.description;
54
- // Validate description if provided
55
- if (args.description !== undefined) {
56
- const descError = await RoadmapValidator.validateDescription(args.description, "action");
57
- if (descError) {
58
- throw new Error(`${descError.message}`);
59
- }
60
- targetAction.description = args.description;
61
- }
62
- // Validate and update status if provided
63
- if (args.status !== undefined) {
64
- const statusTransitionError = await RoadmapValidator.validateStatusProgression(targetAction.status, args.status);
65
- if (statusTransitionError) {
66
- throw new Error(`${statusTransitionError.message} Current status: "${targetAction.status}", requested: "${args.status}"`);
67
- }
68
- targetAction.status = args.status;
69
- }
70
- await storage.write(roadmap);
71
- const changes = [];
72
- if (args.description !== undefined && oldDescription !== args.description) {
73
- changes.push(`description updated`);
74
- }
75
- if (args.status !== undefined && oldStatus !== args.status) {
76
- changes.push(`status: "${oldStatus}" → "${args.status}"`);
77
- }
78
- // Check if all actions are completed
79
- let allCompleted = true;
80
- for (const feature of roadmap.features) {
81
- for (const action of feature.actions) {
82
- if (action.status !== "completed") {
83
- allCompleted = false;
84
- break;
85
- }
86
- }
87
- if (!allCompleted)
88
- break;
89
- }
90
- let archiveMsg = "";
91
- if (allCompleted) {
92
- const archiveName = await storage.archive();
93
- archiveMsg = `\n\n🎉 All actions completed! Roadmap archived to "${archiveName}".`;
94
- }
95
- // Format feature context
96
- const featureCompleted = targetFeature.actions.filter((a) => a.status === "completed").length;
97
- const featureTotal = targetFeature.actions.length;
98
- let featureContext = `\n\nFeature ${targetFeature.number}: ${targetFeature.title} (${featureCompleted}/${featureTotal} complete)\n`;
99
- featureContext += `Description: ${targetFeature.description}\n`;
100
- for (const action of targetFeature.actions) {
101
- const statusIcon = action.status === "completed" ? "✓" : action.status === "in_progress" ? "→" : "○";
102
- featureContext += `${action.number} ${statusIcon} ${action.description} [${action.status}]\n`;
103
- }
104
- return `Updated action ${args.actionNumber} in feature "${targetFeature.title}": ${changes.join(", ")}${featureContext}${archiveMsg}`;
105
- },
106
- });
107
- }