@avleon/cli 0.0.44 → 0.0.46

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/dist/create.js CHANGED
@@ -44,305 +44,455 @@ const path_1 = __importDefault(require("path"));
44
44
  const os_1 = __importDefault(require("os"));
45
45
  const inquirer_1 = __importDefault(require("inquirer"));
46
46
  const child_process_1 = require("child_process");
47
- const util_1 = __importDefault(require("util"));
48
47
  const lodash_1 = require("lodash");
49
- const execAsync = util_1.default.promisify(child_process_1.exec);
50
- const getShell = () => {
51
- if (os_1.default.platform() === 'win32') {
52
- return process.env.ComSpec || 'cmd.exe'; // Windows fallback
53
- }
54
- return process.env.SHELL || '/bin/sh'; // macOS/Linux fallback
48
+ // ─── Colors ───────────────────────────────────────────────────────────────────
49
+ const C = {
50
+ reset: '\x1b[0m',
51
+ dim: '\x1b[90m',
52
+ bold: '\x1b[1m',
53
+ cyan: '\x1b[36m',
54
+ green: '\x1b[32m',
55
+ yellow: '\x1b[33m',
56
+ red: '\x1b[31m',
57
+ white: '\x1b[37m',
58
+ };
59
+ const icon = {
60
+ success: `${C.green}✔${C.reset}`,
61
+ error: `${C.red}✖${C.reset}`,
62
+ warn: `${C.yellow}⚠${C.reset}`,
63
+ arrow: `${C.dim}→${C.reset}`,
55
64
  };
65
+ function step(label, detail) {
66
+ const pad = label.padEnd(30, ' ');
67
+ console.log(` ${icon.arrow} ${C.white}${pad}${C.reset}${detail ? C.dim + detail + C.reset : ''}`);
68
+ }
69
+ function success(msg) {
70
+ console.log(` ${icon.success} ${C.green}${msg}${C.reset}`);
71
+ }
72
+ function fail(msg) {
73
+ console.log(` ${icon.error} ${C.red}${msg}${C.reset}`);
74
+ }
75
+ function section(title) {
76
+ console.log(`\n${C.bold}${C.cyan} ${title}${C.reset}`);
77
+ console.log(` ${'─'.repeat(40)}`);
78
+ }
79
+ // ─── Version resolver ─────────────────────────────────────────────────────────
80
+ function resolveDepVersion(pkgName, fallback = 'latest') {
81
+ try {
82
+ const pkgJsonPath = require.resolve(`${pkgName}/package.json`);
83
+ const pkg = JSON.parse(fs_extra_1.default.readFileSync(pkgJsonPath, 'utf8'));
84
+ return `^${pkg.version}`;
85
+ }
86
+ catch {
87
+ return fallback;
88
+ }
89
+ }
90
+ function getAvleonVersions() {
91
+ try {
92
+ // Read CLI's own package.json (this file lives inside @avleon/cli)
93
+ const cliPkgPath = path_1.default.resolve(__dirname, '../../package.json');
94
+ const cliPkg = JSON.parse(fs_extra_1.default.readFileSync(cliPkgPath, 'utf8'));
95
+ // Core version — resolve from installed node_modules
96
+ const corePkgPath = require.resolve('@avleon/core/package.json');
97
+ const corePkg = JSON.parse(fs_extra_1.default.readFileSync(corePkgPath, 'utf8'));
98
+ return {
99
+ core: `^${corePkg.version}`,
100
+ cli: `^${cliPkg.version}`,
101
+ };
102
+ }
103
+ catch {
104
+ return { core: 'latest', cli: 'latest' };
105
+ }
106
+ }
107
+ // ─── Banner ───────────────────────────────────────────────────────────────────
108
+ function banner(fname) {
109
+ const { core } = getAvleonVersions();
110
+ console.clear();
111
+ console.log();
112
+ console.log(`${C.bold}${C.cyan} ╔═══════════════════════════════════════╗${C.reset}`);
113
+ console.log(`${C.bold}${C.cyan} ║ AVLEON APPLICATION CLI ║${C.reset}`);
114
+ console.log(`${C.bold}${C.cyan} ╚═══════════════════════════════════════╝${C.reset}`);
115
+ console.log();
116
+ console.log(` ${C.dim}Creating project :${C.reset} ${C.bold}${C.white}${fname}${C.reset}`);
117
+ console.log(` ${C.dim}Avleon version :${C.reset} ${C.bold}${C.cyan}${core}${C.reset}`);
118
+ console.log();
119
+ }
120
+ // ─── Shell ────────────────────────────────────────────────────────────────────
121
+ const getShell = () => os_1.default.platform() === 'win32'
122
+ ? process.env.ComSpec || 'cmd.exe'
123
+ : process.env.SHELL || '/bin/sh';
124
+ // ─── Prompts ──────────────────────────────────────────────────────────────────
56
125
  async function askForPackageManager() {
57
126
  const { packageManager } = await inquirer_1.default.prompt([
58
127
  {
59
128
  type: 'list',
60
129
  name: 'packageManager',
61
- message: 'Which package manager do you want to use?',
62
- choices: ['npm', 'yarn', 'pnpm'],
63
- default: 'npm'
64
- }
130
+ message: 'Package manager:',
131
+ choices: [
132
+ { name: 'npm', value: 'npm' },
133
+ { name: 'yarn', value: 'yarn' },
134
+ { name: 'pnpm (recommended)', value: 'pnpm' },
135
+ ],
136
+ default: 'npm',
137
+ },
65
138
  ]);
66
139
  return packageManager;
67
140
  }
68
- async function installDependencies(pm, fname) {
141
+ async function askForFeatures() {
142
+ const { features } = await inquirer_1.default.prompt([
143
+ {
144
+ type: 'checkbox',
145
+ name: 'features',
146
+ message: 'Select features to include:',
147
+ choices: [
148
+ { name: 'OpenAPI / Swagger docs', value: 'openapi', checked: true },
149
+ { name: 'CORS support', value: 'cors', checked: false },
150
+ { name: 'TypeORM database', value: 'typeorm', checked: false },
151
+ { name: 'Scalar UI (replaces Swagger UI)', value: 'scalar', checked: false },
152
+ ],
153
+ },
154
+ ]);
155
+ return features;
156
+ }
157
+ // ─── Installer ────────────────────────────────────────────────────────────────
158
+ async function installDependencies(pm, cwd) {
69
159
  return new Promise((resolve, reject) => {
70
160
  const child = (0, child_process_1.spawn)(pm, ['install'], {
71
- cwd: fname,
161
+ cwd,
72
162
  shell: getShell(),
73
163
  stdio: 'inherit',
74
164
  });
75
165
  child.on('exit', (code) => {
76
- if (code === 0) {
77
- console.log('✅ Dependencies installed successfully.');
78
- console.log(`cd ${fname}`);
79
- console.log(pm == 'npm' ? `npm run start:dev` : `${pm} start:dev`);
80
- resolve();
81
- }
82
- else {
83
- console.error(`❌ Installation failed with exit code ${code}`);
84
- reject(new Error(`Install failed with code ${code}`));
85
- }
86
- });
87
- child.on('error', (err) => {
88
- console.error('❌ Failed to start install process:', err);
89
- reject(err);
166
+ code === 0
167
+ ? resolve()
168
+ : reject(new Error(`Install failed with exit code ${code}`));
90
169
  });
170
+ child.on('error', reject);
91
171
  });
92
172
  }
93
- async function createApplication(fname, flags = {}) {
94
- let appPaths = (0, paths_1.appPaths)(fname);
95
- const pm = await askForPackageManager();
96
- // const dirs = Object.values(appPaths);
97
- // for (let i = 0; i < dirs.length; i++) {
98
- // await fs.ensureDir(dirs[i]);
99
- // }
100
- await fs_extra_1.default.ensureDir(appPaths.root + "/src");
101
- await fs_extra_1.default.ensureDir(appPaths.root + "/src/controllers");
102
- await fs_extra_1.default.ensureDir(appPaths.root + "/src/config");
103
- await fs_extra_1.default.ensureDir(appPaths.root + "/src/services");
104
- // await fs.ensureDir(appPaths.root + "/src/models");
105
- await fs_extra_1.default.ensureDir(appPaths.root + "/test");
106
- await fs_extra_1.default.ensureDir(appPaths.root + "/public");
107
- // copy
108
- // home controller
109
- const homeService = `
110
- import { AppService } from '@avleon/core';
111
-
112
-
113
- @AppService
114
- export class HomeService{
115
-
116
- sayHello(){
117
- return "Hello World!";
118
- }
119
- }
120
- `;
121
- const formatHomeService = await prettier.format(homeService, {
173
+ // ─── Format helper ────────────────────────────────────────────────────────────
174
+ async function fmt(source, parser = 'typescript') {
175
+ return prettier.format(source, {
122
176
  singleQuote: true,
123
177
  singleAttributePerLine: true,
124
- parser: "typescript",
178
+ parser,
125
179
  });
126
- await fs_extra_1.default.writeFile(path_1.default.join(appPaths.services, "home.service.ts"), formatHomeService);
127
- // home controller
128
- const homeContrlelr = `
129
- import { ApiController, Get } from '@avleon/core';
130
- import { HomeService } from '../services/home.service';
131
-
132
-
133
- @ApiController
134
- export class HomeController{
135
-
136
- constructor(
137
- private readonly homeService: HomeService
138
- ){}
139
-
140
- @Get()
141
- sayHello(){
142
- return this.homeService.sayHello();
143
- }
144
180
  }
181
+ // ─── File writer ──────────────────────────────────────────────────────────────
182
+ async function writeFile(filePath, content, label) {
183
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath));
184
+ await fs_extra_1.default.writeFile(filePath, content);
185
+ if (label)
186
+ step(label, path_1.default.relative(process.cwd(), filePath));
187
+ }
188
+ // ─── Templates ────────────────────────────────────────────────────────────────
189
+ function homeServiceTemplate() {
190
+ return `
191
+ import { AppService } from '@avleon/core';
192
+
193
+ @AppService
194
+ export class HomeService {
195
+ sayHello() {
196
+ return { message: 'Hello World!' };
197
+ }
198
+ }
145
199
  `;
146
- const formatHomeController = await prettier.format(homeContrlelr, {
147
- singleQuote: true,
148
- singleAttributePerLine: true,
149
- parser: "typescript",
150
- });
151
- await fs_extra_1.default.writeFile(path_1.default.join(appPaths.controllers, "home.controller.ts"), formatHomeController);
152
- const homeContrlelrTest = `
153
- import { AvleonTest } from "@avleon/core";
154
- import { HomeController } from "./home.controller"
155
-
156
- describe('Home Controller', () => {
157
- let controller!: HomeController;
158
-
159
- beforeAll(()=>{
160
- controller = AvleonTest.getController(HomeController);
161
- })
162
-
163
- it('should be defined', () => {
164
- expect(controller).toBeDefined()
165
- })
166
-
167
- it('should have sayhello function', () => {
168
- expect(controller.sayHello).toBeDefined
169
- })
170
-
171
- it('should return sayhello', () => {
172
- expect(controller.sayHello()).toBe("Hello World!");
173
- })
174
- })
200
+ }
201
+ function homeControllerTemplate() {
202
+ return `
203
+ import { ApiController, Get } from '@avleon/core';
204
+ import { HomeService } from '../services/home.service';
205
+
206
+ @ApiController('/')
207
+ export class HomeController {
208
+ constructor(private readonly homeService: HomeService) {}
209
+
210
+ @Get()
211
+ sayHello() {
212
+ return this.homeService.sayHello();
213
+ }
214
+ }
175
215
  `;
176
- const formatHomeControllerTest = await prettier.format(homeContrlelrTest, {
177
- singleQuote: true,
178
- singleAttributePerLine: true,
179
- parser: "typescript",
180
- });
181
- await fs_extra_1.default.writeFile(path_1.default.join(appPaths.controllers, "home.controller.spec.ts"), formatHomeControllerTest);
182
- // openapi config file
183
- const openapiConfigFile = `
184
- import { AppConfig, Environment, IConfig } from "@avleon/core";
185
-
186
- @AppConfig
187
- export class OpenApiConfig implements IConfig {
188
-
189
- config(env: Environment) {
190
- return {
191
- info: {
192
- title: '${(0, lodash_1.capitalize)(fname)}',
193
- version: '0.0.1',
194
- },
195
- }
196
- }
197
216
  }
217
+ function homeControllerTestTemplate() {
218
+ return `
219
+ import { AvleonTest } from '@avleon/core';
220
+ import { HomeController } from './home.controller';
221
+
222
+ describe('HomeController', () => {
223
+ let controller!: HomeController;
224
+
225
+ beforeAll(() => {
226
+ controller = AvleonTest.getController(HomeController);
227
+ });
228
+
229
+ it('should be defined', () => {
230
+ expect(controller).toBeDefined();
231
+ });
232
+
233
+ it('should have sayHello method', () => {
234
+ expect(typeof controller.sayHello).toBe('function');
235
+ });
236
+
237
+ it('should return hello message', () => {
238
+ expect(controller.sayHello()).toEqual({ message: 'Hello World!' });
239
+ });
240
+ });
198
241
  `;
199
- const formatConfigFile = await prettier.format(openapiConfigFile, {
200
- singleQuote: true,
201
- singleAttributePerLine: true,
202
- parser: "typescript",
203
- });
204
- await fs_extra_1.default.writeFile(path_1.default.join(appPaths.configs, "openapi.config.ts"), formatConfigFile);
205
- // main file
206
- const mainFunc = `
207
- import { Avleon } from '@avleon/core';
208
- import { OpenApiConfig } from './config/openapi.config';
209
- import { HomeController } from './controllers/home.controller';
210
-
211
-
212
- const app = Avleon.createApplication();
213
-
214
- if(app.isDevelopment()){
215
- app.useOpenApi(OpenApiConfig);
216
242
  }
217
-
218
- app.useControllers([ HomeController ]);
219
- app.useStaticFiles();
220
-
221
- export default app;
243
+ function openApiConfigTemplate(fname, useScalar) {
244
+ return `
245
+ import { AppConfig, Environment, IConfig } from '@avleon/core';
246
+
247
+ @AppConfig
248
+ export class OpenApiConfig implements IConfig {
249
+ config(env: Environment) {
250
+ return {
251
+ info: {
252
+ title: '${(0, lodash_1.capitalize)(fname)} API',
253
+ version: '0.0.1',
254
+ description: 'API documentation for ${(0, lodash_1.capitalize)(fname)}',
255
+ },
256
+ ${useScalar ? "provider: 'scalar'," : ''}
257
+ routePrefix: '/docs',
258
+ };
259
+ }
260
+ }
222
261
  `;
223
- const formatMainFunc = await prettier.format(mainFunc, {
224
- singleQuote: true,
225
- singleAttributePerLine: true,
226
- parser: "typescript",
227
- });
228
- await fs_extra_1.default.writeFile(path_1.default.join(appPaths.src, "app.ts"), formatMainFunc);
229
- // create serve.ts
230
- const serveFunc = `
231
- import app from "./app";
232
-
233
- async function start(){
234
- app.run(); // default run on :4000
235
262
  }
236
-
237
- start();
263
+ function appTemplate(features) {
264
+ const hasOpenApi = features.includes('openapi');
265
+ const hasCors = features.includes('cors');
266
+ const hasTypeorm = features.includes('typeorm');
267
+ const imports = [`import { Avleon } from '@avleon/core';`];
268
+ imports.push(`import { HomeController } from './controllers/home.controller';`);
269
+ if (hasOpenApi) {
270
+ imports.push(`import { OpenApiConfig } from './config/openapi.config';`);
271
+ }
272
+ const body = [`const app = Avleon.createApplication();`, ''];
273
+ if (hasCors) {
274
+ body.push(`app.useCors({ origin: '*' });`);
275
+ }
276
+ if (hasOpenApi) {
277
+ body.push(`if (app.isDevelopment()) {`);
278
+ body.push(` app.useOpenApi(OpenApiConfig);`);
279
+ body.push(`}`);
280
+ body.push('');
281
+ }
282
+ if (hasTypeorm) {
283
+ body.push(`// TODO: configure datasource in src/config/database.config.ts`);
284
+ body.push(`// app.useDatasource(AppDataSource);`);
285
+ body.push('');
286
+ }
287
+ body.push(`app.useControllers([HomeController]);`);
288
+ body.push('');
289
+ body.push(`export default app;`);
290
+ return `${imports.join('\n')}\n\n${body.join('\n')}\n`;
291
+ }
292
+ function serveTemplate() {
293
+ return `
294
+ import app from './app';
295
+
296
+ async function start() {
297
+ await app.run(); // default port: 4000
298
+ }
299
+
300
+ start();
238
301
  `;
239
- const formatServeFunc = await prettier.format(serveFunc, {
240
- singleQuote: true,
241
- singleAttributePerLine: true,
242
- parser: "typescript",
243
- });
244
- await fs_extra_1.default.writeFile(path_1.default.join(appPaths.src, "serve.ts"), formatServeFunc);
245
- // create package.json
246
- const packageJson = `
247
- {
248
- "name": "${(0, lodash_1.snakeCase)(fname)}",
249
- "version": "0.0.1",
250
- "description": "Basic avleon application",
251
- "main": "dist/serve.js",
252
- "scripts": {
253
- "start:dev": "nodemon ./src/serve.ts",
254
- "start:debug": "nodemon --watch ./src --ext ts --exec node --inspect=0.0.0.0:9229 -r ts-node/register ./src/serve.ts",
255
- "clean": "rimraf ./dist",
256
- "build": "npm run clean & tsc --project ./tsconfig.build.json",
257
- "start": "node ./dist/serve.js",
258
- "test": "jest",
259
- "test:watch": "jest --watch",
260
- "test:cov": "jest --coverage",
261
- "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
262
- "test:e2e": "jest --config ./test/e2e-spec.json"
263
- },
264
- "keywords": ["avleon","nodejs","typescript"],
265
- "license": "ISC",
266
- "dependencies": {
267
- "@avleon/core": "^0.0.36",
268
- "@avleon/cli": "^0.0.42",
269
- "class-transformer": "^0.5.1",
270
- "class-validator": "^0.14.1",
271
- "rimraf": "^6.0.1",
272
- "reflect-metadata": "^0.2.2"
273
- },
274
- "devDependencies": {
275
- "@types/jest": "^29.5.14",
276
- "@types/node": "^24.0.3",
277
- "jest": "^29.7.0",
278
- "ts-jest": "^29.2.6",
279
- "nodemon": "^3.1.7",
280
- "ts-node": "^10.9.2",
281
- "typescript": "^5.7.2"
282
- },
283
- "jest": {
284
- "preset": "ts-jest",
285
- "rootDir": "src",
286
- "testRegex": ".*.spec.ts$",
287
- "collectCoverageFrom": [
288
- "**/*.(t|j)s"
289
- ],
290
- "coverageDirectory": "../coverage",
291
- "testEnvironment": "node"
292
- }
293
-
294
302
  }
303
+ function packageJsonTemplate(fname, features) {
304
+ // ✅ Always resolve current installed versions at generation time
305
+ const { core, cli } = getAvleonVersions();
306
+ const deps = {
307
+ '@avleon/core': core,
308
+ '@avleon/cli': cli,
309
+ 'class-transformer': resolveDepVersion('class-transformer', '^0.5.1'),
310
+ 'class-validator': resolveDepVersion('class-validator', '^0.14.1'),
311
+ 'rimraf': resolveDepVersion('rimraf', '^6.0.1'),
312
+ 'reflect-metadata': resolveDepVersion('reflect-metadata', '^0.2.2'),
313
+ };
314
+ if (features.includes('cors')) {
315
+ deps['@fastify/cors'] = resolveDepVersion('@fastify/cors', '^10.0.0');
316
+ }
317
+ if (features.includes('typeorm')) {
318
+ deps['typeorm'] = resolveDepVersion('typeorm', '^0.3.20');
319
+ deps['pg'] = resolveDepVersion('pg', '^8.11.0');
320
+ }
321
+ if (features.includes('scalar')) {
322
+ deps['@scalar/fastify-api-reference'] = resolveDepVersion('@scalar/fastify-api-reference', '^1.0.0');
323
+ }
324
+ const devDeps = {
325
+ '@types/jest': resolveDepVersion('@types/jest', '^29.5.14'),
326
+ '@types/node': resolveDepVersion('@types/node', '^24.0.3'),
327
+ 'jest': resolveDepVersion('jest', '^29.7.0'),
328
+ 'ts-jest': resolveDepVersion('ts-jest', '^29.2.6'),
329
+ 'typescript': resolveDepVersion('typescript', '^5.7.2'),
330
+ };
331
+ return JSON.stringify({
332
+ name: (0, lodash_1.kebabCase)(fname),
333
+ version: '0.0.1',
334
+ description: `${(0, lodash_1.capitalize)(fname)} API built with Avleon`,
335
+ main: 'dist/serve.js',
336
+ scripts: {
337
+ 'start:dev': 'avleon serve',
338
+ 'clean': 'rimraf ./dist',
339
+ 'build': 'npm run clean && tsc --project ./tsconfig.build.json',
340
+ 'start': 'node ./dist/serve.js',
341
+ 'test': 'jest',
342
+ 'test:watch': 'jest --watch',
343
+ 'test:cov': 'jest --coverage',
344
+ 'test:e2e': 'jest --config ./test/e2e-spec.json',
345
+ },
346
+ keywords: ['avleon', 'nodejs', 'typescript'],
347
+ license: 'ISC',
348
+ dependencies: deps,
349
+ devDependencies: devDeps,
350
+ jest: {
351
+ preset: 'ts-jest',
352
+ rootDir: 'src',
353
+ testRegex: '.*.spec.ts$',
354
+ collectCoverageFrom: ['**/*.(t|j)s'],
355
+ coverageDirectory: '../coverage',
356
+ testEnvironment: 'node',
357
+ },
358
+ }, null, 2);
359
+ }
360
+ function tsconfigTemplate() {
361
+ return JSON.stringify({
362
+ compilerOptions: {
363
+ target: 'ES2017',
364
+ module: 'CommonJS',
365
+ moduleResolution: 'node',
366
+ experimentalDecorators: true,
367
+ emitDecoratorMetadata: true,
368
+ outDir: './dist',
369
+ esModuleInterop: true,
370
+ allowSyntheticDefaultImports: true,
371
+ forceConsistentCasingInFileNames: true,
372
+ skipLibCheck: true,
373
+ strict: false,
374
+ },
375
+ include: ['src/**/*'],
376
+ exclude: ['node_modules', 'dist'],
377
+ }, null, 2);
378
+ }
379
+ function tsconfigBuildTemplate() {
380
+ return JSON.stringify({
381
+ extends: './tsconfig.json',
382
+ exclude: ['node_modules', 'coverage', 'test', 'dist', '**/*spec.ts'],
383
+ }, null, 2);
384
+ }
385
+ function e2eConfigTemplate() {
386
+ return JSON.stringify({
387
+ moduleFileExtensions: ['js', 'json', 'ts'],
388
+ rootDir: '.',
389
+ testEnvironment: 'node',
390
+ testRegex: '.e2e-spec.ts$',
391
+ transform: { '^.+\\.(t|j)s$': 'ts-jest' },
392
+ }, null, 2);
393
+ }
394
+ function gitignoreTemplate() {
395
+ return `node_modules/\ndist/\n.env\n.env.local\ncoverage/\n`;
396
+ }
397
+ function envTemplate() {
398
+ return `NODE_ENV=development\nPORT=4000\n`;
399
+ }
400
+ function readmeTemplate(fname, pm) {
401
+ const runCmd = pm === 'npm' ? 'npm run start:dev' : `${pm} start:dev`;
402
+ return `# ${(0, lodash_1.capitalize)(fname)}
403
+
404
+ Built with [Avleon](https://github.com/xtareq/avleon).
405
+
406
+ ## Getting Started
407
+
408
+ \`\`\`bash
409
+ ${runCmd}
410
+ \`\`\`
411
+
412
+ ## Scripts
413
+
414
+ | Script | Description |
415
+ |--------|-------------|
416
+ | \`start:dev\` | Start development server with hot reload |
417
+ | \`build\` | Compile TypeScript to JavaScript |
418
+ | \`start\` | Start production server |
419
+ | \`test\` | Run unit tests |
420
+
421
+ ## Project Structure
422
+
423
+ \`\`\`
424
+ src/
425
+ ├── controllers/ # Route controllers
426
+ ├── services/ # Business logic
427
+ ├── config/ # App configuration
428
+ ├── app.ts # App setup
429
+ └── serve.ts # Entry point
430
+ \`\`\`
295
431
  `;
296
- const formattedPackageJson = await prettier.format(packageJson, {
297
- parser: "json",
298
- });
299
- await fs_extra_1.default.writeFile(path_1.default.join(appPaths.root, "package.json"), formattedPackageJson);
300
- const tsconfigJson = await prettier.format(`{
301
- "compilerOptions": {
302
- "target": "ES2017",
303
- "module": "CommonJS",
304
- "moduleResolution": "node",
305
- "experimentalDecorators": true,
306
- "emitDecoratorMetadata": true,
307
- "outDir": "./dist",
308
- "esModuleInterop": true,
309
- "allowSyntheticDefaultImports": true,
310
- "forceConsistentCasingInFileNames": true,
311
- "skipLibCheck": true
312
- },
313
- "include": ["src/**/*"],
314
- "exclude": ["node_modules", "dist"]
315
- }`, { parser: "json" });
316
- await fs_extra_1.default.writeFile(path_1.default.join(appPaths.root, "tsconfig.json"), tsconfigJson);
317
- const tsconfigBuildJson = await prettier.format(`{
318
- "extends": "./tsconfig.json",
319
- "exclude": ["node_modules","coverage", "test", "dist", "**/*spec.ts"]
320
432
  }
321
- `, { parser: "json" });
322
- await fs_extra_1.default.writeFile(path_1.default.join(appPaths.root, "tsconfig.build.json"), tsconfigBuildJson);
323
- // write e2e-test.json
324
- const e2eJson = await prettier.format(`{
325
- "moduleFileExtensions": [
326
- "js",
327
- "json",
328
- "ts"
329
- ],
330
- "rootDir": ".",
331
- "testEnvironment": "node",
332
- "testRegex": ".e2e-spec.ts$",
333
- "transform": {
334
- "^.+/\/\.(t|j)s$": "ts-jest"
335
- }
336
- }`, { parser: "json" });
337
- await fs_extra_1.default.writeFile(path_1.default.join(appPaths.root, "test/e2e-spec.json"), e2eJson);
338
- await fs_extra_1.default.writeFile(path_1.default.join(appPaths.root, ".gitignore"), `node_modules/\ndist/\n.env`);
433
+ // ─── Main ─────────────────────────────────────────────────────────────────────
434
+ async function createApplication(fname, flags = {}) {
435
+ const appPaths = (0, paths_1.appPaths)(fname);
436
+ banner(fname);
437
+ // ── Prompts ───────────────────────────────────────────────────────────────
438
+ const pm = await askForPackageManager();
439
+ const features = await askForFeatures();
440
+ const useScalar = features.includes('scalar');
441
+ // ── Directories ───────────────────────────────────────────────────────────
442
+ section('Scaffolding project structure');
443
+ const dirs = [
444
+ appPaths.root + '/src',
445
+ appPaths.root + '/src/controllers',
446
+ appPaths.root + '/src/config',
447
+ appPaths.root + '/src/services',
448
+ appPaths.root + '/test',
449
+ appPaths.root + '/public',
450
+ ];
451
+ for (const dir of dirs) {
452
+ await fs_extra_1.default.ensureDir(dir);
453
+ step('Created directory', path_1.default.relative(process.cwd(), dir));
454
+ }
455
+ // ── Source files ──────────────────────────────────────────────────────────
456
+ section('Generating source files');
457
+ await writeFile(path_1.default.join(appPaths.services, 'home.service.ts'), await fmt(homeServiceTemplate()), 'HomeService');
458
+ await writeFile(path_1.default.join(appPaths.controllers, 'home.controller.ts'), await fmt(homeControllerTemplate()), 'HomeController');
459
+ await writeFile(path_1.default.join(appPaths.controllers, 'home.controller.spec.ts'), await fmt(homeControllerTestTemplate()), 'HomeController spec');
460
+ if (features.includes('openapi')) {
461
+ await writeFile(path_1.default.join(appPaths.configs, 'openapi.config.ts'), await fmt(openApiConfigTemplate(fname, useScalar)), 'OpenApiConfig');
462
+ }
463
+ await writeFile(path_1.default.join(appPaths.src, 'app.ts'), await fmt(appTemplate(features)), 'app.ts');
464
+ await writeFile(path_1.default.join(appPaths.src, 'serve.ts'), await fmt(serveTemplate()), 'serve.ts');
465
+ // ── Config files ──────────────────────────────────────────────────────────
466
+ section('Generating config files');
467
+ await writeFile(path_1.default.join(appPaths.root, 'package.json'), await fmt(packageJsonTemplate(fname, features), 'json'), 'package.json');
468
+ await writeFile(path_1.default.join(appPaths.root, 'tsconfig.json'), await fmt(tsconfigTemplate(), 'json'), 'tsconfig.json');
469
+ await writeFile(path_1.default.join(appPaths.root, 'tsconfig.build.json'), await fmt(tsconfigBuildTemplate(), 'json'), 'tsconfig.build.json');
470
+ await writeFile(path_1.default.join(appPaths.root, 'test/e2e-spec.json'), await fmt(e2eConfigTemplate(), 'json'), 'e2e-spec.json');
471
+ await writeFile(path_1.default.join(appPaths.root, '.gitignore'), gitignoreTemplate(), '.gitignore');
472
+ await writeFile(path_1.default.join(appPaths.root, '.env'), envTemplate(), '.env');
473
+ await writeFile(path_1.default.join(appPaths.root, 'README.md'), readmeTemplate(fname, pm), 'README.md');
474
+ // ── Install ───────────────────────────────────────────────────────────────
475
+ section('Installing dependencies');
339
476
  try {
340
- console.log(`Running ${pm} install...`);
341
- await execAsync(`cd ${fname}`);
342
- await installDependencies(pm, fname);
477
+ await installDependencies(pm, appPaths.root);
478
+ success('Dependencies installed');
343
479
  }
344
480
  catch (error) {
345
- console.error('❌ Installation failed:', error);
481
+ fail(`Installation failed: ${error instanceof Error ? error.message : error}`);
482
+ console.log(`\n ${C.yellow}Install manually:${C.reset}`);
483
+ console.log(` ${C.dim}cd ${fname} && ${pm} install${C.reset}\n`);
484
+ }
485
+ // ── Done ──────────────────────────────────────────────────────────────────
486
+ const runCmd = pm === 'npm' ? 'npm run start:dev' : `${pm} start:dev`;
487
+ console.log();
488
+ console.log(`${C.bold}${C.green} ✔ Project created successfully!${C.reset}`);
489
+ console.log();
490
+ console.log(` ${C.dim}Next steps:${C.reset}`);
491
+ console.log(` ${C.cyan} cd ${fname}${C.reset}`);
492
+ console.log(` ${C.cyan} ${runCmd}${C.reset}`);
493
+ if (features.includes('openapi')) {
494
+ console.log();
495
+ console.log(` ${C.dim}API docs:${C.reset} ${C.white}http://localhost:4000/docs${C.reset}`);
346
496
  }
347
- console.info("Enjoy :D");
497
+ console.log();
348
498
  }