@vibescope/mcp-server 0.0.1
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 +98 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.js +356 -0
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +367 -0
- package/dist/handlers/__test-utils__.d.ts +72 -0
- package/dist/handlers/__test-utils__.js +176 -0
- package/dist/handlers/blockers.d.ts +18 -0
- package/dist/handlers/blockers.js +81 -0
- package/dist/handlers/bodies-of-work.d.ts +34 -0
- package/dist/handlers/bodies-of-work.js +614 -0
- package/dist/handlers/checkouts.d.ts +37 -0
- package/dist/handlers/checkouts.js +377 -0
- package/dist/handlers/cost.d.ts +39 -0
- package/dist/handlers/cost.js +247 -0
- package/dist/handlers/decisions.d.ts +16 -0
- package/dist/handlers/decisions.js +64 -0
- package/dist/handlers/deployment.d.ts +36 -0
- package/dist/handlers/deployment.js +1062 -0
- package/dist/handlers/discovery.d.ts +14 -0
- package/dist/handlers/discovery.js +870 -0
- package/dist/handlers/fallback.d.ts +18 -0
- package/dist/handlers/fallback.js +216 -0
- package/dist/handlers/findings.d.ts +18 -0
- package/dist/handlers/findings.js +110 -0
- package/dist/handlers/git-issues.d.ts +22 -0
- package/dist/handlers/git-issues.js +247 -0
- package/dist/handlers/ideas.d.ts +19 -0
- package/dist/handlers/ideas.js +188 -0
- package/dist/handlers/index.d.ts +29 -0
- package/dist/handlers/index.js +65 -0
- package/dist/handlers/knowledge-query.d.ts +22 -0
- package/dist/handlers/knowledge-query.js +253 -0
- package/dist/handlers/knowledge.d.ts +12 -0
- package/dist/handlers/knowledge.js +108 -0
- package/dist/handlers/milestones.d.ts +20 -0
- package/dist/handlers/milestones.js +179 -0
- package/dist/handlers/organizations.d.ts +36 -0
- package/dist/handlers/organizations.js +428 -0
- package/dist/handlers/progress.d.ts +14 -0
- package/dist/handlers/progress.js +149 -0
- package/dist/handlers/project.d.ts +20 -0
- package/dist/handlers/project.js +278 -0
- package/dist/handlers/requests.d.ts +16 -0
- package/dist/handlers/requests.js +131 -0
- package/dist/handlers/roles.d.ts +30 -0
- package/dist/handlers/roles.js +281 -0
- package/dist/handlers/session.d.ts +20 -0
- package/dist/handlers/session.js +791 -0
- package/dist/handlers/tasks.d.ts +52 -0
- package/dist/handlers/tasks.js +1111 -0
- package/dist/handlers/tasks.test.d.ts +1 -0
- package/dist/handlers/tasks.test.js +431 -0
- package/dist/handlers/types.d.ts +94 -0
- package/dist/handlers/types.js +1 -0
- package/dist/handlers/validation.d.ts +16 -0
- package/dist/handlers/validation.js +188 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2707 -0
- package/dist/knowledge.d.ts +6 -0
- package/dist/knowledge.js +121 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +2498 -0
- package/dist/utils.d.ts +149 -0
- package/dist/utils.js +317 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +532 -0
- package/dist/validators.d.ts +35 -0
- package/dist/validators.js +111 -0
- package/dist/validators.test.d.ts +1 -0
- package/dist/validators.test.js +176 -0
- package/package.json +44 -0
- package/src/cli.test.ts +442 -0
- package/src/cli.ts +439 -0
- package/src/handlers/__test-utils__.ts +217 -0
- package/src/handlers/blockers.test.ts +390 -0
- package/src/handlers/blockers.ts +110 -0
- package/src/handlers/bodies-of-work.test.ts +1276 -0
- package/src/handlers/bodies-of-work.ts +783 -0
- package/src/handlers/cost.test.ts +436 -0
- package/src/handlers/cost.ts +322 -0
- package/src/handlers/decisions.test.ts +401 -0
- package/src/handlers/decisions.ts +86 -0
- package/src/handlers/deployment.test.ts +516 -0
- package/src/handlers/deployment.ts +1289 -0
- package/src/handlers/discovery.test.ts +254 -0
- package/src/handlers/discovery.ts +969 -0
- package/src/handlers/fallback.test.ts +687 -0
- package/src/handlers/fallback.ts +260 -0
- package/src/handlers/findings.test.ts +565 -0
- package/src/handlers/findings.ts +153 -0
- package/src/handlers/ideas.test.ts +753 -0
- package/src/handlers/ideas.ts +247 -0
- package/src/handlers/index.ts +69 -0
- package/src/handlers/milestones.test.ts +584 -0
- package/src/handlers/milestones.ts +217 -0
- package/src/handlers/organizations.test.ts +997 -0
- package/src/handlers/organizations.ts +550 -0
- package/src/handlers/progress.test.ts +369 -0
- package/src/handlers/progress.ts +188 -0
- package/src/handlers/project.test.ts +562 -0
- package/src/handlers/project.ts +352 -0
- package/src/handlers/requests.test.ts +531 -0
- package/src/handlers/requests.ts +150 -0
- package/src/handlers/session.test.ts +459 -0
- package/src/handlers/session.ts +912 -0
- package/src/handlers/tasks.test.ts +602 -0
- package/src/handlers/tasks.ts +1393 -0
- package/src/handlers/types.ts +88 -0
- package/src/handlers/validation.test.ts +880 -0
- package/src/handlers/validation.ts +223 -0
- package/src/index.ts +3205 -0
- package/src/knowledge.ts +132 -0
- package/src/tmpclaude-0078-cwd +1 -0
- package/src/tmpclaude-0ee1-cwd +1 -0
- package/src/tmpclaude-2dd5-cwd +1 -0
- package/src/tmpclaude-344c-cwd +1 -0
- package/src/tmpclaude-3860-cwd +1 -0
- package/src/tmpclaude-4b63-cwd +1 -0
- package/src/tmpclaude-5c73-cwd +1 -0
- package/src/tmpclaude-5ee3-cwd +1 -0
- package/src/tmpclaude-6795-cwd +1 -0
- package/src/tmpclaude-709e-cwd +1 -0
- package/src/tmpclaude-9839-cwd +1 -0
- package/src/tmpclaude-d829-cwd +1 -0
- package/src/tmpclaude-e072-cwd +1 -0
- package/src/tmpclaude-f6ee-cwd +1 -0
- package/src/utils.test.ts +681 -0
- package/src/utils.ts +375 -0
- package/src/validators.test.ts +223 -0
- package/src/validators.ts +122 -0
- package/tmpclaude-0439-cwd +1 -0
- package/tmpclaude-132f-cwd +1 -0
- package/tmpclaude-15bb-cwd +1 -0
- package/tmpclaude-165a-cwd +1 -0
- package/tmpclaude-1ba9-cwd +1 -0
- package/tmpclaude-21a3-cwd +1 -0
- package/tmpclaude-2a38-cwd +1 -0
- package/tmpclaude-2adf-cwd +1 -0
- package/tmpclaude-2f56-cwd +1 -0
- package/tmpclaude-3626-cwd +1 -0
- package/tmpclaude-3727-cwd +1 -0
- package/tmpclaude-40bc-cwd +1 -0
- package/tmpclaude-436f-cwd +1 -0
- package/tmpclaude-4783-cwd +1 -0
- package/tmpclaude-4b6d-cwd +1 -0
- package/tmpclaude-4ba4-cwd +1 -0
- package/tmpclaude-51e6-cwd +1 -0
- package/tmpclaude-5ecf-cwd +1 -0
- package/tmpclaude-6f97-cwd +1 -0
- package/tmpclaude-7fb2-cwd +1 -0
- package/tmpclaude-825c-cwd +1 -0
- package/tmpclaude-8baf-cwd +1 -0
- package/tmpclaude-8d9f-cwd +1 -0
- package/tmpclaude-975c-cwd +1 -0
- package/tmpclaude-9983-cwd +1 -0
- package/tmpclaude-a045-cwd +1 -0
- package/tmpclaude-ac4a-cwd +1 -0
- package/tmpclaude-b593-cwd +1 -0
- package/tmpclaude-b891-cwd +1 -0
- package/tmpclaude-c032-cwd +1 -0
- package/tmpclaude-cf43-cwd +1 -0
- package/tmpclaude-d040-cwd +1 -0
- package/tmpclaude-dcdd-cwd +1 -0
- package/tmpclaude-dcee-cwd +1 -0
- package/tmpclaude-e16b-cwd +1 -0
- package/tmpclaude-ecd2-cwd +1 -0
- package/tmpclaude-f48d-cwd +1 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +13 -0
package/src/utils.ts
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Utilities Module - Extracted from index.ts for testability
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Agent Persona Pool
|
|
9
|
+
// Short, memorable names for distinguishing agents on the dashboard
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export const AGENT_PERSONAS = [
|
|
13
|
+
'Apex',
|
|
14
|
+
'Arc',
|
|
15
|
+
'Atlas',
|
|
16
|
+
'Blaze',
|
|
17
|
+
'Bolt',
|
|
18
|
+
'Byte',
|
|
19
|
+
'Cipher',
|
|
20
|
+
'Core',
|
|
21
|
+
'Dash',
|
|
22
|
+
'Drift',
|
|
23
|
+
'Echo',
|
|
24
|
+
'Edge',
|
|
25
|
+
'Ember',
|
|
26
|
+
'Flux',
|
|
27
|
+
'Forge',
|
|
28
|
+
'Ghost',
|
|
29
|
+
'Glitch',
|
|
30
|
+
'Haze',
|
|
31
|
+
'Hex',
|
|
32
|
+
'Ion',
|
|
33
|
+
'Iris',
|
|
34
|
+
'Jade',
|
|
35
|
+
'Jet',
|
|
36
|
+
'Karma',
|
|
37
|
+
'Kite',
|
|
38
|
+
'Lux',
|
|
39
|
+
'Mako',
|
|
40
|
+
'Neon',
|
|
41
|
+
'Nova',
|
|
42
|
+
'Onyx',
|
|
43
|
+
'Orbit',
|
|
44
|
+
'Pixel',
|
|
45
|
+
'Pulse',
|
|
46
|
+
'Quest',
|
|
47
|
+
'Rex',
|
|
48
|
+
'Rune',
|
|
49
|
+
'Shade',
|
|
50
|
+
'Spark',
|
|
51
|
+
'Storm',
|
|
52
|
+
'Trace',
|
|
53
|
+
'Turbo',
|
|
54
|
+
'Vex',
|
|
55
|
+
'Volt',
|
|
56
|
+
'Warp',
|
|
57
|
+
'Wave',
|
|
58
|
+
'Zen',
|
|
59
|
+
'Zero',
|
|
60
|
+
] as const;
|
|
61
|
+
|
|
62
|
+
export type AgentPersona = typeof AGENT_PERSONAS[number];
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Fallback Activities for Idle Agents
|
|
66
|
+
// Suggested productive activities when no pending tasks exist
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
export const FALLBACK_ACTIVITIES = [
|
|
70
|
+
{
|
|
71
|
+
activity: 'feature_ideation',
|
|
72
|
+
title: 'Generate feature ideas',
|
|
73
|
+
description: 'Review the codebase and suggest improvements or new features. Use add_idea to record your findings.',
|
|
74
|
+
prompt: 'Explore the codebase, identify areas for improvement, and suggest 2-3 actionable feature ideas using add_idea.',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
activity: 'feature_planning',
|
|
78
|
+
title: 'Plan features from ideas',
|
|
79
|
+
description: 'Take raw ideas and develop them into planned features with documentation.',
|
|
80
|
+
prompt: 'Call get_ideas to find ideas with status "raw" or "exploring". Pick one and develop it: research requirements, identify implementation steps, write a feature spec. Use update_idea to change status to "planned" and add a doc_url if you create documentation.',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
activity: 'code_review',
|
|
84
|
+
title: 'Code standards review',
|
|
85
|
+
description: 'Check for inconsistencies, missing types, code smells, or patterns that could be improved.',
|
|
86
|
+
prompt: 'Review the codebase for code quality issues. Look for missing types, inconsistent patterns, or technical debt. Log findings with add_idea or add_task.',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
activity: 'performance_audit',
|
|
90
|
+
title: 'Performance audit',
|
|
91
|
+
description: 'Identify potential performance bottlenecks or optimization opportunities.',
|
|
92
|
+
prompt: 'Analyze the codebase for performance issues: N+1 queries, unnecessary re-renders, expensive operations. Create tasks for fixes with add_task.',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
activity: 'ux_review',
|
|
96
|
+
title: 'UX review',
|
|
97
|
+
description: 'Evaluate user flows and identify usability improvements.',
|
|
98
|
+
prompt: 'Review the UI components and user flows. Identify confusing interactions, missing feedback, or accessibility issues. Log improvements with add_idea.',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
activity: 'cost_analysis',
|
|
102
|
+
title: 'Cost analysis',
|
|
103
|
+
description: 'Review infrastructure and API usage for cost optimization opportunities.',
|
|
104
|
+
prompt: 'Analyze the codebase for expensive operations: unnecessary API calls, inefficient queries, large bundle sizes. Suggest cost-saving alternatives with add_idea.',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
activity: 'security_review',
|
|
108
|
+
title: 'Security review',
|
|
109
|
+
description: 'Check for common security issues: auth gaps, input validation, data exposure.',
|
|
110
|
+
prompt: 'Review the codebase for security concerns: authentication gaps, missing input validation, sensitive data handling. Create high-priority tasks for issues found.',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
activity: 'test_coverage',
|
|
114
|
+
title: 'Test coverage analysis',
|
|
115
|
+
description: 'Find untested code paths and suggest test cases.',
|
|
116
|
+
prompt: 'Identify areas of the codebase with missing or weak test coverage. Create tasks for writing tests using add_task.',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
activity: 'documentation_review',
|
|
120
|
+
title: 'Documentation review',
|
|
121
|
+
description: 'Identify missing or outdated documentation.',
|
|
122
|
+
prompt: 'Review existing docs and code comments. Identify gaps, outdated information, or confusing explanations. Log improvements with add_idea.',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
activity: 'dependency_audit',
|
|
126
|
+
title: 'Dependency audit',
|
|
127
|
+
description: 'Review dependencies for updates, security issues, or unused packages.',
|
|
128
|
+
prompt: 'Check package.json for outdated dependencies, security vulnerabilities, or unused packages. Create tasks for updates with add_task.',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
activity: 'validate_completed_tasks',
|
|
132
|
+
title: 'Validate completed tasks',
|
|
133
|
+
description: 'Review tasks completed by other agents to ensure quality.',
|
|
134
|
+
prompt: 'Call get_tasks_awaiting_validation to find completed tasks that need review. Validate each one by checking the implementation and running tests if applicable.',
|
|
135
|
+
},
|
|
136
|
+
] as const;
|
|
137
|
+
|
|
138
|
+
export type FallbackActivity = typeof FALLBACK_ACTIVITIES[number];
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get a random fallback activity
|
|
142
|
+
*/
|
|
143
|
+
export function getRandomFallbackActivity(): FallbackActivity {
|
|
144
|
+
const index = Math.floor(Math.random() * FALLBACK_ACTIVITIES.length);
|
|
145
|
+
return FALLBACK_ACTIVITIES[index];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Select an available persona from the pool (randomly)
|
|
150
|
+
* @param usedPersonas Set of personas currently in use
|
|
151
|
+
* @param instanceId The instance ID for fallback naming
|
|
152
|
+
* @returns The selected persona name
|
|
153
|
+
*/
|
|
154
|
+
export function selectPersona(usedPersonas: Set<string>, instanceId: string): string {
|
|
155
|
+
const availablePersonas = AGENT_PERSONAS.filter((p) => !usedPersonas.has(p));
|
|
156
|
+
if (availablePersonas.length === 0) {
|
|
157
|
+
return `Agent-${instanceId.slice(0, 6)}`;
|
|
158
|
+
}
|
|
159
|
+
const randomIndex = Math.floor(Math.random() * availablePersonas.length);
|
|
160
|
+
return availablePersonas[randomIndex];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ============================================================================
|
|
164
|
+
// Rate Limiter
|
|
165
|
+
// ============================================================================
|
|
166
|
+
|
|
167
|
+
export interface RateLimitResult {
|
|
168
|
+
allowed: boolean;
|
|
169
|
+
remaining: number;
|
|
170
|
+
resetIn: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export class RateLimiter {
|
|
174
|
+
private requests: Map<string, { count: number; resetAt: number }> = new Map();
|
|
175
|
+
private readonly maxRequests: number;
|
|
176
|
+
private readonly windowMs: number;
|
|
177
|
+
|
|
178
|
+
constructor(maxRequests: number = 60, windowMs: number = 60000) {
|
|
179
|
+
this.maxRequests = maxRequests;
|
|
180
|
+
this.windowMs = windowMs;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
check(key: string): RateLimitResult {
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
const record = this.requests.get(key);
|
|
186
|
+
|
|
187
|
+
// If no record or window expired, create new record
|
|
188
|
+
if (!record || now >= record.resetAt) {
|
|
189
|
+
this.requests.set(key, { count: 1, resetAt: now + this.windowMs });
|
|
190
|
+
return { allowed: true, remaining: this.maxRequests - 1, resetIn: this.windowMs };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check if limit exceeded
|
|
194
|
+
if (record.count >= this.maxRequests) {
|
|
195
|
+
return { allowed: false, remaining: 0, resetIn: record.resetAt - now };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Increment count
|
|
199
|
+
record.count++;
|
|
200
|
+
return { allowed: true, remaining: this.maxRequests - record.count, resetIn: record.resetAt - now };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Clean up expired entries periodically
|
|
204
|
+
cleanup(): void {
|
|
205
|
+
const now = Date.now();
|
|
206
|
+
for (const [key, record] of this.requests.entries()) {
|
|
207
|
+
if (now >= record.resetAt) {
|
|
208
|
+
this.requests.delete(key);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Expose for testing
|
|
214
|
+
getRequestCount(key: string): number | undefined {
|
|
215
|
+
return this.requests.get(key)?.count;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ============================================================================
|
|
220
|
+
// Task Validation Helpers
|
|
221
|
+
// ============================================================================
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check if a task can be validated by the given session
|
|
225
|
+
* @param task The task to validate
|
|
226
|
+
* @param validatorSessionId The session trying to validate
|
|
227
|
+
* @returns Object with canValidate boolean and reason if not allowed
|
|
228
|
+
*/
|
|
229
|
+
export function canValidateTask(
|
|
230
|
+
task: { status: string; validated_at: string | null; working_agent_session_id: string | null },
|
|
231
|
+
validatorSessionId: string | null
|
|
232
|
+
): { canValidate: boolean; reason?: string } {
|
|
233
|
+
if (task.status !== 'completed') {
|
|
234
|
+
return { canValidate: false, reason: 'Can only validate completed tasks' };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (task.validated_at) {
|
|
238
|
+
return { canValidate: false, reason: 'Task has already been validated' };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Note: Self-validation check would go here if we tracked who completed the task
|
|
242
|
+
// Currently, working_agent_session_id is cleared on completion
|
|
243
|
+
|
|
244
|
+
return { canValidate: true };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Check if a task status transition is valid
|
|
249
|
+
* @param currentStatus Current task status
|
|
250
|
+
* @param newStatus Desired new status
|
|
251
|
+
* @returns Object with isValid boolean and reason if not valid
|
|
252
|
+
*/
|
|
253
|
+
export function isValidStatusTransition(
|
|
254
|
+
currentStatus: string,
|
|
255
|
+
newStatus: string
|
|
256
|
+
): { isValid: boolean; reason?: string } {
|
|
257
|
+
const validTransitions: Record<string, string[]> = {
|
|
258
|
+
pending: ['in_progress', 'cancelled'],
|
|
259
|
+
in_progress: ['completed', 'pending', 'cancelled'],
|
|
260
|
+
completed: ['in_progress'], // Can reopen via validation failure
|
|
261
|
+
cancelled: ['pending'], // Can reactivate cancelled tasks
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const allowed = validTransitions[currentStatus];
|
|
265
|
+
if (!allowed) {
|
|
266
|
+
return { isValid: false, reason: `Unknown current status: ${currentStatus}` };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!allowed.includes(newStatus)) {
|
|
270
|
+
return {
|
|
271
|
+
isValid: false,
|
|
272
|
+
reason: `Cannot transition from '${currentStatus}' to '${newStatus}'. Valid transitions: ${allowed.join(', ')}`
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { isValid: true };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Check if a deployment status transition is valid
|
|
281
|
+
* @param currentStatus Current deployment status
|
|
282
|
+
* @param newStatus Desired new status
|
|
283
|
+
* @returns Object with isValid boolean and reason if not valid
|
|
284
|
+
*/
|
|
285
|
+
export function isValidDeploymentStatusTransition(
|
|
286
|
+
currentStatus: string,
|
|
287
|
+
newStatus: string
|
|
288
|
+
): { isValid: boolean; reason?: string } {
|
|
289
|
+
const validTransitions: Record<string, string[]> = {
|
|
290
|
+
pending: ['validating', 'failed'], // Can claim validation or cancel
|
|
291
|
+
validating: ['ready', 'failed'], // Validation passes or fails
|
|
292
|
+
ready: ['deploying', 'failed'], // Start deployment or cancel
|
|
293
|
+
deploying: ['deployed', 'failed'], // Deployment succeeds or fails
|
|
294
|
+
deployed: [], // Terminal state
|
|
295
|
+
failed: [], // Terminal state (create new deployment to retry)
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const allowed = validTransitions[currentStatus];
|
|
299
|
+
if (!allowed) {
|
|
300
|
+
return { isValid: false, reason: `Unknown current status: ${currentStatus}` };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!allowed.includes(newStatus)) {
|
|
304
|
+
return {
|
|
305
|
+
isValid: false,
|
|
306
|
+
reason: `Cannot transition deployment from '${currentStatus}' to '${newStatus}'. Valid transitions: ${allowed.length ? allowed.join(', ') : 'none (terminal state)'}`
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return { isValid: true };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ============================================================================
|
|
314
|
+
// Git URL Parsing
|
|
315
|
+
// ============================================================================
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Extract project name from a git URL.
|
|
319
|
+
* Handles various git URL formats:
|
|
320
|
+
* - https://github.com/user/repo -> repo
|
|
321
|
+
* - https://github.com/user/repo.git -> repo
|
|
322
|
+
* - git@github.com:user/repo.git -> repo
|
|
323
|
+
* - https://gitlab.com/user/repo -> repo
|
|
324
|
+
* - ssh://git@github.com/user/repo.git -> repo
|
|
325
|
+
*
|
|
326
|
+
* @param gitUrl The git URL to parse
|
|
327
|
+
* @returns The extracted project name, or 'my-project' if parsing fails
|
|
328
|
+
*/
|
|
329
|
+
export function extractProjectNameFromGitUrl(gitUrl: string): string {
|
|
330
|
+
if (!gitUrl) {
|
|
331
|
+
return 'my-project';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Match the last path segment, optionally removing .git suffix
|
|
335
|
+
// Works for:
|
|
336
|
+
// - /user/repo (https URLs)
|
|
337
|
+
// - /user/repo.git
|
|
338
|
+
// - :user/repo (ssh URLs like git@github.com:user/repo)
|
|
339
|
+
// - :user/repo.git
|
|
340
|
+
const match = gitUrl.match(/[/:]([^/:]+?)(?:\.git)?$/);
|
|
341
|
+
if (match && match[1]) {
|
|
342
|
+
return match[1];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return 'my-project';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Normalize a git URL to a consistent HTTPS format.
|
|
350
|
+
* - Removes .git suffix
|
|
351
|
+
* - Converts SSH URLs (git@host:path) to HTTPS format
|
|
352
|
+
*
|
|
353
|
+
* Supports GitHub, GitLab, and Bitbucket.
|
|
354
|
+
*
|
|
355
|
+
* @param gitUrl The git URL to normalize
|
|
356
|
+
* @returns The normalized URL, or the original if no normalization needed
|
|
357
|
+
*
|
|
358
|
+
* @example
|
|
359
|
+
* normalizeGitUrl('git@github.com:user/repo.git')
|
|
360
|
+
* // returns 'https://github.com/user/repo'
|
|
361
|
+
*
|
|
362
|
+
* normalizeGitUrl('https://github.com/user/repo.git')
|
|
363
|
+
* // returns 'https://github.com/user/repo'
|
|
364
|
+
*/
|
|
365
|
+
export function normalizeGitUrl(gitUrl: string): string {
|
|
366
|
+
if (!gitUrl) {
|
|
367
|
+
return gitUrl;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return gitUrl
|
|
371
|
+
.replace(/\.git$/, '')
|
|
372
|
+
.replace(/^git@github\.com:/, 'https://github.com/')
|
|
373
|
+
.replace(/^git@gitlab\.com:/, 'https://gitlab.com/')
|
|
374
|
+
.replace(/^git@bitbucket\.org:/, 'https://bitbucket.org/');
|
|
375
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
ValidationError,
|
|
4
|
+
validateRequired,
|
|
5
|
+
validateUUID,
|
|
6
|
+
validateTaskStatus,
|
|
7
|
+
validateProjectStatus,
|
|
8
|
+
validatePriority,
|
|
9
|
+
validateProgressPercentage,
|
|
10
|
+
validateEstimatedMinutes,
|
|
11
|
+
validateEnvironment,
|
|
12
|
+
VALID_TASK_STATUSES,
|
|
13
|
+
VALID_PROJECT_STATUSES,
|
|
14
|
+
VALID_ENVIRONMENTS,
|
|
15
|
+
} from './validators.js';
|
|
16
|
+
|
|
17
|
+
describe('ValidationError', () => {
|
|
18
|
+
it('should create error with message', () => {
|
|
19
|
+
const error = new ValidationError('Test error');
|
|
20
|
+
expect(error.message).toBe('Test error');
|
|
21
|
+
expect(error.name).toBe('ValidationError');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should include optional fields', () => {
|
|
25
|
+
const error = new ValidationError('Test error', {
|
|
26
|
+
field: 'test_field',
|
|
27
|
+
hint: 'Test hint',
|
|
28
|
+
validValues: ['a', 'b'],
|
|
29
|
+
});
|
|
30
|
+
expect(error.field).toBe('test_field');
|
|
31
|
+
expect(error.hint).toBe('Test hint');
|
|
32
|
+
expect(error.validValues).toEqual(['a', 'b']);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should serialize to JSON correctly', () => {
|
|
36
|
+
const error = new ValidationError('Test error', {
|
|
37
|
+
field: 'test_field',
|
|
38
|
+
hint: 'Test hint',
|
|
39
|
+
validValues: ['a', 'b'],
|
|
40
|
+
});
|
|
41
|
+
const json = error.toJSON();
|
|
42
|
+
expect(json.error).toBe('validation_error');
|
|
43
|
+
expect(json.message).toBe('Test error');
|
|
44
|
+
expect(json.field).toBe('test_field');
|
|
45
|
+
expect(json.hint).toBe('Test hint');
|
|
46
|
+
expect(json.valid_values).toEqual(['a', 'b']);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('validateRequired', () => {
|
|
51
|
+
it('should pass for valid values', () => {
|
|
52
|
+
expect(() => validateRequired('value', 'field')).not.toThrow();
|
|
53
|
+
expect(() => validateRequired(0, 'field')).not.toThrow();
|
|
54
|
+
expect(() => validateRequired(false, 'field')).not.toThrow();
|
|
55
|
+
expect(() => validateRequired({}, 'field')).not.toThrow();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should throw for undefined', () => {
|
|
59
|
+
expect(() => validateRequired(undefined, 'test_field')).toThrow(ValidationError);
|
|
60
|
+
expect(() => validateRequired(undefined, 'test_field')).toThrow('Missing required field: test_field');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should throw for null', () => {
|
|
64
|
+
expect(() => validateRequired(null, 'test_field')).toThrow(ValidationError);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should throw for empty string', () => {
|
|
68
|
+
expect(() => validateRequired('', 'test_field')).toThrow(ValidationError);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('validateUUID', () => {
|
|
73
|
+
it('should pass for valid UUIDs', () => {
|
|
74
|
+
expect(() => validateUUID('123e4567-e89b-12d3-a456-426614174000', 'id')).not.toThrow();
|
|
75
|
+
expect(() => validateUUID('00000000-0000-0000-0000-000000000000', 'id')).not.toThrow();
|
|
76
|
+
expect(() => validateUUID('FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF', 'id')).not.toThrow();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should pass for undefined (optional)', () => {
|
|
80
|
+
expect(() => validateUUID(undefined, 'id')).not.toThrow();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should pass for empty string (treated as optional)', () => {
|
|
84
|
+
expect(() => validateUUID('', 'id')).not.toThrow();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should throw for invalid UUIDs', () => {
|
|
88
|
+
expect(() => validateUUID('not-a-uuid', 'id')).toThrow(ValidationError);
|
|
89
|
+
expect(() => validateUUID('123', 'id')).toThrow(ValidationError);
|
|
90
|
+
expect(() => validateUUID('123e4567-e89b-12d3-a456', 'id')).toThrow(ValidationError);
|
|
91
|
+
expect(() => validateUUID('123e4567-e89b-12d3-a456-426614174000-extra', 'id')).toThrow(ValidationError);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('validateTaskStatus', () => {
|
|
96
|
+
it('should pass for valid task statuses', () => {
|
|
97
|
+
for (const status of VALID_TASK_STATUSES) {
|
|
98
|
+
expect(() => validateTaskStatus(status)).not.toThrow();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should pass for undefined (optional)', () => {
|
|
103
|
+
expect(() => validateTaskStatus(undefined)).not.toThrow();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should throw for invalid status', () => {
|
|
107
|
+
expect(() => validateTaskStatus('invalid')).toThrow(ValidationError);
|
|
108
|
+
expect(() => validateTaskStatus('PENDING')).toThrow(ValidationError);
|
|
109
|
+
expect(() => validateTaskStatus('done')).toThrow(ValidationError);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('validateProjectStatus', () => {
|
|
114
|
+
it('should pass for valid project statuses', () => {
|
|
115
|
+
for (const status of VALID_PROJECT_STATUSES) {
|
|
116
|
+
expect(() => validateProjectStatus(status)).not.toThrow();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should pass for undefined (optional)', () => {
|
|
121
|
+
expect(() => validateProjectStatus(undefined)).not.toThrow();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should throw for invalid status', () => {
|
|
125
|
+
expect(() => validateProjectStatus('invalid')).toThrow(ValidationError);
|
|
126
|
+
expect(() => validateProjectStatus('ACTIVE')).toThrow(ValidationError);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('validatePriority', () => {
|
|
131
|
+
it('should pass for valid priorities 1-5', () => {
|
|
132
|
+
for (let i = 1; i <= 5; i++) {
|
|
133
|
+
expect(() => validatePriority(i)).not.toThrow();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should pass for undefined (optional)', () => {
|
|
138
|
+
expect(() => validatePriority(undefined)).not.toThrow();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should throw for out of range values', () => {
|
|
142
|
+
expect(() => validatePriority(0)).toThrow(ValidationError);
|
|
143
|
+
expect(() => validatePriority(6)).toThrow(ValidationError);
|
|
144
|
+
expect(() => validatePriority(-1)).toThrow(ValidationError);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should throw for non-integers', () => {
|
|
148
|
+
expect(() => validatePriority(1.5)).toThrow(ValidationError);
|
|
149
|
+
expect(() => validatePriority(2.7)).toThrow(ValidationError);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('validateProgressPercentage', () => {
|
|
154
|
+
it('should pass for valid percentages 0-100', () => {
|
|
155
|
+
expect(() => validateProgressPercentage(0)).not.toThrow();
|
|
156
|
+
expect(() => validateProgressPercentage(50)).not.toThrow();
|
|
157
|
+
expect(() => validateProgressPercentage(100)).not.toThrow();
|
|
158
|
+
expect(() => validateProgressPercentage(33.33)).not.toThrow();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should pass for undefined (optional)', () => {
|
|
162
|
+
expect(() => validateProgressPercentage(undefined)).not.toThrow();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should throw for out of range values', () => {
|
|
166
|
+
expect(() => validateProgressPercentage(-1)).toThrow(ValidationError);
|
|
167
|
+
expect(() => validateProgressPercentage(101)).toThrow(ValidationError);
|
|
168
|
+
expect(() => validateProgressPercentage(150)).toThrow(ValidationError);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should throw for non-finite values', () => {
|
|
172
|
+
expect(() => validateProgressPercentage(Infinity)).toThrow(ValidationError);
|
|
173
|
+
expect(() => validateProgressPercentage(NaN)).toThrow(ValidationError);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('validateEstimatedMinutes', () => {
|
|
178
|
+
it('should pass for valid positive integers', () => {
|
|
179
|
+
expect(() => validateEstimatedMinutes(1)).not.toThrow();
|
|
180
|
+
expect(() => validateEstimatedMinutes(30)).not.toThrow();
|
|
181
|
+
expect(() => validateEstimatedMinutes(120)).not.toThrow();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should pass for undefined (optional)', () => {
|
|
185
|
+
expect(() => validateEstimatedMinutes(undefined)).not.toThrow();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should throw for zero', () => {
|
|
189
|
+
expect(() => validateEstimatedMinutes(0)).toThrow(ValidationError);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should throw for negative values', () => {
|
|
193
|
+
expect(() => validateEstimatedMinutes(-1)).toThrow(ValidationError);
|
|
194
|
+
expect(() => validateEstimatedMinutes(-30)).toThrow(ValidationError);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should throw for non-integers', () => {
|
|
198
|
+
expect(() => validateEstimatedMinutes(1.5)).toThrow(ValidationError);
|
|
199
|
+
expect(() => validateEstimatedMinutes(30.5)).toThrow(ValidationError);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('validateEnvironment', () => {
|
|
204
|
+
it('should pass for valid environments', () => {
|
|
205
|
+
for (const env of VALID_ENVIRONMENTS) {
|
|
206
|
+
expect(() => validateEnvironment(env)).not.toThrow();
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should pass for undefined (optional)', () => {
|
|
211
|
+
expect(() => validateEnvironment(undefined)).not.toThrow();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should pass for empty string (treated as optional)', () => {
|
|
215
|
+
expect(() => validateEnvironment('')).not.toThrow();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should throw for invalid environment', () => {
|
|
219
|
+
expect(() => validateEnvironment('invalid')).toThrow(ValidationError);
|
|
220
|
+
expect(() => validateEnvironment('PRODUCTION')).toThrow(ValidationError);
|
|
221
|
+
expect(() => validateEnvironment('prod')).toThrow(ValidationError);
|
|
222
|
+
});
|
|
223
|
+
});
|