webspresso 0.0.100 → 0.0.101

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.
Files changed (65) hide show
  1. package/.agents/ADMIN.md +47 -4
  2. package/.agents/AGENTS.md +2 -0
  3. package/.agents/AUTH.md +63 -0
  4. package/.agents/CLI.md +81 -0
  5. package/.agents/EMAIL.md +84 -2
  6. package/.agents/EVENTS.md +179 -0
  7. package/.agents/ORM.md +59 -0
  8. package/.agents/ROUTING.md +64 -1
  9. package/AGENTS.md +1 -0
  10. package/bin/commands/db-makemigrations.js +83 -0
  11. package/bin/commands/list.js +96 -0
  12. package/bin/commands/make-command.js +93 -0
  13. package/bin/commands/make-event.js +80 -0
  14. package/bin/commands/make-factory.js +72 -0
  15. package/bin/commands/make-mail.js +93 -0
  16. package/bin/commands/make-policy.js +106 -0
  17. package/bin/commands/run.js +195 -0
  18. package/bin/webspresso.js +17 -0
  19. package/core/auth/bouncer.js +196 -0
  20. package/core/auth/index.js +10 -0
  21. package/core/auth/middleware.js +15 -1
  22. package/core/commands/define-command.js +48 -0
  23. package/core/commands/index.js +18 -0
  24. package/core/commands/registry.js +156 -0
  25. package/core/commands/runner.js +76 -0
  26. package/core/compression/compressible.js +1 -1
  27. package/core/compression/middleware.js +35 -251
  28. package/core/content/service.js +8 -2
  29. package/core/events/discovery.js +142 -0
  30. package/core/events/dispatcher.js +552 -0
  31. package/core/events/index.js +23 -0
  32. package/core/events/signal.js +92 -0
  33. package/core/forms/flash.js +234 -0
  34. package/core/forms/form-builder.js +214 -0
  35. package/core/forms/index.js +16 -0
  36. package/core/mail/index.js +23 -0
  37. package/core/mail/mailable.js +250 -0
  38. package/core/mail/mailer.js +184 -0
  39. package/core/mail/transports.js +238 -0
  40. package/core/orm/factory.js +250 -0
  41. package/core/orm/index.js +6 -0
  42. package/core/orm/migrations/diff.js +185 -0
  43. package/core/orm/migrations/index.js +22 -4
  44. package/core/orm/model.js +4 -0
  45. package/index.d.ts +266 -0
  46. package/index.js +40 -0
  47. package/package.json +5 -1
  48. package/plugins/admin-panel/api.js +509 -3
  49. package/plugins/admin-panel/index.js +17 -0
  50. package/plugins/content/models/content-entry.js +4 -3
  51. package/plugins/content/models/content-type.js +4 -3
  52. package/plugins/cors.js +2 -2
  53. package/plugins/email/index.js +48 -0
  54. package/plugins/email/preview-handler.js +555 -0
  55. package/plugins/email/service.js +23 -12
  56. package/plugins/email/transport.js +30 -11
  57. package/plugins/redirect/index.js +1 -1
  58. package/src/api/api-loader.js +1 -1
  59. package/src/app-context.js +48 -6
  60. package/src/dev-reload/index.js +9 -1
  61. package/src/discovery/scan-routes.js +10 -1
  62. package/src/helpers.js +143 -0
  63. package/src/pages/page-loader.js +1 -1
  64. package/src/routing/cascading.js +14 -10
  65. package/src/server.js +180 -26
@@ -0,0 +1,83 @@
1
+ /**
2
+ * DB MakeMigrations Command (Django-inspired)
3
+ * Auto-detect model schema changes against live database and generate migrations
4
+ */
5
+
6
+ const path = require('path');
7
+ const { loadDbConfig, createDbInstance } = require('../utils/db');
8
+ const { makeMigrations } = require('../../core/orm/migrations/diff');
9
+
10
+ function registerCommand(program) {
11
+ program
12
+ .command('db:makemigrations')
13
+ .alias('makemigrations')
14
+ .description('Auto-detect model changes against live database and generate migration files')
15
+ .option('-c, --config <path>', 'Path to database config file (webspresso.db.js or knexfile.js)')
16
+ .option('-m, --models <path>', 'Models directory (overrides config.models)')
17
+ .option('--migrations <path>', 'Migrations directory (overrides config.migrations.directory)')
18
+ .option('-n, --name <name>', 'Custom migration name override')
19
+ .option('--include-drops', 'Generate dropColumn operations for columns removed from models')
20
+ .option('--dry-run', 'Preview migrations that would be created without writing files')
21
+ .action(async (options) => {
22
+ let knex = null;
23
+ try {
24
+ const { config, path: configPath } = loadDbConfig(options.config);
25
+ console.log(`\nšŸ“¦ Using config: ${configPath}\n`);
26
+
27
+ knex = await createDbInstance(config);
28
+
29
+ const modelsDir = options.models || config.models || './models';
30
+ const migrationDir = path.resolve(
31
+ process.cwd(),
32
+ options.migrations || config.migrations?.directory || './migrations'
33
+ );
34
+
35
+ console.log(`šŸ” Inspecting database and comparing with models in ${modelsDir}...`);
36
+
37
+ const result = await makeMigrations({
38
+ knex,
39
+ modelsDir,
40
+ migrationDir,
41
+ dryRun: Boolean(options.dryRun),
42
+ includeDrops: Boolean(options.includeDrops),
43
+ name: options.name || null,
44
+ });
45
+
46
+ if (!result.hasChanges) {
47
+ console.log('\n✨ No changes detected in models. Database schema is up to date.\n');
48
+ return;
49
+ }
50
+
51
+ const verb = options.dryRun ? 'Would generate' : 'Generated';
52
+ console.log(`\nāœ… ${verb} ${result.created.length} migration(s):\n`);
53
+
54
+ for (const change of result.created) {
55
+ console.log(` šŸ“„ ${change.file}`);
56
+ console.log(` Model: ${change.model} (table: ${change.table})`);
57
+ console.log(` Type: ${change.type}`);
58
+ if (change.addedColumns?.length > 0) {
59
+ console.log(` + Added column(s): ${change.addedColumns.join(', ')}`);
60
+ }
61
+ if (change.removedColumns?.length > 0) {
62
+ console.log(` - Dropped column(s): ${change.removedColumns.join(', ')}`);
63
+ }
64
+ console.log();
65
+ }
66
+
67
+ if (options.dryRun) {
68
+ console.log('šŸ’” Dry-run mode: No files were written to disk.\n');
69
+ } else {
70
+ console.log('šŸ“ Next step: Run "webspresso db:migrate" to apply the changes.\n');
71
+ }
72
+ } catch (err) {
73
+ console.error('\nāŒ Migration generation failed:', err.message);
74
+ process.exit(1);
75
+ } finally {
76
+ if (knex) {
77
+ await knex.destroy().catch(() => {});
78
+ }
79
+ }
80
+ });
81
+ }
82
+
83
+ module.exports = { registerCommand };
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso CLI - list Command
5
+ * Lists all built-in and custom project commands with descriptions
6
+ * @module bin/commands/list
7
+ */
8
+
9
+ const path = require('path');
10
+ const { createCommandRegistry } = require('../../core/commands');
11
+
12
+ function registerCommand(program) {
13
+ program
14
+ .command('list')
15
+ .alias('commands')
16
+ .description('List all available Webspresso and custom project management commands')
17
+ .option('-d, --dir <path>', 'Custom commands directory (default: ./commands)')
18
+ .action((options) => {
19
+ const rootDir = process.cwd();
20
+ const commandsDir = options.dir
21
+ ? path.resolve(rootDir, options.dir)
22
+ : path.resolve(rootDir, 'commands');
23
+
24
+ const customRegistry = createCommandRegistry({ commandsDir });
25
+ const customCommands = customRegistry.list();
26
+
27
+ console.log('');
28
+ console.log('ā˜• Webspresso CLI - Command Palette');
29
+ console.log('===================================');
30
+
31
+ const sections = [
32
+ {
33
+ title: 'šŸš€ Scaffolding & Code Generation',
34
+ commands: [
35
+ ['make:model <name>', 'Scaffold a new ORM model definition in models/'],
36
+ ['make:job <name>', 'Scaffold a background queue job worker in jobs/'],
37
+ ['make:policy <name>', 'Scaffold an authorization policy in policies/'],
38
+ ['make:factory <name>', 'Scaffold a test/seeder model factory in factories/'],
39
+ ['make:event <name>', 'Scaffold an event listener in listeners/'],
40
+ ['make:mail <name>', 'Scaffold a declarative Mailable class in emails/'],
41
+ ['make:command <name>', 'Scaffold a custom management command in commands/'],
42
+ ['page <path>', 'Scaffold a new file-based SSR page'],
43
+ ['api <path>', 'Scaffold a new file-based API route'],
44
+ ],
45
+ },
46
+ {
47
+ title: 'šŸ’¾ Database & Migrations',
48
+ commands: [
49
+ ['db:migrate', 'Run all pending Knex migrations'],
50
+ ['db:makemigrations', 'Auto-diff model schemas against database and generate migration'],
51
+ ['db:rollback', 'Rollback the last batch of migrations'],
52
+ ['db:status', 'Show database connection and migration status'],
53
+ ['db:scaffold', 'Generate initial create table migrations from models/'],
54
+ ['db:make <name>', 'Create an empty Knex migration file'],
55
+ ['seed', 'Run database seeders'],
56
+ ],
57
+ },
58
+ {
59
+ title: 'šŸ› ļø Server & Developer Experience',
60
+ commands: [
61
+ ['dev', 'Start local development server with hot-reload'],
62
+ ['start', 'Start production HTTP server'],
63
+ ['build', 'Build client assets and precompile templates'],
64
+ ['routes', 'Inspect all discovered SSR page and API routes'],
65
+ ['doctor', 'Diagnose project configuration, dependencies, and health'],
66
+ ['service [name]', 'Run and test services interactively without HTTP server'],
67
+ ['run <command>', 'Execute a custom management command with full DI context'],
68
+ ['list', 'Display this command index'],
69
+ ],
70
+ },
71
+ ];
72
+
73
+ for (const section of sections) {
74
+ console.log(`\n${section.title}:`);
75
+ for (const [cmd, desc] of section.commands) {
76
+ console.log(` ${cmd.padEnd(28)} ${desc}`);
77
+ }
78
+ }
79
+
80
+ if (customCommands.length > 0) {
81
+ console.log(`\n⚔ Custom Project Commands (${path.relative(rootDir, commandsDir) || 'commands/'}):`);
82
+ for (const cmd of customCommands) {
83
+ console.log(` ${('run ' + cmd.name).padEnd(28)} ${cmd.description || 'Custom management command'}`);
84
+ }
85
+ } else {
86
+ console.log('\n⚔ Custom Project Commands:');
87
+ console.log(' (None discovered. Create with: npx webspresso make:command <name>)');
88
+ }
89
+
90
+ console.log('\nUse "webspresso <command> --help" for more information about any command.\n');
91
+ });
92
+ }
93
+
94
+ module.exports = {
95
+ registerCommand,
96
+ };
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso CLI - make:command Command
5
+ * Scaffolds a custom management command in commands/<name>.js
6
+ * @module bin/commands/make-command
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ function generateCommandContent(commandName, description = '') {
13
+ return `'use strict';
14
+
15
+ const { defineCommand } = require('webspresso');
16
+
17
+ /**
18
+ * Custom Management Command: ${commandName}
19
+ * Run via: npx webspresso run ${commandName}
20
+ */
21
+ module.exports = defineCommand({
22
+ name: '${commandName}',
23
+ description: '${description || `Execute ${commandName} task`}',
24
+ options: [
25
+ { flags: '-d, --dry-run', description: 'Run without making persistent changes', defaultValue: false },
26
+ ],
27
+ handle: async ({ db, queue, services, mailer, events, logger, args, flags }) => {
28
+ logger.log('Executing "${commandName}"...');
29
+ if (flags.dryRun) {
30
+ logger.log('[Dry Run] No actual changes will be committed.');
31
+ }
32
+
33
+ // Business logic goes here:
34
+ // e.g. const users = await db.getRepository('User').find({ active: false });
35
+
36
+ return { success: true };
37
+ },
38
+ });
39
+ `;
40
+ }
41
+
42
+ function registerCommand(program) {
43
+ program
44
+ .command('make:command <name>')
45
+ .alias('command:make')
46
+ .description('Scaffold a custom management command in commands/<name>.js')
47
+ .option('-d, --desc <description>', 'Command description', '')
48
+ .option('--root <dir>', 'Root directory of the project', process.cwd())
49
+ .action((nameArg, options) => {
50
+ const rootDir = path.resolve(options.root || process.cwd());
51
+ const commandsDir = path.join(rootDir, 'commands');
52
+
53
+ if (!fs.existsSync(commandsDir)) {
54
+ fs.mkdirSync(commandsDir, { recursive: true });
55
+ }
56
+
57
+ const commandName = String(nameArg || '').trim();
58
+ if (!commandName) {
59
+ console.error('āŒ Command name is required.');
60
+ process.exitCode = 1;
61
+ return;
62
+ }
63
+
64
+ // Convert colons or slashes to subdirectories or file path
65
+ // e.g. "users:prune" -> commands/users/prune.js or commands/users-prune.js
66
+ const relPath = commandName.includes(':')
67
+ ? commandName.replace(/:/g, '/') + '.js'
68
+ : `${commandName}.js`;
69
+
70
+ const targetFilePath = path.join(commandsDir, relPath);
71
+ const targetDir = path.dirname(targetFilePath);
72
+
73
+ if (!fs.existsSync(targetDir)) {
74
+ fs.mkdirSync(targetDir, { recursive: true });
75
+ }
76
+
77
+ if (fs.existsSync(targetFilePath)) {
78
+ console.error(`āŒ Command file already exists: ${path.relative(rootDir, targetFilePath)}`);
79
+ process.exitCode = 1;
80
+ return;
81
+ }
82
+
83
+ const content = generateCommandContent(commandName, options.desc);
84
+ fs.writeFileSync(targetFilePath, content, 'utf-8');
85
+ console.log(`\nāœ… Created custom management command: ${path.relative(rootDir, targetFilePath)}`);
86
+ console.log(` Execute it with: npx webspresso run ${commandName}\n`);
87
+ });
88
+ }
89
+
90
+ module.exports = {
91
+ registerCommand,
92
+ generateCommandContent,
93
+ };
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso CLI - make:event Command
5
+ * Scaffolds an event / signal listener in listeners/<name>.js
6
+ * @module bin/commands/make-event
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ function generateEventContent(eventName) {
13
+ return `'use strict';
14
+
15
+ /**
16
+ * Event Listener: ${eventName}
17
+ * Triggered by: app.events.emit('${eventName}', payload)
18
+ */
19
+ module.exports = {
20
+ event: '${eventName}',
21
+
22
+ /**
23
+ * Handle incoming event
24
+ * @param {Object} payload - Event payload
25
+ * @param {Object} ctx - Injected context { events, db, queue, mailer, logger }
26
+ */
27
+ async handle(payload, ctx = {}) {
28
+ const { logger, mailer, queue } = ctx;
29
+ if (logger) {
30
+ logger.log('[Event:${eventName}] Received event payload:', payload);
31
+ }
32
+
33
+ // Example: Dispatch background job or send notification email
34
+ // if (queue) await queue.dispatch('notify', payload);
35
+ },
36
+ };
37
+ `;
38
+ }
39
+
40
+ function registerCommand(program) {
41
+ program
42
+ .command('make:event <name>')
43
+ .alias('event:make')
44
+ .description('Scaffold an event listener in listeners/<name>.js')
45
+ .option('--root <dir>', 'Root directory of the project', process.cwd())
46
+ .action((nameArg, options) => {
47
+ const rootDir = path.resolve(options.root || process.cwd());
48
+ const listenersDir = path.join(rootDir, 'listeners');
49
+
50
+ if (!fs.existsSync(listenersDir)) {
51
+ fs.mkdirSync(listenersDir, { recursive: true });
52
+ }
53
+
54
+ const rawName = String(nameArg || '').trim();
55
+ if (!rawName) {
56
+ console.error('āŒ Event name is required.');
57
+ process.exitCode = 1;
58
+ return;
59
+ }
60
+
61
+ const eventName = rawName.replace(/\.js$/, '');
62
+ const fileName = eventName.replace(/:/g, '-');
63
+ const targetFilePath = path.join(listenersDir, `${fileName}.js`);
64
+
65
+ if (fs.existsSync(targetFilePath)) {
66
+ console.error(`āŒ Listener file already exists: ${path.relative(rootDir, targetFilePath)}`);
67
+ process.exitCode = 1;
68
+ return;
69
+ }
70
+
71
+ const content = generateEventContent(eventName);
72
+ fs.writeFileSync(targetFilePath, content, 'utf-8');
73
+ console.log(`\nāœ… Created event listener: ${path.relative(rootDir, targetFilePath)} (event: "${eventName}")\n`);
74
+ });
75
+ }
76
+
77
+ module.exports = {
78
+ registerCommand,
79
+ generateEventContent,
80
+ };
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso CLI - make:factory Command
5
+ * Scaffolds an ORM model factory in factories/<name>.factory.js
6
+ * @module bin/commands/make-factory
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ function generateFactoryContent(modelName) {
13
+ const cleanName = modelName.charAt(0).toUpperCase() + modelName.slice(1);
14
+
15
+ return `'use strict';
16
+
17
+ const { defineFactory } = require('webspresso');
18
+
19
+ /**
20
+ * Model Factory: ${cleanName}
21
+ * Used in tests and seeders:
22
+ * const ${cleanName.toLowerCase()} = await ${cleanName}Factory.create({ status: 'active' });
23
+ */
24
+ module.exports = defineFactory('${cleanName}', ({ faker, seq }) => ({
25
+ title: faker ? faker.lorem.words(3) : \`${cleanName} \${seq}\`,
26
+ status: 'active',
27
+ createdAt: new Date(),
28
+ updatedAt: new Date(),
29
+ }));
30
+ `;
31
+ }
32
+
33
+ function registerCommand(program) {
34
+ program
35
+ .command('make:factory <name>')
36
+ .alias('factory:make')
37
+ .description('Scaffold an ORM model factory in factories/<name>.factory.js')
38
+ .option('--root <dir>', 'Root directory of the project', process.cwd())
39
+ .action((nameArg, options) => {
40
+ const rootDir = path.resolve(options.root || process.cwd());
41
+ const factoriesDir = path.join(rootDir, 'factories');
42
+
43
+ if (!fs.existsSync(factoriesDir)) {
44
+ fs.mkdirSync(factoriesDir, { recursive: true });
45
+ }
46
+
47
+ const rawName = String(nameArg || '').trim();
48
+ if (!rawName) {
49
+ console.error('āŒ Model name is required for factory.');
50
+ process.exitCode = 1;
51
+ return;
52
+ }
53
+
54
+ const cleanName = rawName.replace(/\.factory(\.js)?$/, '').replace(/\.js$/, '');
55
+ const targetFilePath = path.join(factoriesDir, `${cleanName.toLowerCase()}.factory.js`);
56
+
57
+ if (fs.existsSync(targetFilePath)) {
58
+ console.error(`āŒ Factory file already exists: ${path.relative(rootDir, targetFilePath)}`);
59
+ process.exitCode = 1;
60
+ return;
61
+ }
62
+
63
+ const content = generateFactoryContent(cleanName);
64
+ fs.writeFileSync(targetFilePath, content, 'utf-8');
65
+ console.log(`\nāœ… Created model factory: ${path.relative(rootDir, targetFilePath)}\n`);
66
+ });
67
+ }
68
+
69
+ module.exports = {
70
+ registerCommand,
71
+ generateFactoryContent,
72
+ };
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso CLI - make:mail Command
5
+ * Scaffolds a declarative Mailable class in emails/<name>.js
6
+ * @module bin/commands/make-mail
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ function generateMailContent(rawName) {
13
+ const baseName = rawName.replace(/email$/i, '').replace(/mail$/i, '').trim();
14
+ const cleanBase = baseName.charAt(0).toUpperCase() + baseName.slice(1);
15
+ const className = `${cleanBase}Email`;
16
+ const tplName = cleanBase.toLowerCase();
17
+
18
+ return `'use strict';
19
+
20
+ const { Mailable } = require('webspresso');
21
+
22
+ /**
23
+ * Mailable: ${className}
24
+ * Send via: req.mailer.send(new ${className}(user))
25
+ */
26
+ class ${className} extends Mailable {
27
+ constructor(recipient, data = {}) {
28
+ super();
29
+ this.recipient = recipient;
30
+ this.data = data;
31
+ }
32
+
33
+ /**
34
+ * Build email definition
35
+ */
36
+ build() {
37
+ const toAddress = typeof this.recipient === 'string' ? this.recipient : this.recipient?.email;
38
+ const toName = typeof this.recipient === 'object' ? this.recipient?.name : undefined;
39
+
40
+ this.to(toAddress, toName)
41
+ .subject('Hello from Webspresso')
42
+ .view('${tplName}', this.data);
43
+ }
44
+ }
45
+
46
+ module.exports = {
47
+ ${className},
48
+ };
49
+ `;
50
+ }
51
+
52
+ function registerCommand(program) {
53
+ program
54
+ .command('make:mail <name>')
55
+ .alias('mail:make')
56
+ .alias('make:mailable')
57
+ .description('Scaffold a declarative Mailable class in emails/<name>.js')
58
+ .option('--root <dir>', 'Root directory of the project', process.cwd())
59
+ .action((nameArg, options) => {
60
+ const rootDir = path.resolve(options.root || process.cwd());
61
+ const emailsDir = path.join(rootDir, 'emails');
62
+
63
+ if (!fs.existsSync(emailsDir)) {
64
+ fs.mkdirSync(emailsDir, { recursive: true });
65
+ }
66
+
67
+ const rawName = String(nameArg || '').trim();
68
+ if (!rawName) {
69
+ console.error('āŒ Mailable name is required.');
70
+ process.exitCode = 1;
71
+ return;
72
+ }
73
+
74
+ const cleanName = rawName.replace(/\.js$/, '');
75
+ const fileName = cleanName.endsWith('Email') ? cleanName : `${cleanName}Email`;
76
+ const targetFilePath = path.join(emailsDir, `${fileName}.js`);
77
+
78
+ if (fs.existsSync(targetFilePath)) {
79
+ console.error(`āŒ Mailable file already exists: ${path.relative(rootDir, targetFilePath)}`);
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+
84
+ const content = generateMailContent(cleanName);
85
+ fs.writeFileSync(targetFilePath, content, 'utf-8');
86
+ console.log(`\nāœ… Created Mailable class: ${path.relative(rootDir, targetFilePath)}\n`);
87
+ });
88
+ }
89
+
90
+ module.exports = {
91
+ registerCommand,
92
+ generateMailContent,
93
+ };
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso CLI - make:policy Command
5
+ * Scaffolds an authorization policy in policies/<name>.policy.js
6
+ * @module bin/commands/make-policy
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ function generatePolicyContent(policyName) {
13
+ const modelName = policyName.replace(/policy$/i, '').trim();
14
+ const cleanName = modelName.charAt(0).toUpperCase() + modelName.slice(1);
15
+
16
+ return `'use strict';
17
+
18
+ /**
19
+ * Authorization Policy: ${cleanName}
20
+ * Invoked via req.bouncer.authorize('${cleanName.toLowerCase()}.update', record)
21
+ * or {% if fsy.can('${cleanName.toLowerCase()}.update', record) %}
22
+ */
23
+ module.exports = {
24
+ name: '${cleanName.toLowerCase()}',
25
+
26
+ /**
27
+ * Determine whether the user can view any records
28
+ */
29
+ async viewAny(user) {
30
+ return true;
31
+ },
32
+
33
+ /**
34
+ * Determine whether the user can view the specific record
35
+ */
36
+ async view(user, record) {
37
+ return true;
38
+ },
39
+
40
+ /**
41
+ * Determine whether the user can create records
42
+ */
43
+ async create(user) {
44
+ return Boolean(user);
45
+ },
46
+
47
+ /**
48
+ * Determine whether the user can update the record
49
+ */
50
+ async update(user, record) {
51
+ if (!user) return false;
52
+ if (user.isAdmin || user.role === 'admin') return true;
53
+ return record && (record.userId === user.id || record.user_id === user.id);
54
+ },
55
+
56
+ /**
57
+ * Determine whether the user can delete the record
58
+ */
59
+ async delete(user, record) {
60
+ if (!user) return false;
61
+ return Boolean(user.isAdmin || user.role === 'admin');
62
+ },
63
+ };
64
+ `;
65
+ }
66
+
67
+ function registerCommand(program) {
68
+ program
69
+ .command('make:policy <name>')
70
+ .alias('policy:make')
71
+ .description('Scaffold an authorization policy in policies/<name>.policy.js')
72
+ .option('--root <dir>', 'Root directory of the project', process.cwd())
73
+ .action((nameArg, options) => {
74
+ const rootDir = path.resolve(options.root || process.cwd());
75
+ const policiesDir = path.join(rootDir, 'policies');
76
+
77
+ if (!fs.existsSync(policiesDir)) {
78
+ fs.mkdirSync(policiesDir, { recursive: true });
79
+ }
80
+
81
+ const rawName = String(nameArg || '').trim();
82
+ if (!rawName) {
83
+ console.error('āŒ Policy name is required.');
84
+ process.exitCode = 1;
85
+ return;
86
+ }
87
+
88
+ const policyBase = rawName.replace(/\.policy(\.js)?$/, '').replace(/\.js$/, '');
89
+ const targetFilePath = path.join(policiesDir, `${policyBase}.policy.js`);
90
+
91
+ if (fs.existsSync(targetFilePath)) {
92
+ console.error(`āŒ Policy file already exists: ${path.relative(rootDir, targetFilePath)}`);
93
+ process.exitCode = 1;
94
+ return;
95
+ }
96
+
97
+ const content = generatePolicyContent(policyBase);
98
+ fs.writeFileSync(targetFilePath, content, 'utf-8');
99
+ console.log(`\nāœ… Created authorization policy: ${path.relative(rootDir, targetFilePath)}\n`);
100
+ });
101
+ }
102
+
103
+ module.exports = {
104
+ registerCommand,
105
+ generatePolicyContent,
106
+ };