@rpal/cli 1.0.0 → 1.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.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { CreateNewApp } from '../commands/create.js';
3
+ const app = new CreateNewApp();
4
+ app.run().catch((error) => {
5
+ console.error('❌ Error:', error.message);
6
+ process.exit(1);
7
+ });
@@ -0,0 +1,246 @@
1
+ import gradient from 'gradient-string';
2
+ import { downloadTemplate } from 'giget';
3
+ import { execa } from 'execa';
4
+ import { existsSync, readFileSync, writeFileSync, cpSync, rmSync, readdirSync } from 'fs';
5
+ import { join, isAbsolute } from 'path';
6
+ import { templates, getTemplateByAlias } from '../templates.js';
7
+ const VERSION = '1.0.0';
8
+ export class CreateNewApp {
9
+ constructor() {
10
+ this.projectName = '';
11
+ this.projectDir = '';
12
+ this.selectedTemplate = '';
13
+ this.packageManager = 'npm';
14
+ this.gitInit = false;
15
+ this.verbose = false;
16
+ }
17
+ async run() {
18
+ const args = process.argv.slice(2);
19
+ for (let i = 0; i < args.length; i++) {
20
+ const arg = args[i];
21
+ if (arg === '--template' || arg === '-t') {
22
+ this.selectedTemplate = args[++i];
23
+ }
24
+ else if (arg === '--git' || arg === '-g') {
25
+ this.gitInit = true;
26
+ }
27
+ else if (arg === '--verbose' || arg === '-v') {
28
+ this.verbose = true;
29
+ }
30
+ else if (arg === '--help' || arg === '-h') {
31
+ this.showHelp();
32
+ process.exit(0);
33
+ }
34
+ else if (!arg.startsWith('-')) {
35
+ this.projectName = arg;
36
+ }
37
+ }
38
+ const whichPmRuns = await import('which-pm-runs');
39
+ const pm = whichPmRuns.default();
40
+ this.packageManager = pm?.name || 'npm';
41
+ this.printBanner();
42
+ if (!this.projectName) {
43
+ this.projectName = await this.prompt('project name', 'my-pal-app');
44
+ }
45
+ this.projectDir = isAbsolute(this.projectName)
46
+ ? this.projectName
47
+ : join(process.cwd(), this.projectName);
48
+ if (!this.selectedTemplate) {
49
+ this.selectedTemplate = await this.promptChoice('Select a starter kit', templates.map((t) => ({ name: t.name, value: t.alias })));
50
+ }
51
+ const template = getTemplateByAlias(this.selectedTemplate);
52
+ if (!template) {
53
+ throw new Error(`Unknown template: ${this.selectedTemplate}`);
54
+ }
55
+ await this.downloadTemplate(template.source);
56
+ await this.prepareProject();
57
+ await this.installDependencies();
58
+ if (this.gitInit) {
59
+ await this.initGit();
60
+ }
61
+ this.printSuccess();
62
+ }
63
+ showHelp() {
64
+ console.log(`
65
+ Usage: create-pal [project-name] [options]
66
+
67
+ Options:
68
+ -t, --template <name> Starter kit template (api, admin, client)
69
+ -g, --git Initialize git repository
70
+ -v, --verbose Enable verbose mode
71
+ -h, --help Show this help message
72
+
73
+ Examples:
74
+ create-pal my-project
75
+ create-pal my-project --template api
76
+ create-pal my-project --git
77
+ `);
78
+ }
79
+ printBanner() {
80
+ const title = `
81
+ ╔═══════════════════════════════════════════════════════════╗
82
+ ║ ║
83
+ ║ ███████╗██╗ ██╗███████╗████████╗███████╗███╗ ███╗ ║
84
+ ║ ██╔════╝╚██╗ ██╔╝██╔════╝╚══██╔══╝██╔════╝████╗ ████║ ║
85
+ ║ ███████╗ ╚████╔╝ ███████╗ ██║ █████╗ ██╔████╔██║ ║
86
+ ║ ╚════██║ ╚██╔╝ ╚════██║ ██║ ██╔══╝ ██║╚██╔╝██║ ║
87
+ ║ ███████║ ██║ ███████║ ██║ ███████╗██║ ╚═╝ ██║ ║
88
+ ║ ╚══════╝ ╚═╝ ╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ║
89
+ ║ ║
90
+ ║ ${VERSION} - The batteries-included full-stack framework ║
91
+ ║ ║
92
+ ╚═══════════════════════════════════════════════════════════╝
93
+ `.trim();
94
+ console.log(gradient.pastel.multiline(title));
95
+ console.log();
96
+ }
97
+ async prompt(question, defaultValue) {
98
+ const readline = await import('readline');
99
+ const rl = readline.createInterface({
100
+ input: process.stdin,
101
+ output: process.stdout,
102
+ });
103
+ return new Promise((resolve) => {
104
+ rl.question(`${question} (${defaultValue}): `, (answer) => {
105
+ rl.close();
106
+ resolve(answer.trim() || defaultValue);
107
+ });
108
+ });
109
+ }
110
+ async promptChoice(question, choices) {
111
+ console.log(`\n${question}:`);
112
+ choices.forEach((choice, index) => {
113
+ console.log(` ${index + 1}. ${choice.name}`);
114
+ });
115
+ console.log();
116
+ const answer = await this.prompt('Enter choice number', '1');
117
+ const index = parseInt(answer, 10) - 1;
118
+ if (index >= 0 && index < choices.length) {
119
+ return choices[index].value;
120
+ }
121
+ return choices[0].value;
122
+ }
123
+ async downloadTemplate(source) {
124
+ console.log(`\n📦 Downloading template from: ${source}`);
125
+ if (existsSync(this.projectDir)) {
126
+ const files = readdirSync(this.projectDir);
127
+ if (files.length > 0) {
128
+ rmSync(this.projectDir, { recursive: true, force: true });
129
+ }
130
+ }
131
+ await downloadTemplate(source, {
132
+ dir: this.projectDir,
133
+ registry: false,
134
+ });
135
+ console.log('✅ Template downloaded successfully');
136
+ }
137
+ async prepareProject() {
138
+ console.log('\n🔧 Preparing project...');
139
+ const pkgJsonPath = join(this.projectDir, 'package.json');
140
+ if (existsSync(pkgJsonPath)) {
141
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
142
+ pkgJson.name = this.projectName.replace(/[^a-z0-9-]/gi, '-').toLowerCase();
143
+ writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
144
+ }
145
+ const envExample = join(this.projectDir, '.env.example');
146
+ const envFile = join(this.projectDir, '.env');
147
+ if (existsSync(envExample) && !existsSync(envFile)) {
148
+ cpSync(envExample, envFile);
149
+ }
150
+ const gitignore = join(this.projectDir, '.gitignore');
151
+ const gitignoreContent = `
152
+ node_modules/
153
+ dist/
154
+ build/
155
+ .turbo/
156
+ .env
157
+ *.log
158
+ coverage/
159
+ .vscode/
160
+ .idea/
161
+ `;
162
+ if (existsSync(gitignore)) {
163
+ const existing = readFileSync(gitignore, 'utf-8');
164
+ writeFileSync(gitignore, existing + gitignoreContent);
165
+ }
166
+ else {
167
+ writeFileSync(gitignore, gitignoreContent.trim() + '\n');
168
+ }
169
+ const lockFiles = [
170
+ join(this.projectDir, 'package-lock.json'),
171
+ join(this.projectDir, 'pnpm-lock.yaml'),
172
+ join(this.projectDir, 'yarn.lock'),
173
+ ];
174
+ lockFiles.forEach((file) => {
175
+ if (existsSync(file)) {
176
+ rmSync(file);
177
+ }
178
+ });
179
+ console.log('✅ Project prepared successfully');
180
+ }
181
+ async installDependencies() {
182
+ if (!existsSync(join(this.projectDir, 'package.json'))) {
183
+ console.log('⚠️ No package.json found, skipping install');
184
+ return;
185
+ }
186
+ console.log(`\n📦 Installing dependencies using ${this.packageManager}...`);
187
+ const spinner = this.spinner('Installing');
188
+ try {
189
+ await execa(this.packageManager, ['install'], {
190
+ cwd: this.projectDir,
191
+ stdio: this.verbose ? 'inherit' : 'pipe',
192
+ });
193
+ spinner.stop();
194
+ console.log('✅ Dependencies installed successfully');
195
+ }
196
+ catch (error) {
197
+ spinner.stop();
198
+ throw new Error(`Failed to install dependencies: ${error.message}`);
199
+ }
200
+ }
201
+ async initGit() {
202
+ console.log('\n📂 Initializing git repository...');
203
+ try {
204
+ await execa('git', ['init'], {
205
+ cwd: this.projectDir,
206
+ stdio: this.verbose ? 'inherit' : 'pipe',
207
+ });
208
+ console.log('✅ Git repository initialized');
209
+ }
210
+ catch {
211
+ console.log('⚠️ Failed to initialize git (git may not be installed)');
212
+ }
213
+ }
214
+ printSuccess() {
215
+ const message = `
216
+ ╔═══════════════════════════════════════════════════════════╗
217
+ ║ ║
218
+ ║ ✅ Your Pal project has been created successfully! ║
219
+ ║ ║
220
+ ╚═══════════════════════════════════════════════════════════╝
221
+
222
+ Next steps:
223
+ cd ${this.projectName}
224
+ npm run dev
225
+
226
+ Documentation: https://github.com/paljs/paljs
227
+ `.trim();
228
+ console.log(gradient.pastel.multiline(message));
229
+ }
230
+ spinner(message) {
231
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
232
+ let i = 0;
233
+ const interval = setInterval(() => {
234
+ process.stdout.write(`\r${frames[i++ % frames.length]} ${message}`);
235
+ }, 80);
236
+ return {
237
+ stop: () => {
238
+ clearInterval(interval);
239
+ process.stdout.write('\r' + ' '.repeat(50) + '\r');
240
+ },
241
+ update: (msg) => {
242
+ process.stdout.write(`\r${msg}`);
243
+ },
244
+ };
245
+ }
246
+ }
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  const VERSION = '1.0.0';
5
- console.log(`
5
+ const banner = `
6
6
  ╔═══════════════════════════════════════════╗
7
7
  ║ ║
8
8
  ║ ██████╗ ███████╗██╗ ██╗███████╗ ║
@@ -15,7 +15,7 @@ console.log(`
15
15
  ║ CLI Tool v${VERSION} ║
16
16
  ║ ║
17
17
  ╚═══════════════════════════════════════════╝
18
- `);
18
+ `;
19
19
  const args = process.argv.slice(2);
20
20
  const command = args[0];
21
21
  if (!command || command === 'help') {
@@ -26,65 +26,99 @@ switch (command) {
26
26
  case 'make:model':
27
27
  makeModel(args.slice(1));
28
28
  break;
29
- case 'make:repository':
30
- makeRepository(args.slice(1));
31
- break;
32
29
  case 'make:service':
33
30
  makeService(args.slice(1));
34
31
  break;
35
- case 'make:migration':
36
- makeMigration(args.slice(1));
37
- break;
38
32
  case 'make:controller':
39
33
  makeController(args.slice(1));
40
34
  break;
35
+ case 'make:middleware':
36
+ makeMiddleware(args.slice(1));
37
+ break;
38
+ case 'make:migration':
39
+ makeMigration(args.slice(1));
40
+ break;
41
41
  case 'make:factory':
42
42
  makeFactory(args.slice(1));
43
43
  break;
44
- case 'init':
45
- initProject(args.slice(1));
44
+ case 'make:exception':
45
+ makeException(args.slice(1));
46
+ break;
47
+ case 'make:validator':
48
+ makeValidator(args.slice(1));
49
+ break;
50
+ case 'db:migrate':
51
+ dbMigrate();
46
52
  break;
47
- case 'db:push':
48
- dbPush();
53
+ case 'db:rollback':
54
+ dbRollback();
49
55
  break;
50
56
  case 'db:seed':
51
57
  dbSeed();
52
58
  break;
59
+ case 'db:fresh':
60
+ dbFresh();
61
+ break;
62
+ case 'route:list':
63
+ routeList();
64
+ break;
65
+ case 'key:generate':
66
+ keyGenerate();
67
+ break;
68
+ case 'list':
69
+ listRoutes();
70
+ break;
53
71
  default:
54
72
  console.log(`❌ Unknown command: ${command}`);
55
73
  console.log(` Run 'pal help' for available commands`);
56
74
  process.exit(1);
57
75
  }
58
76
  function showHelp() {
77
+ console.log(banner);
59
78
  console.log(`
60
79
  Usage: pal <command> [options]
61
80
 
81
+ Project Commands:
82
+ npm create pal@latest Create a new Pal project
83
+ Run: npm create pal@latest my-project
84
+
62
85
  Make Commands:
63
- make:model <name> Create a new model (entity + repository)
64
- make:repository <name> Create a new repository
65
- make:service <name> Create a new service
66
- make:controller <name> Create a new controller
67
- make:migration <name> Create a new migration
68
- make:factory <name> Create a model factory
86
+ make:model <name> Create a new model
87
+ make:service <name> Create a new service
88
+ make:controller <name> Create a new controller
89
+ make:middleware <name> Create a new middleware
90
+ make:migration <name> Create a new migration
91
+ make:factory <name> Create a model factory
92
+ make:exception <name> Create an exception class
93
+ make:validator <name> Create a validator
69
94
 
70
95
  Database Commands:
71
- db:push Push schema to database
72
- db:seed Run database seeds
96
+ db:migrate Run database migrations
97
+ db:rollback Rollback the last migration
98
+ db:seed Run database seeders
99
+ db:fresh Drop all tables and re-run migrations
73
100
 
74
- Project Commands:
75
- init [name] Initialize a new Pal project
101
+ Utility Commands:
102
+ route:list List all registered routes
103
+ key:generate Generate a new app key
104
+ list Alias for route:list
76
105
 
77
106
  Options:
78
- --resource Generate CRUD methods (for model/repository/controller)
79
- --api Generate API-ready controller
80
- --force Overwrite existing files
107
+ --resource Generate CRUD methods
108
+ --api Generate API-ready controller
109
+ --http Create HTTP controller
110
+ --force Overwrite existing files
81
111
 
82
112
  Examples:
113
+ npm create pal@latest my-project
83
114
  pal make:model User
84
- pal make:repository Post --resource
115
+ pal make:service UserService
85
116
  pal make:controller UserController --api
86
- pal init my-api
87
- `);
117
+ pal make:middleware Auth
118
+ pal make:migration create_users_table
119
+ pal db:migrate
120
+ pal route:list
121
+ `);
88
122
  }
89
123
  function makeModel(args) {
90
124
  const name = args[0];
@@ -95,61 +129,60 @@ function makeModel(args) {
95
129
  }
96
130
  const options = parseOptions(args);
97
131
  const fileName = toKebabCase(name);
98
- const dir = path.join(process.cwd(), 'src', 'models', fileName);
99
- ensureDir(dir);
100
- const entityCode = generateEntity(name, options);
101
- const repositoryCode = generateRepository(name, options);
102
- writeFile(path.join(dir, 'index.ts'), entityCode);
103
- writeFile(path.join(dir, 'repository.ts'), repositoryCode);
104
- console.log(`✅ Created model: src/models/${fileName}/`);
105
- console.log(` - index.ts`);
106
- console.log(` - repository.ts`);
107
- }
108
- function makeRepository(args) {
109
- const name = args[0];
110
- if (!name) {
111
- console.log('❌ Please provide a repository name');
112
- process.exit(1);
113
- }
114
- const options = parseOptions(args);
115
- const fileName = toKebabCase(name);
116
- const dir = path.join(process.cwd(), 'src', 'repositories');
132
+ const dir = path.join(process.cwd(), 'app', 'models', fileName);
117
133
  ensureDir(dir);
118
- const code = generateRepository(name, options);
119
- writeFile(path.join(dir, `${fileName}.repository.ts`), code);
120
- console.log(`✅ Created repository: src/repositories/${fileName}.repository.ts`);
134
+ const modelCode = generateModel(name, options);
135
+ writeFile(path.join(dir, 'index.ts'), modelCode);
136
+ console.log(`✅ Created model: app/models/${fileName}/index.ts`);
121
137
  }
122
138
  function makeService(args) {
123
139
  const name = args[0];
124
140
  if (!name) {
125
141
  console.log('❌ Please provide a service name');
142
+ console.log(' Example: pal make:service UserService');
126
143
  process.exit(1);
127
144
  }
128
145
  const fileName = toKebabCase(name);
129
- const dir = path.join(process.cwd(), 'src', 'services');
146
+ const dir = path.join(process.cwd(), 'app', 'services');
130
147
  ensureDir(dir);
131
148
  const code = generateService(name);
132
149
  writeFile(path.join(dir, `${fileName}.service.ts`), code);
133
- console.log(`✅ Created service: src/services/${fileName}.service.ts`);
150
+ console.log(`✅ Created service: app/services/${fileName}.service.ts`);
134
151
  }
135
152
  function makeController(args) {
136
153
  const name = args[0];
137
154
  if (!name) {
138
155
  console.log('❌ Please provide a controller name');
156
+ console.log(' Example: pal make:controller UserController');
139
157
  process.exit(1);
140
158
  }
141
159
  const options = parseOptions(args);
142
160
  const fileName = toKebabCase(name);
143
- const dir = path.join(process.cwd(), 'src', 'controllers');
161
+ const dir = path.join(process.cwd(), 'app', 'controllers', 'http');
144
162
  ensureDir(dir);
145
163
  const code = generateController(name, options);
146
164
  writeFile(path.join(dir, `${fileName}.controller.ts`), code);
147
- console.log(`✅ Created controller: src/controllers/${fileName}.controller.ts`);
165
+ console.log(`✅ Created controller: app/controllers/http/${fileName}.controller.ts`);
166
+ }
167
+ function makeMiddleware(args) {
168
+ const name = args[0];
169
+ if (!name) {
170
+ console.log('❌ Please provide a middleware name');
171
+ console.log(' Example: pal make:middleware Auth');
172
+ process.exit(1);
173
+ }
174
+ const fileName = toKebabCase(name);
175
+ const dir = path.join(process.cwd(), 'app', 'middleware');
176
+ ensureDir(dir);
177
+ const code = generateMiddleware(name);
178
+ writeFile(path.join(dir, `${fileName}.ts`), code);
179
+ console.log(`✅ Created middleware: app/middleware/${fileName}.ts`);
148
180
  }
149
181
  function makeMigration(args) {
150
182
  const name = args[0];
151
183
  if (!name) {
152
184
  console.log('❌ Please provide a migration name');
185
+ console.log(' Example: pal make:migration create_users_table');
153
186
  process.exit(1);
154
187
  }
155
188
  const timestamp = Date.now();
@@ -164,6 +197,7 @@ function makeFactory(args) {
164
197
  const name = args[0];
165
198
  if (!name) {
166
199
  console.log('❌ Please provide a factory name');
200
+ console.log(' Example: pal make:factory UserFactory');
167
201
  process.exit(1);
168
202
  }
169
203
  const fileName = toKebabCase(name);
@@ -173,68 +207,65 @@ function makeFactory(args) {
173
207
  writeFile(path.join(dir, `${fileName}.factory.ts`), code);
174
208
  console.log(`✅ Created factory: database/factories/${fileName}.factory.ts`);
175
209
  }
176
- function initProject(args) {
177
- const name = args[0] || 'my-pal-app';
178
- console.log(`📦 Initializing Pal project: ${name}...`);
179
- const dir = path.join(process.cwd(), name);
210
+ function makeException(args) {
211
+ const name = args[0];
212
+ if (!name) {
213
+ console.log('❌ Please provide an exception name');
214
+ console.log(' Example: pal make:exception NotFoundException');
215
+ process.exit(1);
216
+ }
217
+ const fileName = toKebabCase(name);
218
+ const dir = path.join(process.cwd(), 'app', 'exceptions');
219
+ ensureDir(dir);
220
+ const code = generateException(name);
221
+ writeFile(path.join(dir, `${fileName}.ts`), code);
222
+ console.log(`✅ Created exception: app/exceptions/${fileName}.ts`);
223
+ }
224
+ function makeValidator(args) {
225
+ const name = args[0];
226
+ if (!name) {
227
+ console.log('❌ Please provide a validator name');
228
+ console.log(' Example: pal make:validator CreateUserValidator');
229
+ process.exit(1);
230
+ }
231
+ const fileName = toKebabCase(name);
232
+ const dir = path.join(process.cwd(), 'app', 'validators');
180
233
  ensureDir(dir);
181
- ensureDir(path.join(dir, 'src', 'models'));
182
- ensureDir(path.join(dir, 'src', 'services'));
183
- ensureDir(path.join(dir, 'src', 'repositories'));
184
- ensureDir(path.join(dir, 'src', 'controllers'));
185
- ensureDir(path.join(dir, 'database', 'migrations'));
186
- ensureDir(path.join(dir, 'database', 'factories'));
187
- ensureDir(path.join(dir, 'tests'));
188
- const pkgJson = {
189
- name: name,
190
- version: '0.0.1',
191
- type: 'module',
192
- scripts: {
193
- dev: 'pal serve',
194
- build: 'pal build',
195
- test: 'pal test',
196
- 'db:push': 'pal db:push',
197
- 'db:seed': 'pal db:seed',
198
- },
199
- dependencies: {
200
- '@paljs/core': 'workspace:*',
201
- '@paljs/orm': 'workspace:*',
202
- '@paljs/http': 'workspace:*',
203
- },
204
- devDependencies: {
205
- 'typescript': '^5.0.0',
206
- '@types/node': '^20.0.0',
207
- 'tsx': '^4.0.0',
208
- },
209
- };
210
- const tsconfig = {
211
- compilerOptions: {
212
- target: 'ES2022',
213
- module: 'ESNext',
214
- moduleResolution: 'bundler',
215
- experimentalDecorators: true,
216
- emitDecoratorMetadata: true,
217
- strict: true,
218
- esModuleInterop: true,
219
- skipLibCheck: true,
220
- outDir: 'dist',
221
- },
222
- include: ['src/**/*'],
223
- };
224
- writeFile(path.join(dir, 'package.json'), JSON.stringify(pkgJson, null, 2));
225
- writeFile(path.join(dir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
226
- writeFile(path.join(dir, 'src', 'index.ts'), '// Welcome to Pal!\n\n');
227
- console.log(`✅ Project created: ${name}/`);
228
- console.log(` Run: cd ${name} && pnpm install && pnpm dev`);
229
- }
230
- function dbPush() {
231
- console.log('🔄 Pushing schema to database...');
232
- console.log(' (This would sync your models to the database)');
234
+ const code = generateValidator(name);
235
+ writeFile(path.join(dir, `${fileName}.ts`), code);
236
+ console.log(`✅ Created validator: app/validators/${fileName}.ts`);
237
+ }
238
+ function dbMigrate() {
239
+ console.log('🔄 Running database migrations...');
240
+ console.log(' (This would run all pending migrations)');
241
+ }
242
+ function dbRollback() {
243
+ console.log('⏪ Rolling back last migration...');
244
+ console.log(' (This would rollback the last migration)');
233
245
  }
234
246
  function dbSeed() {
235
247
  console.log('🌱 Running database seeds...');
236
248
  console.log(' (This would seed your database with initial data)');
237
249
  }
250
+ function dbFresh() {
251
+ console.log('⚠️ Dropping all tables and re-running migrations...');
252
+ console.log(' (This would drop all tables and run migrations)');
253
+ }
254
+ function routeList() {
255
+ console.log('\n📋 Registered Routes:');
256
+ console.log(' (No routes registered yet)');
257
+ console.log('\n Define routes in: start/routes.ts\n');
258
+ }
259
+ function keyGenerate() {
260
+ const { randomBytes } = require('crypto');
261
+ const key = randomBytes(32).toString('hex');
262
+ console.log(`\n🔑 Application Key: ${key}\n`);
263
+ console.log('Add this to your .env file:');
264
+ console.log(`APP_KEY=${key}\n`);
265
+ }
266
+ function listRoutes() {
267
+ routeList();
268
+ }
238
269
  function parseOptions(args) {
239
270
  return args
240
271
  .filter(a => a.startsWith('--'))
@@ -265,180 +296,157 @@ function writeFile(filePath, content) {
265
296
  }
266
297
  fs.writeFileSync(filePath, content);
267
298
  }
268
- function generateEntity(name, options) {
299
+ function generateModel(name, options) {
269
300
  const camelName = toCamelCase(name);
270
301
  const hasResource = options.resource;
271
- return `import { BaseEntity } from '@paljs/orm';
302
+ return `import { BaseEntity } from '@rpal/orm';
272
303
 
273
304
  export interface ${name} extends BaseEntity {
274
- // Add your fields here
275
- name: string;
276
- ${hasResource ? `
277
- // Common fields for CRUD:
278
- // email: string;
279
- // status: 'active' | 'inactive';
280
- // description?: string;
281
- // createdAt: Date;
282
- // updatedAt: Date;` : ''}
305
+ ${hasResource ? `name: string;
306
+ email: string;
307
+ status: 'active' | 'inactive';
308
+ description?: string;` : `// Define your ${name} model fields here
309
+ // Example:
310
+ // name: string;
311
+ // email?: string;`}
283
312
  }
284
313
 
285
314
  export interface Create${name}DTO {
286
- name: string;
287
- ${hasResource ? `// email: string;
288
- // status?: 'active' | 'inactive';
289
- // description?: string;` : ''}
315
+ ${hasResource ? `name: string;
316
+ email: string;
317
+ description?: string;` : `// Define create DTO fields here
318
+ // Example:
319
+ // name: string;`}
290
320
  }
291
321
 
292
322
  export interface Update${name}DTO {
293
- name?: string;
294
- ${hasResource ? `// email?: string;
295
- // status?: 'active' | 'inactive';
296
- // description?: string;` : ''}
323
+ ${hasResource ? `name?: string;
324
+ email?: string;
325
+ status?: 'active' | 'inactive';
326
+ description?: string;` : `// Define update DTO fields here
327
+ // Example:
328
+ // name?: string;`}
297
329
  }
298
- `;
299
- }
300
- function generateRepository(name, options) {
301
- const camelName = toCamelCase(name);
302
- const hasResource = options.resource;
303
- let extraMethods = '';
304
- if (hasResource) {
305
- extraMethods = `
306
- async findByEmail(email: string): Promise<${name} | null> {
307
- return this.findOne({ email });
308
- }
309
-
310
- async findActive(): Promise<${name}[] | null> {
311
- return this.findAll({ status: 'active' });
312
- }
313
-
314
- async exists(email: string, excludeId?: string): Promise<boolean> {
315
- const existing = await this.findByEmail(email);
316
- if (!existing) return false;
317
- if (excludeId && existing.id === excludeId) return false;
318
- return true;
319
- }`;
320
- }
321
- return `import { BaseRepository } from '@paljs/orm';
322
- import { ${name}, Create${name}DTO, Update${name}DTO } from './index';
323
330
 
324
- export class ${name}Repository extends BaseRepository<${name}, Create${name}DTO, Update${name}DTO> {
325
- protected toEntity(data: Create${name}DTO): ${name} {
326
- const now = new Date();
327
- return {
328
- id: this.generateId(),
329
- name: data.name,
330
- ${hasResource ? `// email: data.email,
331
- // status: data.status || 'active',` : ''}
332
- createdAt: now,
333
- };
334
- }
335
-
336
- protected toUpdateData(data: Update${name}DTO): Partial<${name}> {
337
- return {
338
- name: data.name,
339
- ${hasResource ? `// email: data.email,
340
- // status: data.status,
341
- // updatedAt: new Date(),` : ''}
342
- };
343
- }${extraMethods}
344
- }
331
+ export const ${name}Token = Symbol('${name}');
345
332
  `;
346
333
  }
347
334
  function generateService(name) {
348
335
  const camelName = toCamelCase(name);
349
- const repoName = `${name}Repository`;
350
- return `import { Injectable } from '@paljs/core';
351
- import { ${repoName} } from '../repositories/${toKebabCase(name)}.repository';
352
- import { Create${name}DTO, Update${name}DTO } from '../models/${toKebabCase(name)}';
336
+ const modelName = `${name}Model`;
337
+ const modelFileName = toKebabCase(name);
338
+ return `import { Injectable, Inject } from '@rpal/core';
339
+ import { ${modelName}, ${modelName}Token } from '#models/${modelFileName}';
353
340
 
354
341
  @Injectable()
355
342
  export class ${name}Service {
356
- constructor(private ${camelName}Repo: ${repoName}) {}
343
+ constructor(
344
+ @Inject(${modelName}Token) private ${camelName}Model: ${modelName}
345
+ ) {}
357
346
 
358
347
  async findAll() {
359
- return this.${camelName}Repo.findAll();
348
+ return this.${camelName}Model.findAll();
360
349
  }
361
350
 
362
- async findOne(id: string) {
363
- return this.${camelName}Repo.findById(id);
351
+ async findById(id: string) {
352
+ return this.${camelName}Model.findById(id);
364
353
  }
365
354
 
366
- async create(data: Create${name}DTO) {
367
- return this.${camelName}Repo.create(data);
355
+ async create(data: Partial<${modelName}>) {
356
+ return this.${camelName}Model.create(data);
368
357
  }
369
358
 
370
- async update(id: string, data: Update${name}DTO) {
371
- return this.${camelName}Repo.update(id, data);
359
+ async update(id: string, data: Partial<${modelName}>) {
360
+ return this.${camelName}Model.update(id, data);
372
361
  }
373
362
 
374
363
  async delete(id: string) {
375
- return this.${camelName}Repo.delete(id);
364
+ return this.${camelName}Model.delete(id);
376
365
  }
377
366
  }
378
367
  `;
379
368
  }
380
369
  function generateController(name, options) {
381
370
  const camelName = toCamelCase(name);
371
+ const serviceName = `${name}Service`;
372
+ const fileName = toKebabCase(name);
382
373
  const hasApi = options.api;
383
374
  const hasResource = options.resource;
384
- const kebabName = toKebabCase(name);
385
375
  let crudMethods = '';
386
376
  if (hasApi) {
387
377
  crudMethods = `
388
- async index() {
389
- const ${camelName}s = await this.${camelName}Service.findAll();
390
- return { data: ${camelName}s };
378
+ async index({ request, response }: HttpContext) {
379
+ const data = await this.${camelName}Service.findAll();
380
+ return response.json({ data });
391
381
  }
392
382
 
393
- async show(id: string) {
394
- const ${camelName} = await this.${camelName}Service.findOne(id);
383
+ async show({ params, response }: HttpContext) {
384
+ const ${camelName} = await this.${camelName}Service.findById(params.id);
395
385
  if (!${camelName}) {
396
- throw new Error('${name} not found');
386
+ return response.notFound({ message: '${name} not found' });
397
387
  }
398
- return { data: ${camelName} };
388
+ return response.json({ data: ${camelName} });
399
389
  }
400
390
 
401
- async store(data: any) {
391
+ async store({ request, response }: HttpContext) {
392
+ const data = request.body();
402
393
  const ${camelName} = await this.${camelName}Service.create(data);
403
- return { data: ${camelName}, status: 201 };
394
+ return response.created({ data: ${camelName} });
404
395
  }
405
396
 
406
- async update(id: string, data: any) {
407
- const ${camelName} = await this.${camelName}Service.update(id, data);
408
- return { data: ${camelName} };
397
+ async update({ params, request, response }: HttpContext) {
398
+ const data = request.body();
399
+ const ${camelName} = await this.${camelName}Service.update(params.id, data);
400
+ if (!${camelName}) {
401
+ return response.notFound({ message: '${name} not found' });
402
+ }
403
+ return response.json({ data: ${camelName} });
409
404
  }
410
405
 
411
- async destroy(id: string) {
412
- await this.${camelName}Service.delete(id);
413
- return { status: 204 };
406
+ async destroy({ params, response }: HttpContext) {
407
+ await this.${camelName}Service.delete(params.id);
408
+ return response.noContent();
414
409
  }`;
415
410
  }
416
- if (hasResource || hasApi) {
417
- return `import { Injectable } from '@paljs/core';
418
- import { ${name}Service } from '../services/${kebabName}.service';
411
+ return `import type { HttpContext } from '@rpal/http';
412
+ import { Injectable } from '@rpal/core';
413
+ import { ${serviceName} } from '#services/${fileName}.service';
419
414
 
420
415
  @Injectable()
421
416
  export class ${name}Controller {
422
- constructor(private ${camelName}Service: ${name}Service) {} ${crudMethods}
417
+ constructor(private ${camelName}Service: ${serviceName}) {}${crudMethods}
423
418
  }
424
419
  `;
425
- }
426
- return `import { Injectable } from '@paljs/core';
420
+ }
421
+ function generateMiddleware(name) {
422
+ const fileName = toKebabCase(name);
423
+ return `import type { HttpContext } from '@rpal/http';
424
+ import { Middleware } from '@rpal/http';
427
425
 
428
- @Injectable()
429
- export class ${name}Controller {
430
- async index() {
431
- return { data: [] };
426
+ export class ${name}Middleware implements Middleware {
427
+ async handle({ request, response }: HttpContext, next: () => Promise<void>) {
428
+ // Add your middleware logic here
429
+ console.log('${name}Middleware: Processing request', request.url());
430
+
431
+ // Example: Check for authorization header
432
+ // const authHeader = request.header('Authorization');
433
+ // if (!authHeader) {
434
+ // return response.unauthorized({ message: 'Unauthorized' });
435
+ // }
436
+
437
+ await next();
432
438
  }
433
439
  }
434
440
  `;
435
441
  }
436
442
  function generateMigration(name) {
437
- const tableName = toKebabCase(name) + 's';
438
- return `import { Migration } from '@paljs/orm';
443
+ const tableName = toKebabCase(name);
444
+ return `import type { Migration } from '@rpal/orm';
439
445
 
440
- export class Create${name}Table implements Migration {
441
- up() {
446
+ export default class ${toPascalCase(name)}Migration implements Migration {
447
+ name = '${Date.now()}_${tableName}';
448
+
449
+ async up() {
442
450
  return \`
443
451
  CREATE TABLE ${tableName} (
444
452
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -449,20 +457,55 @@ export class Create${name}Table implements Migration {
449
457
  \`;
450
458
  }
451
459
 
452
- down() {
453
- return \`DROP TABLE ${tableName}\`;
460
+ async down() {
461
+ return \`DROP TABLE IF EXISTS ${tableName}\`;
454
462
  }
455
463
  }
456
464
  `;
457
465
  }
458
466
  function generateFactory(name) {
459
- return `import { Factory } from '@paljs/test';
460
- import { ${name} } from '../models/${toKebabCase(name)}';
467
+ const modelFileName = toKebabCase(name);
468
+ return `import { Factory } from '@rpal/palu';
469
+ import { ${name}Model } from '#models/${modelFileName}';
461
470
 
462
- export const ${name}Factory = Factory.define<${name}>(({ sequence }) => ({
471
+ export const ${name}Factory = Factory.define<${name}Model>(({ sequence }) => ({
463
472
  id: \`seed-\${sequence}\`,
464
473
  name: \`Test ${name} \${sequence}\`,
465
474
  createdAt: new Date(),
466
475
  }));
467
476
  `;
468
477
  }
478
+ function generateException(name) {
479
+ return `import { Exception } from '@rpal/http';
480
+
481
+ export class ${name} extends Exception {
482
+ constructor(
483
+ message: string = '${name} error',
484
+ status: number = 500
485
+ ) {
486
+ super(message, status);
487
+ this.name = '${name}';
488
+ }
489
+ }
490
+ `;
491
+ }
492
+ function generateValidator(name) {
493
+ const modelFileName = toKebabCase(name).replace('-', '');
494
+ return `import { z } from 'zod';
495
+
496
+ export const ${name}Schema = z.object({
497
+ // Define your validation schema here
498
+ // Example:
499
+ // name: z.string().min(1).max(255),
500
+ // email: z.string().email(),
501
+ });
502
+
503
+ export type ${name}Input = z.infer<typeof ${name}Schema>;
504
+ `;
505
+ }
506
+ function toPascalCase(str) {
507
+ return str
508
+ .split(/[-_\s]+/)
509
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
510
+ .join('');
511
+ }
@@ -0,0 +1,26 @@
1
+ export const templates = [
2
+ {
3
+ name: 'API',
4
+ alias: 'api',
5
+ hint: 'Type-safe REST API with authentication and database',
6
+ source: 'github:paljs/starter-kits/api',
7
+ },
8
+ {
9
+ name: 'Admin',
10
+ alias: 'admin',
11
+ hint: 'Admin dashboard with authentication and UI components',
12
+ source: 'github:paljs/starter-kits/admin',
13
+ },
14
+ {
15
+ name: 'Client',
16
+ alias: 'client',
17
+ hint: 'Full-stack client application with frontend and API',
18
+ source: 'github:paljs/starter-kits/client',
19
+ },
20
+ ];
21
+ export function getTemplateByAlias(alias) {
22
+ return templates.find((t) => t.alias === alias);
23
+ }
24
+ export function getTemplateByName(name) {
25
+ return templates.find((t) => t.name.toLowerCase() === name.toLowerCase());
26
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rpal/cli",
3
- "version": "1.0.0",
4
- "description": "CLI tool for PalJS",
3
+ "version": "1.1.0",
4
+ "description": "CLI tool for PalJS - The batteries-included full-stack framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -16,15 +16,15 @@
16
16
  "files": [
17
17
  "dist"
18
18
  ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "dev": "tsx src/index.ts"
22
+ },
19
23
  "dependencies": {
20
- "@rpal/core": "^1.0.0"
24
+ "@rpal/core": "workspace:*"
21
25
  },
22
26
  "devDependencies": {
23
27
  "@types/node": "^20.0.0",
24
28
  "tsx": "^4.0.0"
25
- },
26
- "scripts": {
27
- "build": "tsc -p tsconfig.json",
28
- "dev": "tsx src/index.ts"
29
29
  }
30
- }
30
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 paljs
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.