katax-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
Binary file
@@ -0,0 +1,6 @@
1
+ interface AddEndpointOptions {
2
+ method?: string;
3
+ path?: string;
4
+ }
5
+ export declare function addEndpointCommand(name: string, options?: AddEndpointOptions): Promise<void>;
6
+ export {};
@@ -0,0 +1,229 @@
1
+ import inquirer from 'inquirer';
2
+ import ora from 'ora';
3
+ import path from 'path';
4
+ import { success, error, warning, gray, title, info } from '../utils/logger.js';
5
+ import { fileExists, writeFile } from '../utils/file-utils.js';
6
+ import { generateValidator } from '../generators/validator-generator.js';
7
+ import { generateController } from '../generators/controller-generator.js';
8
+ import { generateHandler } from '../generators/handler-generator.js';
9
+ import { generateRoute } from '../generators/route-generator.js';
10
+ import { updateMainRouter } from '../generators/router-updater.js';
11
+ export async function addEndpointCommand(name, options = {}) {
12
+ title(`🎯 Add Endpoint: ${name}`);
13
+ // Check if we're in a Katax project
14
+ if (!fileExists(path.join(process.cwd(), 'package.json'))) {
15
+ error('Not in a project directory!');
16
+ gray('Run this command from your project root\n');
17
+ process.exit(1);
18
+ }
19
+ // Check if katax-core is installed
20
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
21
+ const packageJson = JSON.parse(await import('fs').then(fs => fs.promises.readFile(packageJsonPath, 'utf-8')));
22
+ const hasKataxCore = packageJson.dependencies?.['katax-core'];
23
+ if (!hasKataxCore) {
24
+ warning('katax-core is not installed in this project');
25
+ const { install } = await inquirer.prompt([
26
+ {
27
+ type: 'confirm',
28
+ name: 'install',
29
+ message: 'Would you like to install katax-core?',
30
+ default: true
31
+ }
32
+ ]);
33
+ if (!install) {
34
+ error('Cannot create endpoint without validation library');
35
+ process.exit(1);
36
+ }
37
+ // Install katax-core
38
+ const spinner = ora('Installing katax-core...').start();
39
+ const { execa } = await import('execa');
40
+ await execa('npm', ['install', 'katax-core'], { cwd: process.cwd() });
41
+ spinner.succeed('katax-core installed');
42
+ }
43
+ // Interactive prompts
44
+ const answers = await inquirer.prompt([
45
+ {
46
+ type: 'list',
47
+ name: 'method',
48
+ message: 'HTTP Method:',
49
+ choices: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
50
+ default: options.method?.toUpperCase() || 'POST',
51
+ when: !options.method
52
+ },
53
+ {
54
+ type: 'input',
55
+ name: 'path',
56
+ message: 'Route path:',
57
+ default: `/api/${name.toLowerCase()}`,
58
+ when: !options.path,
59
+ validate: (input) => {
60
+ if (!input.startsWith('/')) {
61
+ return 'Path must start with /';
62
+ }
63
+ return true;
64
+ }
65
+ },
66
+ {
67
+ type: 'confirm',
68
+ name: 'addValidation',
69
+ message: 'Add validation?',
70
+ default: true
71
+ }
72
+ ]);
73
+ const config = {
74
+ name,
75
+ method: (options.method?.toUpperCase() || answers.method),
76
+ path: options.path || answers.path,
77
+ addValidation: answers.addValidation,
78
+ fields: [],
79
+ addAsyncValidators: false,
80
+ dbOperations: []
81
+ };
82
+ // If validation is enabled, collect fields
83
+ if (config.addValidation && config.method !== 'GET' && config.method !== 'DELETE') {
84
+ info('\nDefine request body fields:');
85
+ let addingFields = true;
86
+ while (addingFields) {
87
+ const fieldAnswers = await inquirer.prompt([
88
+ {
89
+ type: 'input',
90
+ name: 'fieldName',
91
+ message: 'Field name (press Enter to finish):',
92
+ validate: (input) => {
93
+ if (input && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(input)) {
94
+ return 'Field name must be a valid identifier';
95
+ }
96
+ return true;
97
+ }
98
+ }
99
+ ]);
100
+ if (!fieldAnswers.fieldName) {
101
+ addingFields = false;
102
+ break;
103
+ }
104
+ const fieldConfig = await inquirer.prompt([
105
+ {
106
+ type: 'list',
107
+ name: 'type',
108
+ message: `Type for "${fieldAnswers.fieldName}":`,
109
+ choices: ['string', 'number', 'boolean', 'date', 'email', 'array', 'object']
110
+ },
111
+ {
112
+ type: 'confirm',
113
+ name: 'required',
114
+ message: 'Required?',
115
+ default: true
116
+ }
117
+ ]);
118
+ const field = {
119
+ name: fieldAnswers.fieldName,
120
+ type: fieldConfig.type,
121
+ required: fieldConfig.required,
122
+ rules: []
123
+ };
124
+ // Add type-specific rules
125
+ if (fieldConfig.type === 'string' || fieldConfig.type === 'email') {
126
+ const stringRules = await inquirer.prompt([
127
+ {
128
+ type: 'input',
129
+ name: 'minLength',
130
+ message: 'Minimum length (press Enter to skip):',
131
+ validate: (input) => !input || !isNaN(parseInt(input)) || 'Must be a number'
132
+ },
133
+ {
134
+ type: 'input',
135
+ name: 'maxLength',
136
+ message: 'Maximum length (press Enter to skip):',
137
+ validate: (input) => !input || !isNaN(parseInt(input)) || 'Must be a number'
138
+ }
139
+ ]);
140
+ if (stringRules.minLength) {
141
+ field.rules.push({ type: 'minLength', value: parseInt(stringRules.minLength) });
142
+ }
143
+ if (stringRules.maxLength) {
144
+ field.rules.push({ type: 'maxLength', value: parseInt(stringRules.maxLength) });
145
+ }
146
+ }
147
+ if (fieldConfig.type === 'number') {
148
+ const numberRules = await inquirer.prompt([
149
+ {
150
+ type: 'input',
151
+ name: 'min',
152
+ message: 'Minimum value (press Enter to skip):',
153
+ validate: (input) => !input || !isNaN(parseFloat(input)) || 'Must be a number'
154
+ },
155
+ {
156
+ type: 'input',
157
+ name: 'max',
158
+ message: 'Maximum value (press Enter to skip):',
159
+ validate: (input) => !input || !isNaN(parseFloat(input)) || 'Must be a number'
160
+ }
161
+ ]);
162
+ if (numberRules.min) {
163
+ field.rules.push({ type: 'min', value: parseFloat(numberRules.min) });
164
+ }
165
+ if (numberRules.max) {
166
+ field.rules.push({ type: 'max', value: parseFloat(numberRules.max) });
167
+ }
168
+ }
169
+ config.fields.push(field);
170
+ gray(` ✓ Added field: ${field.name} (${field.type})`);
171
+ }
172
+ if (config.fields.length > 0) {
173
+ const { addAsync } = await inquirer.prompt([
174
+ {
175
+ type: 'confirm',
176
+ name: 'addAsync',
177
+ message: 'Add async validators (e.g., unique email)?',
178
+ default: false
179
+ }
180
+ ]);
181
+ config.addAsyncValidators = addAsync;
182
+ }
183
+ }
184
+ // Generate files
185
+ const spinner = ora('Generating endpoint files...').start();
186
+ try {
187
+ const basePath = path.join(process.cwd(), 'src', 'api', name.toLowerCase());
188
+ // Generate validator
189
+ if (config.addValidation) {
190
+ const validatorPath = path.join(basePath, `${name.toLowerCase()}.validator.ts`);
191
+ const validatorContent = generateValidator(config);
192
+ await writeFile(validatorPath, validatorContent);
193
+ spinner.text = `Generated ${name.toLowerCase()}.validator.ts`;
194
+ }
195
+ // Generate controller
196
+ const controllerPath = path.join(basePath, `${name.toLowerCase()}.controller.ts`);
197
+ const controllerContent = generateController(config);
198
+ await writeFile(controllerPath, controllerContent);
199
+ spinner.text = `Generated ${name.toLowerCase()}.controller.ts`;
200
+ // Generate handler
201
+ const handlerPath = path.join(basePath, `${name.toLowerCase()}.handler.ts`);
202
+ const handlerContent = generateHandler(config);
203
+ await writeFile(handlerPath, handlerContent);
204
+ spinner.text = `Generated ${name.toLowerCase()}.handler.ts`;
205
+ // Generate route
206
+ const routePath = path.join(basePath, `${name.toLowerCase()}.routes.ts`);
207
+ const routeContent = generateRoute(config);
208
+ await writeFile(routePath, routeContent);
209
+ spinner.text = `Generated ${name.toLowerCase()}.routes.ts`;
210
+ // Update main router
211
+ await updateMainRouter(name, config);
212
+ spinner.succeed('Endpoint files generated');
213
+ success(`\n✨ Endpoint "${name}" created successfully!\n`);
214
+ info('Generated files:');
215
+ gray(` src/api/${name.toLowerCase()}/${name.toLowerCase()}.validator.ts`);
216
+ gray(` src/api/${name.toLowerCase()}/${name.toLowerCase()}.controller.ts`);
217
+ gray(` src/api/${name.toLowerCase()}/${name.toLowerCase()}.handler.ts`);
218
+ gray(` src/api/${name.toLowerCase()}/${name.toLowerCase()}.routes.ts`);
219
+ gray(` Updated: src/api/routes.ts\n`);
220
+ gray(` Updated: src/api/routes.ts\n`);
221
+ info('Test your endpoint:');
222
+ gray(` ${config.method} ${config.path}\n`);
223
+ }
224
+ catch (err) {
225
+ spinner.fail('Failed to generate endpoint');
226
+ error(err instanceof Error ? err.message : 'Unknown error');
227
+ process.exit(1);
228
+ }
229
+ }
@@ -0,0 +1,5 @@
1
+ interface GenerateCrudOptions {
2
+ auth?: boolean;
3
+ }
4
+ export declare function generateCrudCommand(resourceName: string, options?: GenerateCrudOptions): Promise<void>;
5
+ export {};
@@ -0,0 +1,282 @@
1
+ import inquirer from 'inquirer';
2
+ import ora from 'ora';
3
+ import path from 'path';
4
+ import { success, error, warning, gray, title, info } from '../utils/logger.js';
5
+ import { fileExists, writeFile, toPascalCase, toCamelCase } from '../utils/file-utils.js';
6
+ export async function generateCrudCommand(resourceName, options = {}) {
7
+ title(`🔧 Generate CRUD: ${resourceName}`);
8
+ // Check if we're in a project
9
+ if (!fileExists(path.join(process.cwd(), 'package.json'))) {
10
+ error('Not in a project directory!');
11
+ gray('Run this command from your project root\n');
12
+ process.exit(1);
13
+ }
14
+ // Interactive prompts
15
+ const answers = await inquirer.prompt([
16
+ {
17
+ type: 'input',
18
+ name: 'tableName',
19
+ message: 'Database table name:',
20
+ default: `${resourceName.toLowerCase()}s`
21
+ },
22
+ {
23
+ type: 'confirm',
24
+ name: 'addAuth',
25
+ message: 'Require authentication for endpoints?',
26
+ default: options.auth !== false
27
+ }
28
+ ]);
29
+ info('\nDefine fields for the resource:');
30
+ const fields = [];
31
+ let addingFields = true;
32
+ while (addingFields) {
33
+ const fieldAnswers = await inquirer.prompt([
34
+ {
35
+ type: 'input',
36
+ name: 'fieldName',
37
+ message: 'Field name (press Enter to finish):',
38
+ validate: (input) => {
39
+ if (input && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(input)) {
40
+ return 'Field name must be a valid identifier';
41
+ }
42
+ return true;
43
+ }
44
+ }
45
+ ]);
46
+ if (!fieldAnswers.fieldName) {
47
+ addingFields = false;
48
+ break;
49
+ }
50
+ const fieldConfig = await inquirer.prompt([
51
+ {
52
+ type: 'list',
53
+ name: 'type',
54
+ message: `Type for "${fieldAnswers.fieldName}":`,
55
+ choices: ['string', 'number', 'boolean', 'date', 'email']
56
+ },
57
+ {
58
+ type: 'confirm',
59
+ name: 'required',
60
+ message: 'Required?',
61
+ default: true
62
+ }
63
+ ]);
64
+ fields.push({
65
+ name: fieldAnswers.fieldName,
66
+ type: fieldConfig.type,
67
+ required: fieldConfig.required,
68
+ rules: []
69
+ });
70
+ gray(` ✓ Added field: ${fieldAnswers.fieldName} (${fieldConfig.type})`);
71
+ }
72
+ if (fields.length === 0) {
73
+ error('At least one field is required to generate CRUD');
74
+ process.exit(1);
75
+ }
76
+ const config = {
77
+ resourceName,
78
+ tableName: answers.tableName,
79
+ fields,
80
+ addAuth: answers.addAuth
81
+ };
82
+ const spinner = ora('Generating CRUD endpoints...').start();
83
+ try {
84
+ await generateCRUDFiles(config);
85
+ spinner.succeed('CRUD endpoints generated');
86
+ success(`\n✨ CRUD for "${resourceName}" created successfully!\n`);
87
+ info('Generated endpoints:');
88
+ gray(` GET /api/${resourceName.toLowerCase()} - List all`);
89
+ gray(` GET /api/${resourceName.toLowerCase()}/:id - Get one`);
90
+ gray(` POST /api/${resourceName.toLowerCase()} - Create`);
91
+ gray(` PUT /api/${resourceName.toLowerCase()}/:id - Update`);
92
+ gray(` DELETE /api/${resourceName.toLowerCase()}/:id - Delete\n`);
93
+ if (config.addAuth) {
94
+ warning('Remember to implement authentication middleware!');
95
+ }
96
+ }
97
+ catch (err) {
98
+ spinner.fail('Failed to generate CRUD');
99
+ error(err instanceof Error ? err.message : 'Unknown error');
100
+ process.exit(1);
101
+ }
102
+ }
103
+ async function generateCRUDFiles(config) {
104
+ const { resourceName, fields } = config;
105
+ const basePath = path.join(process.cwd(), 'src', 'api', resourceName.toLowerCase());
106
+ // Generate validator with all operations
107
+ const validatorContent = generateCRUDValidator(config);
108
+ await writeFile(path.join(basePath, `${resourceName.toLowerCase()}.validator.ts`), validatorContent);
109
+ // Generate controller with CRUD operations
110
+ const controllerContent = generateCRUDController(config);
111
+ await writeFile(path.join(basePath, `${resourceName.toLowerCase()}.controller.ts`), controllerContent);
112
+ // Generate routes with all CRUD endpoints
113
+ const routesContent = generateCRUDRoutes(config);
114
+ await writeFile(path.join(basePath, `${resourceName.toLowerCase()}.routes.ts`), routesContent);
115
+ // Update main router
116
+ const { updateMainRouter } = await import('../generators/router-updater.js');
117
+ await updateMainRouter(resourceName, {
118
+ name: resourceName,
119
+ method: 'GET',
120
+ path: `/api/${resourceName.toLowerCase()}`,
121
+ addValidation: true,
122
+ fields,
123
+ addAsyncValidators: false
124
+ });
125
+ }
126
+ function generateCRUDValidator(config) {
127
+ const { resourceName, fields } = config;
128
+ const pascalName = toPascalCase(resourceName);
129
+ const camelName = toCamelCase(resourceName);
130
+ let content = `import { k, kataxInfer } from 'katax-core';\n\n`;
131
+ content += `// ==================== SCHEMAS ====================\n\n`;
132
+ // Create schema
133
+ content += `export const create${pascalName}Schema = k.object({\n`;
134
+ fields.forEach((field, index) => {
135
+ const isLast = index === fields.length - 1;
136
+ let fieldSchema = field.type === 'email' ? 'k.string().email()' : `k.${field.type}()`;
137
+ if (!field.required)
138
+ fieldSchema += '.optional()';
139
+ content += ` ${field.name}: ${fieldSchema}${isLast ? '' : ','}\n`;
140
+ });
141
+ content += `});\n\n`;
142
+ // Update schema (all fields optional)
143
+ content += `export const update${pascalName}Schema = k.object({\n`;
144
+ fields.forEach((field, index) => {
145
+ const isLast = index === fields.length - 1;
146
+ const fieldSchema = field.type === 'email' ? 'k.string().email().optional()' : `k.${field.type}().optional()`;
147
+ content += ` ${field.name}: ${fieldSchema}${isLast ? '' : ','}\n`;
148
+ });
149
+ content += `});\n\n`;
150
+ content += `export type Create${pascalName}Data = kataxInfer<typeof create${pascalName}Schema>;\n`;
151
+ content += `export type Update${pascalName}Data = kataxInfer<typeof update${pascalName}Schema>;\n`;
152
+ return content;
153
+ }
154
+ function generateCRUDController(config) {
155
+ const { resourceName, tableName } = config;
156
+ const pascalName = toPascalCase(resourceName);
157
+ const camelName = toCamelCase(resourceName);
158
+ return `import { Create${pascalName}Data, Update${pascalName}Data } from './${resourceName.toLowerCase()}.validator.js';
159
+ import { ControllerResult, createSuccessResult, createErrorResult } from '../../shared/api.utils.js';
160
+
161
+ // List all ${camelName}s
162
+ export async function list${pascalName}s(): Promise<ControllerResult<any[]>> {
163
+ try {
164
+ // TODO: Implement database query
165
+ // const result = await pool.query('SELECT * FROM ${tableName}');
166
+
167
+ const mock${pascalName}s = [
168
+ { id: 1, name: 'Sample ${pascalName} 1', createdAt: new Date().toISOString() },
169
+ { id: 2, name: 'Sample ${pascalName} 2', createdAt: new Date().toISOString() }
170
+ ];
171
+
172
+ return createSuccessResult('${pascalName}s retrieved', mock${pascalName}s);
173
+ } catch (error) {
174
+ return createErrorResult('Failed to list ${camelName}s', error instanceof Error ? error.message : 'Unknown error', 500);
175
+ }
176
+ }
177
+
178
+ // Get single ${camelName}
179
+ export async function get${pascalName}(id: string): Promise<ControllerResult<any>> {
180
+ try {
181
+ // TODO: Implement database query
182
+ // const result = await pool.query('SELECT * FROM ${tableName} WHERE id = $1', [id]);
183
+
184
+ const mock${pascalName} = { id: parseInt(id), name: 'Sample ${pascalName}', createdAt: new Date().toISOString() };
185
+ return createSuccessResult('${pascalName} retrieved', mock${pascalName});
186
+ } catch (error) {
187
+ return createErrorResult('Failed to get ${camelName}', error instanceof Error ? error.message : 'Unknown error', 500);
188
+ }
189
+ }
190
+
191
+ // Create ${camelName}
192
+ export async function create${pascalName}(data: Create${pascalName}Data): Promise<ControllerResult<any>> {
193
+ try {
194
+ // TODO: Implement database insertion
195
+ const new${pascalName} = { id: Math.floor(Math.random() * 1000), ...data, createdAt: new Date().toISOString() };
196
+ return createSuccessResult('${pascalName} created', new${pascalName}, undefined, 201);
197
+ } catch (error) {
198
+ return createErrorResult('Failed to create ${camelName}', error instanceof Error ? error.message : 'Unknown error', 500);
199
+ }
200
+ }
201
+
202
+ // Update ${camelName}
203
+ export async function update${pascalName}(id: string, data: Update${pascalName}Data): Promise<ControllerResult<any>> {
204
+ try {
205
+ // TODO: Implement database update
206
+ const updated${pascalName} = { id: parseInt(id), ...data, updatedAt: new Date().toISOString() };
207
+ return createSuccessResult('${pascalName} updated', updated${pascalName});
208
+ } catch (error) {
209
+ return createErrorResult('Failed to update ${camelName}', error instanceof Error ? error.message : 'Unknown error', 500);
210
+ }
211
+ }
212
+
213
+ // Delete ${camelName}
214
+ export async function delete${pascalName}(id: string): Promise<ControllerResult<void>> {
215
+ try {
216
+ // TODO: Implement database deletion
217
+ return createSuccessResult('${pascalName} deleted');
218
+ } catch (error) {
219
+ return createErrorResult('Failed to delete ${camelName}', error instanceof Error ? error.message : 'Unknown error', 500);
220
+ }
221
+ }
222
+ `;
223
+ }
224
+ function generateCRUDRoutes(config) {
225
+ const { resourceName, addAuth } = config;
226
+ const pascalName = toPascalCase(resourceName);
227
+ return `import { Router, Request, Response } from 'express';
228
+ import {
229
+ list${pascalName}s,
230
+ get${pascalName},
231
+ create${pascalName},
232
+ update${pascalName},
233
+ delete${pascalName}
234
+ } from './${resourceName.toLowerCase()}.controller.js';
235
+ import { create${pascalName}Schema, update${pascalName}Schema } from './${resourceName.toLowerCase()}.validator.js';
236
+ import { sendResponse } from '../../shared/api.utils.js';
237
+ ${addAuth ? "// import { requireAuth } from '../auth/auth.middleware.js';\n" : ''}
238
+ const router = Router();
239
+
240
+ // List all
241
+ router.get('/', ${addAuth ? '/* requireAuth, */ ' : ''}async (req: Request, res: Response) => {
242
+ try {
243
+ const result = await list${pascalName}s();
244
+ res.status(result.statusCode || 200).json(result);
245
+ } catch (error) {
246
+ res.status(500).json({ success: false, message: 'Internal error', error });
247
+ }
248
+ });
249
+
250
+ // Get one
251
+ router.get('/:id', ${addAuth ? '/* requireAuth, */ ' : ''}async (req: Request, res: Response) => {
252
+ try {
253
+ const result = await get${pascalName}(req.params.id);
254
+ res.status(result.statusCode || 200).json(result);
255
+ } catch (error) {
256
+ res.status(500).json({ success: false, message: 'Internal error', error });
257
+ }
258
+ });
259
+
260
+ // Create
261
+ router.post('/', ${addAuth ? '/* requireAuth, */ ' : ''}async (req: Request, res: Response) => {
262
+ await sendResponse(req, res, () => create${pascalName}Schema.safeParse(req.body), (data) => create${pascalName}(data));
263
+ });
264
+
265
+ // Update
266
+ router.put('/:id', ${addAuth ? '/* requireAuth, */ ' : ''}async (req: Request, res: Response) => {
267
+ await sendResponse(req, res, () => update${pascalName}Schema.safeParse(req.body), (data) => update${pascalName}(req.params.id, data));
268
+ });
269
+
270
+ // Delete
271
+ router.delete('/:id', ${addAuth ? '/* requireAuth, */ ' : ''}async (req: Request, res: Response) => {
272
+ try {
273
+ const result = await delete${pascalName}(req.params.id);
274
+ res.status(result.statusCode || 200).json(result);
275
+ } catch (error) {
276
+ res.status(500).json({ success: false, message: 'Internal error', error });
277
+ }
278
+ });
279
+
280
+ export default router;
281
+ `;
282
+ }
@@ -0,0 +1 @@
1
+ export declare function infoCommand(): Promise<void>;
@@ -0,0 +1,80 @@
1
+ import path from 'path';
2
+ import { title, info, gray, error, warning } from '../utils/logger.js';
3
+ import { fileExists } from '../utils/file-utils.js';
4
+ export async function infoCommand() {
5
+ title('📊 Project Information');
6
+ // Check if in project
7
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
8
+ if (!fileExists(packageJsonPath)) {
9
+ error('Not in a project directory!');
10
+ process.exit(1);
11
+ }
12
+ // Read package.json
13
+ const fs = await import('fs');
14
+ const packageJson = JSON.parse(await fs.promises.readFile(packageJsonPath, 'utf-8'));
15
+ info('Project Details:');
16
+ gray(` Name: ${packageJson.name}`);
17
+ gray(` Version: ${packageJson.version}`);
18
+ gray(` Description: ${packageJson.description || 'N/A'}\n`);
19
+ // Check for katax-core
20
+ const hasKataxCore = packageJson.dependencies?.['katax-core'];
21
+ info('Dependencies:');
22
+ gray(` katax-core: ${hasKataxCore ? '✅ Installed' : '❌ Not installed'}`);
23
+ gray(` express: ${packageJson.dependencies?.express ? '✅ Installed' : '❌ Not installed'}`);
24
+ gray(` TypeScript: ${packageJson.devDependencies?.typescript ? '✅ Installed' : '❌ Not installed'}\n`);
25
+ // Scan for API routes
26
+ const apiPath = path.join(process.cwd(), 'src', 'api');
27
+ if (await dirExists(apiPath)) {
28
+ info('API Routes:');
29
+ const routes = await scanRoutes(apiPath);
30
+ if (routes.length > 0) {
31
+ routes.forEach(route => gray(` ${route}`));
32
+ }
33
+ else {
34
+ gray(' No routes found');
35
+ }
36
+ }
37
+ else {
38
+ warning('API directory not found');
39
+ }
40
+ console.log();
41
+ }
42
+ async function dirExists(dirPath) {
43
+ try {
44
+ const fs = await import('fs');
45
+ const stats = await fs.promises.stat(dirPath);
46
+ return stats.isDirectory();
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ async function scanRoutes(apiPath) {
53
+ const routes = [];
54
+ try {
55
+ const fs = await import('fs');
56
+ const entries = await fs.promises.readdir(apiPath, { withFileTypes: true });
57
+ for (const entry of entries) {
58
+ if (entry.isDirectory() && entry.name !== 'routes') {
59
+ const routeFile = path.join(apiPath, entry.name, `${entry.name}.routes.ts`);
60
+ if (await fileExistsAsync(routeFile)) {
61
+ routes.push(`/${entry.name}`);
62
+ }
63
+ }
64
+ }
65
+ }
66
+ catch (err) {
67
+ // Ignore errors
68
+ }
69
+ return routes;
70
+ }
71
+ async function fileExistsAsync(filePath) {
72
+ try {
73
+ const fs = await import('fs');
74
+ await fs.promises.access(filePath);
75
+ return true;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
@@ -0,0 +1,5 @@
1
+ interface InitOptions {
2
+ force?: boolean;
3
+ }
4
+ export declare function initCommand(projectName?: string, options?: InitOptions): Promise<void>;
5
+ export {};