webspresso 0.0.95 → 0.0.96
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 +225 -0
- package/.agents/AGENTS.md +72 -0
- package/.agents/ARCHITECTURE.md +119 -0
- package/.agents/AUTH.md +103 -0
- package/.agents/CLI.md +115 -0
- package/.agents/COMPRESSION.md +59 -0
- package/.agents/CONVENTIONS.md +46 -0
- package/.agents/EMAIL.md +141 -0
- package/.agents/ERRORS.md +201 -0
- package/.agents/MCP.md +103 -0
- package/.agents/ORM.md +230 -0
- package/.agents/PLUGINS.md +419 -0
- package/.agents/QUEUE.md +113 -0
- package/.agents/REALTIME.md +213 -0
- package/.agents/ROUTING.md +292 -0
- package/.agents/SERVICES.md +341 -0
- package/.agents/SHUTDOWN.md +67 -0
- package/.agents/STREAMING.md +73 -0
- package/.agents/WORKFLOW.md +27 -0
- package/.agents/internal/DOCUMENTATION_REVIEW.md +485 -0
- package/.agents/internal/TECHNICAL_REVIEW.md +428 -0
- package/.cursorrules +14 -0
- package/AGENTS.md +175 -0
- package/CLAUDE.md +54 -0
- package/README.md +18 -14
- package/bin/commands/agents-init.js +169 -0
- package/bin/commands/api.js +114 -48
- package/bin/commands/doctor.js +35 -0
- package/bin/commands/new.js +13 -2
- package/bin/commands/page.js +75 -89
- package/bin/commands/service.js +77 -0
- package/bin/webspresso.js +2 -2
- package/core/compileSchema.js +1 -1
- package/core/errors/middleware.js +22 -2
- package/core/errors/normalize.js +29 -2
- package/{utils/schemaCache.js → core/schema-cache.js} +2 -3
- package/index.d.ts +306 -0
- package/index.js +35 -1
- package/package.json +5 -3
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +22 -1
- package/plugins/csrf/index.js +3 -2
- package/plugins/csv/engine.js +447 -0
- package/plugins/csv/index.js +94 -0
- package/plugins/csv/middleware.js +80 -0
- package/plugins/csv/services.js +138 -0
- package/plugins/file-manager/admin-component.js +613 -0
- package/plugins/file-manager/api-handlers.js +261 -0
- package/plugins/file-manager/core.js +601 -0
- package/plugins/file-manager/index.js +139 -0
- package/plugins/file-manager/security.js +207 -0
- package/plugins/index.js +8 -0
- package/plugins/xlsx/engine.js +397 -0
- package/plugins/xlsx/index.js +91 -0
- package/plugins/xlsx/middleware.js +87 -0
- package/plugins/xlsx/services.js +118 -0
- package/src/api/api-loader.js +233 -0
- package/src/api/define-api.js +35 -0
- package/src/discovery/file-route-parser.js +201 -0
- package/src/discovery/scan-routes.js +265 -0
- package/src/file-router.js +6 -0
- package/src/modules/define-module.js +99 -0
- package/src/modules/module-discovery.js +117 -0
- package/src/pages/define-page.js +36 -0
- package/src/pages/page-loader.js +214 -0
- package/src/routing/mount-discovered-routes.js +78 -0
- package/src/routing/route-table.js +191 -0
- package/src/server.js +112 -15
- package/src/services/builtins/file-manager.js +178 -0
- package/src/services/index.js +2 -0
- package/bin/commands/skill.js +0 -187
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +0 -289
- package/templates/skills/webspresso-usage/REFERENCE-kernel.md +0 -93
- package/templates/skills/webspresso-usage/SKILL.md +0 -48
package/bin/commands/page.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Page Command
|
|
3
|
-
* Add a new page
|
|
5
|
+
* Add a new SSR page with Nunjucks template and companion data loader
|
|
6
|
+
* @module bin/commands/page
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
9
|
let _inquirer;
|
|
@@ -15,125 +18,108 @@ const path = require('path');
|
|
|
15
18
|
|
|
16
19
|
function registerCommand(program) {
|
|
17
20
|
program
|
|
18
|
-
.command('page')
|
|
19
|
-
.description('Add a new page
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
.command('page [route]')
|
|
22
|
+
.description('Add a new SSR page (Nunjucks template + companion load() loader)')
|
|
23
|
+
.option('--no-loader', 'Skip creating companion .js loader file')
|
|
24
|
+
.option('--locales', 'Create page-level locale files')
|
|
25
|
+
.action(async (routeArg, options) => {
|
|
26
|
+
const pagesDir = path.resolve(process.cwd(), 'pages');
|
|
27
|
+
if (!fs.existsSync(pagesDir)) {
|
|
28
|
+
console.error('❌ Not a Webspresso project! (pages/ directory not found)');
|
|
23
29
|
process.exit(1);
|
|
24
30
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
name: 'hasLocales',
|
|
47
|
-
message: 'Add locale files?',
|
|
48
|
-
default: false
|
|
49
|
-
}
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
|
-
const route = answers.route.replace(/^\//, '');
|
|
53
|
-
const routePath = path.join('pages', route);
|
|
31
|
+
|
|
32
|
+
let route = routeArg ? String(routeArg).trim() : '';
|
|
33
|
+
|
|
34
|
+
if (!route) {
|
|
35
|
+
const answers = await inquirer.prompt([
|
|
36
|
+
{
|
|
37
|
+
type: 'input',
|
|
38
|
+
name: 'route',
|
|
39
|
+
message: 'Page route path (e.g., about or blog/[slug]):',
|
|
40
|
+
validate: (input) => {
|
|
41
|
+
if (!input || !input.trim()) return 'Route path is required';
|
|
42
|
+
return true;
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
route = answers.route.trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Normalize leading slash
|
|
50
|
+
route = route.replace(/^\/+/, '');
|
|
51
|
+
const routePath = path.join(pagesDir, route);
|
|
54
52
|
const dirPath = path.dirname(routePath);
|
|
55
53
|
const fileName = path.basename(routePath);
|
|
56
|
-
|
|
57
|
-
// Create directory
|
|
54
|
+
|
|
58
55
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
59
|
-
|
|
60
|
-
// Create .njk file
|
|
56
|
+
|
|
61
57
|
const templateName = fileName === 'index' ? 'index' : fileName;
|
|
62
58
|
const njkFile = path.join(dirPath, `${templateName}.njk`);
|
|
63
|
-
|
|
59
|
+
|
|
64
60
|
const njkContent = `{% extends "layout.njk" %}
|
|
65
61
|
|
|
66
62
|
{% block content %}
|
|
67
|
-
<
|
|
68
|
-
<h1>{{
|
|
69
|
-
<p>
|
|
70
|
-
</
|
|
63
|
+
<main class="max-w-4xl mx-auto px-4 py-8">
|
|
64
|
+
<h1 class="text-3xl font-bold tracking-tight text-gray-900 mb-4">{{ title or "${templateName}" }}</h1>
|
|
65
|
+
<p class="text-gray-600">Page content for <code>${route}</code>.</p>
|
|
66
|
+
</main>
|
|
71
67
|
{% endblock %}
|
|
72
68
|
`;
|
|
73
|
-
|
|
74
|
-
fs.writeFileSync(njkFile, njkContent);
|
|
75
|
-
console.log(
|
|
76
|
-
|
|
77
|
-
// Create
|
|
78
|
-
if (
|
|
69
|
+
|
|
70
|
+
fs.writeFileSync(njkFile, njkContent, 'utf8');
|
|
71
|
+
console.log(`\n✅ Created template:\n ${path.relative(process.cwd(), njkFile)}`);
|
|
72
|
+
|
|
73
|
+
// Create companion loader if requested (default: true)
|
|
74
|
+
if (options.loader !== false) {
|
|
79
75
|
const jsFile = path.join(dirPath, `${templateName}.js`);
|
|
80
|
-
const jsContent = `
|
|
81
|
-
|
|
76
|
+
const jsContent = `'use strict';
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Server-side data loader for ${templateName}.njk
|
|
80
|
+
* @type {import('webspresso').PageRouteModule}
|
|
81
|
+
*/
|
|
82
|
+
module.exports = {
|
|
83
|
+
// Server data loader executed before template rendering
|
|
84
|
+
async load({ req, res, db, ctx }) {
|
|
82
85
|
return {
|
|
83
|
-
|
|
86
|
+
title: '${templateName.charAt(0).toUpperCase() + templateName.slice(1)}',
|
|
84
87
|
};
|
|
85
88
|
},
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
|
|
90
|
+
// Dynamic page metadata
|
|
91
|
+
meta({ data, ctx }) {
|
|
88
92
|
return {
|
|
89
|
-
title:
|
|
90
|
-
description:
|
|
93
|
+
title: data.title || '${templateName}',
|
|
94
|
+
description: 'Page description for ${route}',
|
|
91
95
|
};
|
|
92
|
-
}
|
|
96
|
+
},
|
|
93
97
|
};
|
|
94
98
|
`;
|
|
95
|
-
|
|
96
|
-
fs.writeFileSync(jsFile, jsContent);
|
|
97
|
-
console.log(`✅ Created ${jsFile}`);
|
|
99
|
+
|
|
100
|
+
fs.writeFileSync(jsFile, jsContent, 'utf8');
|
|
101
|
+
console.log(`✅ Created companion loader:\n ${path.relative(process.cwd(), jsFile)}`);
|
|
98
102
|
}
|
|
99
|
-
|
|
103
|
+
|
|
100
104
|
// Create locales if requested
|
|
101
|
-
if (
|
|
105
|
+
if (options.locales) {
|
|
102
106
|
const localesDir = path.join(dirPath, 'locales');
|
|
103
107
|
fs.mkdirSync(localesDir, { recursive: true });
|
|
104
|
-
|
|
108
|
+
|
|
105
109
|
const enContent = {
|
|
106
|
-
title:
|
|
110
|
+
title: templateName,
|
|
107
111
|
description: 'Page description',
|
|
108
|
-
meta: {
|
|
109
|
-
title: `${route} - Webspresso`,
|
|
110
|
-
description: 'Page description'
|
|
111
|
-
}
|
|
112
112
|
};
|
|
113
|
-
|
|
114
113
|
fs.writeFileSync(
|
|
115
114
|
path.join(localesDir, 'en.json'),
|
|
116
|
-
JSON.stringify(enContent, null, 2) + '\n'
|
|
115
|
+
JSON.stringify(enContent, null, 2) + '\n',
|
|
116
|
+
'utf8'
|
|
117
117
|
);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
title: route,
|
|
121
|
-
description: 'Seitenbeschreibung',
|
|
122
|
-
meta: {
|
|
123
|
-
title: `${route} - Webspresso`,
|
|
124
|
-
description: 'Seitenbeschreibung'
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
fs.writeFileSync(
|
|
129
|
-
path.join(localesDir, 'de.json'),
|
|
130
|
-
JSON.stringify(deContent, null, 2) + '\n'
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
console.log(`✅ Created locale files in ${localesDir}`);
|
|
118
|
+
|
|
119
|
+
console.log(`✅ Created locales in:\n ${path.relative(process.cwd(), localesDir)}`);
|
|
134
120
|
}
|
|
135
|
-
|
|
136
|
-
console.log(`\n
|
|
121
|
+
|
|
122
|
+
console.log(`\n🎉 Page ready at: /${route}\n`);
|
|
137
123
|
});
|
|
138
124
|
}
|
|
139
125
|
|
package/bin/commands/service.js
CHANGED
|
@@ -191,6 +191,83 @@ function registerCommand(program) {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
});
|
|
194
|
+
|
|
195
|
+
// Generator command: service:make <name>
|
|
196
|
+
program
|
|
197
|
+
.command('service:make [name]')
|
|
198
|
+
.alias('make:service')
|
|
199
|
+
.description('Scaffold a new service in services/<domain>/<action>.js')
|
|
200
|
+
.option('-t, --transaction', 'Enable ACID transaction wrapper (transaction: true)')
|
|
201
|
+
.option('-a, --auth <role>', 'Require authentication or specific role')
|
|
202
|
+
.action(async (nameArg, options) => {
|
|
203
|
+
const fs = require('fs');
|
|
204
|
+
let serviceName = nameArg ? String(nameArg).trim() : '';
|
|
205
|
+
|
|
206
|
+
if (!serviceName) {
|
|
207
|
+
const answer = await inquirer.prompt([
|
|
208
|
+
{
|
|
209
|
+
type: 'input',
|
|
210
|
+
name: 'serviceName',
|
|
211
|
+
message: 'Service name (e.g., user.create or payment.process):',
|
|
212
|
+
validate: (input) => {
|
|
213
|
+
if (!input || !input.trim()) return 'Service name is required';
|
|
214
|
+
if (!input.includes('.')) return 'Use dot notation, e.g. domain.action (e.g. user.get)';
|
|
215
|
+
return true;
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
]);
|
|
219
|
+
serviceName = answer.serviceName.trim();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const parts = serviceName.split('.');
|
|
223
|
+
const actionName = parts.pop();
|
|
224
|
+
const domainPath = parts.join(path.sep);
|
|
225
|
+
|
|
226
|
+
const servicesDir = path.resolve(process.cwd(), 'services');
|
|
227
|
+
const targetDir = path.join(servicesDir, domainPath);
|
|
228
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
229
|
+
|
|
230
|
+
const serviceFile = path.join(targetDir, `${actionName}.js`);
|
|
231
|
+
if (fs.existsSync(serviceFile)) {
|
|
232
|
+
console.error(`\n❌ Error: Service file already exists at ${path.relative(process.cwd(), serviceFile)}\n`);
|
|
233
|
+
process.exitCode = 1;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const authLine = options.auth
|
|
238
|
+
? `\n auth: ${options.auth === true || options.auth === 'true' ? 'true' : `'${options.auth}'`}, // Required role or boolean`
|
|
239
|
+
: '';
|
|
240
|
+
const trxLine = options.transaction ? '\n transaction: true, // Automatic ACID transaction propagation' : '';
|
|
241
|
+
|
|
242
|
+
const content = `'use strict';
|
|
243
|
+
|
|
244
|
+
const { z } = require('zod');
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Service: ${serviceName}
|
|
248
|
+
* @type {import('webspresso').ServiceDefinition}
|
|
249
|
+
*/
|
|
250
|
+
module.exports = {
|
|
251
|
+
// Input validation schema (Zod)
|
|
252
|
+
schema: z.object({
|
|
253
|
+
// Define input properties here
|
|
254
|
+
}),${authLine}${trxLine}
|
|
255
|
+
|
|
256
|
+
// Business Logic Handler
|
|
257
|
+
handler: async (input, { db, service, user }) => {
|
|
258
|
+
// db.getRepository('ModelName') -> Access repositories
|
|
259
|
+
// service('other.service', payload) -> Call nested services
|
|
260
|
+
return {
|
|
261
|
+
success: true,
|
|
262
|
+
data: input,
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
`;
|
|
267
|
+
|
|
268
|
+
fs.writeFileSync(serviceFile, content, 'utf8');
|
|
269
|
+
console.log(`\n✅ Created service "${serviceName}":\n ${path.relative(process.cwd(), serviceFile)}\n`);
|
|
270
|
+
});
|
|
194
271
|
}
|
|
195
272
|
|
|
196
273
|
module.exports = {
|
package/bin/webspresso.js
CHANGED
|
@@ -30,12 +30,12 @@ const { registerCommand: registerAdminPassword } = require('./commands/admin-pas
|
|
|
30
30
|
const { registerCommand: registerFaviconGenerate } = require('./commands/favicon-generate');
|
|
31
31
|
const { registerCommand: registerAuditPrune } = require('./commands/audit-prune');
|
|
32
32
|
const { registerCommand: registerDoctor } = require('./commands/doctor');
|
|
33
|
-
const { registerCommand: registerSkill } = require('./commands/skill');
|
|
34
33
|
const { registerCommand: registerUpgrade } = require('./commands/upgrade');
|
|
35
34
|
const { registerCommand: registerOrmMap } = require('./commands/orm-map');
|
|
36
35
|
const { registerCommand: registerService } = require('./commands/service');
|
|
37
36
|
const { registerCommand: registerMcp } = require('./commands/mcp');
|
|
38
37
|
const { registerCommand: registerPolarMigrate } = require('./commands/polar-migrate');
|
|
38
|
+
const { registerCommand: registerAgentsInit } = require('./commands/agents-init');
|
|
39
39
|
|
|
40
40
|
registerNew(program);
|
|
41
41
|
registerPage(program);
|
|
@@ -54,12 +54,12 @@ registerAdminPassword(program);
|
|
|
54
54
|
registerFaviconGenerate(program);
|
|
55
55
|
registerAuditPrune(program);
|
|
56
56
|
registerDoctor(program);
|
|
57
|
-
registerSkill(program);
|
|
58
57
|
registerUpgrade(program);
|
|
59
58
|
registerOrmMap(program);
|
|
60
59
|
registerService(program);
|
|
61
60
|
registerMcp(program);
|
|
62
61
|
registerPolarMigrate(program);
|
|
62
|
+
registerAgentsInit(program);
|
|
63
63
|
|
|
64
64
|
// Parse arguments
|
|
65
65
|
program.parse();
|
package/core/compileSchema.js
CHANGED
|
@@ -123,11 +123,31 @@ function createCentralErrorHandler(options = {}) {
|
|
|
123
123
|
// Log errors based on severity
|
|
124
124
|
if (status >= 500) {
|
|
125
125
|
if (typeof console !== 'undefined' && console.error) {
|
|
126
|
-
|
|
126
|
+
if (normalized.route || normalized.source) {
|
|
127
|
+
console.error(
|
|
128
|
+
`[webspresso] 🚨 Error at ${normalized.method || req.method} ${normalized.route || req.path}` +
|
|
129
|
+
(normalized.module ? ` [module: ${normalized.module}]` : '') +
|
|
130
|
+
(normalized.requestId ? ` (reqId: ${normalized.requestId})` : '') +
|
|
131
|
+
`\n Source: ${normalized.source || 'unknown'}` +
|
|
132
|
+
(normalized.phase ? `\n Phase: ${normalized.phase}` : '') +
|
|
133
|
+
`\n`,
|
|
134
|
+
err
|
|
135
|
+
);
|
|
136
|
+
} else {
|
|
137
|
+
console.error('[webspresso] Server Error:', err);
|
|
138
|
+
}
|
|
127
139
|
}
|
|
128
140
|
} else if (isDev && status >= 400) {
|
|
129
141
|
if (typeof console !== 'undefined' && console.warn) {
|
|
130
|
-
|
|
142
|
+
if (normalized.route || normalized.source) {
|
|
143
|
+
console.warn(
|
|
144
|
+
`[webspresso] ⚠️ HTTP ${status} at ${normalized.method || req.method} ${normalized.route || req.path}` +
|
|
145
|
+
(normalized.module ? ` [module: ${normalized.module}]` : '') +
|
|
146
|
+
`: ${normalized.message}`
|
|
147
|
+
);
|
|
148
|
+
} else {
|
|
149
|
+
console.warn(`[webspresso] HTTP ${status}:`, normalized.message);
|
|
150
|
+
}
|
|
131
151
|
}
|
|
132
152
|
}
|
|
133
153
|
|
package/core/errors/normalize.js
CHANGED
|
@@ -79,6 +79,14 @@ function normalizeError(err, isDev = false) {
|
|
|
79
79
|
httpErr.stack = err.stack;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// Retain route trace metadata
|
|
83
|
+
if (err.route) httpErr.route = err.route;
|
|
84
|
+
if (err.method) httpErr.method = err.method;
|
|
85
|
+
if (err.source) httpErr.source = err.source;
|
|
86
|
+
if (err.module) httpErr.module = err.module;
|
|
87
|
+
if (err.phase) httpErr.phase = err.phase;
|
|
88
|
+
if (err.requestId) httpErr.requestId = err.requestId;
|
|
89
|
+
|
|
82
90
|
return httpErr;
|
|
83
91
|
}
|
|
84
92
|
|
|
@@ -99,12 +107,21 @@ function normalizeError(err, isDev = false) {
|
|
|
99
107
|
|
|
100
108
|
const msg = typeof err.message === 'string' ? err.message : getStatusTitle(status);
|
|
101
109
|
|
|
102
|
-
|
|
110
|
+
const httpErr = new HttpError(status, (status < 500 || isDev) ? msg : getStatusTitle(status), {
|
|
103
111
|
code: err.code,
|
|
104
112
|
details: err.details,
|
|
105
113
|
headers: err.headers,
|
|
106
114
|
expose: typeof err.expose === 'boolean' ? err.expose : status < 500,
|
|
107
115
|
});
|
|
116
|
+
|
|
117
|
+
if (err.route) httpErr.route = err.route;
|
|
118
|
+
if (err.method) httpErr.method = err.method;
|
|
119
|
+
if (err.source) httpErr.source = err.source;
|
|
120
|
+
if (err.module) httpErr.module = err.module;
|
|
121
|
+
if (err.phase) httpErr.phase = err.phase;
|
|
122
|
+
if (err.requestId) httpErr.requestId = err.requestId;
|
|
123
|
+
|
|
124
|
+
return httpErr;
|
|
108
125
|
}
|
|
109
126
|
|
|
110
127
|
return new HttpError(500, 'Internal Server Error', { expose: false });
|
|
@@ -151,8 +168,18 @@ function toErrorResponseObject(error, isDev = false) {
|
|
|
151
168
|
payload.details = error.details;
|
|
152
169
|
}
|
|
153
170
|
|
|
154
|
-
// Include stack and
|
|
171
|
+
// Include stack, cause, and trace strictly in development mode
|
|
155
172
|
if (isDev) {
|
|
173
|
+
if (error.route || error.source || error.module || error.requestId || error.phase) {
|
|
174
|
+
payload.trace = {
|
|
175
|
+
route: error.route || undefined,
|
|
176
|
+
method: error.method || undefined,
|
|
177
|
+
source: error.source || undefined,
|
|
178
|
+
module: error.module || undefined,
|
|
179
|
+
phase: error.phase || undefined,
|
|
180
|
+
requestId: error.requestId || undefined,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
156
183
|
if (error.stack) {
|
|
157
184
|
payload.stack = error.stack;
|
|
158
185
|
}
|