@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 +36 -0
- package/GUIDE.md +18 -0
- package/dist/cli.js +2 -0
- package/dist/commands/mcp.d.ts +11 -0
- package/dist/commands/mcp.js +344 -0
- package/dist/generators/generators/resource.js +36 -2
- package/dist/generators/utils/filesystem.d.ts +44 -0
- package/dist/generators/utils/filesystem.js +122 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils/paths.d.ts +12 -0
- package/dist/utils/paths.js +22 -1
- package/package.json +6 -6
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
|
-
|
|
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
|
package/dist/utils/paths.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/paths.js
CHANGED
|
@@ -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.
|
|
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/
|
|
44
|
-
"@veloxts/
|
|
45
|
-
"@veloxts/
|
|
46
|
-
"@veloxts/
|
|
47
|
-
"@veloxts/
|
|
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"
|