webspresso 0.0.94 → 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.
Files changed (83) hide show
  1. package/.agents/ADMIN.md +225 -0
  2. package/.agents/AGENTS.md +72 -0
  3. package/.agents/ARCHITECTURE.md +119 -0
  4. package/.agents/AUTH.md +103 -0
  5. package/.agents/CLI.md +115 -0
  6. package/.agents/COMPRESSION.md +59 -0
  7. package/.agents/CONVENTIONS.md +46 -0
  8. package/.agents/EMAIL.md +141 -0
  9. package/.agents/ERRORS.md +201 -0
  10. package/.agents/MCP.md +103 -0
  11. package/.agents/ORM.md +230 -0
  12. package/.agents/PLUGINS.md +419 -0
  13. package/.agents/QUEUE.md +113 -0
  14. package/.agents/REALTIME.md +213 -0
  15. package/.agents/ROUTING.md +292 -0
  16. package/.agents/SERVICES.md +341 -0
  17. package/.agents/SHUTDOWN.md +67 -0
  18. package/.agents/STREAMING.md +73 -0
  19. package/.agents/WORKFLOW.md +27 -0
  20. package/.agents/internal/DOCUMENTATION_REVIEW.md +485 -0
  21. package/.agents/internal/TECHNICAL_REVIEW.md +428 -0
  22. package/.cursorrules +14 -0
  23. package/AGENTS.md +175 -0
  24. package/CLAUDE.md +54 -0
  25. package/README.md +18 -14
  26. package/bin/commands/agents-init.js +169 -0
  27. package/bin/commands/api.js +114 -48
  28. package/bin/commands/doctor.js +35 -0
  29. package/bin/commands/new.js +13 -2
  30. package/bin/commands/page.js +75 -89
  31. package/bin/commands/service.js +77 -0
  32. package/bin/webspresso.js +2 -2
  33. package/core/compileSchema.js +1 -1
  34. package/core/errors/middleware.js +22 -2
  35. package/core/errors/normalize.js +29 -2
  36. package/{utils/schemaCache.js → core/schema-cache.js} +2 -3
  37. package/index.d.ts +306 -0
  38. package/index.js +35 -1
  39. package/package.json +19 -6
  40. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +22 -1
  41. package/plugins/csrf/index.js +3 -2
  42. package/plugins/csv/engine.js +447 -0
  43. package/plugins/csv/index.js +94 -0
  44. package/plugins/csv/middleware.js +80 -0
  45. package/plugins/csv/services.js +138 -0
  46. package/plugins/file-manager/admin-component.js +613 -0
  47. package/plugins/file-manager/api-handlers.js +261 -0
  48. package/plugins/file-manager/core.js +601 -0
  49. package/plugins/file-manager/index.js +139 -0
  50. package/plugins/file-manager/security.js +207 -0
  51. package/plugins/index.js +8 -0
  52. package/plugins/polar/CHANGELOG.md +18 -0
  53. package/plugins/polar/README.md +30 -7
  54. package/plugins/polar/index.js +6 -1
  55. package/plugins/polar/src/billing-hooks.js +51 -0
  56. package/plugins/polar/src/config.js +2 -0
  57. package/plugins/polar/src/rate-limit.js +96 -28
  58. package/plugins/polar/src/routes.js +3 -2
  59. package/plugins/polar/src/urls.js +26 -2
  60. package/plugins/polar/src/user-resolver.js +5 -1
  61. package/plugins/rate-limit/index.js +5 -1
  62. package/plugins/xlsx/engine.js +397 -0
  63. package/plugins/xlsx/index.js +91 -0
  64. package/plugins/xlsx/middleware.js +87 -0
  65. package/plugins/xlsx/services.js +118 -0
  66. package/src/api/api-loader.js +233 -0
  67. package/src/api/define-api.js +35 -0
  68. package/src/discovery/file-route-parser.js +201 -0
  69. package/src/discovery/scan-routes.js +265 -0
  70. package/src/file-router.js +6 -0
  71. package/src/modules/define-module.js +99 -0
  72. package/src/modules/module-discovery.js +117 -0
  73. package/src/pages/define-page.js +36 -0
  74. package/src/pages/page-loader.js +214 -0
  75. package/src/routing/mount-discovered-routes.js +78 -0
  76. package/src/routing/route-table.js +191 -0
  77. package/src/server.js +112 -15
  78. package/src/services/builtins/file-manager.js +178 -0
  79. package/src/services/index.js +2 -0
  80. package/bin/commands/skill.js +0 -187
  81. package/templates/skills/webspresso-usage/REFERENCE-framework.md +0 -289
  82. package/templates/skills/webspresso-usage/REFERENCE-kernel.md +0 -93
  83. package/templates/skills/webspresso-usage/SKILL.md +0 -48
@@ -1,6 +1,9 @@
1
+ 'use strict';
2
+
1
3
  /**
2
4
  * Page Command
3
- * Add a new page to the current project
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 to the current project')
20
- .action(async () => {
21
- if (!fs.existsSync('pages')) {
22
- console.error('❌ Not a Webspresso project! Run this command in your project directory.');
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
- const answers = await inquirer.prompt([
27
- {
28
- type: 'input',
29
- name: 'route',
30
- message: 'Route path (e.g., /about or /blog/post):',
31
- validate: (input) => {
32
- if (!input.startsWith('/')) {
33
- return 'Route must start with /';
34
- }
35
- return true;
36
- }
37
- },
38
- {
39
- type: 'confirm',
40
- name: 'hasConfig',
41
- message: 'Add route config file (.js)?',
42
- default: false
43
- },
44
- {
45
- type: 'confirm',
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
- <div>
68
- <h1>{{ t('title') or '${route}' }}</h1>
69
- <p>{{ t('description') or 'Page content' }}</p>
70
- </div>
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(`✅ Created ${njkFile}`);
76
-
77
- // Create config file if requested
78
- if (answers.hasConfig) {
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 = `module.exports = {
81
- async load(req, ctx) {
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
- // Add your data here
86
+ title: '${templateName.charAt(0).toUpperCase() + templateName.slice(1)}',
84
87
  };
85
88
  },
86
-
87
- meta(req, ctx) {
89
+
90
+ // Dynamic page metadata
91
+ meta({ data, ctx }) {
88
92
  return {
89
- title: ctx.t('meta.title') || '${route}',
90
- description: ctx.t('meta.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 (answers.hasLocales) {
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: route,
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
- const deContent = {
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✅ Page created at ${route}\n`);
121
+
122
+ console.log(`\n🎉 Page ready at: /${route}\n`);
137
123
  });
138
124
  }
139
125
 
@@ -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();
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  const { z: zForApi } = require('./validation');
7
- const schemaCache = require('../utils/schemaCache');
7
+ const schemaCache = require('./schema-cache');
8
8
 
9
9
  /**
10
10
  * Compile schema from an API module
@@ -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
- console.error('[webspresso] Server Error:', err);
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
- console.warn(`[webspresso] HTTP ${status}:`, normalized.message);
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
 
@@ -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
- return new HttpError(status, (status < 500 || isDev) ? msg : getStatusTitle(status), {
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 cause strictly in development mode
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
  }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Schema Cache
3
3
  * Caches compiled Zod schemas per file path
4
+ * @module core/schema-cache
4
5
  */
5
6
 
6
7
  const cache = new Map();
@@ -54,7 +55,5 @@ module.exports = {
54
55
  set,
55
56
  has,
56
57
  clear,
57
- del
58
+ del,
58
59
  };
59
-
60
-