@codemcp/workflows-core 3.1.16
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/.turbo/turbo-build.log +4 -0
- package/LICENSE +674 -0
- package/dist/config-manager.d.ts +24 -0
- package/dist/config-manager.js +68 -0
- package/dist/config-manager.js.map +1 -0
- package/dist/conversation-manager.d.ts +97 -0
- package/dist/conversation-manager.js +367 -0
- package/dist/conversation-manager.js.map +1 -0
- package/dist/database.d.ts +73 -0
- package/dist/database.js +500 -0
- package/dist/database.js.map +1 -0
- package/dist/file-detection-manager.d.ts +53 -0
- package/dist/file-detection-manager.js +221 -0
- package/dist/file-detection-manager.js.map +1 -0
- package/dist/git-manager.d.ts +14 -0
- package/dist/git-manager.js +59 -0
- package/dist/git-manager.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/instruction-generator.d.ts +69 -0
- package/dist/instruction-generator.js +133 -0
- package/dist/instruction-generator.js.map +1 -0
- package/dist/interaction-logger.d.ts +37 -0
- package/dist/interaction-logger.js +87 -0
- package/dist/interaction-logger.js.map +1 -0
- package/dist/logger.d.ts +64 -0
- package/dist/logger.js +283 -0
- package/dist/logger.js.map +1 -0
- package/dist/path-validation-utils.d.ts +51 -0
- package/dist/path-validation-utils.js +202 -0
- package/dist/path-validation-utils.js.map +1 -0
- package/dist/plan-manager.d.ts +65 -0
- package/dist/plan-manager.js +256 -0
- package/dist/plan-manager.js.map +1 -0
- package/dist/project-docs-manager.d.ts +119 -0
- package/dist/project-docs-manager.js +357 -0
- package/dist/project-docs-manager.js.map +1 -0
- package/dist/state-machine-loader.d.ts +60 -0
- package/dist/state-machine-loader.js +235 -0
- package/dist/state-machine-loader.js.map +1 -0
- package/dist/state-machine-types.d.ts +58 -0
- package/dist/state-machine-types.js +7 -0
- package/dist/state-machine-types.js.map +1 -0
- package/dist/state-machine.d.ts +52 -0
- package/dist/state-machine.js +256 -0
- package/dist/state-machine.js.map +1 -0
- package/dist/system-prompt-generator.d.ts +17 -0
- package/dist/system-prompt-generator.js +113 -0
- package/dist/system-prompt-generator.js.map +1 -0
- package/dist/template-manager.d.ts +61 -0
- package/dist/template-manager.js +229 -0
- package/dist/template-manager.js.map +1 -0
- package/dist/transition-engine.d.ts +70 -0
- package/dist/transition-engine.js +240 -0
- package/dist/transition-engine.js.map +1 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow-manager.d.ts +89 -0
- package/dist/workflow-manager.js +466 -0
- package/dist/workflow-manager.js.map +1 -0
- package/package.json +27 -0
- package/src/config-manager.ts +96 -0
- package/src/conversation-manager.ts +492 -0
- package/src/database.ts +685 -0
- package/src/file-detection-manager.ts +302 -0
- package/src/git-manager.ts +64 -0
- package/src/index.ts +28 -0
- package/src/instruction-generator.ts +210 -0
- package/src/interaction-logger.ts +109 -0
- package/src/logger.ts +353 -0
- package/src/path-validation-utils.ts +261 -0
- package/src/plan-manager.ts +323 -0
- package/src/project-docs-manager.ts +522 -0
- package/src/state-machine-loader.ts +308 -0
- package/src/state-machine-types.ts +72 -0
- package/src/state-machine.ts +370 -0
- package/src/system-prompt-generator.ts +122 -0
- package/src/template-manager.ts +321 -0
- package/src/transition-engine.ts +386 -0
- package/src/types.ts +60 -0
- package/src/workflow-manager.ts +601 -0
- package/test/unit/conversation-manager.test.ts +179 -0
- package/test/unit/custom-workflow-loading.test.ts +174 -0
- package/test/unit/directory-linking-and-extensions.test.ts +338 -0
- package/test/unit/file-linking-integration.test.ts +256 -0
- package/test/unit/git-commit-integration.test.ts +91 -0
- package/test/unit/git-manager.test.ts +86 -0
- package/test/unit/install-workflow.test.ts +138 -0
- package/test/unit/instruction-generator.test.ts +247 -0
- package/test/unit/list-workflows-filtering.test.ts +68 -0
- package/test/unit/none-template-functionality.test.ts +224 -0
- package/test/unit/project-docs-manager.test.ts +337 -0
- package/test/unit/state-machine-loader.test.ts +234 -0
- package/test/unit/template-manager.test.ts +217 -0
- package/test/unit/validate-workflow-name.test.ts +150 -0
- package/test/unit/workflow-domain-filtering.test.ts +75 -0
- package/test/unit/workflow-enum-generation.test.ts +92 -0
- package/test/unit/workflow-manager-enhanced-path-resolution.test.ts +369 -0
- package/test/unit/workflow-manager-path-resolution.test.ts +150 -0
- package/test/unit/workflow-migration.test.ts +155 -0
- package/test/unit/workflow-override-by-name.test.ts +116 -0
- package/test/unit/workflow-prioritization.test.ts +38 -0
- package/test/unit/workflow-validation.test.ts +303 -0
- package/test/utils/e2e-test-setup.ts +453 -0
- package/test/utils/run-server-in-dir.sh +27 -0
- package/test/utils/temp-files.ts +308 -0
- package/test/utils/test-access.ts +79 -0
- package/test/utils/test-helpers.ts +286 -0
- package/test/utils/test-setup.ts +78 -0
- package/tsconfig.build.json +21 -0
- package/tsconfig.json +8 -0
- package/vitest.config.ts +18 -0
package/src/logger.ts
ADDED
@@ -0,0 +1,353 @@
|
|
1
|
+
/**
|
2
|
+
* Logging utility for Vibe Feature MCP Server
|
3
|
+
*
|
4
|
+
* Provides structured logging with proper MCP compliance:
|
5
|
+
* - Uses stderr for all local logging (MCP requirement)
|
6
|
+
* - Supports MCP log message notifications to client
|
7
|
+
* - Provides structured logging with proper levels:
|
8
|
+
* - debug: Tracing and detailed execution flow
|
9
|
+
* - info: Success operations and important milestones
|
10
|
+
* - warn: Expected errors and recoverable issues
|
11
|
+
* - error: Caught but unexpected errors
|
12
|
+
*/
|
13
|
+
|
14
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
15
|
+
|
16
|
+
export enum LogLevel {
|
17
|
+
DEBUG = 0,
|
18
|
+
INFO = 1,
|
19
|
+
WARN = 2,
|
20
|
+
ERROR = 3,
|
21
|
+
SILENT = 4, // Suppress all logging
|
22
|
+
}
|
23
|
+
|
24
|
+
export interface LogContext {
|
25
|
+
component?: string;
|
26
|
+
conversationId?: string;
|
27
|
+
phase?: string;
|
28
|
+
operation?: string;
|
29
|
+
[key: string]: unknown;
|
30
|
+
}
|
31
|
+
|
32
|
+
// Global MCP server reference for log notifications
|
33
|
+
let mcpServerInstance: McpServer | null = null;
|
34
|
+
|
35
|
+
// Unified logging level - can be set by MCP client or environment
|
36
|
+
let currentLoggingLevel: LogLevel | null = null;
|
37
|
+
|
38
|
+
// Test mode detection function to check at runtime
|
39
|
+
function isTestMode(): boolean {
|
40
|
+
// Check explicit environment variables
|
41
|
+
if (process.env.NODE_ENV === 'test' || process.env.VITEST === 'true') {
|
42
|
+
return true;
|
43
|
+
}
|
44
|
+
|
45
|
+
// Check if running in a temporary directory (common for tests)
|
46
|
+
const cwd = process.cwd();
|
47
|
+
if (cwd.includes('/tmp/') || cwd.includes('temp') || cwd.includes('test-')) {
|
48
|
+
return true;
|
49
|
+
}
|
50
|
+
|
51
|
+
// Check if LOG_LEVEL is explicitly set to ERROR
|
52
|
+
if (process.env.LOG_LEVEL === 'ERROR') {
|
53
|
+
return true;
|
54
|
+
}
|
55
|
+
|
56
|
+
return false;
|
57
|
+
}
|
58
|
+
|
59
|
+
/**
|
60
|
+
* Set the MCP server instance for log notifications
|
61
|
+
*/
|
62
|
+
export function setMcpServerForLogging(server: McpServer): void {
|
63
|
+
mcpServerInstance = server;
|
64
|
+
}
|
65
|
+
|
66
|
+
/**
|
67
|
+
* Set the logging level from MCP client request
|
68
|
+
*/
|
69
|
+
export function setMcpLoggingLevel(level: string): void {
|
70
|
+
// Map MCP levels to our internal levels
|
71
|
+
const levelMap: Record<string, LogLevel> = {
|
72
|
+
debug: LogLevel.DEBUG,
|
73
|
+
info: LogLevel.INFO,
|
74
|
+
notice: LogLevel.INFO,
|
75
|
+
warning: LogLevel.WARN,
|
76
|
+
error: LogLevel.ERROR,
|
77
|
+
critical: LogLevel.ERROR,
|
78
|
+
alert: LogLevel.ERROR,
|
79
|
+
emergency: LogLevel.ERROR,
|
80
|
+
};
|
81
|
+
|
82
|
+
currentLoggingLevel = levelMap[level] ?? LogLevel.INFO;
|
83
|
+
}
|
84
|
+
|
85
|
+
class Logger {
|
86
|
+
private component: string;
|
87
|
+
private explicitLogLevel?: LogLevel;
|
88
|
+
|
89
|
+
constructor(component: string, logLevel?: LogLevel) {
|
90
|
+
this.component = component;
|
91
|
+
this.explicitLogLevel = logLevel;
|
92
|
+
}
|
93
|
+
|
94
|
+
private getCurrentLogLevel(): LogLevel {
|
95
|
+
// Check environment variable first (allows SILENT to override test mode)
|
96
|
+
const envLevel = this.getLogLevelFromEnv();
|
97
|
+
if (envLevel === LogLevel.SILENT) {
|
98
|
+
return LogLevel.SILENT;
|
99
|
+
}
|
100
|
+
|
101
|
+
// Force ERROR level in test environments (unless SILENT)
|
102
|
+
if (isTestMode()) {
|
103
|
+
return LogLevel.ERROR;
|
104
|
+
}
|
105
|
+
|
106
|
+
// Use MCP-set level if available (takes precedence)
|
107
|
+
if (currentLoggingLevel !== null) {
|
108
|
+
return currentLoggingLevel;
|
109
|
+
}
|
110
|
+
|
111
|
+
// Use environment variable level
|
112
|
+
if (envLevel !== null) {
|
113
|
+
return envLevel;
|
114
|
+
}
|
115
|
+
|
116
|
+
// If explicit log level was provided, use it
|
117
|
+
if (this.explicitLogLevel !== undefined) {
|
118
|
+
return this.explicitLogLevel;
|
119
|
+
}
|
120
|
+
|
121
|
+
// Default to INFO
|
122
|
+
return LogLevel.INFO;
|
123
|
+
}
|
124
|
+
|
125
|
+
private getLogLevelFromEnv(): LogLevel | null {
|
126
|
+
const envLevel = process.env.LOG_LEVEL?.toUpperCase();
|
127
|
+
switch (envLevel) {
|
128
|
+
case 'DEBUG':
|
129
|
+
return LogLevel.DEBUG;
|
130
|
+
case 'INFO':
|
131
|
+
return LogLevel.INFO;
|
132
|
+
case 'WARN':
|
133
|
+
return LogLevel.WARN;
|
134
|
+
case 'ERROR':
|
135
|
+
return LogLevel.ERROR;
|
136
|
+
case 'SILENT':
|
137
|
+
return LogLevel.SILENT;
|
138
|
+
default:
|
139
|
+
return null;
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
private shouldLog(level: LogLevel): boolean {
|
144
|
+
return level >= this.getCurrentLogLevel();
|
145
|
+
}
|
146
|
+
|
147
|
+
private formatMessage(
|
148
|
+
level: string,
|
149
|
+
message: string,
|
150
|
+
context?: LogContext
|
151
|
+
): string {
|
152
|
+
const timestamp = new Date().toISOString();
|
153
|
+
const contextStr = context ? ` ${JSON.stringify(context)}` : '';
|
154
|
+
return `[${timestamp}] ${level.toUpperCase()} [${this.component}] ${message}${contextStr}`;
|
155
|
+
}
|
156
|
+
|
157
|
+
/**
|
158
|
+
* Send log message to MCP client if server is available and level is appropriate
|
159
|
+
*/
|
160
|
+
private async sendMcpLogMessage(
|
161
|
+
level: 'debug' | 'info' | 'warning' | 'error',
|
162
|
+
message: string,
|
163
|
+
context?: LogContext
|
164
|
+
): Promise<void> {
|
165
|
+
if (mcpServerInstance) {
|
166
|
+
try {
|
167
|
+
// Safely serialize context to avoid JSON issues
|
168
|
+
let logData = message;
|
169
|
+
if (context) {
|
170
|
+
try {
|
171
|
+
const contextStr = JSON.stringify(context, null, 0);
|
172
|
+
logData = `${message} ${contextStr}`;
|
173
|
+
} catch (_error) {
|
174
|
+
// If JSON serialization fails, just use the message
|
175
|
+
logData = `${message} [context serialization failed]`;
|
176
|
+
}
|
177
|
+
}
|
178
|
+
|
179
|
+
await mcpServerInstance.server.notification({
|
180
|
+
method: 'notifications/message',
|
181
|
+
params: {
|
182
|
+
level,
|
183
|
+
logger: this.component,
|
184
|
+
data: logData,
|
185
|
+
},
|
186
|
+
});
|
187
|
+
} catch (error) {
|
188
|
+
// Fallback to stderr if MCP notification fails
|
189
|
+
// Don't use this.error to avoid infinite recursion
|
190
|
+
if (!isTestMode()) {
|
191
|
+
process.stderr.write(
|
192
|
+
`[MCP-LOG-ERROR] Failed to send log notification: ${error}\n`
|
193
|
+
);
|
194
|
+
}
|
195
|
+
}
|
196
|
+
}
|
197
|
+
}
|
198
|
+
|
199
|
+
debug(message: string, context?: LogContext): void {
|
200
|
+
if (this.shouldLog(LogLevel.DEBUG)) {
|
201
|
+
const formattedMessage = this.formatMessage('debug', message, context);
|
202
|
+
// Always log to stderr for MCP compliance
|
203
|
+
process.stderr.write(formattedMessage + '\n');
|
204
|
+
// Also send to MCP client if available (only for debug level)
|
205
|
+
this.sendMcpLogMessage('debug', message, context).catch(() => {
|
206
|
+
// Ignore MCP notification errors for debug messages
|
207
|
+
});
|
208
|
+
}
|
209
|
+
}
|
210
|
+
|
211
|
+
info(message: string, context?: LogContext): void {
|
212
|
+
if (this.shouldLog(LogLevel.INFO)) {
|
213
|
+
const formattedMessage = this.formatMessage('info', message, context);
|
214
|
+
// Always log to stderr for MCP compliance
|
215
|
+
process.stderr.write(formattedMessage + '\n');
|
216
|
+
|
217
|
+
// Send enhanced notifications for important events
|
218
|
+
this.sendEnhancedMcpNotification('info', message, context).catch(() => {
|
219
|
+
// Ignore MCP notification errors for info messages
|
220
|
+
});
|
221
|
+
}
|
222
|
+
}
|
223
|
+
|
224
|
+
/**
|
225
|
+
* Send enhanced MCP notifications with better formatting for important events
|
226
|
+
*/
|
227
|
+
private async sendEnhancedMcpNotification(
|
228
|
+
level: 'debug' | 'info' | 'warning' | 'error',
|
229
|
+
message: string,
|
230
|
+
context?: LogContext
|
231
|
+
): Promise<void> {
|
232
|
+
if (mcpServerInstance) {
|
233
|
+
try {
|
234
|
+
let enhancedMessage = message;
|
235
|
+
let notificationLevel = level;
|
236
|
+
|
237
|
+
// Enhance phase transition messages
|
238
|
+
if (
|
239
|
+
context &&
|
240
|
+
(context.from || context.to) &&
|
241
|
+
message.includes('transition')
|
242
|
+
) {
|
243
|
+
const from = context.from
|
244
|
+
? this.capitalizePhase(context.from as string)
|
245
|
+
: '';
|
246
|
+
const to = context.to
|
247
|
+
? this.capitalizePhase(context.to as string)
|
248
|
+
: '';
|
249
|
+
if (from && to) {
|
250
|
+
enhancedMessage = `Phase Transition: ${from} → ${to}`;
|
251
|
+
notificationLevel = 'info';
|
252
|
+
}
|
253
|
+
}
|
254
|
+
|
255
|
+
// Enhance initialization messages
|
256
|
+
if (message.includes('initialized successfully')) {
|
257
|
+
enhancedMessage = '🚀 Vibe Feature MCP Server Ready';
|
258
|
+
notificationLevel = 'info';
|
259
|
+
}
|
260
|
+
|
261
|
+
// Safely serialize context to avoid JSON issues
|
262
|
+
let logData = enhancedMessage;
|
263
|
+
if (context) {
|
264
|
+
try {
|
265
|
+
const contextStr = JSON.stringify(context, null, 0);
|
266
|
+
logData = `${enhancedMessage} ${contextStr}`;
|
267
|
+
} catch (_error) {
|
268
|
+
// If JSON serialization fails, just use the message
|
269
|
+
logData = `${enhancedMessage} [context serialization failed]`;
|
270
|
+
}
|
271
|
+
}
|
272
|
+
|
273
|
+
// Use the underlying server's notification method
|
274
|
+
await mcpServerInstance.server.notification({
|
275
|
+
method: 'notifications/message',
|
276
|
+
params: {
|
277
|
+
level: notificationLevel,
|
278
|
+
logger: this.component,
|
279
|
+
data: logData,
|
280
|
+
},
|
281
|
+
});
|
282
|
+
} catch (error) {
|
283
|
+
// Fallback to stderr if MCP notification fails
|
284
|
+
// Don't use this.error to avoid infinite recursion
|
285
|
+
if (!isTestMode()) {
|
286
|
+
process.stderr.write(
|
287
|
+
`[MCP-LOG-ERROR] Failed to send log notification: ${error}\n`
|
288
|
+
);
|
289
|
+
}
|
290
|
+
}
|
291
|
+
}
|
292
|
+
}
|
293
|
+
|
294
|
+
/**
|
295
|
+
* Capitalize phase name for display
|
296
|
+
*/
|
297
|
+
private capitalizePhase(phase: string): string {
|
298
|
+
return phase
|
299
|
+
.split('_')
|
300
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
301
|
+
.join(' ');
|
302
|
+
}
|
303
|
+
|
304
|
+
warn(message: string, context?: LogContext): void {
|
305
|
+
if (this.shouldLog(LogLevel.WARN)) {
|
306
|
+
const formattedMessage = this.formatMessage('warn', message, context);
|
307
|
+
// Always log to stderr for MCP compliance
|
308
|
+
process.stderr.write(formattedMessage + '\n');
|
309
|
+
// Also send to MCP client if available
|
310
|
+
this.sendEnhancedMcpNotification('warning', message, context).catch(
|
311
|
+
() => {
|
312
|
+
// Ignore MCP notification errors for warn messages
|
313
|
+
}
|
314
|
+
);
|
315
|
+
}
|
316
|
+
}
|
317
|
+
|
318
|
+
error(message: string, error?: Error, context?: LogContext): void {
|
319
|
+
if (this.shouldLog(LogLevel.ERROR)) {
|
320
|
+
const errorContext = error
|
321
|
+
? { ...context, error: error.message, stack: error.stack }
|
322
|
+
: context;
|
323
|
+
const formattedMessage = this.formatMessage(
|
324
|
+
'error',
|
325
|
+
message,
|
326
|
+
errorContext
|
327
|
+
);
|
328
|
+
// Always log to stderr for MCP compliance
|
329
|
+
process.stderr.write(formattedMessage + '\n');
|
330
|
+
// Also send to MCP client if available
|
331
|
+
this.sendEnhancedMcpNotification('error', message, errorContext).catch(
|
332
|
+
() => {
|
333
|
+
// Ignore MCP notification errors for error messages
|
334
|
+
}
|
335
|
+
);
|
336
|
+
}
|
337
|
+
}
|
338
|
+
|
339
|
+
child(childComponent: string): Logger {
|
340
|
+
return new Logger(
|
341
|
+
`${this.component}:${childComponent}`,
|
342
|
+
this.explicitLogLevel
|
343
|
+
);
|
344
|
+
}
|
345
|
+
}
|
346
|
+
|
347
|
+
// Factory function to create loggers
|
348
|
+
export function createLogger(component: string, logLevel?: LogLevel): Logger {
|
349
|
+
return new Logger(component, logLevel);
|
350
|
+
}
|
351
|
+
|
352
|
+
// Default logger for the main application
|
353
|
+
export const logger = createLogger('VibeFeatureMCP');
|
@@ -0,0 +1,261 @@
|
|
1
|
+
/**
|
2
|
+
* Path Validation Utilities
|
3
|
+
*
|
4
|
+
* Provides utilities for validating file paths, resolving relative paths,
|
5
|
+
* and ensuring security constraints for the file linking functionality.
|
6
|
+
*/
|
7
|
+
|
8
|
+
import { access, stat } from 'node:fs/promises';
|
9
|
+
import { resolve, isAbsolute, join, normalize } from 'node:path';
|
10
|
+
import { createLogger } from './logger.js';
|
11
|
+
|
12
|
+
const logger = createLogger('PathValidationUtils');
|
13
|
+
|
14
|
+
export interface PathValidationResult {
|
15
|
+
isValid: boolean;
|
16
|
+
resolvedPath?: string;
|
17
|
+
error?: string;
|
18
|
+
}
|
19
|
+
|
20
|
+
export class PathValidationUtils {
|
21
|
+
/**
|
22
|
+
* Validate if a string is a known template name
|
23
|
+
*/
|
24
|
+
static isTemplateName(value: string, availableTemplates: string[]): boolean {
|
25
|
+
return availableTemplates.includes(value);
|
26
|
+
}
|
27
|
+
|
28
|
+
/**
|
29
|
+
* Validate and resolve a file path
|
30
|
+
*/
|
31
|
+
static async validateFilePath(
|
32
|
+
filePath: string,
|
33
|
+
projectPath: string
|
34
|
+
): Promise<PathValidationResult> {
|
35
|
+
try {
|
36
|
+
// Resolve the path to absolute
|
37
|
+
const resolvedPath = this.resolvePath(filePath, projectPath);
|
38
|
+
|
39
|
+
// Security validation - prevent directory traversal
|
40
|
+
if (!this.isPathSafe(resolvedPath, projectPath)) {
|
41
|
+
return {
|
42
|
+
isValid: false,
|
43
|
+
error: 'Path is outside project boundaries for security reasons',
|
44
|
+
};
|
45
|
+
}
|
46
|
+
|
47
|
+
// Check if file exists and is readable
|
48
|
+
await access(resolvedPath);
|
49
|
+
|
50
|
+
// Verify it's a file (not a directory)
|
51
|
+
const stats = await stat(resolvedPath);
|
52
|
+
if (!stats.isFile()) {
|
53
|
+
return {
|
54
|
+
isValid: false,
|
55
|
+
error: 'Path points to a directory, not a file',
|
56
|
+
};
|
57
|
+
}
|
58
|
+
|
59
|
+
logger.debug('File path validated successfully', {
|
60
|
+
originalPath: filePath,
|
61
|
+
resolvedPath,
|
62
|
+
});
|
63
|
+
|
64
|
+
return {
|
65
|
+
isValid: true,
|
66
|
+
resolvedPath,
|
67
|
+
};
|
68
|
+
} catch (error) {
|
69
|
+
const errorMessage =
|
70
|
+
error instanceof Error ? error.message : 'Unknown error';
|
71
|
+
|
72
|
+
logger.debug('File path validation failed', {
|
73
|
+
filePath,
|
74
|
+
error: errorMessage,
|
75
|
+
});
|
76
|
+
|
77
|
+
return {
|
78
|
+
isValid: false,
|
79
|
+
error: `File not found or not accessible: ${errorMessage}`,
|
80
|
+
};
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
/**
|
85
|
+
* Validate and resolve a file or directory path
|
86
|
+
*/
|
87
|
+
static async validateFileOrDirectoryPath(
|
88
|
+
filePath: string,
|
89
|
+
projectPath: string
|
90
|
+
): Promise<PathValidationResult> {
|
91
|
+
try {
|
92
|
+
// Resolve the path to absolute
|
93
|
+
const resolvedPath = this.resolvePath(filePath, projectPath);
|
94
|
+
|
95
|
+
// Security validation - prevent directory traversal
|
96
|
+
if (!this.isPathSafe(resolvedPath, projectPath)) {
|
97
|
+
return {
|
98
|
+
isValid: false,
|
99
|
+
error: 'Path is outside project boundaries for security reasons',
|
100
|
+
};
|
101
|
+
}
|
102
|
+
|
103
|
+
// Check if file or directory exists and is readable
|
104
|
+
await access(resolvedPath);
|
105
|
+
|
106
|
+
// Verify it's either a file or directory
|
107
|
+
const stats = await stat(resolvedPath);
|
108
|
+
if (!stats.isFile() && !stats.isDirectory()) {
|
109
|
+
return {
|
110
|
+
isValid: false,
|
111
|
+
error: 'Path is neither a file nor a directory',
|
112
|
+
};
|
113
|
+
}
|
114
|
+
|
115
|
+
logger.debug('File or directory path validated successfully', {
|
116
|
+
originalPath: filePath,
|
117
|
+
resolvedPath,
|
118
|
+
isFile: stats.isFile(),
|
119
|
+
isDirectory: stats.isDirectory(),
|
120
|
+
});
|
121
|
+
|
122
|
+
return {
|
123
|
+
isValid: true,
|
124
|
+
resolvedPath,
|
125
|
+
};
|
126
|
+
} catch (error) {
|
127
|
+
const errorMessage =
|
128
|
+
error instanceof Error ? error.message : 'Unknown error';
|
129
|
+
|
130
|
+
logger.debug('File or directory path validation failed', {
|
131
|
+
filePath,
|
132
|
+
error: errorMessage,
|
133
|
+
});
|
134
|
+
|
135
|
+
return {
|
136
|
+
isValid: false,
|
137
|
+
error: `File or directory not found or not accessible: ${errorMessage}`,
|
138
|
+
};
|
139
|
+
}
|
140
|
+
}
|
141
|
+
|
142
|
+
/**
|
143
|
+
* Resolve a file path to absolute, handling various formats
|
144
|
+
*/
|
145
|
+
static resolvePath(filePath: string, projectPath: string): string {
|
146
|
+
// If already absolute, return as-is
|
147
|
+
if (isAbsolute(filePath)) {
|
148
|
+
return normalize(filePath);
|
149
|
+
}
|
150
|
+
|
151
|
+
// Handle relative paths (./file, ../file, file)
|
152
|
+
return resolve(projectPath, filePath);
|
153
|
+
}
|
154
|
+
|
155
|
+
/**
|
156
|
+
* Check if a resolved path is within safe boundaries
|
157
|
+
* Prevents directory traversal attacks
|
158
|
+
*/
|
159
|
+
static isPathSafe(resolvedPath: string, projectPath: string): boolean {
|
160
|
+
const normalizedResolved = normalize(resolvedPath);
|
161
|
+
const normalizedProject = normalize(projectPath);
|
162
|
+
|
163
|
+
// Allow paths within the project directory
|
164
|
+
if (normalizedResolved.startsWith(normalizedProject)) {
|
165
|
+
return true;
|
166
|
+
}
|
167
|
+
|
168
|
+
// Allow paths in common documentation locations relative to project
|
169
|
+
const allowedPaths = [
|
170
|
+
normalize(join(projectPath, '..')), // Parent directory (for monorepos)
|
171
|
+
'/usr/share/doc', // System documentation
|
172
|
+
'/opt/docs', // Optional documentation
|
173
|
+
];
|
174
|
+
|
175
|
+
return allowedPaths.some(allowedPath =>
|
176
|
+
normalizedResolved.startsWith(allowedPath)
|
177
|
+
);
|
178
|
+
}
|
179
|
+
|
180
|
+
/**
|
181
|
+
* Validate parameter as either template name or file path
|
182
|
+
*/
|
183
|
+
static async validateParameter(
|
184
|
+
value: string,
|
185
|
+
availableTemplates: string[],
|
186
|
+
projectPath: string
|
187
|
+
): Promise<{
|
188
|
+
isTemplate: boolean;
|
189
|
+
isFilePath: boolean;
|
190
|
+
resolvedPath?: string;
|
191
|
+
error?: string;
|
192
|
+
}> {
|
193
|
+
// First check if it's a template name
|
194
|
+
if (this.isTemplateName(value, availableTemplates)) {
|
195
|
+
return {
|
196
|
+
isTemplate: true,
|
197
|
+
isFilePath: false,
|
198
|
+
};
|
199
|
+
}
|
200
|
+
|
201
|
+
// Then validate as file or directory path
|
202
|
+
const pathValidation = await this.validateFileOrDirectoryPath(
|
203
|
+
value,
|
204
|
+
projectPath
|
205
|
+
);
|
206
|
+
|
207
|
+
if (pathValidation.isValid) {
|
208
|
+
return {
|
209
|
+
isTemplate: false,
|
210
|
+
isFilePath: true,
|
211
|
+
resolvedPath: pathValidation.resolvedPath,
|
212
|
+
};
|
213
|
+
}
|
214
|
+
|
215
|
+
// Neither template nor valid file/directory path
|
216
|
+
return {
|
217
|
+
isTemplate: false,
|
218
|
+
isFilePath: false,
|
219
|
+
error: `Invalid parameter: not a known template (${availableTemplates.join(', ')}) and not a valid file or directory path (${pathValidation.error})`,
|
220
|
+
};
|
221
|
+
}
|
222
|
+
|
223
|
+
/**
|
224
|
+
* Get common file patterns for documentation
|
225
|
+
*/
|
226
|
+
static getCommonDocumentationPatterns(): {
|
227
|
+
architecture: string[];
|
228
|
+
requirements: string[];
|
229
|
+
design: string[];
|
230
|
+
} {
|
231
|
+
return {
|
232
|
+
architecture: [
|
233
|
+
'ARCHITECTURE.md',
|
234
|
+
'ARCHITECTURE.txt',
|
235
|
+
'architecture.md',
|
236
|
+
'Architecture.md',
|
237
|
+
'docs/ARCHITECTURE.md',
|
238
|
+
'docs/architecture.md',
|
239
|
+
'README.md', // Can contain architecture info
|
240
|
+
],
|
241
|
+
requirements: [
|
242
|
+
'REQUIREMENTS.md',
|
243
|
+
'REQUIREMENTS.txt',
|
244
|
+
'requirements.md',
|
245
|
+
'Requirements.md',
|
246
|
+
'docs/REQUIREMENTS.md',
|
247
|
+
'docs/requirements.md',
|
248
|
+
'README.md', // Often contains requirements
|
249
|
+
],
|
250
|
+
design: [
|
251
|
+
'DESIGN.md',
|
252
|
+
'DESIGN.txt',
|
253
|
+
'design.md',
|
254
|
+
'Design.md',
|
255
|
+
'docs/DESIGN.md',
|
256
|
+
'docs/design.md',
|
257
|
+
'README.md', // Can contain design info
|
258
|
+
],
|
259
|
+
};
|
260
|
+
}
|
261
|
+
}
|