@appxdigital/appx-core-cli 1.0.10 → 1.0.12

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/wizard.js CHANGED
@@ -1,43 +1,46 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { program } from 'commander';
3
+ // Ensure node 20 at least
4
+ const [major] = process.versions.node.split('.').map(Number);
5
+ if (major < 20) {
6
+ console.error('❌ Node.js version 20 or higher is required to run this script.');
7
+ process.exit(1);
8
+ }
9
+
10
+ // Dependencies
11
+ import {program} from 'commander';
4
12
  import inquirer from 'inquirer';
5
13
  import fs from 'fs-extra';
6
14
  import fsCore from 'fs';
7
- import { execSync } from 'child_process';
15
+ import {execSync} from 'child_process';
8
16
  import path from 'path';
9
- import { fileURLToPath } from 'url';
17
+ import {fileURLToPath} from 'url';
10
18
  import cliProgress from 'cli-progress';
11
- import { gatherFileUploadSettings, insertFileUploadConfiguration } from './utils/fileUploadConfig.js';
12
- import packageJson from './package.json' assert { type: 'json' };
13
19
  import crypto from 'crypto';
20
+ import pg from "pg";
21
+ import mysql from "mysql2/promise";
22
+
23
+ // Utils and data
24
+ import {gatherFileUploadSettings, insertFileUploadConfiguration} from './utils/fileUploadConfig.js';
25
+ import packageJson from './package.json' with {type: 'json'};
14
26
 
15
27
  const __filename = fileURLToPath(import.meta.url);
16
28
  const __dirname = path.dirname(__filename);
17
29
 
18
- const dependencyVersions = JSON.parse(
19
- fsCore.readFileSync(path.join(__dirname, 'utils', 'dependency-versions.json'), 'utf-8')
20
- );
30
+ const CORE_VERSION = '0.1.115';
21
31
 
22
32
  const phaseDurations = {
23
- nestjs: 20,
24
- appmodule: 5,
25
- installDependencies: 80,
26
- installCore: 120,
27
- prismaSetup: 10,
28
- setupConcluded: 5,
29
- end: 5
33
+ installDependencies: 30,
34
+ setupProjectStructure: 5,
35
+ createPrismaMigration: 5,
36
+ generateCoreModels: 10,
37
+ gitInit: 1,
38
+ migrateDatabase: 5,
39
+ fileUploadSetup: 5
30
40
  };
31
41
 
32
- const calculateTotalETA = (includeBackoffice) => {
33
- let totalTime = phaseDurations.nestjs +
34
- phaseDurations.appmodule +
35
- phaseDurations.installDependencies +
36
- phaseDurations.installCore +
37
- phaseDurations.prismaSetup +
38
- phaseDurations.setupConcluded
39
- + phaseDurations.end;
40
- return totalTime;
42
+ const calculateTotalETA = () => {
43
+ return Object.values(phaseDurations).reduce((acc, curr) => acc + curr, 0);
41
44
  };
42
45
 
43
46
  const logo = `\x1b[36m
@@ -52,34 +55,38 @@ CLI Version: ${packageJson.version}
52
55
  `;
53
56
 
54
57
  console.log(logo);
55
- program.version(packageJson.version).description('NestJS Project Initialization Wizard');
58
+ program.version(packageJson.version).description('AppX Core Project Initialization Wizard');
56
59
  let progressBar;
57
60
  let remainingETA = 0;
58
61
  program
59
62
  .command('create')
60
- .description('Create a new NestJS project')
63
+ .description('Create a new AppX Core project')
61
64
  .action(() => {
62
65
  console.log('Starting AppX Core...');
63
66
  promptUser();
64
67
  });
65
68
 
69
+ function coreGenerate (showOutput) {
70
+ // run file inside node_modules/appx-core/dist/config/generate-all.js
71
+ const generatePath = path.join(process.cwd(), 'node_modules', '@appxdigital/appx-core', 'dist', 'config', 'generate-all.js');
72
+ if (!fsCore.existsSync(generatePath)) {
73
+ console.error('❌ Not inside a valid AppX Core project directory. Please run this command inside your project directory.');
74
+ process.exit(1);
75
+ }
76
+ // Execute the script
77
+ try {
78
+ executeCommand(`node ${generatePath}`, showOutput);
79
+ console.log('✅ Prisma client and GraphQL schema generated successfully.');
80
+ } catch (error) {
81
+ console.error('❌ An error occurred while generating Prisma client and GraphQL schema:', error.message);
82
+ process.exit(1);
83
+ }
84
+ }
85
+
66
86
  program.command('generate')
67
87
  .description('Updates Prisma client and generates the GraphQL schema')
68
88
  .action(() => {
69
- // run file inside node_modules/appx-core/dist/config/generate-all.js
70
- const generatePath = path.join(process.cwd(), 'node_modules', '@appxdigital/appx-core', 'dist', 'config', 'generate-all.js');
71
- if (!fsCore.existsSync(generatePath)) {
72
- console.error('❌ Not inside a valid AppX Core project directory. Please run this command inside your project directory.');
73
- process.exit(1);
74
- }
75
- // Execute the script
76
- try {
77
- execSync(`node ${generatePath}`, { stdio: 'inherit' });
78
- console.log('✅ Prisma client and GraphQL schema generated successfully.');
79
- } catch (error) {
80
- console.error('❌ An error occurred while generating Prisma client and GraphQL schema:', error.message);
81
- process.exit(1);
82
- }
89
+ coreGenerate(true);
83
90
  });
84
91
 
85
92
  program.command('setup:fileupload')
@@ -93,7 +100,7 @@ program.command('setup:fileupload')
93
100
  }
94
101
  // Execute the script
95
102
  try {
96
- execSync(`node ${generatePath}`, { stdio: 'inherit' });
103
+ execSync(`node ${generatePath}`, {stdio: 'inherit'});
97
104
  console.log('✅ File upload configuration added successfully.');
98
105
  } catch (error) {
99
106
  console.error('❌ An error occurred while adding file upload configuration:', error.message);
@@ -101,82 +108,248 @@ program.command('setup:fileupload')
101
108
  }
102
109
  });
103
110
 
104
- async function promptUser() {
105
- let fileUploadConfigData = {};
106
- try {
107
- const answers = await inquirer.prompt([
108
- {
109
- type: 'input',
110
- name: 'projectName',
111
- message: 'Project name:',
112
- default: 'nestjs-project'
113
- },
114
- {
115
- type: 'list',
116
- name: 'dbProvider',
117
- message: 'Database provider:',
118
- choices: ['mysql', 'postgresql'],
119
- default: 'mysql'
120
- },
121
- {
122
- type: 'input',
123
- name: 'dbHost',
124
- message: 'Database Host:',
125
- default: '127.0.0.1'
126
- },
127
- {
128
- type: 'input',
129
- name: 'dbPort',
130
- message: 'Database Port:',
131
- default: (answers) => answers.dbProvider === 'mysql' ? '3306' : '5432'
132
- },
111
+ async function checkDb (config) {
112
+ const {dbProvider, dbHost, dbPort, dbUser, dbPassword, dbName} = config;
113
+
114
+ if (dbProvider === "mysql") {
115
+ const conn = await mysql.createConnection({
116
+ host: dbHost,
117
+ port: Number(dbPort),
118
+ user: dbUser,
119
+ password: dbPassword,
120
+ // Important: don't set database here yet; we first check it exists
121
+ connectTimeout: 5000,
122
+ });
123
+
124
+ try {
125
+ // 1) DB exists?
126
+ const [dbRows] = await conn.query(
127
+ "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?",
128
+ [dbName]
129
+ );
130
+
131
+ if (dbRows.length === 0) {
132
+ return {ok: false, reason: `Database "${dbName}" does not exist.`};
133
+ }
134
+
135
+ // 2) Any tables?
136
+ const [tableRows] = await conn.query(
137
+ "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? LIMIT 1",
138
+ [dbName]
139
+ );
140
+
141
+ if (tableRows.length > 0) {
142
+ return {ok: false, reason: `Database "${dbName}" is not empty (has tables).`};
143
+ }
144
+
145
+ return {ok: true};
146
+ } finally {
147
+ await conn.end();
148
+ }
149
+ }
150
+
151
+ if (dbProvider === "postgresql") {
152
+ const client = new pg.Client({
153
+ host: dbHost,
154
+ port: Number(dbPort),
155
+ user: dbUser,
156
+ password: dbPassword,
157
+ database: "postgres", // connect to a known db first to check existence
158
+ connectionTimeoutMillis: 5000,
159
+ });
160
+
161
+ await client.connect();
162
+ try {
163
+ // 1) DB exists?
164
+ const dbRes = await client.query(
165
+ "SELECT 1 FROM pg_database WHERE datname = $1",
166
+ [dbName]
167
+ );
168
+ if (dbRes.rowCount === 0) {
169
+ return {ok: false, reason: `Database "${dbName}" does not exist.`};
170
+ }
171
+
172
+ // 2) Connect to target db and check tables in public schema (and others if you want)
173
+ const client2 = new pg.Client({
174
+ host: dbHost,
175
+ port: Number(dbPort),
176
+ user: dbUser,
177
+ password: dbPassword,
178
+ database: dbName,
179
+ connectionTimeoutMillis: 5000,
180
+ });
181
+
182
+ await client2.connect();
183
+ try {
184
+ const tableRes = await client2.query(
185
+ `
186
+ SELECT 1
187
+ FROM information_schema.tables
188
+ WHERE table_type = 'BASE TABLE'
189
+ AND table_schema NOT IN ('pg_catalog', 'information_schema') LIMIT 1
190
+ `
191
+ );
192
+
193
+ if (tableRes.rowCount > 0) {
194
+ return {ok: false, reason: `Database "${dbName}" is not empty (has tables).`};
195
+ }
196
+
197
+ return {ok: true};
198
+ } finally {
199
+ await client2.end();
200
+ }
201
+ } finally {
202
+ await client.end();
203
+ }
204
+ }
205
+
206
+ return {ok: false, reason: "Unsupported dbProvider."};
207
+ }
208
+
209
+ async function promptDbConfig () {
210
+ return inquirer.prompt([
211
+ {
212
+ type: "list",
213
+ name: "dbProvider",
214
+ message: "Database provider:",
215
+ choices: ["mysql", "postgresql"],
216
+ default: "mysql",
217
+ },
218
+ {
219
+ type: "input",
220
+ name: "dbHost",
221
+ message: "Database Host:",
222
+ default: "127.0.0.1",
223
+ },
224
+ {
225
+ type: "input",
226
+ name: "dbPort",
227
+ message: "Database Port:",
228
+ default: (answers) => (answers.dbProvider === "mysql" ? "3306" : "5432"),
229
+ validate: (v) => (!Number.isNaN(Number(v)) ? true : "Port must be a number"),
230
+ },
231
+ {
232
+ type: "input",
233
+ name: "dbUser",
234
+ message: "Database User:",
235
+ default: "root",
236
+ },
237
+ {
238
+ type: "password",
239
+ name: "dbPassword",
240
+ message: "Database Password:",
241
+ mask: "*",
242
+ },
243
+ {
244
+ type: "input",
245
+ name: "dbName",
246
+ message: "Database Name:",
247
+ default: "generic",
248
+ },
249
+ ]);
250
+ }
251
+
252
+ async function promptDbUntilValid () {
253
+ while (true) {
254
+ const dbConfig = await promptDbConfig();
255
+
256
+ try {
257
+ const result = await checkDb(dbConfig);
258
+
259
+ if (result.ok) {
260
+ return dbConfig;
261
+ }
262
+
263
+ process.stdout.write(`❌ Database check failed: ${result.reason}\n`);
264
+ } catch (err) {
265
+ process.stdout.write(`❌ Database check failed: ${err.message}\n`);
266
+ }
267
+
268
+ const {retry} = await inquirer.prompt([
133
269
  {
134
- type: 'input',
135
- name: 'dbUser',
136
- message: 'Database User:',
137
- default: 'root'
270
+ type: "confirm",
271
+ name: "retry",
272
+ message: "Re-enter all database settings?",
273
+ default: true,
138
274
  },
275
+ ]);
276
+
277
+ if (!retry) {
278
+ throw new Error("User aborted database configuration.");
279
+ }
280
+ }
281
+ }
282
+
283
+ async function promptUser () {
284
+ let fileUploadConfigData = {};
285
+ try {
286
+ const baseAnswers = await inquirer.prompt([
139
287
  {
140
- type: 'password',
141
- name: 'dbPassword',
142
- message: 'Database Password:',
143
- mask: '*'
288
+ type: "input",
289
+ name: "projectName",
290
+ message: "Project name:",
291
+ default: "nestjs-project",
144
292
  },
293
+ ]);
294
+
295
+ const dbAnswers = await promptDbUntilValid();
296
+
297
+ const rest = await inquirer.prompt([
145
298
  {
146
- type: 'input',
147
- name: 'dbName',
148
- message: 'Database Name:',
149
- default: 'generic'
299
+ type: "confirm",
300
+ name: "showOutput",
301
+ message: "Show installation output?",
302
+ default: false,
150
303
  },
151
304
  {
152
- type: 'confirm',
153
- name: 'showOutput',
154
- message: 'Show installation output?',
155
- default: false
305
+ type: "confirm",
306
+ name: "configureFileUpload",
307
+ message: "Configure file upload?",
308
+ default: false,
156
309
  },
157
- {
158
- type: 'confirm',
159
- name: 'configureFileUpload',
160
- message: 'Configure file upload?',
161
- default: false
162
- }
163
310
  ]);
164
311
 
312
+ const answers = {...baseAnswers, ...dbAnswers, ...rest};
313
+
165
314
  if (answers.configureFileUpload) {
166
315
  fileUploadConfigData = await gatherFileUploadSettings();
167
316
  }
168
317
 
169
- remainingETA = calculateTotalETA(answers.backoffice);
318
+ remainingETA = calculateTotalETA();
170
319
  createProject(answers, fileUploadConfigData);
171
320
  } catch (error) {
172
321
  console.error('Error prompting user:', error);
173
322
  }
174
323
  }
175
324
 
176
- async function createProject(answers, fileUploadConfigData) {
177
- const { projectName, dbProvider, dbHost, dbPort, dbUser, dbPassword, dbName, showOutput, backoffice, configureFileUpload } = answers;
325
+ async function createProject (answers, fileUploadConfigData) {
326
+ const {projectName, showOutput, backoffice} = answers;
327
+
178
328
  console.log(`Creating project: ${projectName}`);
179
- const projectPath = `${process.cwd()}/${projectName}`;
329
+
330
+ // If current directory is empty, use it as project directory
331
+ let projectPath = process.cwd();
332
+
333
+ // Ignore DS_STORE files when checking if directory is empty
334
+ if (fs.readdirSync(projectPath).filter(f => f.toLowerCase() !== '.DS_STORE').length > 0) {
335
+ projectPath += `/${projectName.replace(/\s+/g, '_').toLowerCase()}`;
336
+ }
337
+
338
+ console.log(`Project path: ${projectPath}`);
339
+
340
+ const project_config = {
341
+ DB_PROVIDER: answers.dbProvider === 'mysql' ? 'mysql' : 'postgresql',
342
+ DB_HOST: answers.dbHost,
343
+ DB_PORT: answers.dbPort,
344
+ DB_USER: answers.dbUser,
345
+ DB_PASSWORD: answers.dbPassword,
346
+ DB_NAME: answers.dbName,
347
+ SESSION_SECRET: crypto.randomBytes(32).toString('hex'),
348
+ SESSION_COOKIE_NAME: 'session_' + projectName.replace(/\s+/g, '_').toLowerCase(),
349
+ JWT_SECRET: crypto.randomBytes(32).toString('hex'),
350
+ JWT_REFRESH_SECRET: crypto.randomBytes(32).toString('hex'),
351
+ }
352
+
180
353
  if (!showOutput) {
181
354
  const phases = backoffice ? 8 : 7;
182
355
  progressBar = new cliProgress.SingleBar(
@@ -187,69 +360,79 @@ async function createProject(answers, fileUploadConfigData) {
187
360
  },
188
361
  cliProgress.Presets.shades_classic
189
362
  );
190
- progressBar.update(0, { remainingETA });
363
+ progressBar.update(0, {remainingETA});
191
364
  progressBar.start(phases, 0);
192
365
  }
193
366
 
194
367
  try {
195
368
  fs.ensureDirSync(projectPath);
196
- fs.emptyDirSync(projectPath);
369
+
370
+ // Directory must be empty
371
+ if (fs.readdirSync(projectPath).filter(f => f.toLowerCase() !== '.DS_STORE').length > 0) {
372
+ console.error('🚫 The target directory is not empty. Please choose an empty directory or remove existing files. Check for hidden files as well.');
373
+ process.exit(1);
374
+ }
375
+
197
376
  process.chdir(projectPath);
198
377
 
199
- setupProjectStructure(projectPath, answers);
200
- incrementProgress(answers?.showOutput, "setupConcluded");
201
- const envContent = `
202
- ##DataBase Configurations##
203
- DB_HOST=${dbHost}
204
- DB_PORT=${dbPort}
205
- DB_USER=${dbUser}
206
- DB_PASSWORD=${dbPassword}
207
- DB_NAME=${dbName}
208
- DB_PROVIDER=${dbProvider}
209
- DB_URL="\${DB_PROVIDER}://\${DB_USER}:\${DB_PASSWORD}@\${DB_HOST}:\${DB_PORT}/\${DB_NAME}"
378
+ // TODO test database connection here and exit if it fails
210
379
 
211
380
 
212
- ##Project configurations##
381
+ // Install dependencies
382
+ createPackageJson(projectPath, projectName);
383
+ executeCommand('npm --logevel=error install', answers.showOutput);
384
+ incrementProgress(answers?.showOutput, "installDependencies");
213
385
 
214
- #Port
215
- APP_PORT=3000
386
+ // Update PATH to include local node_modules/.bin
387
+ const localBinPath = path.join(projectPath, 'node_modules', '.bin');
388
+ process.env.PATH = `${localBinPath}${path.delimiter}${process.env.PATH}`;
216
389
 
217
- #Default behaviour for use of transactions
218
- USE_TRANSACTION=true
390
+ // Create Core project structure
391
+ setupProjectStructure(projectPath, project_config);
219
392
 
220
- #Session secret
221
- SESSION_SECRET="${crypto.randomBytes(32).toString('hex')}"
393
+ // Create .gitignore
394
+ await createGitignore(projectPath);
222
395
 
223
- #Cookie name for the session token
224
- SESSION_COOKIE_NAME="APPXCORE"
396
+ // Create tsconfig.json and tsconfig.build.json
397
+ createTsConfig(projectPath);
225
398
 
226
- #Expiration time for the session token in seconds
227
- SESSION_TTL=86400
399
+ incrementProgress(answers?.showOutput, "setupProjectStructure");
228
400
 
229
- # JWT
230
- JWT_SECRET="${crypto.randomBytes(32).toString('hex')}"
231
- JWT_REFRESH_SECRET="${crypto.randomBytes(32).toString('hex')}"
232
- `;
401
+ // Create prisma migration file
402
+ executeCommand(`prisma migrate dev --name init --create-only`, answers?.showOutput);
403
+ incrementProgress(answers?.showOutput, "createPrismaMigration");
404
+
405
+ // Generate Core Models
406
+ coreGenerate(answers?.showOutput);
407
+ incrementProgress(answers?.showOutput, "generateCoreModels");
408
+
409
+ // Format code
410
+ executeCommand('npx --yes prettier --write .', answers?.showOutput);
411
+
412
+ // Initialize git repo
413
+ initializeGitRepo(projectPath, answers?.showOutput);
414
+ incrementProgress(answers?.showOutput, "gitInit");
415
+
416
+ // Migrate database
417
+ executeCommand(`prisma migrate dev`, answers?.showOutput);
418
+
419
+ incrementProgress(answers?.showOutput, "migrateDatabase");
233
420
 
234
- fs.writeFileSync(`${projectPath}/.env`, envContent);
235
421
  if (fileUploadConfigData.provider) {
236
422
  await insertFileUploadConfiguration(fileUploadConfigData, projectPath);
237
423
  incrementProgress(showOutput, "fileUploadSetup");
238
424
  }
239
- updateGitignore(projectPath);
240
- configureProjectSettings(projectPath);
241
- executeCommand('npx prettier --write .', answers?.showOutput);
242
- initializeGitRepo(projectPath);
243
- incrementProgress(answers?.showOutput, "end");
425
+
244
426
  if (!showOutput) {
245
427
  progressBar.stop();
246
428
  }
247
429
  console.log('Project created successfully!');
430
+
248
431
  } catch (error) {
249
432
  console.error('❌ An error occurred during project creation:', error.message);
250
433
  if (fs.existsSync(projectPath)) {
251
- console.log('🧹 Cleaning up incomplete project files...');
252
- fs.removeSync(projectPath);
434
+ //console.log('🧹 Cleaning up incomplete project files...');
435
+ //fs.removeSync(projectPath);
253
436
  }
254
437
  console.error('🚫 Project creation aborted due to the above error.');
255
438
  if (!showOutput && progressBar) {
@@ -259,297 +442,87 @@ JWT_REFRESH_SECRET="${crypto.randomBytes(32).toString('hex')}"
259
442
  }
260
443
  }
261
444
 
262
- function setupProjectStructure(projectPath, answers) {
263
- ensureAndRunNestCli(projectPath, answers?.showOutput);
264
- incrementProgress(answers?.showOutput, "nestjs");
265
-
266
- // Copy main README.md
267
- const readmePath = path.join(__dirname, 'README.md');
268
- fs.copyFileSync(readmePath, `${projectPath}/README.md`);
269
-
270
- const bootstrapContent = getBootstrapContent();
271
- fs.writeFileSync(`${projectPath}/src/main.ts`, bootstrapContent);
272
-
273
- const tsConfigPath = path.join(projectPath, 'tsconfig.json');
274
- if (fs.existsSync(tsConfigPath)) {
275
- const tsConfig = fs.readJsonSync(tsConfigPath);
276
-
277
- if (!tsConfig.compilerOptions) {
278
- tsConfig.compilerOptions = {};
445
+ function createPackageJson (projectPath, projectName) {
446
+ const packageJsonContent = {
447
+ name: projectName,
448
+ version: "0.0.1",
449
+ description: "",
450
+ author: "Powered by AppX Digital",
451
+ private: true,
452
+ license: "UNLICENSED",
453
+ scripts: {
454
+ "build": "nest build",
455
+ "start": "nest start",
456
+ "start:dev": "cross-env NODE_ENV=development nest start --watch",
457
+ "start:prod": "cross-env NODE_ENV=production node dist/main",
458
+ "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
459
+ "db-pull": "npm run db-pull --prefix ./node_modules/appx-core"
460
+ },
461
+ dependencies: {
462
+ "@appxdigital/appx-core": CORE_VERSION,
463
+ },
464
+ devDependencies: {
465
+ "@nestjs/cli": "~11.0.0",
466
+ "cross-env": "~10.1.0",
279
467
  }
280
- tsConfig.compilerOptions.target = 'ES2017';
281
- fs.writeJsonSync(tsConfigPath, tsConfig, { spaces: 2 });
282
- } else {
283
- console.warn('Could not find generated tsconfig.json to modify.');
284
- }
468
+ };
285
469
 
286
- const appModuleContent = getAppModuleTemplate();
287
- fs.writeFileSync(`${projectPath}/src/app.module.ts`, appModuleContent);
288
- incrementProgress(answers?.showOutput, "appmodule");
289
- installDependenciesFromManifest(answers?.showOutput);
290
- executeCommand('npm install --save-dev @types/multer @types/express cross-env', answers?.showOutput);
291
- incrementProgress(answers?.showOutput, "installDependencies");
292
-
293
- // const tgzPath = path.resolve('D:/Projects/core/appx-core', fs.readdirSync('D:/Projects/core/appx-core').find(f => f.startsWith('appx-core-package') && f.endsWith('.tgz')));
294
- // executeCommand(`npm install "${tgzPath}"`, answers?.showOutput);
295
- executeCommand('npm install @appxdigital/appx-core@latest', answers?.showOutput);
296
- incrementProgress(answers?.showOutput, "installCore");
297
-
298
- //Prisma setup
299
- const prismaVersion = dependencyVersions.devDependencies?.prisma;
300
- if (!prismaVersion) {
301
- console.error('Prisma version not found in dependency-versions.json devDependencies. Cannot run prisma init.');
302
- process.exit(1);
303
- }
304
- executeCommand(`npx prisma@${prismaVersion} init`, answers?.showOutput);
305
- customizePrismaSchema(projectPath, answers);
306
- incrementProgress(answers?.showOutput, "prismaSetup");
307
- createPrismaModule(projectPath);
308
- createPermissionsConfig(projectPath);
309
- setupAdminJS(projectPath);
310
- createAdminConfig(projectPath);
311
- addScriptsToPackageJson(projectPath);
470
+ fs.writeJsonSync(path.join(projectPath, 'package.json'), packageJsonContent, {spaces: 2});
312
471
  }
313
472
 
314
- function ensureAndRunNestCli(projectPath, showOutput = false) {
315
- const options = showOutput ? { stdio: 'inherit' } : { stdio: 'ignore' };
316
- const command = 'npx --yes @nestjs/cli new . --skip-install --package-manager npm';
317
- try {
318
- execSync('npx --no-install @nestjs/cli --version', options);
319
- } catch {
320
- try {
321
- execSync('npx @nestjs/cli --version', options);
322
- } catch (fetchError) {
323
- console.error('❌ Failed to fetch @nestjs/cli via npx. Please check your internet connection.');
324
- process.exit(1);
325
- }
326
- }
327
- try {
328
- execSync(command, { cwd: projectPath, ...options });
329
- } catch (creationError) {
330
- console.error(`❌ Failed to create the project: ${creationError.message}`);
331
- process.exit(1);
473
+ function setupProjectStructure (projectPath, project_config) {
474
+ // Create scaffold files from scaffold directory recursively, replacing placeholders from *.template files
475
+ const scaffoldDir = path.join(__dirname, 'scaffold');
476
+
477
+ function copyAndReplaceTemplates (srcDir, destDir) {
478
+ fs.readdirSync(srcDir).forEach((item) => {
479
+ const srcPath = path.join(srcDir, item);
480
+ let destPath = path.join(destDir, item.replace('.template', ''));
481
+ const stats = fs.statSync(srcPath);
482
+ if (stats.isDirectory()) {
483
+ fs.ensureDirSync(destPath);
484
+ copyAndReplaceTemplates(srcPath, destPath);
485
+ } else {
486
+ let content = fs.readFileSync(srcPath, 'utf-8');
487
+
488
+ // Replace placeholders
489
+ for (const [key, value] of Object.entries(project_config)) {
490
+ const placeholder = `{{${key}}}`;
491
+ content = content.replace(new RegExp(placeholder, 'g'), value);
492
+ }
493
+ fs.writeFileSync(destPath, content, 'utf-8');
494
+ }
495
+ });
332
496
  }
333
- }
334
-
335
- function getBootstrapContent() {
336
- const bootstrapFilePath = path.join(__dirname, 'templates', 'bootstrap.template.js');
337
- return fs.readFileSync(bootstrapFilePath, 'utf8');
338
- }
339
-
340
- function getAppModuleTemplate() {
341
- const appModuleTemplatePath = path.join(__dirname, 'templates', 'app.module.template.js');
342
- return fs.readFileSync(appModuleTemplatePath, 'utf8');
343
- }
344
-
345
- function setupAdminJS(projectPath) {
346
- const backoffice = path.join(projectPath, 'src/backoffice');
347
- fs.ensureDirSync(backoffice);
348
-
349
- const backofficeComponents = path.join(backoffice, 'components');
350
- fs.ensureDirSync(backofficeComponents);
351
-
352
- const dashboardContent = getDashboardTemplate();
353
- fs.writeFileSync(path.join(backofficeComponents, 'dashboard.tsx'), dashboardContent);
354
- }
355
-
356
- function getComponentLoaderTemplate() {
357
- const template = path.join(__dirname, 'templates', 'adminjs', 'component-loader.template.js');
358
- return fs.readFileSync(template, 'utf8');
359
- }
360
-
361
- function getDashboardTemplate() {
362
- const template = path.join(__dirname, 'templates', 'adminjs', 'dashboard.template.js');
363
- return fs.readFileSync(template, 'utf8');
364
- }
365
-
366
- function getUtilsTemplate() {
367
- const template = path.join(__dirname, 'templates', 'adminjs', 'utils.template.js');
368
- return fs.readFileSync(template, 'utf8');
369
- }
370
-
371
- function getAdminTemplate() {
372
- const template = path.join(__dirname, 'templates', 'adminjs', 'admin.template.js');
373
- return fs.readFileSync(template, 'utf8');
374
- }
375
-
376
- function getAdminAppModuleTemplate() {
377
- const appModuleTemplatePath = path.join(__dirname, 'templates', 'adminjs', 'adminjs-app.module.template.js');
378
- return fs.readFileSync(appModuleTemplatePath, 'utf8');
379
- }
380
-
381
- function createPermissionsConfig(projectPath) {
382
- const configDir = path.join(projectPath, 'src/config');
383
- fs.ensureDirSync(configDir);
384
-
385
- const templatePath = path.join(__dirname, 'templates', 'permissions.config.template.js');
386
- const permissionsConfigContent = fs.readFileSync(templatePath, 'utf-8');
387
-
388
- fs.writeFileSync(path.join(configDir, 'permissions.config.ts'), permissionsConfigContent);
389
- }
390
-
391
- function createAdminConfig (projectPath) {
392
- const configDir = path.join(projectPath, 'src/config');
393
- fs.ensureDirSync(configDir);
394
-
395
- const templatePath = path.join(__dirname, 'templates', 'admin.config.template.js');
396
- const permissionsConfigContent = fs.readFileSync(templatePath, 'utf-8');
397
-
398
- fs.writeFileSync(path.join(configDir, 'admin.config.ts'), permissionsConfigContent);
399
- }
400
-
401
- function customizePrismaSchema(projectPath, answers) {
402
- const prismaDir = `${projectPath}/prisma`;
403
- const schemaFile = `${prismaDir}/schema.prisma`;
404
-
405
- fs.ensureDirSync(prismaDir);
406
-
407
- const schemaContent = `
408
- datasource db {
409
- provider = "${answers.dbProvider}"
410
- url = env("DB_URL")
411
- }
412
-
413
- generator client {
414
- provider = "prisma-client-js"
415
- }
416
-
417
- generator nestgraphql {
418
- provider = "node node_modules/prisma-nestjs-graphql"
419
- output = "../src/generated/"
420
- }
421
-
422
- model User {
423
- id Int @id @default(autoincrement())
424
- email String @unique
425
- name String?
426
- password String? /// @Role(none)
427
- role Role @default(GUEST)
428
- refreshTokens UserRefreshToken[]
429
- }
430
497
 
431
- model Session {
432
- id String @id
433
- sid String @unique
434
- data String
435
- expiresAt DateTime
436
- userId Int?
498
+ copyAndReplaceTemplates(scaffoldDir, projectPath);
437
499
  }
438
500
 
439
- model UserRefreshToken {
440
- id String @id @default(cuid())
441
- token String @unique @db.VarChar(255)
442
- userId Int
443
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
444
- expiresAt DateTime
445
- createdAt DateTime @default(now())
446
- revokedAt DateTime?
447
-
448
- @@index([userId])
449
- }
450
-
451
- enum Role {
452
- ADMIN
453
- GUEST
454
- }
455
- `;
456
-
457
- fs.writeFileSync(schemaFile, schemaContent);
458
- }
459
-
460
- function createPrismaModule(projectPath) {
461
- const prismaDir = path.join(projectPath, 'src/prisma');
462
- fs.ensureDirSync(prismaDir);
463
-
464
- const templatePath = path.join(__dirname, 'templates', 'prisma.module.template.js');
465
- const prismaModuleContent = fs.readFileSync(templatePath, 'utf-8');
466
-
467
- fs.writeFileSync(path.join(prismaDir, 'prisma.module.ts'), prismaModuleContent);
468
- }
469
-
470
- function addScriptsToPackageJson(projectPath) {
471
- const packageJsonPath = path.join(projectPath, 'package.json');
472
- const packageJson = fs.readJsonSync(packageJsonPath);
473
-
474
- packageJson.scripts = packageJson.scripts || {};
475
- packageJson.scripts["start:dev"] = "cross-env NODE_ENV=development nest start --watch";
476
- packageJson.scripts["start:prod"] = "cross-env NODE_ENV=production node dist/main";
477
- packageJson.scripts["db-pull"] = "npm run db-pull --prefix ./node_modules/appx-core";
478
-
479
- fs.writeJsonSync(packageJsonPath, packageJson, { spaces: 2 });
480
- }
501
+ function initializeGitRepo (projectPath, showOutput = false) {
502
+ // If git is installed in the system, initialize a git repository and make the initial commit
503
+ let installed = true;
504
+ try {
505
+ execSync('git --version', {stdio: 'ignore'});
506
+ } catch (error) {
507
+ console.log('Git is not installed. Skipping git initialization.');
508
+ return;
509
+ }
481
510
 
482
- function initializeGitRepo(projectPath) {
483
511
  try {
484
- execSync('git add .', { cwd: projectPath, stdio: 'ignore' });
485
- execSync('git commit -m "Project init"', { cwd: projectPath, stdio: 'ignore' });
512
+ executeCommand('git init', showOutput);
513
+ executeCommand('git add .', showOutput);
514
+ executeCommand('git commit -m "Project init"', showOutput);
486
515
  } catch (error) {
487
516
  console.error("Failed to initialize git repository or make the initial commit:", error);
517
+ throw error;
488
518
  }
489
519
  }
490
520
 
491
-
492
- function configureProjectSettings(projectPath) {
493
- const prettierConfig = {
494
- singleQuote: true,
495
- trailingComma: 'all',
496
- endOfLine: 'lf',
497
- tabWidth: 2,
498
- semi: true,
499
- bracketSpacing: true,
500
- jsxBracketSameLine: true
501
- };
502
-
503
- fs.writeFileSync(
504
- path.join(projectPath, '.prettierrc'),
505
- JSON.stringify(prettierConfig, null, 2)
506
- );
507
-
508
- const eslintConfig = `module.exports = {
509
- parser: '@typescript-eslint/parser',
510
- parserOptions: {
511
- project: 'tsconfig.json',
512
- tsconfigRootDir: __dirname,
513
- sourceType: 'module',
514
- },
515
- plugins: ['@typescript-eslint/eslint-plugin'],
516
- extends: [
517
- 'plugin:@typescript-eslint/recommended',
518
- 'plugin:prettier/recommended',
519
- ],
520
- root: true,
521
- env: {
522
- node: true,
523
- jest: true,
524
- },
525
- ignorePatterns: ['.eslintrc.js'],
526
- rules: {
527
- '@typescript-eslint/interface-name-prefix': 'off',
528
- '@typescript-eslint/explicit-function-return-type': 'off',
529
- '@typescript-eslint/explicit-module-boundary-types': 'off',
530
- '@typescript-eslint/no-explicit-any': 'off',
531
- 'prettier/prettier': ['error', { 'endOfLine': 'lf' }],
532
- },
533
- };`;
534
-
535
- fs.writeFileSync(path.join(projectPath, '.eslintrc.js'), eslintConfig);
536
-
537
- const gitattributesContent = `
538
- * text=auto eol=lf
539
- `;
540
- fs.writeFileSync(path.join(projectPath, '.gitattributes'), gitattributesContent);
541
- }
542
-
543
- function updateGitignore(projectPath) {
544
- const gitignorePath = path.join(projectPath, '.gitignore');
521
+ async function createGitignore (projectPath) {
545
522
  const envFilesToAdd = ['.env', '.env.production'];
546
- let gitignoreContent = '';
547
- if (fs.existsSync(gitignorePath)) {
548
- gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
549
- } else {
550
- console.log(`.gitignore file not found. Creating a new one.`);
551
- }
552
- const existingLines = gitignoreContent.split('\n');
523
+
524
+ const {defaultGitIgnore} = await import(path.join(projectPath, 'node_modules', '@nestjs/cli/lib/configuration/defaults.js'));
525
+ const existingLines = defaultGitIgnore.split('\n');
553
526
  const updatedLines = new Set(existingLines);
554
527
  envFilesToAdd.forEach((file) => {
555
528
  if (!existingLines.includes(file)) {
@@ -557,42 +530,45 @@ function updateGitignore(projectPath) {
557
530
  }
558
531
  });
559
532
  const finalContent = Array.from(updatedLines).join('\n').trim() + '\n';
533
+
534
+ const gitignorePath = path.join(projectPath, '.gitignore');
560
535
  fs.writeFileSync(gitignorePath, finalContent, 'utf-8');
561
536
  }
562
537
 
538
+ function createTsConfig (projectPath) {
539
+ let tsConfig = fs.readFileSync(path.join(projectPath, 'node_modules/@nestjs/schematics/dist/lib/application/files/ts/tsconfig.json'), 'utf-8');
540
+ // Replace '<%= strict %>' with 'false'
541
+ tsConfig = tsConfig.replaceAll("<%= strict %>", 'false');
542
+
543
+ let tsConfigBuild = fs.readFileSync(path.join(projectPath, 'node_modules/@nestjs/schematics/dist/lib/application/files/ts/tsconfig.build.json'), 'utf-8');
563
544
 
564
- function incrementProgress(showOutput, phase) {
545
+ // Ensure target is ES2017
546
+ tsConfig = JSON.parse(tsConfig);
547
+ if (!tsConfig.compilerOptions) {
548
+ tsConfig.compilerOptions = {};
549
+ }
550
+ tsConfig.compilerOptions.target = 'ES2017';
551
+ tsConfig = JSON.stringify(tsConfig, null, 2);
552
+
553
+ fs.writeFileSync(path.join(projectPath, 'tsconfig.json'), tsConfig);
554
+ fs.writeFileSync(path.join(projectPath, 'tsconfig.build.json'), tsConfigBuild);
555
+ }
556
+
557
+
558
+ function incrementProgress (showOutput, phase) {
565
559
  if (!showOutput) {
566
560
  remainingETA -= phaseDurations[phase];
567
- progressBar.update(progressBar.value + phase === 'end' ? 0 : 1, { remainingETA });
561
+ progressBar.update(progressBar.value + phase === 'end' ? 0 : 1, {remainingETA});
568
562
  }
569
563
  }
570
564
 
571
- function executeCommand(command, showOutput = answers?.showOutput) {
572
- const options = showOutput ? { stdio: 'inherit' } : { stdio: 'ignore' };
565
+ function executeCommand (command, showOutput = answers?.showOutput) {
566
+ const options = showOutput ? {stdio: 'inherit'} : {stdio: 'ignore'};
573
567
  try {
574
568
  execSync(command, options);
575
569
  } catch (error) {
576
570
  console.error(`Failed to execute command: ${command}`, error);
577
- }
578
- }
579
-
580
- function installDependenciesFromManifest(showOutput = true) {
581
- const { dependencies, devDependencies } = dependencyVersions;
582
- const depList = Object.entries(dependencies)
583
- .map(([pkg, version]) => `${pkg}@${version}`)
584
- .join(' ');
585
-
586
- const devDepList = Object.entries(devDependencies)
587
- .map(([pkg, version]) => `${pkg}@${version}`)
588
- .join(' ');
589
-
590
- if (depList) {
591
- executeCommand(`npm install ${depList}`, showOutput);
592
- }
593
-
594
- if (devDepList) {
595
- executeCommand(`npm install -D ${devDepList}`, showOutput);
571
+ throw error;
596
572
  }
597
573
  }
598
574