servcraft 0.2.0 → 0.3.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/.github/workflows/ci.yml +9 -4
- package/README.md +44 -2
- package/ROADMAP.md +72 -34
- package/dist/cli/index.cjs +901 -289
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +984 -387
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/completion.ts +146 -0
- package/src/cli/commands/doctor.ts +116 -1
- package/src/cli/commands/generate.ts +30 -0
- package/src/cli/commands/list.ts +1 -1
- package/src/cli/commands/update.ts +221 -0
- package/src/cli/index.ts +8 -0
- package/src/cli/templates/controller-test.ts +110 -0
- package/src/cli/templates/integration-test.ts +139 -0
- package/src/cli/templates/service-test.ts +100 -0
- package/tests/cli/add.test.ts +32 -0
- package/tests/cli/completion.test.ts +35 -0
- package/tests/cli/doctor.test.ts +23 -0
- package/tests/cli/dry-run.test.ts +39 -0
- package/tests/cli/errors.test.ts +29 -0
- package/tests/cli/generate.test.ts +39 -0
- package/tests/cli/init.test.ts +63 -0
- package/tests/cli/list.test.ts +25 -0
- package/tests/cli/remove.test.ts +28 -0
- package/tests/cli/update.test.ts +34 -0
package/package.json
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/* eslint-disable no-useless-escape */
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
|
|
5
|
+
const bashScript = `
|
|
6
|
+
# servcraft bash completion script
|
|
7
|
+
_servcraft_completions() {
|
|
8
|
+
local cur prev words cword
|
|
9
|
+
_init_completion || return
|
|
10
|
+
|
|
11
|
+
# Main commands
|
|
12
|
+
local commands="init add generate list remove doctor update completion docs --version --help"
|
|
13
|
+
|
|
14
|
+
# Generate subcommands
|
|
15
|
+
local generate_subcommands="module controller service repository types schema routes m c s r t"
|
|
16
|
+
|
|
17
|
+
case "\${words[1]}" in
|
|
18
|
+
generate|g)
|
|
19
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
20
|
+
COMPREPLY=( \$(compgen -W "\${generate_subcommands}" -- "\${cur}") )
|
|
21
|
+
fi
|
|
22
|
+
;;
|
|
23
|
+
add|remove|rm|update)
|
|
24
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
25
|
+
# Get available modules
|
|
26
|
+
local modules="auth cache rate-limit notification payment oauth mfa queue websocket upload"
|
|
27
|
+
COMPREPLY=( \$(compgen -W "\${modules}" -- "\${cur}") )
|
|
28
|
+
fi
|
|
29
|
+
;;
|
|
30
|
+
completion)
|
|
31
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
32
|
+
COMPREPLY=( \$(compgen -W "bash zsh" -- "\${cur}") )
|
|
33
|
+
fi
|
|
34
|
+
;;
|
|
35
|
+
*)
|
|
36
|
+
if [[ \${cword} -eq 1 ]]; then
|
|
37
|
+
COMPREPLY=( \$(compgen -W "\${commands}" -- "\${cur}") )
|
|
38
|
+
fi
|
|
39
|
+
;;
|
|
40
|
+
esac
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
complete -F _servcraft_completions servcraft
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const zshScript = `
|
|
47
|
+
#compdef servcraft
|
|
48
|
+
|
|
49
|
+
_servcraft() {
|
|
50
|
+
local context state state_descr line
|
|
51
|
+
typeset -A opt_args
|
|
52
|
+
|
|
53
|
+
_arguments -C \\
|
|
54
|
+
'1: :_servcraft_commands' \\
|
|
55
|
+
'*::arg:->args'
|
|
56
|
+
|
|
57
|
+
case $state in
|
|
58
|
+
args)
|
|
59
|
+
case $line[1] in
|
|
60
|
+
generate|g)
|
|
61
|
+
_servcraft_generate
|
|
62
|
+
;;
|
|
63
|
+
add|remove|rm|update)
|
|
64
|
+
_servcraft_modules
|
|
65
|
+
;;
|
|
66
|
+
completion)
|
|
67
|
+
_arguments '1: :(bash zsh)'
|
|
68
|
+
;;
|
|
69
|
+
esac
|
|
70
|
+
;;
|
|
71
|
+
esac
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_servcraft_commands() {
|
|
75
|
+
local commands
|
|
76
|
+
commands=(
|
|
77
|
+
'init:Initialize a new ServCraft project'
|
|
78
|
+
'add:Add a pre-built module to your project'
|
|
79
|
+
'generate:Generate code files (controller, service, etc.)'
|
|
80
|
+
'list:List available and installed modules'
|
|
81
|
+
'remove:Remove an installed module'
|
|
82
|
+
'doctor:Diagnose project configuration'
|
|
83
|
+
'update:Update installed modules'
|
|
84
|
+
'completion:Generate shell completion scripts'
|
|
85
|
+
'docs:Open documentation'
|
|
86
|
+
'--version:Show version'
|
|
87
|
+
'--help:Show help'
|
|
88
|
+
)
|
|
89
|
+
_describe 'command' commands
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_servcraft_generate() {
|
|
93
|
+
local subcommands
|
|
94
|
+
subcommands=(
|
|
95
|
+
'module:Generate a complete module (controller + service + routes)'
|
|
96
|
+
'controller:Generate a controller'
|
|
97
|
+
'service:Generate a service'
|
|
98
|
+
'repository:Generate a repository'
|
|
99
|
+
'types:Generate TypeScript types'
|
|
100
|
+
'schema:Generate validation schema'
|
|
101
|
+
'routes:Generate routes file'
|
|
102
|
+
'm:Alias for module'
|
|
103
|
+
'c:Alias for controller'
|
|
104
|
+
's:Alias for service'
|
|
105
|
+
'r:Alias for repository'
|
|
106
|
+
't:Alias for types'
|
|
107
|
+
)
|
|
108
|
+
_describe 'subcommand' subcommands
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_servcraft_modules() {
|
|
112
|
+
local modules
|
|
113
|
+
modules=(
|
|
114
|
+
'auth:Authentication & Authorization'
|
|
115
|
+
'cache:Redis caching'
|
|
116
|
+
'rate-limit:Rate limiting'
|
|
117
|
+
'notification:Email/SMS notifications'
|
|
118
|
+
'payment:Payment integration'
|
|
119
|
+
'oauth:OAuth providers'
|
|
120
|
+
'mfa:Multi-factor authentication'
|
|
121
|
+
'queue:Background jobs'
|
|
122
|
+
'websocket:WebSocket support'
|
|
123
|
+
'upload:File upload handling'
|
|
124
|
+
)
|
|
125
|
+
_describe 'module' modules
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_servcraft "$@"
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
export const completionCommand = new Command('completion')
|
|
132
|
+
.description('Generate shell completion scripts')
|
|
133
|
+
.argument('<shell>', 'Shell type (bash or zsh)')
|
|
134
|
+
.action((shell: string) => {
|
|
135
|
+
const shellLower = shell.toLowerCase();
|
|
136
|
+
|
|
137
|
+
if (shellLower === 'bash') {
|
|
138
|
+
console.log(bashScript);
|
|
139
|
+
} else if (shellLower === 'zsh') {
|
|
140
|
+
console.log(zshScript);
|
|
141
|
+
} else {
|
|
142
|
+
console.error(`Unsupported shell: ${shell}`);
|
|
143
|
+
console.error('Supported shells: bash, zsh');
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
@@ -1,8 +1,123 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
1
2
|
import { Command } from 'commander';
|
|
2
3
|
import chalk from 'chalk';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
|
|
6
|
+
interface Check {
|
|
7
|
+
name: string;
|
|
8
|
+
status: 'pass' | 'warn' | 'fail';
|
|
9
|
+
message: string;
|
|
10
|
+
suggestion?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function checkNodeVersion(): Promise<Check> {
|
|
14
|
+
const version = process.version;
|
|
15
|
+
const major = parseInt(version.slice(1).split('.')[0] || '0', 10);
|
|
16
|
+
|
|
17
|
+
if (major >= 18) {
|
|
18
|
+
return { name: 'Node.js', status: 'pass', message: `${version} ✓` };
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
name: 'Node.js',
|
|
22
|
+
status: 'fail',
|
|
23
|
+
message: `${version} (< 18)`,
|
|
24
|
+
suggestion: 'Upgrade to Node.js 18+',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function checkPackageJson(): Promise<Check[]> {
|
|
29
|
+
const checks: Check[] = [];
|
|
30
|
+
try {
|
|
31
|
+
const content = await fs.readFile('package.json', 'utf-8');
|
|
32
|
+
const pkg = JSON.parse(content);
|
|
33
|
+
|
|
34
|
+
checks.push({ name: 'package.json', status: 'pass', message: 'Found' });
|
|
35
|
+
|
|
36
|
+
if (pkg.dependencies?.fastify) {
|
|
37
|
+
checks.push({ name: 'Fastify', status: 'pass', message: 'Installed' });
|
|
38
|
+
} else {
|
|
39
|
+
checks.push({
|
|
40
|
+
name: 'Fastify',
|
|
41
|
+
status: 'fail',
|
|
42
|
+
message: 'Missing',
|
|
43
|
+
suggestion: 'npm install fastify',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
checks.push({
|
|
48
|
+
name: 'package.json',
|
|
49
|
+
status: 'fail',
|
|
50
|
+
message: 'Not found',
|
|
51
|
+
suggestion: 'Run servcraft init',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return checks;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function checkDirectories(): Promise<Check[]> {
|
|
58
|
+
const checks: Check[] = [];
|
|
59
|
+
const dirs = ['src', 'node_modules', '.git', '.env'];
|
|
60
|
+
|
|
61
|
+
for (const dir of dirs) {
|
|
62
|
+
try {
|
|
63
|
+
await fs.access(dir);
|
|
64
|
+
checks.push({ name: dir, status: 'pass', message: 'Exists' });
|
|
65
|
+
} catch {
|
|
66
|
+
const isCritical = dir === 'src' || dir === 'node_modules';
|
|
67
|
+
checks.push({
|
|
68
|
+
name: dir,
|
|
69
|
+
status: isCritical ? 'fail' : 'warn',
|
|
70
|
+
message: 'Not found',
|
|
71
|
+
suggestion:
|
|
72
|
+
dir === 'node_modules' ? 'npm install' : dir === '.env' ? 'Create .env file' : undefined,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return checks;
|
|
77
|
+
}
|
|
3
78
|
|
|
4
79
|
export const doctorCommand = new Command('doctor')
|
|
5
80
|
.description('Diagnose project configuration and dependencies')
|
|
6
81
|
.action(async () => {
|
|
7
|
-
console.log(chalk.bold.cyan('\
|
|
82
|
+
console.log(chalk.bold.cyan('\n🔍 ServCraft Doctor\n'));
|
|
83
|
+
|
|
84
|
+
const allChecks: Check[] = [];
|
|
85
|
+
|
|
86
|
+
allChecks.push(await checkNodeVersion());
|
|
87
|
+
allChecks.push(...(await checkPackageJson()));
|
|
88
|
+
allChecks.push(...(await checkDirectories()));
|
|
89
|
+
|
|
90
|
+
// Display results
|
|
91
|
+
allChecks.forEach((check) => {
|
|
92
|
+
const icon =
|
|
93
|
+
check.status === 'pass'
|
|
94
|
+
? chalk.green('✓')
|
|
95
|
+
: check.status === 'warn'
|
|
96
|
+
? chalk.yellow('⚠')
|
|
97
|
+
: chalk.red('✗');
|
|
98
|
+
const color =
|
|
99
|
+
check.status === 'pass' ? chalk.green : check.status === 'warn' ? chalk.yellow : chalk.red;
|
|
100
|
+
|
|
101
|
+
console.log(`${icon} ${check.name.padEnd(20)} ${color(check.message)}`);
|
|
102
|
+
if (check.suggestion) {
|
|
103
|
+
console.log(chalk.gray(` → ${check.suggestion}`));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const pass = allChecks.filter((c) => c.status === 'pass').length;
|
|
108
|
+
const warn = allChecks.filter((c) => c.status === 'warn').length;
|
|
109
|
+
const fail = allChecks.filter((c) => c.status === 'fail').length;
|
|
110
|
+
|
|
111
|
+
console.log(chalk.gray('\n' + '─'.repeat(60)));
|
|
112
|
+
console.log(
|
|
113
|
+
`\n${chalk.green(pass + ' passed')} | ${chalk.yellow(warn + ' warnings')} | ${chalk.red(fail + ' failed')}\n`
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (fail === 0 && warn === 0) {
|
|
117
|
+
console.log(chalk.green.bold('✨ Everything looks good!\n'));
|
|
118
|
+
} else if (fail > 0) {
|
|
119
|
+
console.log(chalk.red.bold('✗ Fix critical issues before using ServCraft.\n'));
|
|
120
|
+
} else {
|
|
121
|
+
console.log(chalk.yellow.bold('⚠ Some warnings, but should work.\n'));
|
|
122
|
+
}
|
|
8
123
|
});
|
|
@@ -45,6 +45,9 @@ import { prismaModelTemplate } from '../templates/prisma-model.js';
|
|
|
45
45
|
import { dynamicTypesTemplate } from '../templates/dynamic-types.js';
|
|
46
46
|
import { dynamicSchemasTemplate, type ValidatorType } from '../templates/dynamic-schemas.js';
|
|
47
47
|
import { dynamicPrismaTemplate } from '../templates/dynamic-prisma.js';
|
|
48
|
+
import { controllerTestTemplate } from '../templates/controller-test.js';
|
|
49
|
+
import { serviceTestTemplate } from '../templates/service-test.js';
|
|
50
|
+
import { integrationTestTemplate } from '../templates/integration-test.js';
|
|
48
51
|
|
|
49
52
|
export const generateCommand = new Command('generate')
|
|
50
53
|
.alias('g')
|
|
@@ -62,6 +65,7 @@ generateCommand
|
|
|
62
65
|
.option('--prisma', 'Generate Prisma model suggestion')
|
|
63
66
|
.option('--validator <type>', 'Validator type: zod, joi, yup', 'zod')
|
|
64
67
|
.option('-i, --interactive', 'Interactive mode to define fields')
|
|
68
|
+
.option('--with-tests', 'Generate test files (__tests__ directory)')
|
|
65
69
|
.option('--dry-run', 'Preview changes without writing files')
|
|
66
70
|
.action(async (name: string, fieldsArgs: string[], options) => {
|
|
67
71
|
enableDryRunIfNeeded(options);
|
|
@@ -139,6 +143,26 @@ generateCommand
|
|
|
139
143
|
await writeFile(path.join(moduleDir, file.name), file.content);
|
|
140
144
|
}
|
|
141
145
|
|
|
146
|
+
// Generate test files if --with-tests flag is provided
|
|
147
|
+
if (options.withTests) {
|
|
148
|
+
const testDir = path.join(moduleDir, '__tests__');
|
|
149
|
+
|
|
150
|
+
await writeFile(
|
|
151
|
+
path.join(testDir, `${kebabName}.controller.test.ts`),
|
|
152
|
+
controllerTestTemplate(kebabName, pascalName, camelName)
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
await writeFile(
|
|
156
|
+
path.join(testDir, `${kebabName}.service.test.ts`),
|
|
157
|
+
serviceTestTemplate(kebabName, pascalName, camelName)
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
await writeFile(
|
|
161
|
+
path.join(testDir, `${kebabName}.integration.test.ts`),
|
|
162
|
+
integrationTestTemplate(kebabName, pascalName, camelName)
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
142
166
|
spinner.succeed(`Module "${pascalName}" generated successfully!`);
|
|
143
167
|
|
|
144
168
|
// Show Prisma model if requested or fields provided
|
|
@@ -169,6 +193,12 @@ generateCommand
|
|
|
169
193
|
console.log('\n📁 Files created:');
|
|
170
194
|
files.forEach((f) => success(` src/modules/${kebabName}/${f.name}`));
|
|
171
195
|
|
|
196
|
+
if (options.withTests) {
|
|
197
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.controller.test.ts`);
|
|
198
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.service.test.ts`);
|
|
199
|
+
success(` src/modules/${kebabName}/__tests__/${kebabName}.integration.test.ts`);
|
|
200
|
+
}
|
|
201
|
+
|
|
172
202
|
console.log('\n📌 Next steps:');
|
|
173
203
|
if (!hasFields) {
|
|
174
204
|
info(' 1. Update the types in ' + `${kebabName}.types.ts`);
|
package/src/cli/commands/list.ts
CHANGED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
import { getProjectRoot, getModulesDir } from '../utils/helpers.js';
|
|
9
|
+
import { validateProject, displayError } from '../utils/error-handler.js';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
// Get list of available modules (same as list.ts)
|
|
15
|
+
const AVAILABLE_MODULES = [
|
|
16
|
+
'auth',
|
|
17
|
+
'users',
|
|
18
|
+
'email',
|
|
19
|
+
'mfa',
|
|
20
|
+
'oauth',
|
|
21
|
+
'rate-limit',
|
|
22
|
+
'cache',
|
|
23
|
+
'upload',
|
|
24
|
+
'search',
|
|
25
|
+
'notification',
|
|
26
|
+
'webhook',
|
|
27
|
+
'websocket',
|
|
28
|
+
'queue',
|
|
29
|
+
'payment',
|
|
30
|
+
'i18n',
|
|
31
|
+
'feature-flag',
|
|
32
|
+
'analytics',
|
|
33
|
+
'media-processing',
|
|
34
|
+
'api-versioning',
|
|
35
|
+
'audit',
|
|
36
|
+
'swagger',
|
|
37
|
+
'validation',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
async function getInstalledModules(): Promise<string[]> {
|
|
41
|
+
try {
|
|
42
|
+
const modulesDir = getModulesDir();
|
|
43
|
+
|
|
44
|
+
const entries = await fs.readdir(modulesDir, { withFileTypes: true });
|
|
45
|
+
const installedModules = entries
|
|
46
|
+
.filter((entry) => entry.isDirectory())
|
|
47
|
+
.map((entry) => entry.name)
|
|
48
|
+
.filter((name) => AVAILABLE_MODULES.includes(name));
|
|
49
|
+
|
|
50
|
+
return installedModules;
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function copyModuleFiles(moduleName: string, _projectRoot: string): Promise<void> {
|
|
57
|
+
const cliRoot = path.resolve(__dirname, '../../../');
|
|
58
|
+
const sourceModulePath = path.join(cliRoot, 'src', 'modules', moduleName);
|
|
59
|
+
const targetModulesDir = getModulesDir();
|
|
60
|
+
const targetModulePath = path.join(targetModulesDir, moduleName);
|
|
61
|
+
|
|
62
|
+
// Check if source module exists
|
|
63
|
+
try {
|
|
64
|
+
await fs.access(sourceModulePath);
|
|
65
|
+
} catch {
|
|
66
|
+
throw new Error(`Module source not found: ${moduleName}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Copy module files
|
|
70
|
+
await fs.cp(sourceModulePath, targetModulePath, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function updateModule(moduleName: string, options: { check?: boolean }): Promise<void> {
|
|
74
|
+
const projectError = validateProject();
|
|
75
|
+
if (projectError) {
|
|
76
|
+
displayError(projectError);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const projectRoot = getProjectRoot();
|
|
81
|
+
const installedModules = await getInstalledModules();
|
|
82
|
+
|
|
83
|
+
if (!installedModules.includes(moduleName)) {
|
|
84
|
+
console.log(chalk.yellow(`\n⚠ Module "${moduleName}" is not installed\n`));
|
|
85
|
+
console.log(
|
|
86
|
+
chalk.gray(`Run ${chalk.cyan(`servcraft add ${moduleName}`)} to install it first.\n`)
|
|
87
|
+
);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (options.check) {
|
|
92
|
+
console.log(chalk.cyan(`\n📦 Checking updates for "${moduleName}"...\n`));
|
|
93
|
+
console.log(chalk.gray('Note: Version tracking will be implemented in a future release.'));
|
|
94
|
+
console.log(chalk.gray('Currently, update will always reinstall the latest version.\n'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Confirm update
|
|
99
|
+
const { confirmed } = await inquirer.prompt([
|
|
100
|
+
{
|
|
101
|
+
type: 'confirm',
|
|
102
|
+
name: 'confirmed',
|
|
103
|
+
message: `Update "${moduleName}" module? This will overwrite existing files.`,
|
|
104
|
+
default: false,
|
|
105
|
+
},
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
if (!confirmed) {
|
|
109
|
+
console.log(chalk.yellow('\n⚠ Update cancelled\n'));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(chalk.cyan(`\n🔄 Updating "${moduleName}" module...\n`));
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await copyModuleFiles(moduleName, projectRoot);
|
|
117
|
+
console.log(chalk.green(`✔ Module "${moduleName}" updated successfully!\n`));
|
|
118
|
+
console.log(
|
|
119
|
+
chalk.gray('Note: Remember to review any breaking changes in the documentation.\n')
|
|
120
|
+
);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error instanceof Error) {
|
|
123
|
+
console.error(chalk.red(`\n✗ Failed to update module: ${error.message}\n`));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function updateAllModules(options: { check?: boolean }): Promise<void> {
|
|
129
|
+
const projectError = validateProject();
|
|
130
|
+
if (projectError) {
|
|
131
|
+
displayError(projectError);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const installedModules = await getInstalledModules();
|
|
136
|
+
|
|
137
|
+
if (installedModules.length === 0) {
|
|
138
|
+
console.log(chalk.yellow('\n⚠ No modules installed\n'));
|
|
139
|
+
console.log(chalk.gray(`Run ${chalk.cyan('servcraft list')} to see available modules.\n`));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (options.check) {
|
|
144
|
+
console.log(chalk.cyan('\n📦 Checking updates for all modules...\n'));
|
|
145
|
+
console.log(chalk.bold('Installed modules:'));
|
|
146
|
+
installedModules.forEach((mod) => {
|
|
147
|
+
console.log(` • ${chalk.cyan(mod)}`);
|
|
148
|
+
});
|
|
149
|
+
console.log();
|
|
150
|
+
console.log(chalk.gray('Note: Version tracking will be implemented in a future release.'));
|
|
151
|
+
console.log(chalk.gray('Currently, update will always reinstall the latest version.\n'));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(chalk.cyan(`\n📦 Found ${installedModules.length} installed module(s):\n`));
|
|
156
|
+
installedModules.forEach((mod) => {
|
|
157
|
+
console.log(` • ${chalk.cyan(mod)}`);
|
|
158
|
+
});
|
|
159
|
+
console.log();
|
|
160
|
+
|
|
161
|
+
// Confirm update all
|
|
162
|
+
const { confirmed } = await inquirer.prompt([
|
|
163
|
+
{
|
|
164
|
+
type: 'confirm',
|
|
165
|
+
name: 'confirmed',
|
|
166
|
+
message: 'Update all modules? This will overwrite existing files.',
|
|
167
|
+
default: false,
|
|
168
|
+
},
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
if (!confirmed) {
|
|
172
|
+
console.log(chalk.yellow('\n⚠ Update cancelled\n'));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(chalk.cyan('\n🔄 Updating all modules...\n'));
|
|
177
|
+
|
|
178
|
+
const projectRoot = getProjectRoot();
|
|
179
|
+
let successCount = 0;
|
|
180
|
+
let failCount = 0;
|
|
181
|
+
|
|
182
|
+
for (const moduleName of installedModules) {
|
|
183
|
+
try {
|
|
184
|
+
await copyModuleFiles(moduleName, projectRoot);
|
|
185
|
+
console.log(chalk.green(`✔ Updated: ${moduleName}`));
|
|
186
|
+
successCount++;
|
|
187
|
+
} catch {
|
|
188
|
+
console.error(chalk.red(`✗ Failed: ${moduleName}`));
|
|
189
|
+
failCount++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log();
|
|
194
|
+
console.log(
|
|
195
|
+
chalk.bold(
|
|
196
|
+
`\n✔ Update complete: ${chalk.green(successCount)} succeeded, ${chalk.red(failCount)} failed\n`
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
if (successCount > 0) {
|
|
201
|
+
console.log(
|
|
202
|
+
chalk.gray('Note: Remember to review any breaking changes in the documentation.\n')
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const updateCommand = new Command('update')
|
|
208
|
+
.description('Update installed modules to latest version')
|
|
209
|
+
.argument('[module]', 'Specific module to update')
|
|
210
|
+
.option('--check', 'Check for updates without applying')
|
|
211
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
212
|
+
.action(async (moduleName?: string, options?: { check?: boolean; yes?: boolean }) => {
|
|
213
|
+
// If --yes flag is provided, we'll handle it by auto-confirming in the inquirer prompts
|
|
214
|
+
// For now, we'll just pass through to the update functions
|
|
215
|
+
|
|
216
|
+
if (moduleName) {
|
|
217
|
+
await updateModule(moduleName, { check: options?.check });
|
|
218
|
+
} else {
|
|
219
|
+
await updateAllModules({ check: options?.check });
|
|
220
|
+
}
|
|
221
|
+
});
|
package/src/cli/index.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { docsCommand } from './commands/docs.js';
|
|
|
9
9
|
import { listCommand } from './commands/list.js';
|
|
10
10
|
import { removeCommand } from './commands/remove.js';
|
|
11
11
|
import { doctorCommand } from './commands/doctor.js';
|
|
12
|
+
import { updateCommand } from './commands/update.js';
|
|
13
|
+
import { completionCommand } from './commands/completion.js';
|
|
12
14
|
|
|
13
15
|
const program = new Command();
|
|
14
16
|
|
|
@@ -41,4 +43,10 @@ program.addCommand(removeCommand);
|
|
|
41
43
|
// Diagnose project
|
|
42
44
|
program.addCommand(doctorCommand);
|
|
43
45
|
|
|
46
|
+
// Update modules
|
|
47
|
+
program.addCommand(updateCommand);
|
|
48
|
+
|
|
49
|
+
// Shell completion
|
|
50
|
+
program.addCommand(completionCommand);
|
|
51
|
+
|
|
44
52
|
program.parse();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export function controllerTestTemplate(
|
|
2
|
+
name: string,
|
|
3
|
+
pascalName: string,
|
|
4
|
+
camelName: string
|
|
5
|
+
): string {
|
|
6
|
+
return `import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
7
|
+
import { build } from '../../app.js';
|
|
8
|
+
import { FastifyInstance } from 'fastify';
|
|
9
|
+
|
|
10
|
+
describe('${pascalName}Controller', () => {
|
|
11
|
+
let app: FastifyInstance;
|
|
12
|
+
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
app = await build();
|
|
15
|
+
await app.ready();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterAll(async () => {
|
|
19
|
+
await app.close();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('GET /${name}', () => {
|
|
23
|
+
it('should return list of ${name}', async () => {
|
|
24
|
+
const response = await app.inject({
|
|
25
|
+
method: 'GET',
|
|
26
|
+
url: '/${name}',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(response.statusCode).toBe(200);
|
|
30
|
+
expect(response.json()).toHaveProperty('data');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('GET /${name}/:id', () => {
|
|
35
|
+
it('should return a single ${camelName}', async () => {
|
|
36
|
+
// TODO: Create test ${camelName} first
|
|
37
|
+
const response = await app.inject({
|
|
38
|
+
method: 'GET',
|
|
39
|
+
url: '/${name}/1',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(response.statusCode).toBe(200);
|
|
43
|
+
expect(response.json()).toHaveProperty('data');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should return 404 for non-existent ${camelName}', async () => {
|
|
47
|
+
const response = await app.inject({
|
|
48
|
+
method: 'GET',
|
|
49
|
+
url: '/${name}/999999',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(response.statusCode).toBe(404);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('POST /${name}', () => {
|
|
57
|
+
it('should create a new ${camelName}', async () => {
|
|
58
|
+
const response = await app.inject({
|
|
59
|
+
method: 'POST',
|
|
60
|
+
url: '/${name}',
|
|
61
|
+
payload: {
|
|
62
|
+
// TODO: Add required fields
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(response.statusCode).toBe(201);
|
|
67
|
+
expect(response.json()).toHaveProperty('data');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should return 400 for invalid data', async () => {
|
|
71
|
+
const response = await app.inject({
|
|
72
|
+
method: 'POST',
|
|
73
|
+
url: '/${name}',
|
|
74
|
+
payload: {},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(response.statusCode).toBe(400);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('PUT /${name}/:id', () => {
|
|
82
|
+
it('should update a ${camelName}', async () => {
|
|
83
|
+
// TODO: Create test ${camelName} first
|
|
84
|
+
const response = await app.inject({
|
|
85
|
+
method: 'PUT',
|
|
86
|
+
url: '/${name}/1',
|
|
87
|
+
payload: {
|
|
88
|
+
// TODO: Add fields to update
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(response.statusCode).toBe(200);
|
|
93
|
+
expect(response.json()).toHaveProperty('data');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('DELETE /${name}/:id', () => {
|
|
98
|
+
it('should delete a ${camelName}', async () => {
|
|
99
|
+
// TODO: Create test ${camelName} first
|
|
100
|
+
const response = await app.inject({
|
|
101
|
+
method: 'DELETE',
|
|
102
|
+
url: '/${name}/1',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(response.statusCode).toBe(204);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
`;
|
|
110
|
+
}
|