webspresso 0.0.74 → 0.0.76
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 +66 -4
- package/bin/commands/admin-password.js +21 -57
- package/bin/commands/orm-map.js +139 -0
- package/bin/commands/skill.js +22 -8
- package/bin/utils/orm-map-html.js +689 -0
- package/bin/utils/orm-map-load.js +85 -0
- package/bin/utils/orm-map-snapshot.js +179 -0
- package/bin/utils/resolve-webspresso-orm.js +23 -0
- package/bin/webspresso.js +2 -0
- package/core/auth/manager.js +14 -1
- package/core/kernel/app.js +96 -0
- package/core/kernel/base-repository.js +143 -0
- package/core/kernel/events.js +101 -0
- package/core/kernel/flow.js +22 -0
- package/core/kernel/index.js +17 -0
- package/core/kernel/plugin.js +23 -0
- package/core/kernel/plugins/sample-seo.js +26 -0
- package/core/kernel/run-demo.js +58 -0
- package/core/kernel/view.js +167 -0
- package/core/openapi/build-from-api-routes.js +8 -2
- package/core/orm/model.js +3 -1
- package/core/url-path-normalize.js +30 -0
- package/index.d.ts +168 -1
- package/index.js +20 -2
- package/package.json +11 -1
- package/plugins/admin-panel/api.js +43 -15
- package/plugins/admin-panel/client/README.md +39 -0
- package/plugins/admin-panel/client/load-parts.js +74 -0
- package/plugins/admin-panel/client/manifest.parts.json +12 -0
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +150 -0
- package/plugins/admin-panel/client/parts/02-filter-components.js +554 -0
- package/plugins/admin-panel/client/parts/03-pagination-intro.js +70 -0
- package/plugins/admin-panel/client/parts/04-field-renderers.js +287 -0
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +335 -0
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +125 -0
- package/plugins/admin-panel/client/parts/07-model-list.js +596 -0
- package/plugins/admin-panel/client/parts/08-record-list.js +536 -0
- package/plugins/admin-panel/client/parts/09-record-form.js +170 -0
- package/plugins/admin-panel/client/parts/10-export-registry.js +11 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +32 -0
- package/plugins/admin-panel/client/vite.config.example.mjs +22 -0
- package/plugins/admin-panel/components.js +4 -2640
- package/plugins/admin-panel/core/api-extensions.js +100 -10
- package/plugins/admin-panel/index.js +3 -0
- package/plugins/admin-panel/lib/is-rich-text-empty.js +23 -0
- package/plugins/admin-panel/lib/sanitize-rich-html.js +106 -0
- package/plugins/admin-panel/modules/dashboard.js +3 -2
- package/plugins/admin-panel/modules/user-management.js +90 -20
- package/plugins/index.js +4 -0
- package/plugins/rate-limit/index.js +178 -0
- package/plugins/redirect/index.js +204 -0
- package/plugins/rest-resources/index.js +2 -1
- package/plugins/swagger.js +2 -1
- package/plugins/upload/local-file-provider.js +6 -2
- package/src/file-router.js +191 -50
- package/src/njk-frontmatter.js +156 -0
- package/src/plugin-manager.js +4 -2
- package/src/server.js +26 -9
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +277 -0
- package/templates/skills/webspresso-usage/REFERENCE-kernel.md +93 -0
- 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
|
|
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/
|
|
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
|
|
|
@@ -2128,10 +2165,34 @@ const { app } = createApp({
|
|
|
2128
2165
|
```
|
|
2129
2166
|
|
|
2130
2167
|
- **ORM:** `zdb.file({ maxLength: 2048, nullable: true })` — string column for the stored public URL or path; migrations use `table.string(..., maxLength)`.
|
|
2131
|
-
- **Admin:**
|
|
2168
|
+
- **Admin forms:** columns with `zdb.file()` automatically render a drag-and-drop upload widget (or a manual URL text field when `uploadUrl` is not configured). Optional `ui: { label, hint, accept, maxBytes }` on the column customizes the widget. For existing `zdb.string()` columns you can use `admin.customFields: { columnName: { type: 'file-upload' } }` instead of changing the schema type.
|
|
2169
|
+
- **Admin:** the panel reads **`settings.uploadUrl`** from the registry (set automatically when `uploadPlugin` is registered **before** `adminPanelPlugin`, or pass **`adminPanelPlugin({ uploadUrl: '/api/upload' })`**). File fields (`type: 'file'` or `customFields` type `file-upload`) POST to that URL with credentials; the saved record stores the returned **`url`** / **`publicUrl`** string.
|
|
2132
2170
|
- **Response:** `{ url, publicUrl, key? }` — clients typically persist **`url`** / **`publicUrl`** in the model.
|
|
2133
2171
|
- **Custom storage:** `uploadPlugin({ provider: { async put({ buffer, originalName, mimeType, size, req }) { return { publicUrl: '...' }; } } })`.
|
|
2134
2172
|
|
|
2173
|
+
**Admin model example** (`zdb.file()` picks up the upload widget from schema; no extra `customFields` needed):
|
|
2174
|
+
|
|
2175
|
+
```javascript
|
|
2176
|
+
const { defineModel, zdb } = require('webspresso');
|
|
2177
|
+
|
|
2178
|
+
const Post = defineModel({
|
|
2179
|
+
name: 'Post',
|
|
2180
|
+
table: 'posts',
|
|
2181
|
+
schema: zdb.schema({
|
|
2182
|
+
id: zdb.id(),
|
|
2183
|
+
title: zdb.string(),
|
|
2184
|
+
cover_image: zdb.file({
|
|
2185
|
+
maxLength: 2048,
|
|
2186
|
+
nullable: true,
|
|
2187
|
+
ui: { label: 'Cover', accept: 'image/*', hint: 'JPEG or PNG' },
|
|
2188
|
+
}),
|
|
2189
|
+
}),
|
|
2190
|
+
admin: { enabled: true, label: 'Posts' },
|
|
2191
|
+
});
|
|
2192
|
+
```
|
|
2193
|
+
|
|
2194
|
+
For a plain string column, use `admin.customFields: { attachment: { type: 'file-upload' } }` instead of `zdb.file()`.
|
|
2195
|
+
|
|
2135
2196
|
### Health check plugin
|
|
2136
2197
|
|
|
2137
2198
|
Exposes a lightweight **GET** endpoint for load balancers and orchestrators (Kubernetes, Docker healthcheck, etc.). **Enabled by default** in all environments; set `enabled: false` to turn it off.
|
|
@@ -2210,8 +2271,9 @@ npm run test:watch
|
|
|
2210
2271
|
# Run tests with coverage
|
|
2211
2272
|
npm run test:coverage
|
|
2212
2273
|
|
|
2213
|
-
# Micro-benchmarks (Vitest bench;
|
|
2274
|
+
# Micro-benchmarks (Vitest bench; CI: main push uploads benchmark-baseline artifact, PRs compare against it)
|
|
2214
2275
|
npm run bench
|
|
2276
|
+
# Local baseline + compare: npm run bench:ci:local
|
|
2215
2277
|
```
|
|
2216
2278
|
|
|
2217
2279
|
## License
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Reset admin user password via CLI
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const inquirer = require('inquirer');
|
|
7
|
+
const { hash } = require('../../core/auth/hash');
|
|
7
8
|
const { loadDbConfig, createDbInstance } = require('../utils/db');
|
|
8
9
|
|
|
9
10
|
function registerCommand(program) {
|
|
@@ -32,17 +33,14 @@ function registerCommand(program) {
|
|
|
32
33
|
// Get email (interactive if not provided)
|
|
33
34
|
let email = options.email;
|
|
34
35
|
if (!email) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
resolve(answer.trim());
|
|
44
|
-
});
|
|
45
|
-
});
|
|
36
|
+
const answers = await inquirer.prompt([
|
|
37
|
+
{
|
|
38
|
+
type: 'input',
|
|
39
|
+
name: 'email',
|
|
40
|
+
message: 'Enter admin email:',
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
43
|
+
email = answers.email.trim();
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
if (!email) {
|
|
@@ -70,49 +68,15 @@ function registerCommand(program) {
|
|
|
70
68
|
// Get new password (interactive if not provided)
|
|
71
69
|
let password = options.password;
|
|
72
70
|
if (!password) {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
let pwd = '';
|
|
83
|
-
process.stdin.setRawMode(true);
|
|
84
|
-
process.stdin.resume();
|
|
85
|
-
process.stdin.on('data', (char) => {
|
|
86
|
-
char = char.toString();
|
|
87
|
-
if (char === '\n' || char === '\r') {
|
|
88
|
-
process.stdin.setRawMode(false);
|
|
89
|
-
process.stdin.pause();
|
|
90
|
-
console.log(); // New line after password
|
|
91
|
-
resolve(pwd);
|
|
92
|
-
} else if (char === '\u0003') {
|
|
93
|
-
// Ctrl+C
|
|
94
|
-
process.exit();
|
|
95
|
-
} else if (char === '\u007F') {
|
|
96
|
-
// Backspace
|
|
97
|
-
if (pwd.length > 0) {
|
|
98
|
-
pwd = pwd.slice(0, -1);
|
|
99
|
-
process.stdout.write('\b \b');
|
|
100
|
-
}
|
|
101
|
-
} else {
|
|
102
|
-
pwd += char;
|
|
103
|
-
process.stdout.write('*');
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
rl.close();
|
|
108
|
-
} else {
|
|
109
|
-
password = await new Promise((resolve) => {
|
|
110
|
-
rl.question('Enter new password: ', (answer) => {
|
|
111
|
-
rl.close();
|
|
112
|
-
resolve(answer);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
}
|
|
71
|
+
const answers = await inquirer.prompt([
|
|
72
|
+
{
|
|
73
|
+
type: 'password',
|
|
74
|
+
name: 'password',
|
|
75
|
+
message: 'Enter new password:',
|
|
76
|
+
mask: '*',
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
79
|
+
password = answers.password;
|
|
116
80
|
}
|
|
117
81
|
|
|
118
82
|
if (!password || password.length < 6) {
|
|
@@ -121,8 +85,8 @@ function registerCommand(program) {
|
|
|
121
85
|
process.exit(1);
|
|
122
86
|
}
|
|
123
87
|
|
|
124
|
-
// Hash the password
|
|
125
|
-
const hashedPassword = await
|
|
88
|
+
// Hash the password (same rounds as admin panel setup)
|
|
89
|
+
const hashedPassword = await hash(password, 10);
|
|
126
90
|
|
|
127
91
|
// Update the password
|
|
128
92
|
await db('admin_users')
|
|
@@ -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 };
|
package/bin/commands/skill.js
CHANGED
|
@@ -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
|
|
31
|
+
/** Bundled presets: CLI flag → template directory (all *.md copied) */
|
|
32
32
|
const PRESETS = {
|
|
33
33
|
webspresso: {
|
|
34
34
|
skillName: 'webspresso-usage',
|
|
35
|
-
|
|
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(
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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 ${
|
|
107
|
-
console.log(
|
|
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
|
|