@veloxts/cli 0.6.52 → 0.6.55

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/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # @veloxts/cli
2
2
 
3
+ ## 0.6.55
4
+
5
+ ### Patch Changes
6
+
7
+ - feat(mcp): add static TypeScript analyzer for procedure discovery
8
+ - Updated dependencies
9
+ - @veloxts/auth@0.6.55
10
+ - @veloxts/core@0.6.55
11
+ - @veloxts/orm@0.6.55
12
+ - @veloxts/router@0.6.55
13
+ - @veloxts/validation@0.6.55
14
+
15
+ ## 0.6.54
16
+
17
+ ### Patch Changes
18
+
19
+ - feat(cli): add velox mcp init command for Claude Desktop setup
20
+ - Updated dependencies
21
+ - @veloxts/auth@0.6.54
22
+ - @veloxts/core@0.6.54
23
+ - @veloxts/orm@0.6.54
24
+ - @veloxts/router@0.6.54
25
+ - @veloxts/validation@0.6.54
26
+
27
+ ## 0.6.53
28
+
29
+ ### Patch Changes
30
+
31
+ - feat(cli): add duplicate file detection to resource generator
32
+ - Updated dependencies
33
+ - @veloxts/auth@0.6.53
34
+ - @veloxts/core@0.6.53
35
+ - @veloxts/orm@0.6.53
36
+ - @veloxts/router@0.6.53
37
+ - @veloxts/validation@0.6.53
38
+
3
39
  ## 0.6.52
4
40
 
5
41
  ### Patch Changes
package/GUIDE.md CHANGED
@@ -93,6 +93,24 @@ velox make seeder user # Scaffold database seeder
93
93
  velox make factory user # Scaffold model factory
94
94
  ```
95
95
 
96
+ ### velox mcp init
97
+
98
+ Set up Model Context Protocol (MCP) server for Claude Desktop:
99
+
100
+ ```bash
101
+ velox mcp init # Configure Claude Desktop for VeloxTS
102
+ velox mcp init --dry-run # Preview configuration changes
103
+ velox mcp init --force # Overwrite existing configuration
104
+ velox mcp init --json # Output as JSON for scripting
105
+ ```
106
+
107
+ The MCP server exposes your VeloxTS project's context to Claude Desktop and other AI assistants that support the Model Context Protocol. After running this command, restart Claude Desktop to activate the integration.
108
+
109
+ Supported platforms:
110
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
111
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
112
+ - Linux: `~/.config/Claude/claude_desktop_config.json`
113
+
96
114
  ## Database Seeding
97
115
 
98
116
  ### Creating Seeders
package/dist/cli.js CHANGED
@@ -13,6 +13,7 @@ import { createDbCommand } from './commands/db.js';
13
13
  import { createDevCommand } from './commands/dev.js';
14
14
  import { createIntrospectCommand } from './commands/introspect.js';
15
15
  import { createMakeCommand } from './commands/make.js';
16
+ import { createMcpCommand } from './commands/mcp.js';
16
17
  import { createMigrateCommand } from './commands/migrate.js';
17
18
  import { createProceduresCommand } from './commands/procedures.js';
18
19
  import { createScheduleCommand } from './commands/schedule.js';
@@ -33,6 +34,7 @@ function createCLI() {
33
34
  program.addCommand(createDevCommand(CLI_VERSION));
34
35
  program.addCommand(createIntrospectCommand());
35
36
  program.addCommand(createMakeCommand());
37
+ program.addCommand(createMcpCommand());
36
38
  program.addCommand(createMigrateCommand());
37
39
  program.addCommand(createProceduresCommand());
38
40
  program.addCommand(createScheduleCommand());
@@ -0,0 +1,11 @@
1
+ /**
2
+ * MCP command - Model Context Protocol configuration
3
+ *
4
+ * Provides subcommands for managing MCP server integration:
5
+ * - mcp:init - Set up Claude Desktop configuration for VeloxTS MCP server
6
+ */
7
+ import { Command } from 'commander';
8
+ /**
9
+ * Create the mcp command with subcommands
10
+ */
11
+ export declare function createMcpCommand(): Command;
@@ -0,0 +1,344 @@
1
+ /**
2
+ * MCP command - Model Context Protocol configuration
3
+ *
4
+ * Provides subcommands for managing MCP server integration:
5
+ * - mcp:init - Set up Claude Desktop configuration for VeloxTS MCP server
6
+ */
7
+ import { homedir, platform } from 'node:os';
8
+ import { join } from 'node:path';
9
+ import * as p from '@clack/prompts';
10
+ import { Command } from 'commander';
11
+ import pc from 'picocolors';
12
+ import { error, formatCommand, formatPath, info, success, warning } from '../utils/output.js';
13
+ import { createDirectory, fileExists, readJsonFile, writeJsonFile } from '../utils/paths.js';
14
+ // ============================================================================
15
+ // Constants
16
+ // ============================================================================
17
+ const MCP_SERVER_NAME = 'veloxts';
18
+ const MCP_SERVER_CONFIG = {
19
+ command: 'npx',
20
+ args: ['@veloxts/mcp'],
21
+ };
22
+ // ============================================================================
23
+ // Command Factory
24
+ // ============================================================================
25
+ /**
26
+ * Create the mcp command with subcommands
27
+ */
28
+ export function createMcpCommand() {
29
+ const mcp = new Command('mcp').description('Model Context Protocol server configuration');
30
+ mcp.addCommand(createMcpInitCommand());
31
+ return mcp;
32
+ }
33
+ /**
34
+ * Create the mcp:init subcommand
35
+ */
36
+ function createMcpInitCommand() {
37
+ return new Command('init')
38
+ .description('Set up Claude Desktop configuration for VeloxTS MCP server')
39
+ .option('-d, --dry-run', 'Preview changes without writing files', false)
40
+ .option('-f, --force', 'Overwrite existing VeloxTS MCP configuration', false)
41
+ .option('--json', 'Output results as JSON', false)
42
+ .action(async (options) => {
43
+ await runMcpInit(options);
44
+ })
45
+ .addHelpText('after', `
46
+ Examples:
47
+ ${formatCommand('velox mcp:init')} Set up MCP server configuration
48
+ ${formatCommand('velox mcp:init --dry-run')} Preview configuration changes
49
+ ${formatCommand('velox mcp:init --force')} Overwrite existing configuration
50
+ ${formatCommand('velox mcp:init --json')} Output as JSON for scripting
51
+
52
+ About:
53
+ The MCP (Model Context Protocol) server exposes your VeloxTS project's
54
+ context to Claude Desktop and other AI assistants. This command automates
55
+ the configuration process.
56
+
57
+ After running this command, restart Claude Desktop to use the MCP server.
58
+ `);
59
+ }
60
+ // ============================================================================
61
+ // Platform Detection
62
+ // ============================================================================
63
+ /**
64
+ * Get the Claude Desktop configuration path for the current platform
65
+ */
66
+ function getClaudeConfigPath() {
67
+ const platformName = platform();
68
+ switch (platformName) {
69
+ case 'darwin': // macOS
70
+ return join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
71
+ case 'win32': // Windows
72
+ return join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json');
73
+ case 'linux':
74
+ return join(homedir(), '.config', 'Claude', 'claude_desktop_config.json');
75
+ default:
76
+ return null;
77
+ }
78
+ }
79
+ // ============================================================================
80
+ // Configuration Management
81
+ // ============================================================================
82
+ /**
83
+ * Check if Claude Desktop is likely installed
84
+ */
85
+ function isClaudeDesktopInstalled(configPath) {
86
+ // Check if parent directory exists (more reliable than checking for config file)
87
+ const configDir = join(configPath, '..');
88
+ return fileExists(configDir);
89
+ }
90
+ /**
91
+ * Read existing Claude Desktop configuration
92
+ */
93
+ async function readClaudeConfig(configPath) {
94
+ if (!fileExists(configPath)) {
95
+ return null;
96
+ }
97
+ try {
98
+ const config = await readJsonFile(configPath);
99
+ return config;
100
+ }
101
+ catch (err) {
102
+ throw new Error(`Failed to parse Claude Desktop configuration: ${err instanceof Error ? err.message : String(err)}`);
103
+ }
104
+ }
105
+ /**
106
+ * Check if VeloxTS MCP server is already configured
107
+ */
108
+ function isVeloxMcpConfigured(config) {
109
+ if (!config?.mcpServers) {
110
+ return false;
111
+ }
112
+ return MCP_SERVER_NAME in config.mcpServers;
113
+ }
114
+ /**
115
+ * Add or update VeloxTS MCP server configuration
116
+ */
117
+ function addVeloxMcpConfig(config) {
118
+ const newConfig = config ?? {};
119
+ if (!newConfig.mcpServers) {
120
+ newConfig.mcpServers = {};
121
+ }
122
+ newConfig.mcpServers[MCP_SERVER_NAME] = MCP_SERVER_CONFIG;
123
+ return newConfig;
124
+ }
125
+ /**
126
+ * Write Claude Desktop configuration
127
+ */
128
+ async function writeClaudeConfig(configPath, config) {
129
+ const configDir = join(configPath, '..');
130
+ // Ensure directory exists
131
+ if (!fileExists(configDir)) {
132
+ await createDirectory(configDir);
133
+ }
134
+ await writeJsonFile(configPath, config);
135
+ }
136
+ // ============================================================================
137
+ // Command Implementation
138
+ // ============================================================================
139
+ /**
140
+ * Run the mcp:init command
141
+ */
142
+ async function runMcpInit(options) {
143
+ const { dryRun, force, json } = options;
144
+ const isInteractive = !json;
145
+ try {
146
+ // Detect platform and config path
147
+ const configPath = getClaudeConfigPath();
148
+ if (!configPath) {
149
+ const result = {
150
+ success: false,
151
+ action: 'error',
152
+ configPath: 'unknown',
153
+ message: 'Unsupported platform',
154
+ error: `Platform '${platform()}' is not supported`,
155
+ };
156
+ if (json) {
157
+ console.log(JSON.stringify(result, null, 2));
158
+ }
159
+ else {
160
+ error('Unsupported platform.');
161
+ info(`This command supports macOS, Windows, and Linux.`);
162
+ info(`Your platform: ${platform()}`);
163
+ }
164
+ process.exit(1);
165
+ }
166
+ // Check if Claude Desktop is installed
167
+ if (!isClaudeDesktopInstalled(configPath) && isInteractive) {
168
+ warning('Claude Desktop may not be installed.');
169
+ console.log('');
170
+ console.log(` ${pc.dim('Expected config directory:')} ${formatPath(join(configPath, '..'))}`);
171
+ console.log('');
172
+ const shouldContinue = await p.confirm({
173
+ message: 'Continue anyway?',
174
+ initialValue: false,
175
+ });
176
+ if (p.isCancel(shouldContinue) || !shouldContinue) {
177
+ info('Setup cancelled.');
178
+ return;
179
+ }
180
+ }
181
+ // Read existing configuration
182
+ let config = null;
183
+ let configExists = false;
184
+ try {
185
+ config = await readClaudeConfig(configPath);
186
+ configExists = config !== null;
187
+ }
188
+ catch (err) {
189
+ const result = {
190
+ success: false,
191
+ action: 'error',
192
+ configPath,
193
+ message: 'Failed to read configuration',
194
+ error: err instanceof Error ? err.message : String(err),
195
+ };
196
+ if (json) {
197
+ console.log(JSON.stringify(result, null, 2));
198
+ }
199
+ else {
200
+ error('Failed to read Claude Desktop configuration.');
201
+ console.log(` ${pc.dim('Path:')} ${formatPath(configPath)}`);
202
+ console.log(` ${pc.dim('Error:')} ${err instanceof Error ? err.message : String(err)}`);
203
+ console.log('');
204
+ info('The configuration file may be corrupted or have invalid JSON.');
205
+ info('You can manually delete it and run this command again.');
206
+ }
207
+ process.exit(1);
208
+ }
209
+ // Check if VeloxTS MCP is already configured
210
+ const isAlreadyConfigured = isVeloxMcpConfigured(config);
211
+ if (isAlreadyConfigured && !force) {
212
+ const result = {
213
+ success: true,
214
+ action: 'exists',
215
+ configPath,
216
+ message: 'VeloxTS MCP server is already configured',
217
+ };
218
+ if (json) {
219
+ console.log(JSON.stringify(result, null, 2));
220
+ }
221
+ else {
222
+ info('VeloxTS MCP server is already configured.');
223
+ console.log('');
224
+ console.log(` ${pc.dim('Config path:')} ${formatPath(configPath)}`);
225
+ console.log('');
226
+ warning(`Use ${formatCommand('--force')} to overwrite the existing configuration.`);
227
+ }
228
+ return;
229
+ }
230
+ // Show what will be done
231
+ if (isInteractive && !dryRun) {
232
+ console.log('');
233
+ if (isAlreadyConfigured) {
234
+ info('Updating VeloxTS MCP server configuration:');
235
+ }
236
+ else if (configExists) {
237
+ info('Adding VeloxTS MCP server to existing configuration:');
238
+ }
239
+ else {
240
+ info('Creating Claude Desktop configuration:');
241
+ }
242
+ console.log('');
243
+ console.log(` ${pc.dim('Path:')} ${formatPath(configPath)}`);
244
+ console.log(` ${pc.dim('Server:')} ${MCP_SERVER_NAME}`);
245
+ console.log(` ${pc.dim('Command:')} ${MCP_SERVER_CONFIG.command} ${MCP_SERVER_CONFIG.args.join(' ')}`);
246
+ console.log('');
247
+ }
248
+ // Add VeloxTS MCP configuration
249
+ const newConfig = addVeloxMcpConfig(config);
250
+ // Dry run mode
251
+ if (dryRun) {
252
+ const result = {
253
+ success: true,
254
+ action: configExists ? 'updated' : 'created',
255
+ configPath,
256
+ message: 'Dry run - no changes made',
257
+ };
258
+ if (json) {
259
+ console.log(JSON.stringify({
260
+ ...result,
261
+ dryRun: true,
262
+ preview: newConfig,
263
+ }, null, 2));
264
+ }
265
+ else {
266
+ console.log('Configuration preview:');
267
+ console.log('');
268
+ console.log(JSON.stringify(newConfig, null, 2));
269
+ console.log('');
270
+ warning('Dry run mode - no changes made.');
271
+ console.log(` ${pc.dim('Remove --dry-run to apply changes.')}`);
272
+ }
273
+ return;
274
+ }
275
+ // Write configuration
276
+ try {
277
+ await writeClaudeConfig(configPath, newConfig);
278
+ }
279
+ catch (err) {
280
+ const result = {
281
+ success: false,
282
+ action: 'error',
283
+ configPath,
284
+ message: 'Failed to write configuration',
285
+ error: err instanceof Error ? err.message : String(err),
286
+ };
287
+ if (json) {
288
+ console.log(JSON.stringify(result, null, 2));
289
+ }
290
+ else {
291
+ error('Failed to write Claude Desktop configuration.');
292
+ console.log(` ${pc.dim('Path:')} ${formatPath(configPath)}`);
293
+ console.log(` ${pc.dim('Error:')} ${err instanceof Error ? err.message : String(err)}`);
294
+ console.log('');
295
+ // Check for permission errors
296
+ if (err instanceof Error && err.message.includes('EACCES')) {
297
+ info('You may not have write permission to this directory.');
298
+ info('Try running this command with appropriate permissions.');
299
+ }
300
+ }
301
+ process.exit(1);
302
+ }
303
+ // Success!
304
+ const action = isAlreadyConfigured ? 'updated' : configExists ? 'updated' : 'created';
305
+ const result = {
306
+ success: true,
307
+ action,
308
+ configPath,
309
+ message: 'VeloxTS MCP server configured successfully',
310
+ instructions: 'Restart Claude Desktop to activate the MCP server',
311
+ };
312
+ if (json) {
313
+ console.log(JSON.stringify(result, null, 2));
314
+ }
315
+ else {
316
+ success('VeloxTS MCP server configured successfully!');
317
+ console.log('');
318
+ console.log(` ${pc.dim('Config path:')} ${formatPath(configPath)}`);
319
+ console.log('');
320
+ info('Next steps:');
321
+ console.log(` ${pc.dim('1.')} Restart Claude Desktop`);
322
+ console.log(` ${pc.dim('2.')} Open Claude Desktop`);
323
+ console.log(` ${pc.dim('3.')} The VeloxTS MCP server will provide project context automatically`);
324
+ console.log('');
325
+ }
326
+ }
327
+ catch (err) {
328
+ const result = {
329
+ success: false,
330
+ action: 'error',
331
+ configPath: 'unknown',
332
+ message: 'Unexpected error',
333
+ error: err instanceof Error ? err.message : String(err),
334
+ };
335
+ if (json) {
336
+ console.log(JSON.stringify(result, null, 2));
337
+ }
338
+ else {
339
+ error('An unexpected error occurred.');
340
+ console.log(` ${pc.dim('Error:')} ${err instanceof Error ? err.message : String(err)}`);
341
+ }
342
+ process.exit(1);
343
+ }
344
+ }
@@ -16,6 +16,7 @@ import pc from 'picocolors';
16
16
  import { BaseGenerator } from '../base.js';
17
17
  import { collectFields } from '../fields/index.js';
18
18
  import { generateInjectablePrismaContent, generateResourceFiles, } from '../templates/resource.js';
19
+ import { findAllSimilarFiles, formatSimilarFilesWarning } from '../utils/filesystem.js';
19
20
  import { deriveEntityNames } from '../utils/naming.js';
20
21
  import { promptAndRunMigration } from '../utils/prisma-migration.js';
21
22
  import { analyzePrismaSchema, findPrismaSchema, hasModel, injectIntoSchema, } from '../utils/prisma-schema.js';
@@ -192,6 +193,40 @@ export class ResourceGenerator extends BaseGenerator {
192
193
  project: config.project,
193
194
  options: templateOptions,
194
195
  };
196
+ // Check for similar files (singular/plural variants) before generating
197
+ // This helps prevent accidental duplicate entities like user.ts vs users.ts
198
+ if (!config.force && !config.dryRun) {
199
+ const targetPaths = [
200
+ `src/procedures/${ctx.entity.kebab}.ts`,
201
+ `src/schemas/${ctx.entity.kebab}.schema.ts`,
202
+ ];
203
+ const similarResult = findAllSimilarFiles(config.cwd, targetPaths, ctx.entity.kebab);
204
+ if (similarResult.hasSimilar) {
205
+ const warningMessage = formatSimilarFilesWarning(similarResult, config.entityName);
206
+ p.log.warn(warningMessage);
207
+ // In interactive mode, ask user to confirm
208
+ if (interactive) {
209
+ const shouldContinue = await p.confirm({
210
+ message: 'Do you want to continue anyway?',
211
+ initialValue: false,
212
+ });
213
+ if (p.isCancel(shouldContinue) || !shouldContinue) {
214
+ return {
215
+ files: [],
216
+ postInstructions: 'Generation cancelled due to similar existing files.',
217
+ };
218
+ }
219
+ }
220
+ else {
221
+ // In non-interactive mode, abort and suggest --force
222
+ p.log.error('Use --force to generate anyway, or choose a different entity name.');
223
+ return {
224
+ files: [],
225
+ postInstructions: 'Generation aborted: similar files already exist. Use --force to override.',
226
+ };
227
+ }
228
+ }
229
+ }
195
230
  // Show spinner during actual file generation
196
231
  // Only show if we're in interactive mode (we collected fields or user opted to skip)
197
232
  const showSpinner = interactive && !skipFields && !config.dryRun;
@@ -274,12 +309,11 @@ export class ResourceGenerator extends BaseGenerator {
274
309
  }
275
310
  // Step 3: Run Prisma migration (if model was injected and migration not skipped)
276
311
  if (result.prismaInjected && !flags.skipMigration) {
277
- const migResult = await promptAndRunMigration({
312
+ result.migrationRun = await promptAndRunMigration({
278
313
  cwd: projectRoot,
279
314
  autoRun: flags.autoMigrate,
280
315
  skip: false,
281
316
  });
282
- result.migrationRun = migResult;
283
317
  }
284
318
  return result;
285
319
  }
@@ -65,3 +65,47 @@ export declare function formatWriteResults(results: ReadonlyArray<WriteResult>,
65
65
  * Format write results as JSON for machine consumption
66
66
  */
67
67
  export declare function formatWriteResultsJson(results: ReadonlyArray<WriteResult>): string;
68
+ /**
69
+ * Information about a similar file that was found
70
+ */
71
+ export interface SimilarFile {
72
+ /** Path to the similar file */
73
+ path: string;
74
+ /** Why this is considered similar */
75
+ reason: 'singular' | 'plural' | 'different-suffix';
76
+ /** The file we were looking for when we found this */
77
+ targetPath: string;
78
+ }
79
+ /**
80
+ * Result of checking for similar files
81
+ */
82
+ export interface SimilarFilesResult {
83
+ /** Whether any similar files were found */
84
+ hasSimilar: boolean;
85
+ /** List of similar files found */
86
+ files: SimilarFile[];
87
+ }
88
+ /**
89
+ * Find similar files that might conflict with intended generation.
90
+ *
91
+ * This checks for naming variations that could indicate duplicate entities:
92
+ * - Singular vs plural forms (user.ts vs users.ts)
93
+ * - Different file suffixes (user.ts vs user.schema.ts)
94
+ *
95
+ * @example
96
+ * findSimilarFiles('/project', 'src/procedures/user.ts', 'user')
97
+ * // Might find: src/procedures/users.ts
98
+ *
99
+ * @param projectRoot - Project root directory
100
+ * @param targetPath - Path we intend to create
101
+ * @param entityKebab - Entity name in kebab-case (e.g., 'user', 'blog-post')
102
+ */
103
+ export declare function findSimilarFiles(projectRoot: string, targetPath: string, entityKebab: string): SimilarFilesResult;
104
+ /**
105
+ * Check for similar files across multiple target paths
106
+ */
107
+ export declare function findAllSimilarFiles(projectRoot: string, targetPaths: string[], entityKebab: string): SimilarFilesResult;
108
+ /**
109
+ * Format similar files warning message
110
+ */
111
+ export declare function formatSimilarFilesWarning(result: SimilarFilesResult, entityName: string): string;
@@ -4,11 +4,12 @@
4
4
  * Safe file operations with conflict detection, directory creation,
5
5
  * and interactive prompting support.
6
6
  */
7
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
- import { dirname, relative } from 'node:path';
7
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { dirname, join, normalize, relative } from 'node:path';
9
9
  import * as p from '@clack/prompts';
10
10
  import pc from 'picocolors';
11
11
  import { GeneratorError, GeneratorErrorCode } from '../types.js';
12
+ import { pluralize, singularize } from './naming.js';
12
13
  /**
13
14
  * Write a single file with conflict handling
14
15
  */
@@ -214,3 +215,122 @@ export function formatWriteResultsJson(results) {
214
215
  })),
215
216
  }, null, 2);
216
217
  }
218
+ /**
219
+ * Find similar files that might conflict with intended generation.
220
+ *
221
+ * This checks for naming variations that could indicate duplicate entities:
222
+ * - Singular vs plural forms (user.ts vs users.ts)
223
+ * - Different file suffixes (user.ts vs user.schema.ts)
224
+ *
225
+ * @example
226
+ * findSimilarFiles('/project', 'src/procedures/user.ts', 'user')
227
+ * // Might find: src/procedures/users.ts
228
+ *
229
+ * @param projectRoot - Project root directory
230
+ * @param targetPath - Path we intend to create
231
+ * @param entityKebab - Entity name in kebab-case (e.g., 'user', 'blog-post')
232
+ */
233
+ export function findSimilarFiles(projectRoot, targetPath, entityKebab) {
234
+ const result = {
235
+ hasSimilar: false,
236
+ files: [],
237
+ };
238
+ const dir = dirname(targetPath);
239
+ const fullDir = join(projectRoot, dir);
240
+ // If directory doesn't exist, no similar files
241
+ if (!existsSync(fullDir)) {
242
+ return result;
243
+ }
244
+ // Get all files in the directory
245
+ let filesInDir;
246
+ try {
247
+ filesInDir = readdirSync(fullDir);
248
+ }
249
+ catch {
250
+ return result;
251
+ }
252
+ // Generate variants to check
253
+ const singularName = singularize(entityKebab);
254
+ const pluralName = pluralize(entityKebab);
255
+ // Patterns to match (without extension)
256
+ const patterns = new Set([
257
+ singularName, // user
258
+ pluralName, // users
259
+ `${singularName}.schema`, // user.schema
260
+ `${pluralName}.schema`, // users.schema
261
+ ]);
262
+ // Normalize target path for consistent comparison across platforms
263
+ const normalizedTargetPath = normalize(targetPath);
264
+ for (const file of filesInDir) {
265
+ // Skip non-TS/JS files
266
+ if (!/\.(ts|js|tsx|jsx)$/.test(file)) {
267
+ continue;
268
+ }
269
+ // Get base name without extension
270
+ const fileBase = file.replace(/\.(ts|js|tsx|jsx)$/, '');
271
+ const filePath = normalize(join(dir, file));
272
+ // Skip the exact target (will be handled by normal conflict detection)
273
+ if (filePath === normalizedTargetPath) {
274
+ continue;
275
+ }
276
+ // Check if this file matches any of our patterns
277
+ if (patterns.has(fileBase)) {
278
+ // Determine the reason: singular/plural or different naming convention
279
+ const reason = fileBase === singularName
280
+ ? 'singular'
281
+ : fileBase === pluralName
282
+ ? 'plural'
283
+ : 'different-suffix';
284
+ // File exists since we got it from readdirSync
285
+ result.files.push({
286
+ path: filePath,
287
+ reason,
288
+ targetPath,
289
+ });
290
+ result.hasSimilar = true;
291
+ }
292
+ }
293
+ return result;
294
+ }
295
+ /**
296
+ * Check for similar files across multiple target paths
297
+ */
298
+ export function findAllSimilarFiles(projectRoot, targetPaths, entityKebab) {
299
+ const allFiles = [];
300
+ for (const targetPath of targetPaths) {
301
+ const result = findSimilarFiles(projectRoot, targetPath, entityKebab);
302
+ allFiles.push(...result.files);
303
+ }
304
+ // Deduplicate by path
305
+ const uniqueFiles = Array.from(new Map(allFiles.map((f) => [f.path, f])).values());
306
+ return {
307
+ hasSimilar: uniqueFiles.length > 0,
308
+ files: uniqueFiles,
309
+ };
310
+ }
311
+ /**
312
+ * Format similar files warning message
313
+ */
314
+ export function formatSimilarFilesWarning(result, entityName) {
315
+ if (!result.hasSimilar) {
316
+ return '';
317
+ }
318
+ const lines = [
319
+ pc.yellow(`⚠ Found existing files that may conflict with "${entityName}":`),
320
+ '',
321
+ ];
322
+ for (const file of result.files) {
323
+ const reasonText = file.reason === 'singular'
324
+ ? '(singular form)'
325
+ : file.reason === 'plural'
326
+ ? '(plural form)'
327
+ : '(different naming convention)';
328
+ lines.push(` ${pc.cyan(file.path)} ${pc.dim(reasonText)}`);
329
+ }
330
+ lines.push('');
331
+ lines.push(pc.dim('This may indicate a duplicate entity. Options:'));
332
+ lines.push(pc.dim(' • Use --force to create anyway'));
333
+ lines.push(pc.dim(' • Rename your entity to avoid confusion'));
334
+ lines.push(pc.dim(' • Delete the existing files if they are unused'));
335
+ return lines.join('\n');
336
+ }
package/dist/index.d.ts CHANGED
@@ -24,6 +24,7 @@ export declare const CLI_VERSION: string;
24
24
  export { createDbCommand } from './commands/db.js';
25
25
  export { createDevCommand } from './commands/dev.js';
26
26
  export { createIntrospectCommand } from './commands/introspect.js';
27
+ export { createMcpCommand } from './commands/mcp.js';
27
28
  export { createMigrateCommand } from './commands/migrate.js';
28
29
  /**
29
30
  * Export error system
package/dist/index.js CHANGED
@@ -28,6 +28,7 @@ export const CLI_VERSION = packageJson.version ?? '0.0.0-unknown';
28
28
  export { createDbCommand } from './commands/db.js';
29
29
  export { createDevCommand } from './commands/dev.js';
30
30
  export { createIntrospectCommand } from './commands/introspect.js';
31
+ export { createMcpCommand } from './commands/mcp.js';
31
32
  export { createMigrateCommand } from './commands/migrate.js';
32
33
  /**
33
34
  * Export error system
@@ -52,3 +52,15 @@ export interface ProjectType {
52
52
  * 2. app.config.ts or app.config.js (Vinxi configuration)
53
53
  */
54
54
  export declare function detectProjectType(cwd?: string): Promise<ProjectType>;
55
+ /**
56
+ * Read and parse a JSON file
57
+ */
58
+ export declare function readJsonFile<T = unknown>(filePath: string): Promise<T>;
59
+ /**
60
+ * Write a JSON file with pretty formatting
61
+ */
62
+ export declare function writeJsonFile(filePath: string, data: unknown): Promise<void>;
63
+ /**
64
+ * Create a directory recursively
65
+ */
66
+ export declare function createDirectory(dirPath: string): void;
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Path utilities for finding project files
3
3
  */
4
- import { existsSync } from 'node:fs';
4
+ import { existsSync, mkdirSync } from 'node:fs';
5
+ import { readFile, writeFile } from 'node:fs/promises';
5
6
  import path from 'node:path';
6
7
  /**
7
8
  * Find the project entry point
@@ -137,3 +138,23 @@ export async function detectProjectType(cwd = process.cwd()) {
137
138
  return result;
138
139
  }
139
140
  }
141
+ /**
142
+ * Read and parse a JSON file
143
+ */
144
+ export async function readJsonFile(filePath) {
145
+ const content = await readFile(filePath, 'utf-8');
146
+ return JSON.parse(content);
147
+ }
148
+ /**
149
+ * Write a JSON file with pretty formatting
150
+ */
151
+ export async function writeJsonFile(filePath, data) {
152
+ const content = JSON.stringify(data, null, 2);
153
+ await writeFile(filePath, content, 'utf-8');
154
+ }
155
+ /**
156
+ * Create a directory recursively
157
+ */
158
+ export function createDirectory(dirPath) {
159
+ mkdirSync(dirPath, { recursive: true });
160
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veloxts/cli",
3
- "version": "0.6.52",
3
+ "version": "0.6.55",
4
4
  "description": "Developer tooling and CLI commands for VeloxTS framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -40,11 +40,11 @@
40
40
  "picocolors": "1.1.1",
41
41
  "pluralize": "8.0.0",
42
42
  "tsx": "4.21.0",
43
- "@veloxts/core": "0.6.52",
44
- "@veloxts/auth": "0.6.52",
45
- "@veloxts/orm": "0.6.52",
46
- "@veloxts/router": "0.6.52",
47
- "@veloxts/validation": "0.6.52"
43
+ "@veloxts/auth": "0.6.55",
44
+ "@veloxts/orm": "0.6.55",
45
+ "@veloxts/core": "0.6.55",
46
+ "@veloxts/validation": "0.6.55",
47
+ "@veloxts/router": "0.6.55"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@prisma/client": ">=7.0.0"