webspresso 0.0.90 → 0.0.93
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/README.md +98 -2509
- package/bin/commands/admin-password.js +8 -2
- package/bin/commands/api.js +7 -1
- package/bin/commands/favicon-generate.js +6 -4
- package/bin/commands/mcp.js +162 -0
- package/bin/commands/new.js +7 -1
- package/bin/commands/page.js +7 -1
- package/bin/commands/seed.js +7 -1
- package/bin/commands/service.js +199 -0
- package/bin/commands/skill.js +7 -1
- package/bin/webspresso.js +4 -0
- package/core/auth/middleware.js +9 -0
- package/core/compileSchema.js +1 -5
- package/core/compression/compressible.js +63 -0
- package/core/compression/compression-stream.js +59 -0
- package/core/compression/index.js +25 -0
- package/core/compression/middleware.js +310 -0
- package/core/compression/negotiate.js +165 -0
- package/core/errors/base.js +31 -0
- package/core/errors/domain.js +193 -0
- package/core/errors/http.js +133 -0
- package/core/errors/index.js +77 -0
- package/core/errors/middleware.js +162 -0
- package/core/errors/normalize.js +171 -0
- package/core/kernel/app.js +2 -1
- package/core/kernel/index.js +5 -1
- package/core/orm/cache/fingerprint.js +23 -5
- package/core/orm/complexity.js +120 -0
- package/core/orm/eager-loader.js +54 -9
- package/core/orm/events.js +64 -14
- package/core/orm/index.js +83 -12
- package/core/orm/model.js +13 -2
- package/core/orm/query-builder.js +30 -4
- package/core/orm/repository.js +57 -25
- package/core/orm/transaction.js +39 -5
- package/core/orm/utils.js +6 -3
- package/core/queue/adapters/database.js +317 -0
- package/core/queue/adapters/memory.js +176 -0
- package/core/queue/adapters/redis.js +357 -0
- package/core/queue/index.js +21 -0
- package/core/queue/job.js +103 -0
- package/core/queue/manager.js +413 -0
- package/core/realtime/adapters/redis.js +431 -0
- package/core/realtime/adapters/socket-io.js +203 -0
- package/core/realtime/adapters/sse.js +210 -0
- package/core/realtime/adapters/websocket.js +337 -0
- package/core/realtime/auth-manager.js +140 -0
- package/core/realtime/core.js +359 -0
- package/core/realtime/emitter.js +98 -0
- package/core/realtime/errors.js +71 -0
- package/core/realtime/index.js +79 -0
- package/core/realtime/reconnect-manager.js +118 -0
- package/core/realtime/registry.js +243 -0
- package/core/shutdown/http-adapter.js +134 -0
- package/core/shutdown/index.js +12 -0
- package/core/shutdown/shutdown-manager.js +312 -0
- package/core/ssr/index.js +13 -0
- package/core/ssr/stream.js +174 -0
- package/core/validation/index.js +70 -0
- package/index.d.ts +924 -53
- package/index.js +110 -1
- package/package.json +30 -2
- package/plugins/admin-panel/api.js +37 -4
- package/plugins/admin-panel/app.js +57 -17
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +10 -2
- package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +51 -27
- package/plugins/admin-panel/client/parts/07-model-list.js +9 -4
- package/plugins/admin-panel/client/parts/08-record-list.js +37 -26
- package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
- package/plugins/admin-panel/core/admin-module.js +2 -2
- package/plugins/admin-panel/core/api-extensions.js +149 -0
- package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
- package/plugins/admin-panel/index.js +155 -23
- package/plugins/admin-panel/modules/bulk-actions.js +9 -2
- package/plugins/admin-panel/modules/custom-pages.js +144 -45
- package/plugins/admin-panel/modules/dashboard.js +8 -1
- package/plugins/admin-panel/modules/menu.js +85 -0
- package/plugins/admin-panel/modules/user-management.js +5 -2
- package/plugins/admin-panel/vendor/mithril.min.js +1 -0
- package/plugins/admin-panel/vendor/quill.min.js +7 -0
- package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
- package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
- package/plugins/basic-auth/index.js +293 -0
- package/plugins/data-exchange/export-xlsx.js +28 -2
- package/plugins/data-exchange/import.js +55 -16
- package/plugins/data-exchange/index.js +19 -0
- package/plugins/data-exchange/parse-table.js +14 -5
- package/plugins/data-exchange/record-selection.js +2 -0
- package/plugins/email/index.js +14 -1
- package/plugins/index.js +33 -0
- package/plugins/mcp/auth.js +104 -0
- package/plugins/mcp/discovery.js +88 -0
- package/plugins/mcp/index.js +208 -0
- package/plugins/mcp/prompts.js +149 -0
- package/plugins/mcp/providers/orm.js +325 -0
- package/plugins/mcp/providers/services.js +190 -0
- package/plugins/mcp/providers/system.js +87 -0
- package/plugins/mcp/server.js +472 -0
- package/plugins/mcp/transports/sse.js +139 -0
- package/plugins/mcp/transports/stdio.js +108 -0
- package/plugins/queue/index.js +108 -0
- package/plugins/realtime/index.js +150 -0
- package/plugins/recaptcha/index.js +11 -0
- package/plugins/redirect/index.js +9 -1
- package/plugins/rest-resources/index.js +280 -11
- package/plugins/upload/index.js +16 -0
- package/src/app-context.js +46 -6
- package/src/file-router.js +366 -130
- package/src/helpers.js +20 -2
- package/src/njk-frontmatter.js +24 -14
- package/src/plugin-manager.js +124 -4
- package/src/server.js +332 -54
- package/src/services/builtins/auth.js +273 -0
- package/src/services/builtins/exchange.js +229 -0
- package/src/services/builtins/mail.js +134 -0
- package/src/services/builtins/media.js +196 -0
- package/src/services/builtins/system.js +221 -0
- package/src/services/discovery.js +141 -0
- package/src/services/executor.js +292 -0
- package/src/services/index.js +70 -0
- package/src/services/memoize.js +256 -0
- package/src/services/registry.js +290 -0
- package/src/services/validator.js +230 -0
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +6 -4
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
* Reset admin user password via CLI
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
const
|
|
6
|
+
let _inquirer;
|
|
7
|
+
const inquirer = {
|
|
8
|
+
prompt: (...args) => {
|
|
9
|
+
if (!_inquirer) _inquirer = require('inquirer');
|
|
10
|
+
return _inquirer.prompt(...args);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
8
13
|
const { loadDbConfig, createDbInstance } = require('../utils/db');
|
|
9
14
|
|
|
10
15
|
function registerCommand(program) {
|
|
@@ -86,6 +91,7 @@ function registerCommand(program) {
|
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
// Hash the password (same rounds as admin panel setup)
|
|
94
|
+
const { hash } = require('../../core/auth/hash');
|
|
89
95
|
const hashedPassword = await hash(password, 10);
|
|
90
96
|
|
|
91
97
|
// Update the password
|
package/bin/commands/api.js
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
* Add a new API endpoint to the current project
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
let _inquirer;
|
|
7
|
+
const inquirer = {
|
|
8
|
+
prompt: (...args) => {
|
|
9
|
+
if (!_inquirer) _inquirer = require('inquirer');
|
|
10
|
+
return _inquirer.prompt(...args);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
7
13
|
const fs = require('fs');
|
|
8
14
|
const path = require('path');
|
|
9
15
|
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
function getSharp() {
|
|
9
|
+
return process.env.NODE_ENV === 'test'
|
|
10
|
+
? require('../../tests/mocks/sharp')
|
|
11
|
+
: require('sharp');
|
|
12
|
+
}
|
|
11
13
|
|
|
12
14
|
const APPLE_SIZES = [57, 60, 72, 76, 114, 120, 144, 152, 180];
|
|
13
15
|
const ANDROID_SIZE = 192;
|
|
@@ -54,7 +56,7 @@ function registerCommand(program) {
|
|
|
54
56
|
const allSizes = [...new Set([...APPLE_SIZES, ANDROID_SIZE, ...FAVICON_SIZES, MS_TILE_SIZE])].sort((a, b) => a - b);
|
|
55
57
|
|
|
56
58
|
for (const size of allSizes) {
|
|
57
|
-
const buffer = await
|
|
59
|
+
const buffer = await getSharp()(sourcePath).resize(size, size).png().toBuffer();
|
|
58
60
|
|
|
59
61
|
if (APPLE_SIZES.includes(size)) {
|
|
60
62
|
const filename = `apple-icon-${size}x${size}.png`;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso CLI: MCP Command
|
|
3
|
+
* Runs the Webspresso Model Context Protocol (MCP) server over standard I/O for IDEs (Antigravity, Claude, Cursor)
|
|
4
|
+
* @module bin/commands/mcp
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Register MCP command in Commander CLI
|
|
13
|
+
* @param {import('commander').Command} program
|
|
14
|
+
*/
|
|
15
|
+
function registerCommand(program) {
|
|
16
|
+
program
|
|
17
|
+
.command('mcp')
|
|
18
|
+
.description('Start Model Context Protocol (MCP) server over Stdio for AI coding assistants')
|
|
19
|
+
.option('-d, --dir <path>', 'Project root directory', process.cwd())
|
|
20
|
+
.option('-c, --config <path>', 'Database config path (knexfile.js or webspresso.db.js)')
|
|
21
|
+
.option('-E, --env <environment>', 'Environment (development, production)', 'development')
|
|
22
|
+
.option('--read-only', 'Restrict tools to read-only queries (blocks mutations/inserts)', false)
|
|
23
|
+
.option('--no-services', 'Disable auto-bridging of services/ directory')
|
|
24
|
+
.option('--no-orm', 'Disable auto-bridging of ORM models and schema')
|
|
25
|
+
.option('--print-config', 'Print sample Claude Desktop / Cursor MCP configuration JSON snippet')
|
|
26
|
+
.action(async (options) => {
|
|
27
|
+
// 1. If print-config flag is passed, print snippet and exit cleanly
|
|
28
|
+
if (options.printConfig) {
|
|
29
|
+
const configSnippet = {
|
|
30
|
+
mcpServers: {
|
|
31
|
+
webspresso: {
|
|
32
|
+
command: 'npx',
|
|
33
|
+
args: ['webspresso', 'mcp'],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
console.log(JSON.stringify(configSnippet, null, 2));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const rootDir = path.resolve(process.cwd(), options.dir || '.');
|
|
42
|
+
const isReadOnly = Boolean(options.readOnly);
|
|
43
|
+
|
|
44
|
+
const {
|
|
45
|
+
McpServer,
|
|
46
|
+
createServiceTools,
|
|
47
|
+
createOrmTools,
|
|
48
|
+
createOrmResources,
|
|
49
|
+
createSystemResources,
|
|
50
|
+
createBuiltinPrompts,
|
|
51
|
+
startStdioTransport,
|
|
52
|
+
discoverMcpDirectory,
|
|
53
|
+
} = require('../../plugins/mcp');
|
|
54
|
+
const { createServiceRegistry } = require('../../src/services');
|
|
55
|
+
const { resolveDbConfigIfExists, createDbInstance } = require('../utils/db');
|
|
56
|
+
const { getAllModels } = require('../../core/orm/model');
|
|
57
|
+
|
|
58
|
+
// Create Server instance
|
|
59
|
+
const server = new McpServer({
|
|
60
|
+
name: 'webspresso-mcp',
|
|
61
|
+
version: require('../../package.json').version,
|
|
62
|
+
instructions: `Webspresso MCP server running in ${rootDir} (readOnly: ${isReadOnly}). Exposes business logic services and ORM models.`,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Register built-in prompts
|
|
66
|
+
const prompts = createBuiltinPrompts();
|
|
67
|
+
for (const p of prompts) {
|
|
68
|
+
server.registerPrompt(p);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let db = null;
|
|
72
|
+
let registry = null;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// 2. Resolve database if not disabled
|
|
76
|
+
if (options.orm !== false) {
|
|
77
|
+
const dbConfigResult = resolveDbConfigIfExists(options.config);
|
|
78
|
+
if (dbConfigResult) {
|
|
79
|
+
try {
|
|
80
|
+
db = await createDbInstance(dbConfigResult.config, options.env);
|
|
81
|
+
server.context.db = db;
|
|
82
|
+
|
|
83
|
+
const ormTools = createOrmTools({
|
|
84
|
+
db,
|
|
85
|
+
readOnly: isReadOnly,
|
|
86
|
+
});
|
|
87
|
+
server.registerTools(ormTools);
|
|
88
|
+
|
|
89
|
+
const { resources, templates } = createOrmResources({ db });
|
|
90
|
+
for (const r of resources) {
|
|
91
|
+
server.registerResource(r);
|
|
92
|
+
}
|
|
93
|
+
for (const t of templates) {
|
|
94
|
+
server.registerResourceTemplate(t);
|
|
95
|
+
}
|
|
96
|
+
} catch (dbErr) {
|
|
97
|
+
process.stderr.write(`[webspresso-mcp] DB initialization skipped: ${dbErr.message}\n`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 3. Resolve services if not disabled
|
|
103
|
+
if (options.services !== false) {
|
|
104
|
+
const servicesDir = path.join(rootDir, 'services');
|
|
105
|
+
if (fs.existsSync(servicesDir)) {
|
|
106
|
+
registry = createServiceRegistry({
|
|
107
|
+
servicesDir,
|
|
108
|
+
isDev: options.env !== 'production',
|
|
109
|
+
});
|
|
110
|
+
server.context.serviceRegistry = registry;
|
|
111
|
+
|
|
112
|
+
const serviceTools = createServiceTools({
|
|
113
|
+
registry,
|
|
114
|
+
readOnly: isReadOnly,
|
|
115
|
+
role: 'admin',
|
|
116
|
+
});
|
|
117
|
+
server.registerTools(serviceTools);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 4. Register system resources
|
|
122
|
+
const sysResources = createSystemResources({
|
|
123
|
+
serviceRegistry: registry,
|
|
124
|
+
db,
|
|
125
|
+
});
|
|
126
|
+
for (const r of sysResources) {
|
|
127
|
+
server.registerResource(r);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 5. Discover custom mcp/ components
|
|
131
|
+
discoverMcpDirectory(server, rootDir);
|
|
132
|
+
|
|
133
|
+
// 6. Start Stdio transport
|
|
134
|
+
const transport = startStdioTransport(server, {
|
|
135
|
+
redirectStderr: true,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
process.stderr.write(
|
|
139
|
+
`[webspresso-mcp] Server started (Stdio transport). Registered ${server.tools.size} tools, ${server.resources.size} resources, ${server.prompts.size} prompts.\n`
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Handle process termination
|
|
143
|
+
const cleanup = async () => {
|
|
144
|
+
transport.close();
|
|
145
|
+
if (db && typeof db.destroy === 'function') {
|
|
146
|
+
await db.destroy();
|
|
147
|
+
}
|
|
148
|
+
process.exit(0);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
process.on('SIGINT', cleanup);
|
|
152
|
+
process.on('SIGTERM', cleanup);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
process.stderr.write(`[webspresso-mcp] Fatal error: ${err.message}\n`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
registerCommand,
|
|
162
|
+
};
|
package/bin/commands/new.js
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
* Creates a new Webspresso project
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
let _inquirer;
|
|
7
|
+
const inquirer = {
|
|
8
|
+
prompt: (...args) => {
|
|
9
|
+
if (!_inquirer) _inquirer = require('inquirer');
|
|
10
|
+
return _inquirer.prompt(...args);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
7
13
|
const fs = require('fs');
|
|
8
14
|
const path = require('path');
|
|
9
15
|
const { runInstallation, startDevServer } = require('../utils/project');
|
package/bin/commands/page.js
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
* Add a new page to the current project
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
let _inquirer;
|
|
7
|
+
const inquirer = {
|
|
8
|
+
prompt: (...args) => {
|
|
9
|
+
if (!_inquirer) _inquirer = require('inquirer');
|
|
10
|
+
return _inquirer.prompt(...args);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
7
13
|
const fs = require('fs');
|
|
8
14
|
const path = require('path');
|
|
9
15
|
|
package/bin/commands/seed.js
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
* Run database seeders to populate database with fake data
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
let _inquirer;
|
|
7
|
+
const inquirer = {
|
|
8
|
+
prompt: (...args) => {
|
|
9
|
+
if (!_inquirer) _inquirer = require('inquirer');
|
|
10
|
+
return _inquirer.prompt(...args);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
7
13
|
const fs = require('fs');
|
|
8
14
|
const path = require('path');
|
|
9
15
|
const { execSync } = require('child_process');
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso CLI: Service Command
|
|
3
|
+
* Run and test services interactively without starting the HTTP server
|
|
4
|
+
* @module bin/commands/service
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
let _inquirer;
|
|
9
|
+
const inquirer = {
|
|
10
|
+
prompt: (...args) => {
|
|
11
|
+
if (!_inquirer) _inquirer = require('inquirer');
|
|
12
|
+
return _inquirer.prompt(...args);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
const { resolveDbConfigIfExists, createDbInstance } = require('../utils/db');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse input string into an object
|
|
19
|
+
* @param {string} inputStr
|
|
20
|
+
* @returns {*}
|
|
21
|
+
*/
|
|
22
|
+
function parseInputPayload(inputStr) {
|
|
23
|
+
if (!inputStr || typeof inputStr !== 'string') {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const trimmed = inputStr.trim();
|
|
28
|
+
if (!trimmed) return {};
|
|
29
|
+
|
|
30
|
+
// Try JSON first
|
|
31
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(trimmed);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
throw new Error(`Invalid JSON input: ${e.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Try key=value pairs (e.g. "id=42 name=Alice" or "id=42&name=Alice")
|
|
40
|
+
const result = {};
|
|
41
|
+
const pairs = trimmed.includes('&') ? trimmed.split('&') : trimmed.split(/\s+/);
|
|
42
|
+
|
|
43
|
+
for (const pair of pairs) {
|
|
44
|
+
if (!pair.includes('=')) continue;
|
|
45
|
+
const [rawKey, ...valParts] = pair.split('=');
|
|
46
|
+
const key = rawKey.trim();
|
|
47
|
+
const val = valParts.join('=').trim();
|
|
48
|
+
|
|
49
|
+
// Auto coerce simple types
|
|
50
|
+
if (val === 'true') result[key] = true;
|
|
51
|
+
else if (val === 'false') result[key] = false;
|
|
52
|
+
else if (val === 'null') result[key] = null;
|
|
53
|
+
else if (!Number.isNaN(Number(val)) && val !== '') result[key] = Number(val);
|
|
54
|
+
else result[key] = val;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return Object.keys(result).length > 0 ? result : trimmed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function registerCommand(program) {
|
|
61
|
+
program
|
|
62
|
+
.command('service [name]')
|
|
63
|
+
.alias('service:run')
|
|
64
|
+
.alias('service:call')
|
|
65
|
+
.description('Run and test services interactively without starting the HTTP server')
|
|
66
|
+
.option('-i, --input <payload>', 'Input payload as JSON string or key=val pairs')
|
|
67
|
+
.option('-d, --dir <path>', 'Path to services directory (default: ./services)')
|
|
68
|
+
.option('-c, --config <path>', 'Path to database config file (optional)')
|
|
69
|
+
.option('-E, --env <environment>', 'Environment (development, production)', 'development')
|
|
70
|
+
.action(async (nameArg, options) => {
|
|
71
|
+
let db = null;
|
|
72
|
+
try {
|
|
73
|
+
const servicesDir = options.dir
|
|
74
|
+
? path.resolve(process.cwd(), options.dir)
|
|
75
|
+
: path.resolve(process.cwd(), 'services');
|
|
76
|
+
|
|
77
|
+
const { createServiceRegistry } = require('../../src/services');
|
|
78
|
+
const registry = createServiceRegistry({
|
|
79
|
+
servicesDir,
|
|
80
|
+
isDev: true,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const serviceNames = registry.list();
|
|
84
|
+
|
|
85
|
+
if (serviceNames.length === 0) {
|
|
86
|
+
console.log(`\n⚠️ No services found in "${path.relative(process.cwd(), servicesDir) || '.'}".`);
|
|
87
|
+
console.log(' Create your first service file in services/user/get.js\n');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let serviceName = nameArg;
|
|
92
|
+
|
|
93
|
+
// Interactive Service Selection if not provided
|
|
94
|
+
if (!serviceName) {
|
|
95
|
+
console.log(`\n⚡ Discovered ${serviceNames.length} service(s) in ${path.relative(process.cwd(), servicesDir) || 'services/'}:\n`);
|
|
96
|
+
|
|
97
|
+
const answer = await inquirer.prompt([
|
|
98
|
+
{
|
|
99
|
+
type: 'list',
|
|
100
|
+
name: 'selectedService',
|
|
101
|
+
message: 'Select a service to execute:',
|
|
102
|
+
choices: serviceNames.map((name) => {
|
|
103
|
+
const def = registry.get(name);
|
|
104
|
+
const hasSchema = !!def?.schema;
|
|
105
|
+
return {
|
|
106
|
+
name: `${name.padEnd(25)} ${hasSchema ? '📋 (has schema)' : ''}`,
|
|
107
|
+
value: name,
|
|
108
|
+
};
|
|
109
|
+
}),
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
serviceName = answer.selectedService;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!registry.has(serviceName)) {
|
|
117
|
+
console.error(`\n❌ Error: Service "${serviceName}" not found.`);
|
|
118
|
+
console.log(` Available services: ${serviceNames.join(', ')}\n`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const serviceDef = registry.get(serviceName);
|
|
123
|
+
|
|
124
|
+
// Input gathering
|
|
125
|
+
let input = {};
|
|
126
|
+
if (options.input) {
|
|
127
|
+
input = parseInputPayload(options.input);
|
|
128
|
+
} else {
|
|
129
|
+
console.log(`\n⚙️ Target: \x1b[36m${serviceName}\x1b[0m (${serviceDef.filePath ? path.relative(process.cwd(), serviceDef.filePath) : 'in-memory'})`);
|
|
130
|
+
|
|
131
|
+
const inputAnswer = await inquirer.prompt([
|
|
132
|
+
{
|
|
133
|
+
type: 'input',
|
|
134
|
+
name: 'rawInput',
|
|
135
|
+
message: 'Enter input (JSON or key=val pairs, leave empty for {}):',
|
|
136
|
+
default: '{}',
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
input = parseInputPayload(inputAnswer.rawInput);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Initialize DB if config exists
|
|
144
|
+
const dbConfigResult = resolveDbConfigIfExists(options.config);
|
|
145
|
+
if (dbConfigResult) {
|
|
146
|
+
try {
|
|
147
|
+
db = await createDbInstance(dbConfigResult.config, options.env);
|
|
148
|
+
} catch (dbErr) {
|
|
149
|
+
console.warn(`\n⚠️ Could not connect to database: ${dbErr.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const ctx = {
|
|
154
|
+
db,
|
|
155
|
+
user: { id: 1, role: 'admin', email: 'cli-admin@webspresso.local' },
|
|
156
|
+
logger: console,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
console.log(`\n🚀 Executing \x1b[36m${serviceName}\x1b[0m with input:`, JSON.stringify(input));
|
|
160
|
+
const startTime = Date.now();
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const result = await registry.call(serviceName, input, ctx);
|
|
164
|
+
const duration = Date.now() - startTime;
|
|
165
|
+
|
|
166
|
+
console.log(`\n✅ \x1b[32mExecution succeeded (${duration}ms)\x1b[0m\n`);
|
|
167
|
+
console.log(JSON.stringify(result, null, 2));
|
|
168
|
+
console.log('');
|
|
169
|
+
} catch (execErr) {
|
|
170
|
+
const duration = Date.now() - startTime;
|
|
171
|
+
console.error(`\n❌ \x1b[31mExecution failed (${duration}ms): ${execErr.message}\x1b[0m\n`);
|
|
172
|
+
|
|
173
|
+
if (execErr.fields) {
|
|
174
|
+
console.error('Validation Details:');
|
|
175
|
+
for (const [f, msg] of Object.entries(execErr.fields)) {
|
|
176
|
+
console.error(` - ${f}: ${msg}`);
|
|
177
|
+
}
|
|
178
|
+
console.error('');
|
|
179
|
+
} else if (execErr.stack) {
|
|
180
|
+
console.error(execErr.stack);
|
|
181
|
+
console.error('');
|
|
182
|
+
}
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
}
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error(`\n❌ CLI Error: ${err.message}\n`);
|
|
187
|
+
process.exitCode = 1;
|
|
188
|
+
} finally {
|
|
189
|
+
if (db && typeof db.destroy === 'function') {
|
|
190
|
+
await db.destroy();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
registerCommand,
|
|
198
|
+
parseInputPayload,
|
|
199
|
+
};
|
package/bin/commands/skill.js
CHANGED
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const os = require('os');
|
|
8
|
-
|
|
8
|
+
let _inquirer;
|
|
9
|
+
const inquirer = {
|
|
10
|
+
prompt: (...args) => {
|
|
11
|
+
if (!_inquirer) _inquirer = require('inquirer');
|
|
12
|
+
return _inquirer.prompt(...args);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
9
15
|
|
|
10
16
|
const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$|^[a-z0-9]$/;
|
|
11
17
|
|
package/bin/webspresso.js
CHANGED
|
@@ -33,6 +33,8 @@ const { registerCommand: registerDoctor } = require('./commands/doctor');
|
|
|
33
33
|
const { registerCommand: registerSkill } = require('./commands/skill');
|
|
34
34
|
const { registerCommand: registerUpgrade } = require('./commands/upgrade');
|
|
35
35
|
const { registerCommand: registerOrmMap } = require('./commands/orm-map');
|
|
36
|
+
const { registerCommand: registerService } = require('./commands/service');
|
|
37
|
+
const { registerCommand: registerMcp } = require('./commands/mcp');
|
|
36
38
|
|
|
37
39
|
registerNew(program);
|
|
38
40
|
registerPage(program);
|
|
@@ -54,6 +56,8 @@ registerDoctor(program);
|
|
|
54
56
|
registerSkill(program);
|
|
55
57
|
registerUpgrade(program);
|
|
56
58
|
registerOrmMap(program);
|
|
59
|
+
registerService(program);
|
|
60
|
+
registerMcp(program);
|
|
57
61
|
|
|
58
62
|
// Parse arguments
|
|
59
63
|
program.parse();
|
package/core/auth/middleware.js
CHANGED
|
@@ -17,6 +17,15 @@ function createAuthMiddleware(authManager) {
|
|
|
17
17
|
|
|
18
18
|
const sessionConfig = authManager.getSessionConfig();
|
|
19
19
|
|
|
20
|
+
if (!sessionConfig.store && process.env.NODE_ENV === 'production') {
|
|
21
|
+
const warnMsg = '[webspresso:auth] Warning: Default MemoryStore is being used in production. It is not designed for production environments and will leak memory under high load. Please configure a persistent session store (e.g. Knex or Redis).';
|
|
22
|
+
if (typeof process !== 'undefined' && typeof process.emitWarning === 'function') {
|
|
23
|
+
process.emitWarning(warnMsg, 'SecurityWarning');
|
|
24
|
+
} else {
|
|
25
|
+
console.warn(warnMsg);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
/**
|
|
21
30
|
* Session middleware
|
|
22
31
|
* Sets up express-session with secure cookie enforcement
|
package/core/compileSchema.js
CHANGED
|
@@ -3,13 +3,9 @@
|
|
|
3
3
|
* Compiles schema definitions from API files
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const z = require('
|
|
7
|
-
const { extendZ } = require('./orm/utils/nanoid');
|
|
6
|
+
const { z: zForApi } = require('./validation');
|
|
8
7
|
const schemaCache = require('../utils/schemaCache');
|
|
9
8
|
|
|
10
|
-
/** Zod plus `z.nanoid()` for API schemas (does not mutate the global `z`). */
|
|
11
|
-
const zForApi = extendZ(z);
|
|
12
|
-
|
|
13
9
|
/**
|
|
14
10
|
* Compile schema from an API module
|
|
15
11
|
* @param {string} filePath - Absolute file path to API module
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-Type Compressibility Filter
|
|
3
|
+
* Determines if a given MIME type benefits from HTTP compression.
|
|
4
|
+
* @module core/compression/compressible
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Regex patterns for MIME types that benefit from compression
|
|
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
|
+
|
|
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;
|
|
12
|
+
|
|
13
|
+
const compressibleCache = new Map();
|
|
14
|
+
const MAX_MIME_CACHE_SIZE = 256;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Determine if a Content-Type is compressible
|
|
18
|
+
* @param {string|undefined|null} contentType - Raw Content-Type header (e.g. 'text/html; charset=utf-8')
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
function isCompressible(contentType) {
|
|
22
|
+
if (!contentType || typeof contentType !== 'string') {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const cached = compressibleCache.get(contentType);
|
|
27
|
+
if (cached !== undefined) {
|
|
28
|
+
return cached;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Extract base MIME type (strip parameters like ; charset=utf-8)
|
|
32
|
+
const semiIndex = contentType.indexOf(';');
|
|
33
|
+
const mime = (semiIndex !== -1 ? contentType.slice(0, semiIndex) : contentType).trim().toLowerCase();
|
|
34
|
+
|
|
35
|
+
if (!mime) {
|
|
36
|
+
if (compressibleCache.size < MAX_MIME_CACHE_SIZE) compressibleCache.set(contentType, false);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fast negative check
|
|
41
|
+
if (INCOMPRESSIBLE_TYPE_REGEX.test(mime)) {
|
|
42
|
+
if (compressibleCache.size < MAX_MIME_CACHE_SIZE) compressibleCache.set(contentType, false);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check compressible match
|
|
47
|
+
let result = false;
|
|
48
|
+
if (COMPRESSIBLE_TYPE_REGEX.test(mime)) {
|
|
49
|
+
result = true;
|
|
50
|
+
} else if (mime.startsWith('text/') || mime.endsWith('+json') || mime.endsWith('+xml') || mime.endsWith('+yaml')) {
|
|
51
|
+
result = true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (compressibleCache.size < MAX_MIME_CACHE_SIZE) {
|
|
55
|
+
compressibleCache.set(contentType, result);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
isCompressible,
|
|
63
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compression Stream Factory
|
|
3
|
+
* Instantiates native node:zlib transform streams (brotli, gzip, deflate).
|
|
4
|
+
* @module core/compression/compression-stream
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const zlib = require('zlib');
|
|
8
|
+
const { supportsBrotli } = require('./negotiate');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a zlib compression transform stream based on encoding
|
|
12
|
+
* @param {'br'|'gzip'|'deflate'} encoding
|
|
13
|
+
* @param {Object} [options]
|
|
14
|
+
* @param {number} [options.level=6] - Default compression level (0-9 for gzip/deflate, 0-11 for brotli)
|
|
15
|
+
* @param {Object} [options.brotli] - Custom options for zlib.createBrotliCompress
|
|
16
|
+
* @param {Object} [options.gzip] - Custom options for zlib.createGzip
|
|
17
|
+
* @param {Object} [options.deflate] - Custom options for zlib.createDeflate
|
|
18
|
+
* @returns {import('stream').Transform|null}
|
|
19
|
+
*/
|
|
20
|
+
function createCompressionStream(encoding, options = {}) {
|
|
21
|
+
const enc = (encoding || '').toLowerCase();
|
|
22
|
+
const level = typeof options.level === 'number' ? options.level : 6;
|
|
23
|
+
|
|
24
|
+
switch (enc) {
|
|
25
|
+
case 'br': {
|
|
26
|
+
if (!supportsBrotli()) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const brotliOpts = { ...(options.brotli || {}) };
|
|
30
|
+
if (!brotliOpts.params) {
|
|
31
|
+
brotliOpts.params = {};
|
|
32
|
+
}
|
|
33
|
+
if (brotliOpts.params[zlib.constants.BROTLI_PARAM_QUALITY] === undefined) {
|
|
34
|
+
// Brotli quality 4 is standard for dynamic HTTP compression (fast & high ratio)
|
|
35
|
+
brotliOpts.params[zlib.constants.BROTLI_PARAM_QUALITY] = typeof options.brotli?.quality === 'number'
|
|
36
|
+
? options.brotli.quality
|
|
37
|
+
: (typeof options.brotliQuality === 'number' ? options.brotliQuality : 4);
|
|
38
|
+
}
|
|
39
|
+
return zlib.createBrotliCompress(brotliOpts);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
case 'gzip': {
|
|
43
|
+
const gzipOpts = { level, ...(options.gzip || {}) };
|
|
44
|
+
return zlib.createGzip(gzipOpts);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
case 'deflate': {
|
|
48
|
+
const deflateOpts = { level, ...(options.deflate || {}) };
|
|
49
|
+
return zlib.createDeflate(deflateOpts);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
default:
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
createCompressionStream,
|
|
59
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso HTTP Response Compression Module
|
|
3
|
+
* @module core/compression
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
supportsBrotli,
|
|
8
|
+
getDefaultSupportedEncodings,
|
|
9
|
+
parseAcceptEncoding,
|
|
10
|
+
selectEncoding,
|
|
11
|
+
} = require('./negotiate');
|
|
12
|
+
const { isCompressible } = require('./compressible');
|
|
13
|
+
const { createCompressionStream } = require('./compression-stream');
|
|
14
|
+
const { createCompressionMiddleware, appendVary } = require('./middleware');
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
supportsBrotli,
|
|
18
|
+
getDefaultSupportedEncodings,
|
|
19
|
+
parseAcceptEncoding,
|
|
20
|
+
selectEncoding,
|
|
21
|
+
isCompressible,
|
|
22
|
+
createCompressionStream,
|
|
23
|
+
createCompressionMiddleware,
|
|
24
|
+
appendVary,
|
|
25
|
+
};
|