webspresso 0.0.74 → 0.0.75

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 (60) hide show
  1. package/README.md +41 -3
  2. package/bin/commands/orm-map.js +139 -0
  3. package/bin/commands/skill.js +22 -8
  4. package/bin/utils/orm-map-html.js +689 -0
  5. package/bin/utils/orm-map-load.js +85 -0
  6. package/bin/utils/orm-map-snapshot.js +179 -0
  7. package/bin/utils/resolve-webspresso-orm.js +23 -0
  8. package/bin/webspresso.js +2 -0
  9. package/core/auth/manager.js +14 -1
  10. package/core/kernel/app.js +96 -0
  11. package/core/kernel/base-repository.js +143 -0
  12. package/core/kernel/events.js +101 -0
  13. package/core/kernel/flow.js +22 -0
  14. package/core/kernel/index.js +17 -0
  15. package/core/kernel/plugin.js +23 -0
  16. package/core/kernel/plugins/sample-seo.js +26 -0
  17. package/core/kernel/run-demo.js +58 -0
  18. package/core/kernel/view.js +167 -0
  19. package/core/openapi/build-from-api-routes.js +8 -2
  20. package/core/orm/model.js +3 -1
  21. package/core/url-path-normalize.js +30 -0
  22. package/index.d.ts +168 -1
  23. package/index.js +20 -2
  24. package/package.json +11 -1
  25. package/plugins/admin-panel/api.js +43 -15
  26. package/plugins/admin-panel/client/README.md +39 -0
  27. package/plugins/admin-panel/client/load-parts.js +74 -0
  28. package/plugins/admin-panel/client/manifest.parts.json +12 -0
  29. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +150 -0
  30. package/plugins/admin-panel/client/parts/02-filter-components.js +554 -0
  31. package/plugins/admin-panel/client/parts/03-pagination-intro.js +70 -0
  32. package/plugins/admin-panel/client/parts/04-field-renderers.js +287 -0
  33. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +335 -0
  34. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +125 -0
  35. package/plugins/admin-panel/client/parts/07-model-list.js +596 -0
  36. package/plugins/admin-panel/client/parts/08-record-list.js +536 -0
  37. package/plugins/admin-panel/client/parts/09-record-form.js +170 -0
  38. package/plugins/admin-panel/client/parts/10-export-registry.js +11 -0
  39. package/plugins/admin-panel/client/verify-spa-parts.js +32 -0
  40. package/plugins/admin-panel/client/vite.config.example.mjs +22 -0
  41. package/plugins/admin-panel/components.js +4 -2640
  42. package/plugins/admin-panel/core/api-extensions.js +100 -10
  43. package/plugins/admin-panel/index.js +3 -0
  44. package/plugins/admin-panel/lib/is-rich-text-empty.js +23 -0
  45. package/plugins/admin-panel/lib/sanitize-rich-html.js +106 -0
  46. package/plugins/admin-panel/modules/dashboard.js +3 -2
  47. package/plugins/admin-panel/modules/user-management.js +90 -20
  48. package/plugins/index.js +4 -0
  49. package/plugins/rate-limit/index.js +178 -0
  50. package/plugins/redirect/index.js +204 -0
  51. package/plugins/rest-resources/index.js +2 -1
  52. package/plugins/swagger.js +2 -1
  53. package/plugins/upload/local-file-provider.js +6 -2
  54. package/src/file-router.js +191 -50
  55. package/src/njk-frontmatter.js +156 -0
  56. package/src/plugin-manager.js +4 -2
  57. package/src/server.js +26 -9
  58. package/templates/skills/webspresso-usage/REFERENCE-framework.md +276 -0
  59. package/templates/skills/webspresso-usage/REFERENCE-kernel.md +93 -0
  60. package/templates/skills/webspresso-usage/SKILL.md +29 -278
package/README.md CHANGED
@@ -19,6 +19,7 @@ A minimal, file-based SSR framework for Node.js with Nunjucks templating.
19
19
  - **Session authentication** (optional): `createAuth` / `quickAuth` in **`webspresso/core/auth`** — pass the manager to **`createApp({ auth })`** for `express-session`, `req.user` / `req.auth`, remember-me tokens, and policy-style authorization. Full walkthrough: **[`doc/index.html#authentication`](doc/index.html#authentication)**.
20
20
  - **Optional client runtime** (Alpine.js + [swup](https://swup.js.org/)): **`createApp({ clientRuntime: { alpine, swup } })`** serves scripts under **`/__webspresso/client-runtime/`** and exposes **`clientRuntime`** in Nunjucks; layouts can include **`views/partials/webspresso-client-runtime.njk`**. Env overrides: **`WEBSPRESSO_ALPINE`**, **`WEBSPRESSO_SWUP`**. Demo: **`examples/alpine-swup-demo/`**. Details: **[`doc/index.html#client-runtime`](doc/index.html#client-runtime)**.
21
21
  - **TypeScript**: Published **`index.d.ts`** (via `package.json` `"types"`) for `createApp`, ORM, plugins, and router helpers — use from TS/JS with IDE autocomplete; runtime stays CommonJS
22
+ - **Application kernel (optional)**: In-process **`kernel`** API (`require('webspresso').kernel`) — event bus (`dispatch` / `publish`), **`kernel.createApp()`** (namespaced differently from SSR **`createApp`**), **`definePlugin`** / **`defineFlow`**, minimal **`{{ }}` view resolver**, and simulated **`BaseRepository`** with `orm.<resource>.*` events. Ships as **`core/kernel/`** on npm. Demo: **`node core/kernel/run-demo.js`**. Docs: **[`doc/index.html#application-kernel`](doc/index.html#application-kernel)**.
22
23
 
23
24
  ## Installation
24
25
 
@@ -40,6 +41,18 @@ Install **`@types/express`** in your app if you want full **`Express.Application
40
41
 
41
42
  Framework development (this repo): run **`npm run check:types`** to typecheck the declarations against a small smoke file (`tests/ts-smoke/`).
42
43
 
44
+ ## Application kernel (`kernel`)
45
+
46
+ Do **not** confuse **`kernel.createApp()`** with the package root **`createApp`** used for SSR—it returns a different object (event bus, optional flows, and a minimal view resolver). It does **not** modify Knex ORM behavior or Express routing.
47
+
48
+ ```javascript
49
+ const { kernel } = require('webspresso');
50
+ const app = kernel.createApp();
51
+ app.events.on('orm.post.afterCreate', async (ctx) => { /* ... */ });
52
+ ```
53
+
54
+ See **[`doc/index.html#application-kernel`](doc/index.html#application-kernel)** · source modules: [`core/kernel/`](core/kernel/). Cursor skill: **`REFERENCE-kernel.md`** (installed via `webspresso skill --preset webspresso`).
55
+
43
56
  ## Quick Start
44
57
 
45
58
  ### Using CLI (Recommended)
@@ -234,7 +247,7 @@ webspresso doctor --strict
234
247
 
235
248
  Scaffold a **Cursor Agent Skill**: creates `.cursor/skills/<name>/SKILL.md` with valid YAML frontmatter (`name`, `description`) and a short markdown template for AI tooling. Use `--global` to write under `~/.cursor/skills/` instead of the current project.
236
249
 
237
- **Bundled preset:** `--preset webspresso` copies the full **Webspresso usage** reference skill (framework routing, ORM, plugins, CLI, pitfalls) into `.cursor/skills/webspresso-usage/SKILL.md` — no prompts.
250
+ **Bundled preset:** `--preset webspresso` copies **`SKILL.md`** (short index), **`REFERENCE-framework.md`** (SSR `createApp`, routes, ORM, auth, plugins, CLI), and **`REFERENCE-kernel.md`** (`kernel` event bus / flows) into **`.cursor/skills/webspresso-usage/`** — no prompts.
238
251
 
239
252
  ```bash
240
253
  webspresso skill my-workflow
@@ -533,6 +546,14 @@ Error templates receive these variables:
533
546
  - `500.njk`: `{ error, status, isDev }`
534
547
  - `503.njk`: `{ url, method, isDev }`
535
548
 
549
+ **How errors reach this handler**
550
+
551
+ Unhandled errors from file-based routes (`pages/**/*.njk` `load()` / middleware / render, and `pages/api/**/*.js` handlers) are forwarded with `next(err)`, so they go through this 4-argument Express error middleware. That means `errorPages.serverError` and `errorPages.timeout` apply to those failures as well (not only to routes you add with `setupRoutes`).
552
+
553
+ - **`pages/_hooks.js` `onError(ctx, err)`** runs **before** the central handler (for both SSR and API file routes). Use it for logging or APM (`Sentry.captureException`, `newrelic.noticeError`, etc.). The error is also on **`ctx.error`**.
554
+
555
+ - **JSON vs HTML:** Requests whose path starts with **`/api`** always get a **JSON** error body from the default branch (and the default **string** `serverError` / `timeout` Nunjucks templates are skipped for those paths). Other clients use **`Accept`** as before: prefer HTML when the client accepts HTML.
556
+
536
557
  **Request Timeout:**
537
558
 
538
559
  Configure request timeout with `connect-timeout`:
@@ -682,6 +703,22 @@ Options:
682
703
  - `path` - Custom dashboard path (default: `/_webspresso`)
683
704
  - `enabled` - Force enable/disable (default: auto based on NODE_ENV)
684
705
 
706
+ **Redirect plugin:**
707
+ - Runs in `register()` **before** file-based routes, so configured paths override SSR pages.
708
+ - Rules: `from` (string or `RegExp`), `to` (path or URL), optional `status` (301/302/303/307/308), optional `methods` (`'*'` or a list; default plugin methods are `GET` and `HEAD` only).
709
+ - `preserveQuery` (default `true`) appends the request query when `to` has no `?`. External `to` values require `allowExternal: true`.
710
+
711
+ ```javascript
712
+ const { redirectPlugin } = require('webspresso/plugins');
713
+
714
+ redirectPlugin({
715
+ rules: [
716
+ { from: '/old-blog', to: '/blog', status: 301 },
717
+ { from: /^\/wiki\/(.*)$/, to: '/docs' },
718
+ ],
719
+ });
720
+ ```
721
+
685
722
  **Sitemap Plugin:**
686
723
  - Generates `/sitemap.xml` from routes automatically
687
724
  - **Dynamic Database Content**: Generate URLs from database records
@@ -768,7 +805,7 @@ Options:
768
805
  - `userManagement` - Site-user admin UI (`enabled`, `model` matching ORM user table, optional `fields` map). SPA routes: `/_admin/users`, `/_admin/users/new`, …; APIs: `/_admin/api/users*`. Admin staff still use **`admin_users`** / `/_admin` login; this is separate from **`req.user`** on the public site
769
806
  - `configure` - Callback `(registry) => void` for manual setup
770
807
 
771
- See **`doc/index.html#admin-user-management`** and **Session authentication** in **`.cursor/skills/webspresso-usage/SKILL.md`** for the split between **`adminUser`** and **`createApp({ auth })`**.
808
+ See **`doc/index.html#admin-user-management`** and **Session authentication** in **`.cursor/skills/webspresso-usage/REFERENCE-framework.md`** for the split between **`adminUser`** and **`createApp({ auth })`**.
772
809
 
773
810
  **Custom Admin Pages (registerModule):**
774
811
 
@@ -2210,8 +2247,9 @@ npm run test:watch
2210
2247
  # Run tests with coverage
2211
2248
  npm run test:coverage
2212
2249
 
2213
- # Micro-benchmarks (Vitest bench; also runs in CI on the test matrix)
2250
+ # Micro-benchmarks (Vitest bench; CI: main push uploads benchmark-baseline artifact, PRs compare against it)
2214
2251
  npm run bench
2252
+ # Local baseline + compare: npm run bench:ci:local
2215
2253
  ```
2216
2254
 
2217
2255
  ## License
@@ -0,0 +1,139 @@
1
+ /**
2
+ * ORM map — interactive single-page HTML of models and relationships
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+ const { execFileSync } = require('child_process');
9
+ const { getWebspressoOrmForProject } = require('../utils/resolve-webspresso-orm');
10
+ const { loadProjectModels } = require('../utils/orm-map-load');
11
+ const { buildSnapshot, buildMermaidErDiagram } = require('../utils/orm-map-snapshot');
12
+ const { buildOrmMapHtml, readPackageName } = require('../utils/orm-map-html');
13
+
14
+ function ensureOpenInsideTrustedRoots(absFile, cwd) {
15
+ if (!fs.existsSync(absFile)) {
16
+ throw new Error(`File does not exist: ${absFile}`);
17
+ }
18
+ let realFile;
19
+ try {
20
+ realFile = fs.realpathSync(absFile);
21
+ } catch (e) {
22
+ throw new Error(`Cannot resolve file path: ${absFile}`, { cause: e });
23
+ }
24
+
25
+ /** @type {string[]} */
26
+ const roots = [];
27
+ for (const base of [os.tmpdir(), cwd]) {
28
+ const resolvedBase = path.resolve(base);
29
+ try {
30
+ roots.push(fs.realpathSync(resolvedBase));
31
+ } catch {
32
+ roots.push(resolvedBase);
33
+ }
34
+ }
35
+
36
+ const fileNorm = path.normalize(realFile + path.sep);
37
+ const trusted = roots.some((rootRaw) => {
38
+ const rr =
39
+ typeof rootRaw === 'string' && fs.existsSync(rootRaw)
40
+ ? fs.realpathSync(rootRaw)
41
+ : path.normalize(rootRaw);
42
+ const rp = rr.endsWith(path.sep) ? rr : rr + path.sep;
43
+ return realFile === rr || fileNorm.startsWith(path.normalize(rp));
44
+ });
45
+
46
+ if (!trusted) {
47
+ throw new Error('Refused to open file outside cwd or OS temp dir');
48
+ }
49
+ }
50
+
51
+ function openFile(filePath, cwd) {
52
+ const fp = path.resolve(filePath);
53
+ ensureOpenInsideTrustedRoots(fp, cwd);
54
+ if (process.platform === 'darwin') {
55
+ execFileSync('open', [fp], { stdio: 'ignore' });
56
+ } else if (process.platform === 'win32') {
57
+ execFileSync('cmd', ['/c', 'start', '', fp], { stdio: 'ignore' });
58
+ } else {
59
+ execFileSync('xdg-open', [fp], { stdio: 'ignore' });
60
+ }
61
+ }
62
+
63
+ function registerCommand(program) {
64
+ program
65
+ .command('orm:map')
66
+ .description(
67
+ 'Generate a self-contained HTML map of ORM models and relations (opens in browser by default)',
68
+ )
69
+ .option('-o, --output <file>', 'Write HTML to this path instead of a temp file')
70
+ .option('--no-open', 'Do not open the browser')
71
+ .option('-c, --config <path>', 'Database config file (webspresso.db.js / knexfile.js) for models path')
72
+ .option('-e, --env <environment>', 'Config environment', 'development')
73
+ .option('-m, --models <dir>', 'Models directory (overrides config default ./models)')
74
+ .action((options) => {
75
+ const cwd = process.cwd();
76
+ const { modelsDir, loaded, errors } = loadProjectModels(cwd, {
77
+ modelsOverride: options.models,
78
+ configPath: options.config,
79
+ env: options.env,
80
+ });
81
+
82
+ for (const err of errors) {
83
+ if (!err.file) {
84
+ console.error(`❌ ${err.message}`);
85
+ } else {
86
+ console.warn(`⚠️ ${err.file}: ${err.message}`);
87
+ }
88
+ }
89
+
90
+ if (loaded.length === 0 && errors.some((e) => e.file === '')) {
91
+ process.exit(1);
92
+ }
93
+
94
+ const orm = getWebspressoOrmForProject(cwd);
95
+ const registry = orm.getAllModels();
96
+ if (registry.size === 0) {
97
+ console.error(
98
+ `❌ No models registered. Checked: ${modelsDir}\n` +
99
+ ' Add defineModel() files or pass --models <dir>.',
100
+ );
101
+ process.exit(1);
102
+ }
103
+
104
+ const snapshot = buildSnapshot(registry);
105
+ const mermaid = buildMermaidErDiagram(snapshot);
106
+ const pkg = readPackageName(cwd);
107
+ const html = buildOrmMapHtml(snapshot, mermaid, {
108
+ title: 'ORM model map',
109
+ packageName: pkg,
110
+ });
111
+
112
+ let outPath;
113
+ if (options.output) {
114
+ outPath = path.resolve(cwd, options.output);
115
+ } else {
116
+ outPath = path.join(
117
+ os.tmpdir(),
118
+ `webspresso-orm-map-${Date.now()}.html`,
119
+ );
120
+ }
121
+
122
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
123
+ fs.writeFileSync(outPath, html, 'utf8');
124
+
125
+ console.log(`\n✅ ORM map (${registry.size} model(s))`);
126
+ console.log(` Models dir: ${modelsDir}`);
127
+ console.log(` Written: ${outPath}\n`);
128
+
129
+ if (options.open !== false) {
130
+ try {
131
+ openFile(outPath, cwd);
132
+ } catch (e) {
133
+ console.warn(`⚠️ Could not open browser: ${e.message}`);
134
+ }
135
+ }
136
+ });
137
+ }
138
+
139
+ module.exports = { registerCommand };
@@ -28,11 +28,11 @@ function defaultDescription(skillName) {
28
28
  return `Guides the agent through tasks for ${label}. Use when the user works on ${label} or asks about related workflows.`;
29
29
  }
30
30
 
31
- /** Bundled presets: CLI flag → template path under bin/ */
31
+ /** Bundled presets: CLI flag → template directory (all *.md copied) */
32
32
  const PRESETS = {
33
33
  webspresso: {
34
34
  skillName: 'webspresso-usage',
35
- templatePath: path.join(__dirname, '../../templates/skills/webspresso-usage/SKILL.md'),
35
+ templateDir: path.join(__dirname, '../../templates/skills/webspresso-usage'),
36
36
  },
37
37
  };
38
38
 
@@ -76,7 +76,10 @@ function registerCommand(program) {
76
76
  .option('-g, --global', 'Write to ~/.cursor/skills/ instead of ./.cursor/skills/')
77
77
  .option('-d, --description <text>', 'Skill description (for YAML frontmatter)')
78
78
  .option('-f, --force', 'Overwrite existing SKILL.md')
79
- .option('-p, --preset <name>', 'Install bundled skill: webspresso → full Webspresso agent reference (SKILL.md)')
79
+ .option(
80
+ '-p, --preset <name>',
81
+ 'Install bundled skill: webspresso → agent reference (SKILL.md + REFERENCE-*.md in .cursor/skills/webspresso-usage/)',
82
+ )
80
83
  .action(async (nameArg, options) => {
81
84
  const presetKey = options.preset ? String(options.preset).trim().toLowerCase() : '';
82
85
 
@@ -86,8 +89,16 @@ function registerCommand(program) {
86
89
  console.error(`❌ Unknown preset "${presetKey}". Available: ${Object.keys(PRESETS).join(', ')}`);
87
90
  process.exit(1);
88
91
  }
89
- if (!fs.existsSync(preset.templatePath)) {
90
- console.error(`❌ Preset template missing: ${preset.templatePath}`);
92
+ const templateDir = preset.templateDir;
93
+ if (!templateDir || !fs.existsSync(templateDir)) {
94
+ console.error(`❌ Preset template directory missing: ${templateDir}`);
95
+ process.exit(1);
96
+ }
97
+ const mdFiles = fs
98
+ .readdirSync(templateDir)
99
+ .filter((f) => f.endsWith('.md'));
100
+ if (mdFiles.length === 0) {
101
+ console.error(`❌ No .md files in preset: ${templateDir}`);
91
102
  process.exit(1);
92
103
  }
93
104
  const root = options.global ? os.homedir() : process.cwd();
@@ -101,10 +112,13 @@ function registerCommand(program) {
101
112
  }
102
113
 
103
114
  fs.mkdirSync(dir, { recursive: true });
104
- fs.copyFileSync(preset.templatePath, skillFile);
115
+ for (const file of mdFiles) {
116
+ fs.copyFileSync(path.join(templateDir, file), path.join(dir, file));
117
+ }
105
118
 
106
- console.log(`\n✅ Installed bundled skill "${presetKey}" →\n ${skillFile}\n`);
107
- console.log(' Edit SKILL.md if needed, then restart Cursor or reload the window.\n');
119
+ console.log(`\n✅ Installed bundled skill "${presetKey}" →\n ${dir}/\n`);
120
+ console.log(` Files: ${mdFiles.join(', ')}\n`);
121
+ console.log(' Edit if needed, then restart Cursor or reload the window.\n');
108
122
  return;
109
123
  }
110
124