@we-scrum/cli 1.0.0 → 1.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@we-scrum/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Cli tool for we-scrum application",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {
@@ -23,30 +23,32 @@
23
23
  ]
24
24
  },
25
25
  "dependencies": {
26
- "@inquirer/prompts": "^8.3.0",
27
- "@modelcontextprotocol/sdk": "^1.26.0",
28
- "commander": "^11.1.0",
26
+ "@inquirer/prompts": "8.3.0",
27
+ "zod": "4.3.6",
28
+ "@modelcontextprotocol/sdk": "1.27.1",
29
+ "commander": "14.0.3",
29
30
  "firebase": "12.10.0",
30
31
  "google-auth-library": "^10.6.1",
31
32
  "open": "^11.0.0"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/node": "22.18.6",
36
+ "@modelcontextprotocol/inspector": "0.21.1",
35
37
  "tsup": "8.5.1",
36
38
  "typescript": "5.9.3",
37
39
  "@my-devkit/cli": "2.1.0",
40
+ "@we-scrum/commands": "1.0.0",
38
41
  "@my-devkit/core": "1.0.0",
39
42
  "@we-scrum/enums": "1.0.0",
40
43
  "@we-scrum/models": "1.0.0",
41
- "@we-scrum/commands": "1.0.0"
44
+ "@we-scrum/utils": "1.0.0"
42
45
  },
43
46
  "scripts": {
44
- "build": "mdk build",
45
- "watch": "mdk watch",
46
- "build:prod": "tsup",
47
- "publish:public": "pnpm publish --access public",
48
- "preinstall": "npx only-allow pnpm",
49
- "global:link": "pnpm link --global",
50
- "global:unlink": "pnpm remove --global @we-scrum/cli"
47
+ "start": "node dist/cli.js",
48
+ "inspect": "mcp-inspector node dist/cli.js mcp",
49
+ "build": "tsup",
50
+ "watch": "tsup --watch",
51
+ "deploy": "pnpm build && pnpm publish --access public",
52
+ "preinstall": "npx only-allow pnpm"
51
53
  }
52
54
  }
package/src/cli.ts CHANGED
@@ -4,16 +4,14 @@ import { Logger } from '@my-devkit/core';
4
4
  import { UserProjectModel } from '@we-scrum/models';
5
5
  import { Command } from 'commander';
6
6
 
7
- import { AuthHelper } from './auth-helper';
8
- import { FirebaseHelper } from './firebase-helper';
7
+ import { AuthHelper, FirebaseHelper, SettingsHelper } from './helpers';
9
8
  import { startMcpServer } from './mcp-server';
10
- import { SettingsHelper } from './settings-helper';
11
9
 
12
10
  Logger.registerLogger(new Logger.ConsoleImplementation());
13
11
 
14
12
  const program = new Command();
15
13
 
16
- program.name('we-scrum').description('CLI for we-scrum application').version('1.0.0');
14
+ program.name('we-scrum').description('CLI for we-scrum application').version('1.0.2');
17
15
 
18
16
  program
19
17
  .command('login')
@@ -18,6 +18,35 @@ export class AuthHelper {
18
18
  return FirebaseHelper.auth.currentUser?.uid;
19
19
  }
20
20
 
21
+ public static async getUserIdToken(): Promise<string> {
22
+ return FirebaseHelper.auth.currentUser.getIdToken();
23
+ }
24
+
25
+ public static async initializeFromStoredCredentials(): Promise<boolean> {
26
+ const credentials = SettingsHelper.get().credentials ?? null;
27
+ if (!credentials?.id_token) {
28
+ return false;
29
+ }
30
+ try {
31
+ const auth = FirebaseHelper.auth;
32
+ let idToken = credentials.id_token;
33
+
34
+ if (credentials.refresh_token) {
35
+ const oAuth2Client = new OAuth2Client(this.cid, this.csec, this.REDIRECT_URI);
36
+ oAuth2Client.setCredentials(credentials);
37
+ const { credentials: refreshed } = await oAuth2Client.refreshAccessToken();
38
+ SettingsHelper.set({ credentials: refreshed });
39
+ idToken = refreshed.id_token;
40
+ }
41
+
42
+ const credential = GoogleAuthProvider.credential(idToken);
43
+ await signInWithCredential(auth, credential);
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
21
50
  public static async loginWithGoogle(): Promise<User> {
22
51
  const auth = FirebaseHelper.auth;
23
52
 
@@ -77,31 +106,13 @@ export class AuthHelper {
77
106
  });
78
107
  }
79
108
 
80
- /**
81
- * Generates PKCE Verifier and Challenge
82
- */
83
- private static generatePKCE() {
84
- const verifier = randomBytes(32).toString('base64url');
85
- const challenge = createHash('sha256').update(verifier).digest('base64url');
86
- return { verifier, challenge };
87
- }
88
-
89
109
  public static logout() {
90
110
  SettingsHelper.set({ credentials: undefined });
91
111
  }
92
112
 
93
- public static async initializeFromStoredCredentials(): Promise<boolean> {
94
- const credentials = SettingsHelper.get().credentials ?? null;
95
- if (!credentials?.id_token) {
96
- return false;
97
- }
98
- try {
99
- const auth = FirebaseHelper.auth;
100
- const credential = GoogleAuthProvider.credential(credentials.id_token);
101
- await signInWithCredential(auth, credential);
102
- return true;
103
- } catch {
104
- return false;
105
- }
113
+ private static generatePKCE() {
114
+ const verifier = randomBytes(32).toString('base64url');
115
+ const challenge = createHash('sha256').update(verifier).digest('base64url');
116
+ return { verifier, challenge };
106
117
  }
107
118
  }
@@ -77,7 +77,7 @@ export class FirebaseHelper {
77
77
  if (!this._app) {
78
78
  // Should depend on targeted environment
79
79
  const firebaseConfig = {
80
- apiKey: 'AIzaSyCodItNw1wtWfx1GAU_x0Pj96mCAUyrWvU',
80
+ apiKey: 'AIzaSyBUY5qDPQY2035MvP6-pZFuhBRmF5JyddI',
81
81
  authDomain: 'we-scrum-prod.firebaseapp.com',
82
82
  projectId: 'we-scrum-prod',
83
83
  };
@@ -0,0 +1,4 @@
1
+ export * from './auth-helper';
2
+ export * from './firebase-helper';
3
+ export * from './settings-helper';
4
+ export * from './we-scrum.helper';
@@ -0,0 +1,240 @@
1
+ import { _sortBy, Command, serialize, TypeHelper } from '@my-devkit/core';
2
+ import {
3
+ MarkEnumerationChangeAsDoneCommand,
4
+ MarkObjectChangeAsDoneCommand,
5
+ MarkRelationChangeAsDoneCommand,
6
+ MarkRouteChangeAsDoneCommand,
7
+ MarkTaskAsDoneCommand,
8
+ TakeEnumerationChangeCommand,
9
+ TakeObjectChangeCommand,
10
+ TakeRelationChangeCommand,
11
+ TakeRouteChangeCommand,
12
+ TakeTaskCommand,
13
+ } from '@we-scrum/commands';
14
+ import { ProgressStatus } from '@we-scrum/enums';
15
+ import {
16
+ DevelopmentPolicyModel,
17
+ ProjectStoryModel,
18
+ ProjectStorySectionModel,
19
+ ProjectStorySectionModelEnumerationChange,
20
+ ProjectStorySectionModelObjectChange,
21
+ ProjectStorySectionModelRelationChange,
22
+ ProjectStorySectionModelRouteChange,
23
+ ProjectStorySectionModelTask,
24
+ } from '@we-scrum/models';
25
+ import { DevelopmentPolicyHelper } from '@we-scrum/utils';
26
+ import { assert } from 'node:console';
27
+ import { AuthHelper } from './auth-helper';
28
+ import { FirebaseHelper } from './firebase-helper';
29
+ import { SettingsHelper } from './settings-helper';
30
+
31
+ export class WeScrumHelper {
32
+ private static backendUrl = 'https://europe-west1-we-scrum-prod.cloudfunctions.net';
33
+ constructor(private userId: string) {}
34
+
35
+ public static get projectId(): string {
36
+ return SettingsHelper.get().selectedProjectId;
37
+ }
38
+
39
+ public static assertProjectIsSelected(): void {
40
+ assert(!!this.projectId, 'No active project selected. Please run "we-scrum use-project" first.');
41
+ }
42
+
43
+ public static async getNextChange(identificationNumber: string) {
44
+ this.assertProjectIsSelected();
45
+
46
+ const story = await this.findStoryByIterationNumber(identificationNumber);
47
+
48
+ assert(!!story, `Story "${identificationNumber}" not found in the active project.`);
49
+
50
+ const sections = await this.getStorySections(story.storyId);
51
+
52
+ const task = _sortBy(sections, (s) => s.name)
53
+ .flatMap((s) => [...s.enumerationChanges, ...s.objectChanges, ...s.routeChanges, ...s.relationChanges, ...s.tasks])
54
+ .find((t) => t.progress?.status !== ProgressStatus.Done);
55
+
56
+ return task;
57
+ }
58
+
59
+ public static async startTask(identificationNumber: string): Promise<void> {
60
+ const change = await this.getNextChange(identificationNumber);
61
+
62
+ assert(!!change, `All tasks for story "${identificationNumber}" are complete.`);
63
+ assert(change.progress.status === ProgressStatus.ToDo, `Task is already in progress (current status: ${change.progress.status}).`);
64
+
65
+ if (change instanceof ProjectStorySectionModelEnumerationChange) {
66
+ await this.post(
67
+ 'enumeration-management/take-enumeration-change',
68
+ TypeHelper.transform(TakeEnumerationChangeCommand, { enumerationChangeId: change.enumerationChangeId }),
69
+ );
70
+ } else if (change instanceof ProjectStorySectionModelObjectChange) {
71
+ await this.post(
72
+ 'object-management/take-object-change',
73
+ TypeHelper.transform(TakeObjectChangeCommand, { objectChangeId: change.objectChangeId }),
74
+ );
75
+ } else if (change instanceof ProjectStorySectionModelRouteChange) {
76
+ await this.post(
77
+ 'route-management/take-route-change',
78
+ TypeHelper.transform(TakeRouteChangeCommand, { routeChangeId: change.routeChangeId }),
79
+ );
80
+ } else if (change instanceof ProjectStorySectionModelRelationChange) {
81
+ await this.post(
82
+ 'relation-management/take-relation-change',
83
+ TypeHelper.transform(TakeRelationChangeCommand, { relationChangeId: change.relationChangeId }),
84
+ );
85
+ } else if (change instanceof ProjectStorySectionModelTask) {
86
+ await this.post('task-management/take-task', TypeHelper.transform(TakeTaskCommand, { taskId: change.taskId }));
87
+ }
88
+ }
89
+
90
+ public static async completeTask(identificationNumber: string): Promise<void> {
91
+ const change = await this.getNextChange(identificationNumber);
92
+
93
+ assert(!!change, `All tasks for story "${identificationNumber}" are complete.`);
94
+ assert(change.progress.status === ProgressStatus.Doing, `Task is not in progress (current status: ${change.progress.status}).`);
95
+
96
+ if (change instanceof ProjectStorySectionModelEnumerationChange) {
97
+ await this.post(
98
+ 'enumeration-management/mark-enumeration-change-as-done',
99
+ TypeHelper.transform(MarkEnumerationChangeAsDoneCommand, { enumerationChangeId: change.enumerationChangeId }),
100
+ );
101
+ } else if (change instanceof ProjectStorySectionModelObjectChange) {
102
+ await this.post(
103
+ 'object-management/mark-object-change-as-done',
104
+ TypeHelper.transform(MarkObjectChangeAsDoneCommand, { objectChangeId: change.objectChangeId }),
105
+ );
106
+ } else if (change instanceof ProjectStorySectionModelRouteChange) {
107
+ await this.post(
108
+ 'route-management/mark-route-change-as-done',
109
+ TypeHelper.transform(MarkRouteChangeAsDoneCommand, { routeChangeId: change.routeChangeId }),
110
+ );
111
+ } else if (change instanceof ProjectStorySectionModelRelationChange) {
112
+ await this.post(
113
+ 'relation-management/mark-relation-change-as-done',
114
+ TypeHelper.transform(MarkRelationChangeAsDoneCommand, { relationChangeId: change.relationChangeId }),
115
+ );
116
+ } else if (change instanceof ProjectStorySectionModelTask) {
117
+ await this.post('task-management/mark-task-as-done', TypeHelper.transform(MarkTaskAsDoneCommand, { taskId: change.taskId }));
118
+ }
119
+ }
120
+
121
+ public static async getChangeDevelopmentPolicy(change: DevelopmentPolicyHelper.Change) {
122
+ const developmentPolicies = await this.getDevelopmentPolicies();
123
+
124
+ return DevelopmentPolicyHelper.getMatchingDevelopmentPolicy(change, developmentPolicies);
125
+ }
126
+
127
+ private static async findStoryByIterationNumber(identificationNumber: string) {
128
+ return FirebaseHelper.findDocument<ProjectStoryModel>(`/projects/${this.projectId}/stories`, [
129
+ ['identificationNumber', '==', identificationNumber],
130
+ ]);
131
+ }
132
+
133
+ private static async getStorySections(storyId: string) {
134
+ return FirebaseHelper.getCollection<ProjectStorySectionModel>(`projects/${this.projectId}/stories/${storyId}/sections`);
135
+ }
136
+
137
+ private static async getDevelopmentPolicies() {
138
+ return FirebaseHelper.getCollection<DevelopmentPolicyModel>(`/projects/${this.projectId}/development-policies`);
139
+ }
140
+
141
+ // public async createEnumeration(enumeration: Enumeration): Promise<string> {
142
+ // Logger.info(`Creating enumeration ${enumeration.name}`);
143
+
144
+ // const createCommand = TypeHelper.transform(CreateEnumerationCommand, {
145
+ // projectId: this.projectId,
146
+ // name: enumeration.name,
147
+ // });
148
+
149
+ // const enumerationId = await this.post<CreateEnumerationCommand, string>('enumeration-management/create-enumeration', createCommand);
150
+ // for (const property of enumeration.properties) {
151
+ // const propertyCommand = TypeHelper.transform(CreateEnumerationPropertyCommand, {
152
+ // enumerationId,
153
+ // enumerationPropertyId: guid(),
154
+ // name: property.label,
155
+ // value: property.value,
156
+ // });
157
+ // await this.post<CreateEnumerationPropertyCommand, string>(
158
+ // 'enumeration-management/create-enumeration-property',
159
+ // propertyCommand,
160
+ // );
161
+ // }
162
+
163
+ // return enumerationId;
164
+ // }
165
+
166
+ // public async createObject(object: Object, getEnumerationId: (name: string) => string): Promise<string> {
167
+ // Logger.info(`Creating object ${object.name}`);
168
+
169
+ // const createCommand = TypeHelper.transform(CreateObjectCommand, {
170
+ // projectId: this.projectId,
171
+ // name: object.name,
172
+ // });
173
+ // const objectId = await this.post<CreateObjectCommand, string>('object-management/create-object', createCommand);
174
+
175
+ // const propertyMap = new Map<string, string>();
176
+ // for (const property of object.properties) {
177
+ // const objectPropertyId = guid();
178
+ // const propertyCommand = TypeHelper.transform(CreateObjectPropertyCommand, {
179
+ // objectId,
180
+ // objectPropertyId,
181
+ // objectPropertyParentId: propertyMap.get(property.parent) || null,
182
+ // name: property.name,
183
+ // objectPropertyType: property.type,
184
+ // isArray: property.isArray,
185
+ // enumerationId: property.enumerationName ? getEnumerationId(property.enumerationName) : null,
186
+ // });
187
+ // await this.post<CreateObjectPropertyCommand, string>('object-management/create-object-property', propertyCommand);
188
+
189
+ // propertyMap.set(property.path, objectPropertyId);
190
+ // }
191
+
192
+ // return objectId;
193
+ // }
194
+
195
+ // public async createHandler(handler: Handler, getObjectId: (name: string) => string): Promise<void> {
196
+ // Logger.info(`Creating handler ${handler.name}`);
197
+
198
+ // for (const relation of handler.relations) {
199
+ // const createCommand = TypeHelper.transform(CreateRelationCommand, {
200
+ // projectId: this.projectId,
201
+ // boundedContext: handler.boundedContext,
202
+ // handlerName: handler.name,
203
+ // objectIds: relation.map((o) => getObjectId(o)),
204
+ // subscriptionName: null,
205
+ // });
206
+ // await this.post<CreateRelationCommand, string>('relation-management/create-relation', createCommand);
207
+ // }
208
+ // }
209
+
210
+ // public async createRoute(route: Route, getObjectId: (name: string) => string): Promise<string> {
211
+ // Logger.info(`Creating route ${route.method} ${route.path}`);
212
+
213
+ // const createCommand = TypeHelper.transform(CreateRouteCommand, {
214
+ // projectId: this.projectId,
215
+ // method: route.method,
216
+ // path: route.path,
217
+ // objectId: getObjectId(route.object),
218
+ // permission: null,
219
+ // });
220
+
221
+ // return this.post<CreateRouteCommand, string>('route-management/create-route', createCommand);
222
+ // }
223
+
224
+ private static async post<C extends Command, R>(route: string, command: C): Promise<R> {
225
+ const userIdToken = await AuthHelper.getUserIdToken();
226
+
227
+ const response = await fetch(`${this.backendUrl}/command/${route}`, {
228
+ method: 'POST',
229
+ body: JSON.stringify(serialize(command)),
230
+ headers: {
231
+ 'Content-Type': 'application/json',
232
+ Authorization: `Bearer ${userIdToken}`,
233
+ },
234
+ signal: AbortSignal.timeout(60 * 1000),
235
+ });
236
+
237
+ const body = (await response.json()) as { result: R };
238
+ return body.result;
239
+ }
240
+ }
@@ -0,0 +1 @@
1
+ export * from './mcp-server';
@@ -0,0 +1,82 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+
5
+ import { AuthHelper } from '@helpers';
6
+ import { completeTask, getNextTask, getUnitTestGuidelines, startTask } from './tools';
7
+
8
+ const identificationNumberSchema = z.object({
9
+ identificationNumber: z.string().describe('The story identification number, e.g. "144154"'),
10
+ });
11
+
12
+ function createToolCallback<T extends Record<string, string>>(fn: (identificationNumber: string) => Promise<T>) {
13
+ return async ({ identificationNumber }: { identificationNumber: string }) => {
14
+ try {
15
+ const result = await fn(identificationNumber);
16
+ return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
17
+ } catch (error) {
18
+ return {
19
+ content: [{ type: 'text' as const, text: JSON.stringify({ error: error.message }) }],
20
+ isError: true,
21
+ };
22
+ }
23
+ };
24
+ }
25
+
26
+ export async function startMcpServer(): Promise<void> {
27
+ const initialized = await AuthHelper.initializeFromStoredCredentials();
28
+ if (!initialized) {
29
+ process.stderr.write('Error: Not authenticated. Please run "we-scrum login" first.\n');
30
+ process.exit(1);
31
+ }
32
+
33
+ const server = new McpServer({ name: 'we-scrum-mcp-server', version: '1.0.0' });
34
+
35
+ server.registerTool(
36
+ 'get_next_task',
37
+ {
38
+ description: `Get the next pending task (status: ToDo) for a story.
39
+ Returns the task description, the task type template (development guidelines),
40
+ and the full section analysis specifying what must be implemented
41
+ (route changes, object/DTO changes, enumeration changes, relation/handler changes).
42
+ Also returns storyId, sectionId and taskId needed to call start_task and complete_task.`,
43
+ inputSchema: identificationNumberSchema,
44
+ },
45
+ createToolCallback(getNextTask),
46
+ );
47
+
48
+ server.registerTool(
49
+ 'start_task',
50
+ {
51
+ description: `Mark the next pending task of a story as in progress.
52
+ Call this before making any changes to the codebase, as instructed by get_next_task.`,
53
+ inputSchema: identificationNumberSchema,
54
+ },
55
+ createToolCallback(startTask),
56
+ );
57
+
58
+ server.registerTool(
59
+ 'get_unit_test_guidelines',
60
+ {
61
+ description: `Get unit test writing guidelines for the current pending task of a story.
62
+ Call this tool when get_next_task returns unitTests instructions to run it.`,
63
+ inputSchema: identificationNumberSchema,
64
+ },
65
+ createToolCallback(getUnitTestGuidelines),
66
+ );
67
+
68
+ server.registerTool(
69
+ 'complete_task',
70
+ {
71
+ description: `Mark the current in-progress task of a story as done.
72
+ Call this after committing all changes, as instructed by get_next_task.`,
73
+ inputSchema: identificationNumberSchema,
74
+ },
75
+ createToolCallback(completeTask),
76
+ );
77
+
78
+ const transport = new StdioServerTransport();
79
+ await server.connect(transport);
80
+
81
+ process.stderr.write('we-scrum MCP server running.\n');
82
+ }
@@ -0,0 +1,6 @@
1
+ import { WeScrumHelper } from '@helpers';
2
+
3
+ export async function completeTask(identificationNumber: string): Promise<Record<string, string>> {
4
+ await WeScrumHelper.completeTask(identificationNumber);
5
+ return { message: 'Task completed successfully.' };
6
+ }
@@ -0,0 +1,149 @@
1
+ import { WeScrumHelper } from '@helpers';
2
+ import { _compact } from '@my-devkit/core';
3
+ import { Operation, ProgressStatus } from '@we-scrum/enums';
4
+ import {
5
+ ProjectStorySectionModelEnumerationChange,
6
+ ProjectStorySectionModelObjectChange,
7
+ ProjectStorySectionModelRelationChange,
8
+ ProjectStorySectionModelRouteChange,
9
+ ProjectStorySectionModelTask,
10
+ } from '@we-scrum/models';
11
+
12
+ export async function getNextTask(identificationNumber: string): Promise<Record<string, string>> {
13
+ const nextTask = await WeScrumHelper.getNextChange(identificationNumber);
14
+
15
+ if (!nextTask) {
16
+ return { message: `All tasks for story "${identificationNumber}" are complete.` };
17
+ }
18
+
19
+ const isTodo = nextTask.progress.status === ProgressStatus.ToDo;
20
+ const generalInstructions = _compact([
21
+ 'Implement the task described in the fields below.',
22
+ 'Refer to `implementationGuidelines` for coding conventions and project-specific instructions.',
23
+ isTodo ? 'Before making any changes, run `start_task` to mark the task as in progress.' : null,
24
+ 'If anything is unclear, stop and ask before making any changes.',
25
+ 'Once all changes are done, commit your code and run `complete_task`.',
26
+ ]).join('\n');
27
+
28
+ const developmentPolicy = await WeScrumHelper.getChangeDevelopmentPolicy(nextTask);
29
+ const implementationGuidelines = developmentPolicy?.codeGuidelines ?? 'No guidelines found for this task... :-(';
30
+
31
+ const unitTestInstructions = 'Run get_unit_test_guidelines to get instructions to generate unit tests for this task';
32
+ const unitTests = developmentPolicy?.areUnitTestsMandatory ? unitTestInstructions : 'No unit test needed.';
33
+
34
+ if (nextTask instanceof ProjectStorySectionModelEnumerationChange) {
35
+ const propertyChanges = nextTask.propertyChanges
36
+ .filter((pc) => !!pc.operation)
37
+ .map((pc) => `${pc.operation} property ${pc.propertyName} (= '${pc.propertyValue}')`)
38
+ .sort()
39
+ .join('\n');
40
+
41
+ return {
42
+ generalInstructions,
43
+ taskType: `${nextTask.operation} enumeration`,
44
+ enumerationName: nextTask.enumerationName,
45
+ propertyChanges: propertyChanges || 'No change on properties',
46
+ taskDescription: nextTask.description ?? undefined,
47
+ implementationGuidelines,
48
+ unitTests,
49
+ };
50
+ }
51
+ if (nextTask instanceof ProjectStorySectionModelObjectChange) {
52
+ const propertyMap = new Map(nextTask.propertyChanges.map((pc) => [pc.objectPropertyId, pc]));
53
+
54
+ const buildPath = (pc: (typeof nextTask.propertyChanges)[0]): string => {
55
+ const name = pc.isArray ? `${pc.propertyName}[]` : pc.propertyName;
56
+ if (!pc.objectPropertyParentId) return name;
57
+ const parent = propertyMap.get(pc.objectPropertyParentId);
58
+ return parent ? `${buildPath(parent)}.${name}` : name;
59
+ };
60
+
61
+ const changedProperties = nextTask.propertyChanges.filter((pc) => !!pc.operation);
62
+ const parentIdsWithChangedChildren = new Set(changedProperties.map((pc) => pc.objectPropertyParentId).filter(Boolean));
63
+
64
+ const propertyChanges = changedProperties
65
+ .filter((pc) => !(pc.operation === Operation.Create && parentIdsWithChangedChildren.has(pc.objectPropertyId)))
66
+ .map((pc) => {
67
+ const enumInfo = pc.enumerationName ? ` (enum: ${pc.enumerationName})` : '';
68
+ return `${pc.operation} property ${buildPath(pc)}: ${pc.propertyType}${enumInfo}`;
69
+ })
70
+ .sort()
71
+ .join('\n');
72
+
73
+ return {
74
+ generalInstructions,
75
+ taskType: `${nextTask.operation} object`,
76
+ objectName: nextTask.objectName,
77
+ renamedFrom: nextTask.initialObjectName
78
+ ? `Object renamed from "${nextTask.initialObjectName}" to "${nextTask.objectName}"`
79
+ : undefined,
80
+ propertyChanges: propertyChanges || 'No change on properties',
81
+ taskDescription: nextTask.description ?? undefined,
82
+ implementationGuidelines,
83
+ unitTests,
84
+ };
85
+ }
86
+ if (nextTask instanceof ProjectStorySectionModelRouteChange) {
87
+ const queryParamChanges = nextTask.queryParamChanges
88
+ .filter((qp) => !!qp.operation)
89
+ .map((qp) => {
90
+ const enumInfo = qp.enumerationName ? ` (enum: ${qp.enumerationName})` : '';
91
+ return `${qp.operation} query param ${qp.routeQueryParamName}: ${qp.routeQueryParamType}${enumInfo}`;
92
+ })
93
+ .sort()
94
+ .join('\n');
95
+
96
+ return {
97
+ generalInstructions,
98
+ taskType: `${nextTask.operation} route`,
99
+ route: `${nextTask.routeMethod} ${nextTask.routePath}`,
100
+ routeObject: nextTask.routeObjectName ?? undefined,
101
+ pathChangedFrom:
102
+ nextTask.initialRoutePath && nextTask.initialRoutePath !== nextTask.routePath ? nextTask.initialRoutePath : undefined,
103
+ methodChangedFrom:
104
+ nextTask.initialRouteMethod && nextTask.initialRouteMethod !== nextTask.routeMethod
105
+ ? nextTask.initialRouteMethod
106
+ : undefined,
107
+ objectChangedFrom:
108
+ nextTask.initialRouteObjectName && nextTask.initialRouteObjectName !== nextTask.routeObjectName
109
+ ? nextTask.initialRouteObjectName
110
+ : undefined,
111
+ permissionChangedFrom:
112
+ nextTask.initialRoutePermission && nextTask.initialRoutePermission !== nextTask.routePermission
113
+ ? nextTask.initialRoutePermission
114
+ : undefined,
115
+ queryParamChanges: queryParamChanges || 'No change on query params',
116
+ taskDescription: nextTask.description ?? undefined,
117
+ implementationGuidelines,
118
+ unitTests,
119
+ };
120
+ }
121
+ if (nextTask instanceof ProjectStorySectionModelRelationChange) {
122
+ const relations = nextTask.relations
123
+ .filter((r) => !!r.operation)
124
+ .map((r) => `${r.operation} [${r.objects.map((o) => o.objectName).join(' ⇒ ')}]`)
125
+ .sort()
126
+ .join('\n');
127
+
128
+ return {
129
+ generalInstructions,
130
+ taskType: 'relation change',
131
+ handlerName: nextTask.handlerName,
132
+ boundedContext: nextTask.boundedContext,
133
+ subscriptionName: nextTask.subscriptionName ?? undefined,
134
+ relations: relations || 'No relation changes',
135
+ taskDescription: nextTask.description ?? undefined,
136
+ implementationGuidelines,
137
+ unitTests,
138
+ };
139
+ }
140
+ if (nextTask instanceof ProjectStorySectionModelTask) {
141
+ return {
142
+ generalInstructions,
143
+ taskType: nextTask.taskTypeName,
144
+ taskDescription: nextTask.description ?? undefined,
145
+ implementationGuidelines,
146
+ unitTests,
147
+ };
148
+ }
149
+ }
@@ -0,0 +1,15 @@
1
+ import { WeScrumHelper } from '@helpers';
2
+
3
+ export async function getUnitTestGuidelines(identificationNumber: string): Promise<Record<string, string>> {
4
+ const nextTask = await WeScrumHelper.getNextChange(identificationNumber);
5
+
6
+ if (!nextTask) {
7
+ return { message: `All tasks for story "${identificationNumber}" are complete.` };
8
+ }
9
+
10
+ const developmentPolicy = await WeScrumHelper.getChangeDevelopmentPolicy(nextTask);
11
+
12
+ return {
13
+ unitTestGuidelines: developmentPolicy?.unitTestsGuidelines ?? 'No unit test guidelines found for this task.',
14
+ };
15
+ }
@@ -0,0 +1,4 @@
1
+ export * from './complete-task';
2
+ export * from './get-next-task';
3
+ export * from './get-unit-test-guidelines';
4
+ export * from './start-task';
@@ -0,0 +1,6 @@
1
+ import { WeScrumHelper } from '@helpers';
2
+
3
+ export async function startTask(identificationNumber: string): Promise<Record<string, string>> {
4
+ await WeScrumHelper.startTask(identificationNumber);
5
+ return { message: 'Task started successfully. You can now make changes to the codebase.' };
6
+ }