webspresso 0.0.99 → 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.
- package/.agents/ADMIN.md +47 -4
- package/.agents/AGENTS.md +4 -2
- package/.agents/AUTH.md +309 -0
- package/.agents/CLI.md +128 -0
- package/.agents/EMAIL.md +84 -2
- package/.agents/ERRORS.md +59 -0
- package/.agents/EVENTS.md +179 -0
- package/.agents/ORM.md +149 -0
- package/.agents/PLUGINS.md +44 -7
- package/.agents/QUEUE.md +18 -10
- package/.agents/ROUTING.md +97 -1
- package/.agents/STREAMING.md +35 -0
- package/AGENTS.md +2 -0
- package/README.md +145 -93
- package/bin/commands/add-docker.js +467 -0
- package/bin/commands/db-makemigrations.js +83 -0
- package/bin/commands/deploy-check.js +339 -0
- package/bin/commands/list.js +96 -0
- package/bin/commands/make-command.js +93 -0
- package/bin/commands/make-event.js +80 -0
- package/bin/commands/make-factory.js +72 -0
- package/bin/commands/make-mail.js +93 -0
- package/bin/commands/make-policy.js +106 -0
- package/bin/commands/run.js +195 -0
- package/bin/webspresso.js +21 -0
- package/core/auth/bouncer.js +196 -0
- package/core/auth/index.js +73 -1
- package/core/auth/magic-link.js +170 -0
- package/core/auth/manager.js +343 -3
- package/core/auth/middleware.js +45 -6
- package/core/auth/session-manager.js +255 -0
- package/core/auth/sessions.js +351 -0
- package/core/auth/tokens.js +1 -0
- package/core/auth/totp.js +4 -1
- package/core/auth/user-agent.js +161 -0
- package/core/commands/define-command.js +48 -0
- package/core/commands/index.js +18 -0
- package/core/commands/registry.js +156 -0
- package/core/commands/runner.js +76 -0
- package/core/compression/compressible.js +1 -1
- package/core/compression/middleware.js +35 -251
- package/core/content/service.js +8 -2
- package/core/events/discovery.js +142 -0
- package/core/events/dispatcher.js +552 -0
- package/core/events/index.js +23 -0
- package/core/events/signal.js +92 -0
- package/core/forms/flash.js +234 -0
- package/core/forms/form-builder.js +214 -0
- package/core/forms/index.js +16 -0
- package/core/mail/index.js +23 -0
- package/core/mail/mailable.js +250 -0
- package/core/mail/mailer.js +184 -0
- package/core/mail/transports.js +238 -0
- package/core/orm/factory.js +250 -0
- package/core/orm/index.js +8 -1
- package/core/orm/migrations/diff.js +185 -0
- package/core/orm/migrations/index.js +22 -4
- package/core/orm/model.js +55 -1
- package/core/orm/repository.js +283 -13
- package/core/orm/scopes.js +3 -2
- package/core/orm/types.js +6 -1
- package/core/queue/cron.js +252 -39
- package/core/queue/manager.js +20 -2
- package/core/queue/scheduler.js +115 -59
- package/core/ssr/stream.js +62 -2
- package/index.d.ts +540 -14
- package/index.js +44 -0
- package/package.json +11 -5
- package/plugins/admin-panel/api.js +509 -3
- package/plugins/admin-panel/index.js +17 -0
- package/plugins/admin-panel/modules/admin-users.js +1 -1
- package/plugins/audit-log/admin-component.js +54 -54
- package/plugins/audit-log/index.js +2 -2
- package/plugins/content/client/inline-edit.js +2 -46
- package/plugins/content/models/content-entry.js +4 -3
- package/plugins/content/models/content-type.js +4 -3
- package/plugins/cors.js +15 -4
- package/plugins/email/auth-bridge.js +66 -0
- package/plugins/email/bundled-templates.js +15 -0
- package/plugins/email/index.js +48 -0
- package/plugins/email/preview-handler.js +555 -0
- package/plugins/email/service.js +23 -12
- package/plugins/email/templates/auth-magic-link.mjml +13 -0
- package/plugins/email/transport.js +30 -11
- package/plugins/file-manager/admin-component.js +49 -49
- package/plugins/file-manager/index.js +4 -4
- package/plugins/index.js +2 -0
- package/plugins/redirect/index.js +1 -1
- package/plugins/webhooks/admin-component.js +608 -0
- package/plugins/webhooks/api-handlers.js +237 -0
- package/plugins/webhooks/crypto.js +144 -0
- package/plugins/webhooks/dispatcher.js +708 -0
- package/plugins/webhooks/index.js +263 -0
- package/plugins/webhooks/matcher.js +113 -0
- package/plugins/webhooks/migration-template.js +75 -0
- package/plugins/webhooks/models.js +98 -0
- package/plugins/webhooks/sender.js +176 -0
- package/src/api/api-loader.js +1 -1
- package/src/app-context.js +48 -6
- package/src/bundle/index.js +2 -1
- package/src/dev-reload/index.js +9 -1
- package/src/discovery/scan-routes.js +10 -1
- package/src/file-router.js +173 -4
- package/src/helpers.js +150 -1
- package/src/njk-frontmatter.js +3 -0
- package/src/pages/page-loader.js +1 -1
- package/src/routing/cascading.js +120 -4
- package/src/server.js +180 -26
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Zero-Dependency User-Agent & Device Parser
|
|
3
|
+
* Extracts browser, operating system, and device category from User-Agent header
|
|
4
|
+
* @module core/auth/user-agent
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} DeviceInfo
|
|
11
|
+
* @property {string} browser - Browser name with major version (e.g. 'Chrome 128', 'Safari 17')
|
|
12
|
+
* @property {string} os - Operating system name (e.g. 'macOS 14', 'Windows 11', 'iOS 17')
|
|
13
|
+
* @property {'desktop'|'mobile'|'tablet'|'bot'|'unknown'} device - Device category
|
|
14
|
+
* @property {boolean} isMobile
|
|
15
|
+
* @property {boolean} isTablet
|
|
16
|
+
* @property {boolean} isDesktop
|
|
17
|
+
* @property {boolean} isBot
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extract client IP address handling proxies
|
|
22
|
+
* @param {Object} req - Express / Node.js HTTP request
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
function getClientIp(req) {
|
|
26
|
+
if (!req) return '127.0.0.1';
|
|
27
|
+
|
|
28
|
+
const forwarded = req.headers?.['x-forwarded-for'];
|
|
29
|
+
if (forwarded) {
|
|
30
|
+
const ips = typeof forwarded === 'string' ? forwarded.split(',') : forwarded;
|
|
31
|
+
const first = Array.isArray(ips) ? ips[0] : ips;
|
|
32
|
+
if (first) return first.trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return req.headers?.['x-real-ip'] ||
|
|
36
|
+
req.ip ||
|
|
37
|
+
req.connection?.remoteAddress ||
|
|
38
|
+
req.socket?.remoteAddress ||
|
|
39
|
+
'127.0.0.1';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse raw User-Agent header string into structured device info
|
|
44
|
+
* Zero external dependencies — pure regex extraction
|
|
45
|
+
*
|
|
46
|
+
* @param {string} [userAgent='']
|
|
47
|
+
* @returns {DeviceInfo}
|
|
48
|
+
*/
|
|
49
|
+
function parseUserAgent(userAgent = '') {
|
|
50
|
+
const ua = String(userAgent || '').trim();
|
|
51
|
+
|
|
52
|
+
if (!ua) {
|
|
53
|
+
return {
|
|
54
|
+
browser: 'Unknown',
|
|
55
|
+
os: 'Unknown',
|
|
56
|
+
device: 'unknown',
|
|
57
|
+
isMobile: false,
|
|
58
|
+
isTablet: false,
|
|
59
|
+
isDesktop: false,
|
|
60
|
+
isBot: false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 1. Detect Bots / Crawlers
|
|
65
|
+
const botPattern = /googlebot|bingbot|yandex|baiduspider|duckduckbot|slurp|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest\/|slackbot|vkshare|w3c_validator|whatsapp|curl|wget|postman/i;
|
|
66
|
+
const isBot = botPattern.test(ua);
|
|
67
|
+
|
|
68
|
+
// 2. Detect Operating System
|
|
69
|
+
let os = 'Unknown';
|
|
70
|
+
if (/iPad/i.test(ua)) {
|
|
71
|
+
const match = ua.match(/OS ([\d_\.]+)/i);
|
|
72
|
+
os = match ? `iPadOS ${match[1].replace(/_/g, '.')}` : 'iPadOS';
|
|
73
|
+
} else if (/iPhone|iPod/i.test(ua)) {
|
|
74
|
+
const match = ua.match(/OS ([\d_\.]+)/i);
|
|
75
|
+
os = match ? `iOS ${match[1].replace(/_/g, '.')}` : 'iOS';
|
|
76
|
+
} else if (/Android/i.test(ua)) {
|
|
77
|
+
const match = ua.match(/Android (\d+(\.\d+)?)/i);
|
|
78
|
+
os = match ? `Android ${match[1]}` : 'Android';
|
|
79
|
+
} else if (/Macintosh|Mac OS X/i.test(ua)) {
|
|
80
|
+
const match = ua.match(/Mac OS X ([\d_\.]+)/i);
|
|
81
|
+
if (match) {
|
|
82
|
+
const ver = match[1].replace(/_/g, '.');
|
|
83
|
+
os = `macOS ${ver}`;
|
|
84
|
+
} else {
|
|
85
|
+
os = 'macOS';
|
|
86
|
+
}
|
|
87
|
+
} else if (/Windows/i.test(ua)) {
|
|
88
|
+
if (/Windows NT 10\.0/i.test(ua)) os = 'Windows 10/11';
|
|
89
|
+
else if (/Windows NT 6\.3/i.test(ua)) os = 'Windows 8.1';
|
|
90
|
+
else if (/Windows NT 6\.2/i.test(ua)) os = 'Windows 8';
|
|
91
|
+
else if (/Windows NT 6\.1/i.test(ua)) os = 'Windows 7';
|
|
92
|
+
else os = 'Windows';
|
|
93
|
+
} else if (/CrOS/i.test(ua)) {
|
|
94
|
+
os = 'ChromeOS';
|
|
95
|
+
} else if (/Linux/i.test(ua)) {
|
|
96
|
+
if (/Ubuntu/i.test(ua)) os = 'Ubuntu Linux';
|
|
97
|
+
else if (/Fedora/i.test(ua)) os = 'Fedora Linux';
|
|
98
|
+
else os = 'Linux';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 3. Detect Browser
|
|
102
|
+
let browser = 'Unknown';
|
|
103
|
+
if (/Edg(e|A|iOS)?\/(\d+)/i.test(ua)) {
|
|
104
|
+
const match = ua.match(/Edg(e|A|iOS)?\/(\d+)/i);
|
|
105
|
+
browser = `Edge ${match ? match[2] : ''}`.trim();
|
|
106
|
+
} else if (/OPR\/(\d+)/i.test(ua)) {
|
|
107
|
+
const match = ua.match(/OPR\/(\d+)/i);
|
|
108
|
+
browser = `Opera ${match ? match[1] : ''}`.trim();
|
|
109
|
+
} else if (/SamsungBrowser\/(\d+)/i.test(ua)) {
|
|
110
|
+
const match = ua.match(/SamsungBrowser\/(\d+)/i);
|
|
111
|
+
browser = `Samsung Internet ${match ? match[1] : ''}`.trim();
|
|
112
|
+
} else if (/Brave\/(\d+)/i.test(ua)) {
|
|
113
|
+
const match = ua.match(/Brave\/(\d+)/i);
|
|
114
|
+
browser = `Brave ${match ? match[1] : ''}`.trim();
|
|
115
|
+
} else if (/Vivaldi\/(\d+)/i.test(ua)) {
|
|
116
|
+
const match = ua.match(/Vivaldi\/(\d+)/i);
|
|
117
|
+
browser = `Vivaldi ${match ? match[1] : ''}`.trim();
|
|
118
|
+
} else if (/Chrome\/(\d+)/i.test(ua) && !/Chromium/i.test(ua)) {
|
|
119
|
+
const match = ua.match(/Chrome\/(\d+)/i);
|
|
120
|
+
browser = `Chrome ${match ? match[1] : ''}`.trim();
|
|
121
|
+
} else if (/Firefox\/(\d+)/i.test(ua) || /FxiOS\/(\d+)/i.test(ua)) {
|
|
122
|
+
const match = ua.match(/(?:Firefox|FxiOS)\/(\d+)/i);
|
|
123
|
+
browser = `Firefox ${match ? match[1] : ''}`.trim();
|
|
124
|
+
} else if (/Safari/i.test(ua) && /Version\/(\d+)/i.test(ua)) {
|
|
125
|
+
const match = ua.match(/Version\/(\d+)/i);
|
|
126
|
+
browser = `Safari ${match ? match[1] : ''}`.trim();
|
|
127
|
+
} else if (/Safari/i.test(ua) && !/Chrome/i.test(ua)) {
|
|
128
|
+
browser = 'Safari';
|
|
129
|
+
} else if (/MSIE (\d+)/i.test(ua) || (/Trident/i.test(ua) && /rv:(\d+)/i.test(ua))) {
|
|
130
|
+
browser = 'Internet Explorer';
|
|
131
|
+
} else if (isBot) {
|
|
132
|
+
const botNameMatch = ua.match(/(googlebot|bingbot|duckduckbot|slurp|twitterbot|slackbot|curl|wget)/i);
|
|
133
|
+
browser = botNameMatch ? botNameMatch[1] : 'Bot';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 4. Detect Device Category
|
|
137
|
+
const isTablet = /iPad|tablet/i.test(ua) || (/android/i.test(ua) && !/mobile/i.test(ua));
|
|
138
|
+
const isMobile = !isTablet && /mobile|iphone|ipod|android|blackberry|opera mini|windows phone/i.test(ua);
|
|
139
|
+
const isDesktop = !isTablet && !isMobile && !isBot;
|
|
140
|
+
|
|
141
|
+
let device = 'unknown';
|
|
142
|
+
if (isBot) device = 'bot';
|
|
143
|
+
else if (isTablet) device = 'tablet';
|
|
144
|
+
else if (isMobile) device = 'mobile';
|
|
145
|
+
else if (isDesktop) device = 'desktop';
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
browser,
|
|
149
|
+
os,
|
|
150
|
+
device,
|
|
151
|
+
isMobile,
|
|
152
|
+
isTablet,
|
|
153
|
+
isDesktop,
|
|
154
|
+
isBot,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = {
|
|
159
|
+
getClientIp,
|
|
160
|
+
parseUserAgent,
|
|
161
|
+
};
|
|
@@ -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;
|