webspresso 0.0.100 → 0.0.102

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 (88) hide show
  1. package/.agents/ADMIN.md +47 -4
  2. package/.agents/AGENTS.md +3 -1
  3. package/.agents/ARCHITECTURE.md +11 -0
  4. package/.agents/AUTH.md +63 -0
  5. package/.agents/CLI.md +81 -0
  6. package/.agents/CONVENTIONS.md +4 -0
  7. package/.agents/EMAIL.md +84 -2
  8. package/.agents/EVENTS.md +179 -0
  9. package/.agents/ORM.md +59 -0
  10. package/.agents/PLUGINS.md +32 -6
  11. package/.agents/ROUTING.md +163 -3
  12. package/.agents/skills/webspresso-framework/SKILL.md +32 -0
  13. package/.agents/skills/webspresso-routing-and-api/SKILL.md +4 -4
  14. package/AGENTS.md +7 -0
  15. package/README.md +18 -5
  16. package/bin/commands/add-tailwind.js +10 -0
  17. package/bin/commands/build.js +34 -0
  18. package/bin/commands/db-makemigrations.js +83 -0
  19. package/bin/commands/list.js +96 -0
  20. package/bin/commands/make-command.js +93 -0
  21. package/bin/commands/make-event.js +80 -0
  22. package/bin/commands/make-factory.js +72 -0
  23. package/bin/commands/make-mail.js +93 -0
  24. package/bin/commands/make-policy.js +106 -0
  25. package/bin/commands/routes.js +8 -0
  26. package/bin/commands/run.js +195 -0
  27. package/bin/webspresso.js +17 -0
  28. package/core/applySchema.js +18 -1
  29. package/core/auth/bouncer.js +196 -0
  30. package/core/auth/index.js +10 -0
  31. package/core/auth/middleware.js +15 -1
  32. package/core/commands/define-command.js +48 -0
  33. package/core/commands/index.js +18 -0
  34. package/core/commands/registry.js +156 -0
  35. package/core/commands/runner.js +76 -0
  36. package/core/compression/compressible.js +1 -1
  37. package/core/compression/middleware.js +35 -251
  38. package/core/content/service.js +8 -2
  39. package/core/events/discovery.js +142 -0
  40. package/core/events/dispatcher.js +552 -0
  41. package/core/events/index.js +23 -0
  42. package/core/events/signal.js +92 -0
  43. package/core/forms/flash.js +234 -0
  44. package/core/forms/form-builder.js +214 -0
  45. package/core/forms/index.js +16 -0
  46. package/core/mail/index.js +23 -0
  47. package/core/mail/mailable.js +250 -0
  48. package/core/mail/mailer.js +184 -0
  49. package/core/mail/transports.js +238 -0
  50. package/core/orm/factory.js +250 -0
  51. package/core/orm/index.js +6 -0
  52. package/core/orm/migrations/diff.js +185 -0
  53. package/core/orm/migrations/index.js +22 -4
  54. package/core/orm/model.js +4 -0
  55. package/index.d.ts +422 -1
  56. package/index.js +111 -16
  57. package/package.json +9 -5
  58. package/plugins/admin-panel/api.js +509 -3
  59. package/plugins/admin-panel/index.js +29 -0
  60. package/plugins/content/models/content-entry.js +4 -3
  61. package/plugins/content/models/content-type.js +4 -3
  62. package/plugins/cors.js +2 -2
  63. package/plugins/csrf/index.js +1 -1
  64. package/plugins/email/index.js +48 -0
  65. package/plugins/email/preview-handler.js +555 -0
  66. package/plugins/email/service.js +23 -12
  67. package/plugins/email/transport.js +30 -11
  68. package/plugins/file-manager/index.js +13 -0
  69. package/plugins/index.js +2 -0
  70. package/plugins/lazy.js +258 -0
  71. package/plugins/redirect/index.js +1 -1
  72. package/src/api/api-loader.js +18 -2
  73. package/src/api/validation-i18n.js +231 -0
  74. package/src/app-context.js +48 -6
  75. package/src/dev-reload/index.js +9 -1
  76. package/src/discovery/route-manifest.js +197 -0
  77. package/src/discovery/scan-routes.js +80 -5
  78. package/src/file-router.js +153 -91
  79. package/src/helpers.js +143 -0
  80. package/src/njk-frontmatter.js +17 -1
  81. package/src/pages/page-loader.js +1 -1
  82. package/src/plugin-manager.js +12 -0
  83. package/src/routing/cascading.js +14 -10
  84. package/src/routing/route-table.js +1 -1
  85. package/src/server.js +243 -34
  86. package/src/services/builtins/system.js +4 -6
  87. package/src/services/registry.js +12 -2
  88. package/src/template/precompile.js +247 -0
package/bin/webspresso.js CHANGED
@@ -26,6 +26,7 @@ const { registerCommand: registerDbRollback } = require('./commands/db-rollback'
26
26
  const { registerCommand: registerDbStatus } = require('./commands/db-status');
27
27
  const { registerCommand: registerDbMake } = require('./commands/db-make');
28
28
  const { registerCommand: registerDbScaffold } = require('./commands/db-scaffold');
29
+ const { registerCommand: registerDbMakeMigrations } = require('./commands/db-makemigrations');
29
30
  const { registerCommand: registerSeed } = require('./commands/seed');
30
31
  const { registerCommand: registerAdminSetup } = require('./commands/admin-setup');
31
32
  const { registerCommand: registerAdminPassword } = require('./commands/admin-password');
@@ -49,6 +50,13 @@ const { registerCommand: registerOpenapi } = require('./commands/openapi');
49
50
  const { registerCommand: registerTypegen } = require('./commands/typegen');
50
51
  const { registerCommand: registerAddDocker } = require('./commands/add-docker');
51
52
  const { registerCommand: registerDeployCheck } = require('./commands/deploy-check');
53
+ const { registerCommand: registerRun } = require('./commands/run');
54
+ const { registerCommand: registerList } = require('./commands/list');
55
+ const { registerCommand: registerMakeCommand } = require('./commands/make-command');
56
+ const { registerCommand: registerMakePolicy } = require('./commands/make-policy');
57
+ const { registerCommand: registerMakeFactory } = require('./commands/make-factory');
58
+ const { registerCommand: registerMakeEvent } = require('./commands/make-event');
59
+ const { registerCommand: registerMakeMail } = require('./commands/make-mail');
52
60
 
53
61
  registerNew(program);
54
62
  registerPage(program);
@@ -64,6 +72,7 @@ registerDbRollback(program);
64
72
  registerDbStatus(program);
65
73
  registerDbMake(program);
66
74
  registerDbScaffold(program);
75
+ registerDbMakeMigrations(program);
67
76
  registerSeed(program);
68
77
  registerAdminSetup(program);
69
78
  registerAdminPassword(program);
@@ -84,8 +93,16 @@ registerAgentsInit(program);
84
93
  registerRoutes(program);
85
94
  registerMakeModel(program);
86
95
  registerMakeJob(program);
96
+ registerMakeCommand(program);
97
+ registerMakePolicy(program);
98
+ registerMakeFactory(program);
99
+ registerMakeEvent(program);
100
+ registerMakeMail(program);
101
+ registerRun(program);
102
+ registerList(program);
87
103
  registerOpenapi(program);
88
104
  registerTypegen(program);
89
105
 
90
106
  // Parse arguments
91
107
  program.parse();
108
+
@@ -49,8 +49,25 @@ function applySchema(req, compiledSchema) {
49
49
  }
50
50
  }
51
51
 
52
+ let _translateValidationIssues = null;
53
+ function getTranslateValidationIssues() {
54
+ if (!_translateValidationIssues) {
55
+ try {
56
+ _translateValidationIssues = require('../src/api/validation-i18n').translateValidationIssues;
57
+ } catch {
58
+ _translateValidationIssues = (issues) => issues;
59
+ }
60
+ }
61
+ return _translateValidationIssues;
62
+ }
63
+
64
+ function translateValidationIssues(issues, options) {
65
+ return getTranslateValidationIssues()(issues, options);
66
+ }
67
+
52
68
  module.exports = {
53
- applySchema
69
+ applySchema,
70
+ translateValidationIssues,
54
71
  };
55
72
 
56
73
 
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Webspresso Auth - Bouncer & Policy Authorization Engine (AdonisJS Bouncer & Django Permissions-inspired)
3
+ * Context-aware authorization helper for routes, handlers, and templates
4
+ * @module core/auth/bouncer
5
+ */
6
+
7
+ const { defaultPolicyManager, AuthorizationError } = require('./policy');
8
+ const { ForbiddenError, UnauthorizedError } = require('../errors');
9
+
10
+ /**
11
+ * Context-aware Bouncer instance bound to a specific user
12
+ */
13
+ class Bouncer {
14
+ /**
15
+ * @param {Object|null} user - The subject user (or null for guest)
16
+ * @param {import('./policy').PolicyManager} [policyManager]
17
+ */
18
+ constructor(user = null, policyManager = defaultPolicyManager) {
19
+ this.user = user;
20
+ this.policyManager = policyManager;
21
+ }
22
+
23
+ /**
24
+ * Create a new Bouncer instance for another user
25
+ * @param {Object|null} user
26
+ * @returns {Bouncer}
27
+ */
28
+ forUser(user) {
29
+ return new Bouncer(user, this.policyManager);
30
+ }
31
+
32
+ /**
33
+ * Parse policy and action from arguments
34
+ * Supports:
35
+ * 1. ('Post.update', post) -> policy='Post', action='update', resource=post
36
+ * 2. ('Post', 'update', post) -> policy='Post', action='update', resource=post
37
+ * 3. ('gate-name', resource) -> policy=null, action='gate-name', resource=resource
38
+ * 4. (functionRule, resource) -> direct function check
39
+ * @protected
40
+ */
41
+ _parseArgs(actionOrPolicy, ...rest) {
42
+ if (typeof actionOrPolicy === 'function') {
43
+ return {
44
+ isFn: true,
45
+ fn: actionOrPolicy,
46
+ resource: rest[0],
47
+ };
48
+ }
49
+
50
+ if (typeof actionOrPolicy === 'string') {
51
+ // 1. Dot-notation: 'Post.update'
52
+ if (actionOrPolicy.includes('.')) {
53
+ const parts = actionOrPolicy.split('.');
54
+ const policy = parts[0];
55
+ const action = parts.slice(1).join('.');
56
+ return { policy, action, resource: rest[0] };
57
+ }
58
+
59
+ // 2. Separate strings: ('Post', 'update', post)
60
+ if (rest.length > 0 && typeof rest[0] === 'string') {
61
+ const policy = actionOrPolicy;
62
+ const action = rest[0];
63
+ const resource = rest[1];
64
+ return { policy, action, resource };
65
+ }
66
+
67
+ // 3. Single gate / permission name: ('edit-settings', setting)
68
+ return { policy: null, action: actionOrPolicy, resource: rest[0] };
69
+ }
70
+
71
+ return { policy: null, action: String(actionOrPolicy), resource: rest[0] };
72
+ }
73
+
74
+ /**
75
+ * Check if current user is allowed to perform an action
76
+ * @param {string|Function} actionOrPolicy
77
+ * @param {...any} rest
78
+ * @returns {boolean}
79
+ */
80
+ allows(actionOrPolicy, ...rest) {
81
+ const parsed = this._parseArgs(actionOrPolicy, ...rest);
82
+
83
+ // Direct function rule
84
+ if (parsed.isFn) {
85
+ try {
86
+ return Boolean(parsed.fn(this.user, parsed.resource));
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ const { policy, action, resource } = parsed;
93
+
94
+ // 1. Check Policy Manager (Gates & Policies)
95
+ if (policy) {
96
+ if (this.policyManager.hasPolicy(policy)) {
97
+ return this.policyManager.can(this.user, action, policy, resource);
98
+ }
99
+ } else {
100
+ if (this.policyManager.hasGate(action)) {
101
+ return this.policyManager.can(this.user, action, null, resource);
102
+ }
103
+ }
104
+
105
+ // 2. Django-style user permissions matrix check
106
+ // e.g. user.permissions = ['post.create', 'post.update'] or user.hasPerm(...)
107
+ if (this.user) {
108
+ const fullPerm = policy ? `${policy.toLowerCase()}.${action.toLowerCase()}` : action.toLowerCase();
109
+
110
+ // Check hasPerm / hasPermission method on user object
111
+ if (typeof this.user.hasPerm === 'function') {
112
+ if (this.user.hasPerm(fullPerm, resource)) return true;
113
+ }
114
+ if (typeof this.user.hasPermission === 'function') {
115
+ if (this.user.hasPermission(fullPerm, resource)) return true;
116
+ }
117
+
118
+ // Check permissions array or set
119
+ const perms = this.user.permissions;
120
+ if (Array.isArray(perms) || perms instanceof Set) {
121
+ const permSet = perms instanceof Set ? perms : new Set(perms);
122
+ if (permSet.has('*') || permSet.has(fullPerm)) {
123
+ return true;
124
+ }
125
+ if (policy && permSet.has(`${policy.toLowerCase()}.*`)) {
126
+ return true;
127
+ }
128
+ }
129
+
130
+ // If user has superuser / is_admin flag
131
+ if (this.user.isSuperuser || this.user.is_superuser) {
132
+ return true;
133
+ }
134
+ }
135
+
136
+ return false;
137
+ }
138
+
139
+ /**
140
+ * Check if current user is denied from performing an action
141
+ * @param {string|Function} actionOrPolicy
142
+ * @param {...any} rest
143
+ * @returns {boolean}
144
+ */
145
+ denies(actionOrPolicy, ...rest) {
146
+ return !this.allows(actionOrPolicy, ...rest);
147
+ }
148
+
149
+ /**
150
+ * Authorize an action. Throws ForbiddenError (or UnauthorizedError) if access is denied.
151
+ * @param {string|Function} actionOrPolicy
152
+ * @param {...any} rest
153
+ * @throws {ForbiddenError|UnauthorizedError}
154
+ */
155
+ authorize(actionOrPolicy, ...rest) {
156
+ if (this.denies(actionOrPolicy, ...rest)) {
157
+ if (!this.user) {
158
+ throw new UnauthorizedError('Authentication required to perform this action');
159
+ }
160
+ const label = typeof actionOrPolicy === 'string' ? actionOrPolicy : 'action';
161
+ throw new ForbiddenError(`You are not authorized to perform "${label}"`);
162
+ }
163
+ return true;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Create a Bouncer instance for a user
169
+ * @param {Object|null} user
170
+ * @param {import('./policy').PolicyManager} [policyManager]
171
+ * @returns {Bouncer}
172
+ */
173
+ function createBouncer(user = null, policyManager = defaultPolicyManager) {
174
+ return new Bouncer(user, policyManager);
175
+ }
176
+
177
+ /**
178
+ * Express middleware that attaches `req.bouncer` automatically
179
+ * @param {Object} [options={}]
180
+ * @returns {import('express').RequestHandler}
181
+ */
182
+ function createBouncerMiddleware(options = {}) {
183
+ const policyManager = options.policyManager || defaultPolicyManager;
184
+
185
+ return function bouncerMiddleware(req, res, next) {
186
+ const user = req.user || req.auth?.user || null;
187
+ req.bouncer = new Bouncer(user, policyManager);
188
+ next();
189
+ };
190
+ }
191
+
192
+ module.exports = {
193
+ Bouncer,
194
+ createBouncer,
195
+ createBouncerMiddleware,
196
+ };
@@ -225,6 +225,16 @@ module.exports = {
225
225
  // Classes
226
226
  AuthManager,
227
227
  PolicyManager,
228
+ Bouncer: require('./bouncer').Bouncer,
229
+ createBouncer: require('./bouncer').createBouncer,
230
+ createBouncerMiddleware: require('./bouncer').createBouncerMiddleware,
231
+
232
+ // Policies & Authorization (AdonisJS Bouncer & Django Permissions-inspired)
233
+ definePolicy: require('./policy').definePolicy,
234
+ defineGate: require('./policy').defineGate,
235
+ can: require('./policy').can,
236
+ cannot: require('./policy').cannot,
237
+ authorize: require('./policy').authorize,
228
238
 
229
239
  // Errors
230
240
  AuthenticationError,
@@ -44,7 +44,21 @@ function createAuthMiddleware(authManager) {
44
44
  /**
45
45
  * Cookie parser middleware (needed for signed cookies / remember me)
46
46
  */
47
- const cookieMiddleware = cookieParser(config.session.secret);
47
+ const baseCookieMiddleware = cookieParser(config.session?.secret);
48
+ const cookieMiddleware = (req, res, next) => {
49
+ if (!req.cookies) {
50
+ return baseCookieMiddleware(req, res, next);
51
+ }
52
+ if (config.session?.secret) {
53
+ if (!req.secret) {
54
+ req.secret = config.session.secret;
55
+ }
56
+ if (!req.signedCookies || Object.keys(req.signedCookies).length === 0) {
57
+ req.signedCookies = cookieParser.signedCookies(req.cookies, config.session.secret);
58
+ }
59
+ }
60
+ next();
61
+ };
48
62
 
49
63
  /**
50
64
  * Authenticate middleware
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso - Custom Command Definition
5
+ * Inspired by Django BaseCommand & AdonisJS Ace CLI
6
+ * @module core/commands/define-command
7
+ */
8
+
9
+ /**
10
+ * Define a custom CLI management command
11
+ * @param {Object} definition
12
+ * @param {string} definition.name - Command identifier (e.g. 'users:sync' or 'send-invoices')
13
+ * @param {string} [definition.description] - Human-readable command description
14
+ * @param {Array<{flags: string, description?: string, defaultValue?: any}>} [definition.options] - CLI flags
15
+ * @param {Function} definition.handle - Execution function receiving ({ db, queue, services, mailer, events, logger, args, flags })
16
+ * @returns {Object} Validated command specification
17
+ */
18
+ function defineCommand(definition) {
19
+ if (!definition || typeof definition !== 'object') {
20
+ throw new TypeError('defineCommand requires a configuration object');
21
+ }
22
+ if (!definition.name || typeof definition.name !== 'string' || !definition.name.trim()) {
23
+ throw new TypeError('Command definition must specify a non-empty string "name"');
24
+ }
25
+ const handleFn = definition.handle || definition.run;
26
+ if (typeof handleFn !== 'function') {
27
+ throw new TypeError(`Command "${definition.name}" must provide an executable "handle" function`);
28
+ }
29
+
30
+ const options = Array.isArray(definition.options)
31
+ ? definition.options.map((opt) => ({
32
+ flags: String(opt.flags || opt.name || '').trim(),
33
+ description: opt.description || '',
34
+ defaultValue: opt.defaultValue !== undefined ? opt.defaultValue : opt.default,
35
+ })).filter((opt) => opt.flags.length > 0)
36
+ : [];
37
+
38
+ return {
39
+ name: definition.name.trim(),
40
+ description: definition.description || '',
41
+ options,
42
+ handle: handleFn,
43
+ };
44
+ }
45
+
46
+ module.exports = {
47
+ defineCommand,
48
+ };
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso - Management Commands Core Module
5
+ * @module core/commands
6
+ */
7
+
8
+ const { defineCommand } = require('./define-command');
9
+ const { createCommandRegistry, findCommandFiles, deriveCommandName } = require('./registry');
10
+ const { executeCommand } = require('./runner');
11
+
12
+ module.exports = {
13
+ defineCommand,
14
+ createCommandRegistry,
15
+ findCommandFiles,
16
+ deriveCommandName,
17
+ executeCommand,
18
+ };
@@ -0,0 +1,156 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso - Command Registry & Auto-Discovery
5
+ * Scans commands/ directory and registers custom management commands
6
+ * @module core/commands/registry
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { defineCommand } = require('./define-command');
12
+
13
+ /**
14
+ * Recursively find command files in directory
15
+ * @param {string} dir
16
+ * @returns {string[]}
17
+ */
18
+ function findCommandFiles(dir) {
19
+ if (!fs.existsSync(dir)) return [];
20
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
21
+ const files = [];
22
+
23
+ for (const entry of entries) {
24
+ const fullPath = path.join(dir, entry.name);
25
+ if (entry.isDirectory()) {
26
+ files.push(...findCommandFiles(fullPath));
27
+ } else if (
28
+ entry.isFile() &&
29
+ entry.name.endsWith('.js') &&
30
+ !entry.name.startsWith('_') &&
31
+ !entry.name.endsWith('.test.js') &&
32
+ !entry.name.endsWith('.spec.js')
33
+ ) {
34
+ files.push(fullPath);
35
+ }
36
+ }
37
+
38
+ return files;
39
+ }
40
+
41
+ /**
42
+ * Derive command name from relative path
43
+ * e.g. "users/prune.js" -> "users:prune", "send-invoices.js" -> "send-invoices"
44
+ * @param {string} relPath
45
+ * @returns {string}
46
+ */
47
+ function deriveCommandName(relPath) {
48
+ const withoutExt = relPath.replace(/\.js$/, '');
49
+ const segments = withoutExt.split(/[\\/]/).filter(Boolean);
50
+ return segments.join(':');
51
+ }
52
+
53
+ /**
54
+ * Create a CommandRegistry instance
55
+ * @param {Object} [options]
56
+ * @param {string} [options.commandsDir] - Root directory for commands (default: ./commands)
57
+ * @param {boolean} [options.autoLoad=true] - Whether to discover files immediately
58
+ * @returns {Object} CommandRegistry
59
+ */
60
+ function createCommandRegistry(options = {}) {
61
+ const commandsDir = options.commandsDir
62
+ ? path.resolve(process.cwd(), options.commandsDir)
63
+ : path.resolve(process.cwd(), 'commands');
64
+
65
+ /** @type {Map<string, Object>} */
66
+ const commands = new Map();
67
+
68
+ function register(cmdDef) {
69
+ if (!cmdDef || typeof cmdDef !== 'object') {
70
+ throw new TypeError('Invalid command definition object');
71
+ }
72
+ const def = typeof cmdDef.handle === 'function' && defIsNormalized(cmdDef)
73
+ ? cmdDef
74
+ : defineCommand(cmdDef);
75
+
76
+ commands.set(def.name, def);
77
+ return def;
78
+ }
79
+
80
+ function defIsNormalized(obj) {
81
+ return typeof obj.name === 'string' && typeof obj.handle === 'function' && Array.isArray(obj.options);
82
+ }
83
+
84
+ function load() {
85
+ commands.clear();
86
+ const files = findCommandFiles(commandsDir);
87
+
88
+ for (const file of files) {
89
+ try {
90
+ delete require.cache[require.resolve(file)];
91
+ const exported = require(file);
92
+ const rel = path.relative(commandsDir, file);
93
+ const derivedName = deriveCommandName(rel);
94
+
95
+ let cmdDef = null;
96
+
97
+ if (typeof exported === 'function') {
98
+ cmdDef = defineCommand({
99
+ name: derivedName,
100
+ handle: exported,
101
+ });
102
+ } else if (exported && typeof exported === 'object') {
103
+ // Check if exported is an ES default or CJS module
104
+ const target = exported.default || exported;
105
+ const name = target.name || derivedName;
106
+ const handle = target.handle || target.run;
107
+
108
+ if (typeof handle === 'function') {
109
+ cmdDef = defineCommand({
110
+ name,
111
+ description: target.description || '',
112
+ options: target.options || [],
113
+ handle,
114
+ });
115
+ }
116
+ }
117
+
118
+ if (cmdDef) {
119
+ commands.set(cmdDef.name, cmdDef);
120
+ }
121
+ } catch (err) {
122
+ console.warn(`[commands] Failed to load command from "${file}":`, err.message);
123
+ }
124
+ }
125
+ }
126
+
127
+ if (options.autoLoad !== false) {
128
+ load();
129
+ }
130
+
131
+ return {
132
+ register,
133
+ reload: load,
134
+ has(name) {
135
+ return commands.has(name);
136
+ },
137
+ get(name) {
138
+ return commands.get(name) || null;
139
+ },
140
+ list() {
141
+ return Array.from(commands.values());
142
+ },
143
+ names() {
144
+ return Array.from(commands.keys());
145
+ },
146
+ get commandsDir() {
147
+ return commandsDir;
148
+ },
149
+ };
150
+ }
151
+
152
+ module.exports = {
153
+ createCommandRegistry,
154
+ findCommandFiles,
155
+ deriveCommandName,
156
+ };
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso - Command Runner & Execution Engine
5
+ * Injects framework services, database, queue, mailer, events and manages execution lifecycle
6
+ * @module core/commands/runner
7
+ */
8
+
9
+ /**
10
+ * Execute a command definition with dependency injection
11
+ * @param {Object} commandDef - Command specification returned by defineCommand
12
+ * @param {Object} [executionOptions]
13
+ * @param {Array<string>} [executionOptions.args] - Positional CLI arguments
14
+ * @param {Object} [executionOptions.flags] - Named flags / options
15
+ * @param {Object} [executionOptions.db] - Knex / ORM database instance
16
+ * @param {Object} [executionOptions.queue] - Background job queue dispatcher
17
+ * @param {Object} [executionOptions.services] - Service registry or caller
18
+ * @param {Object} [executionOptions.mailer] - Mailer service
19
+ * @param {Object} [executionOptions.events] - Application event dispatcher
20
+ * @param {Object} [executionOptions.logger] - Logger instance (default: console)
21
+ * @returns {Promise<{ success: boolean, result?: any, error?: Error, durationMs: number }>}
22
+ */
23
+ async function executeCommand(commandDef, executionOptions = {}) {
24
+ if (!commandDef || typeof commandDef !== 'object') {
25
+ throw new TypeError('executeCommand requires a command definition');
26
+ }
27
+
28
+ const handleFn = commandDef.handle || commandDef.run;
29
+ if (typeof handleFn !== 'function') {
30
+ throw new TypeError(`Command "${commandDef.name || 'anonymous'}" has no executable handle function`);
31
+ }
32
+
33
+ const {
34
+ args = [],
35
+ flags = {},
36
+ db = null,
37
+ queue = null,
38
+ services = null,
39
+ mailer = null,
40
+ events = null,
41
+ logger = console,
42
+ } = executionOptions;
43
+
44
+ const start = Date.now();
45
+ const context = {
46
+ db,
47
+ queue,
48
+ services,
49
+ mailer,
50
+ events,
51
+ logger,
52
+ args: Array.isArray(args) ? args : [],
53
+ flags: flags && typeof flags === 'object' ? flags : {},
54
+ };
55
+
56
+ try {
57
+ const result = await handleFn(context);
58
+ const durationMs = Date.now() - start;
59
+ return {
60
+ success: true,
61
+ result,
62
+ durationMs,
63
+ };
64
+ } catch (error) {
65
+ const durationMs = Date.now() - start;
66
+ return {
67
+ success: false,
68
+ error,
69
+ durationMs,
70
+ };
71
+ }
72
+ }
73
+
74
+ module.exports = {
75
+ executeCommand,
76
+ };
@@ -8,7 +8,7 @@
8
8
  const COMPRESSIBLE_TYPE_REGEX = /^(?:text\/[a-z0-9.+_-]+|application\/(?:javascript|x-javascript|json|ld\+json|manifest\+json|graphql\+json|geo\+json|xml|atom\+xml|rss\+xml|xhtml\+xml|x-yaml|yaml|vnd\.api\+json|schema\+json|hal\+json)|image\/svg\+xml)$/i;
9
9
 
10
10
  // Regex patterns for already-compressed or non-compressible types
11
- const INCOMPRESSIBLE_TYPE_REGEX = /^(?:image\/(?:jpeg|png|gif|webp|avif|heic|heif)|video\/[a-z0-9.+_-]+|audio\/[a-z0-9.+_-]+|application\/(?:zip|gzip|x-gzip|x-bzip2|x-compress|x-7z-compressed|x-rar-compressed|pdf|wasm|octet-stream))$/i;
11
+ const INCOMPRESSIBLE_TYPE_REGEX = /^(?:text\/event-stream|image\/(?:jpeg|png|gif|webp|avif|heic|heif)|video\/[a-z0-9.+_-]+|audio\/[a-z0-9.+_-]+|application\/(?:zip|gzip|x-gzip|x-bzip2|x-compress|x-7z-compressed|x-rar-compressed|pdf|wasm|octet-stream))$/i;
12
12
 
13
13
  const compressibleCache = new Map();
14
14
  const MAX_MIME_CACHE_SIZE = 256;