webspresso 0.0.81 → 0.0.83

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 CHANGED
@@ -245,9 +245,10 @@ webspresso doctor --strict
245
245
 
246
246
  ### `webspresso skill`
247
247
 
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.
248
+ Scaffold an **Agent Skill**: creates `.agents/skills/<name>/SKILL.md` with valid YAML frontmatter (`name`, `description`) and a short markdown template for AI tooling. Use `--global` to write under `~/.agents/skills/` instead of the current project.
249
+
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 **`.agents/skills/webspresso-usage/`** — no prompts.
249
251
 
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.
251
252
 
252
253
  ```bash
253
254
  webspresso skill my-workflow
@@ -805,16 +806,18 @@ Options:
805
806
  - `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
806
807
  - `configure` - Callback `(registry) => void` for manual setup
807
808
 
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 })`**.
809
+ See **`doc/index.html#admin-user-management`** and **Session authentication** in **`.agents/skills/webspresso-usage/REFERENCE-framework.md`** for the split between **`adminUser`** and **`createApp({ auth })`**.
810
+
809
811
 
810
812
  **Custom Admin Pages (registerModule):**
811
813
 
812
- Plugins can add custom admin pages using `registerModule` in `onRoutesReady`:
814
+ Plugins can add custom admin pages using `registerModule` (or `registerPageDir`) in `onRoutesReady`:
813
815
 
814
816
  ```javascript
815
817
  // In your plugin's onRoutesReady(ctx)
816
818
  const adminApi = ctx.usePlugin('admin-panel');
817
819
  if (adminApi) {
820
+ // Option 1: File-based registration with auto Layout wrapping
818
821
  adminApi.registerModule({
819
822
  id: 'my-module',
820
823
 
@@ -824,7 +827,8 @@ if (adminApi) {
824
827
  path: '/reports',
825
828
  icon: 'chart',
826
829
  description: 'View reports',
827
- component: `window.__customPages["reports"] = { view: () => m("div", "My Report") };`, // Mithril.js
830
+ componentFile: path.join(__dirname, 'admin/reports.js'), // JS component file
831
+ layout: true, // Auto-wraps view in Admin Layout (default: true)
828
832
  }],
829
833
 
830
834
  menu: [{ id: 'reports', label: 'Reports', path: '/reports', icon: 'chart', order: 5 }],
@@ -835,15 +839,10 @@ if (adminApi) {
835
839
  { method: 'get', path: '/summary', handler: getSummaryHandler, auth: true },
836
840
  ],
837
841
  },
838
-
839
- widgets: [{
840
- id: 'reports-widget',
841
- title: 'Quick Stats',
842
- dataLoader: async () => ({ count: 42 }),
843
- }],
844
-
845
- menuGroups: [{ id: 'analytics', label: 'Analytics', order: 2 }],
846
842
  });
843
+
844
+ // Option 2: Folder-based auto registration (dir containing page.json + component.js)
845
+ adminApi.registerPageDir(path.join(__dirname, 'admin/pages'));
847
846
  }
848
847
  ```
849
848
 
@@ -851,13 +850,41 @@ if (adminApi) {
851
850
  | Field | Description |
852
851
  |-------|-------------|
853
852
  | `id` | Unique module identifier (required) |
854
- | `pages` | Custom admin pages (each: `id`, `title`, `path`, `icon`, `description`, optional `component`) |
853
+ | `pages` | Custom admin pages (`id`, `title`, `path`, `icon`, `description`, `componentFile`, `component`, `layout`) |
854
+ | `pagesDir` | Path to directory containing page folders/configs for automatic discovery |
855
855
  | `menu` | Sidebar menu items (`id`, `label`, `path`, `icon`, `order`) |
856
856
  | `menuGroups` | Collapsible menu groups (`id`, `label`, `order`) |
857
857
  | `api` | API routes (`prefix`, `routes`: `method`, `path`, `handler`, `auth`) |
858
858
  | `widgets` | Dashboard widgets (`id`, `title`, `dataLoader`) |
859
859
 
860
- For pages with `component`: provide Mithril.js code that assigns to `window.__customPages[pageId]`. Without `component`, the page shows a static placeholder.
860
+ Pages support `componentFile` (path to a `.js` file exporting/returning a Mithril component), `pagesDir` (directory auto-discovery), or `component` (code string). By default (`layout: true`), page views are automatically wrapped in the Admin Layout with Breadcrumbs. Set `layout: false` for full-screen / custom canvas pages.
861
+
862
+ **Example component file (`admin/reports.js`):**
863
+
864
+ ```javascript
865
+ // Clean Mithril.js component object (no manual m(Layout, ...) needed!)
866
+ ({
867
+ oninit: async (vnode) => {
868
+ vnode.state.data = [];
869
+ try {
870
+ const res = await api.get('/reports/summary');
871
+ vnode.state.data = res.data || [];
872
+ m.redraw();
873
+ } catch (err) {
874
+ console.error('Failed to load report data:', err);
875
+ }
876
+ },
877
+ view: (vnode) => {
878
+ const data = vnode.state.data || [];
879
+ return m('div.space-y-4', [
880
+ m('h1.text-2xl.font-bold.text-gray-900.dark:text-slate-100', 'Report Summary'),
881
+ m('p.text-gray-600.dark:text-slate-400', `Total records: ${data.length}`),
882
+ ]);
883
+ }
884
+ })
885
+ ```
886
+
887
+
861
888
 
862
889
  **Manual registry API** (alternative to registerModule):
863
890
 
@@ -20,9 +20,10 @@ function validateSkillName(name) {
20
20
  }
21
21
 
22
22
  function skillDir(base, skillName) {
23
- return path.join(base, '.cursor', 'skills', skillName);
23
+ return path.join(base, '.agents', 'skills', skillName);
24
24
  }
25
25
 
26
+
26
27
  function defaultDescription(skillName) {
27
28
  const label = skillName.replace(/-/g, ' ');
28
29
  return `Guides the agent through tasks for ${label}. Use when the user works on ${label} or asks about related workflows.`;
@@ -72,14 +73,15 @@ Example prompt or command
72
73
  function registerCommand(program) {
73
74
  program
74
75
  .command('skill [name]')
75
- .description('Create an Agent Skill folder with SKILL.md (Cursor / AI tools)')
76
- .option('-g, --global', 'Write to ~/.cursor/skills/ instead of ./.cursor/skills/')
76
+ .description('Create an Agent Skill folder with SKILL.md')
77
+ .option('-g, --global', 'Write to ~/.agents/skills/ instead of ./.agents/skills/')
77
78
  .option('-d, --description <text>', 'Skill description (for YAML frontmatter)')
78
79
  .option('-f, --force', 'Overwrite existing SKILL.md')
79
80
  .option(
80
81
  '-p, --preset <name>',
81
- 'Install bundled skill: webspresso → agent reference (SKILL.md + REFERENCE-*.md in .cursor/skills/webspresso-usage/)',
82
+ 'Install bundled skill: webspresso → agent reference (SKILL.md + REFERENCE-*.md in .agents/skills/webspresso-usage/)',
82
83
  )
84
+
83
85
  .action(async (nameArg, options) => {
84
86
  const presetKey = options.preset ? String(options.preset).trim().toLowerCase() : '';
85
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.81",
3
+ "version": "0.0.83",
4
4
  "description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -221,6 +221,55 @@ var routes = {
221
221
  },
222
222
  };
223
223
 
224
+ function wrapCustomPageWithLayout(page, rawComp) {
225
+ if (!rawComp) return createCustomPage(page);
226
+ if (page.layout === false) return rawComp;
227
+
228
+ return {
229
+ oninit: function(vnode) {
230
+ vnode.state.instance = typeof rawComp === 'function' ? rawComp.call(this, vnode) : rawComp;
231
+ if (vnode.state.instance && typeof vnode.state.instance.oninit === 'function') {
232
+ return vnode.state.instance.oninit.call(this, vnode);
233
+ }
234
+ },
235
+ oncreate: function(vnode) {
236
+ const comp = vnode.state.instance || rawComp;
237
+ if (comp && typeof comp.oncreate === 'function') return comp.oncreate.call(this, vnode);
238
+ },
239
+ onupdate: function(vnode) {
240
+ const comp = vnode.state.instance || rawComp;
241
+ if (comp && typeof comp.onupdate === 'function') return comp.onupdate.call(this, vnode);
242
+ },
243
+ onbeforeremove: function(vnode) {
244
+ const comp = vnode.state.instance || rawComp;
245
+ if (comp && typeof comp.onbeforeremove === 'function') return comp.onbeforeremove.call(this, vnode);
246
+ },
247
+ onremove: function(vnode) {
248
+ const comp = vnode.state.instance || rawComp;
249
+ if (comp && typeof comp.onremove === 'function') return comp.onremove.call(this, vnode);
250
+ },
251
+ view: function(vnode) {
252
+ const comp = vnode.state.instance || rawComp;
253
+ var content;
254
+ if (comp && typeof comp.view === 'function') {
255
+ content = comp.view.call(this, vnode);
256
+ } else if (typeof comp === 'function') {
257
+ content = comp.call(this, vnode);
258
+ } else {
259
+ content = comp;
260
+ }
261
+ if (content && (content.tag === Layout || (content.tag && content.tag.name === 'Layout'))) {
262
+ return content;
263
+ }
264
+ var breadcrumbs = [{ label: page.title, href: page.path }];
265
+ return m(Layout, [
266
+ m(Breadcrumb, { items: breadcrumbs }),
267
+ content
268
+ ]);
269
+ }
270
+ };
271
+ }
272
+
224
273
  // Dynamic routes for custom pages registered via plugins
225
274
  var config = window.__ADMIN_CONFIG__;
226
275
  if (config && config.pages) {
@@ -234,7 +283,7 @@ if (config && config.pages) {
234
283
  return;
235
284
  }
236
285
  if (window.__customPages && window.__customPages[page.id]) {
237
- return window.__customPages[page.id];
286
+ return wrapCustomPageWithLayout(page, window.__customPages[page.id]);
238
287
  }
239
288
  return createCustomPage(page);
240
289
  }
@@ -245,3 +294,4 @@ if (config && config.pages) {
245
294
 
246
295
  m.route(document.getElementById('app'), '/', routes);
247
296
  `;
297
+
@@ -1,25 +1,118 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
1
4
  /**
2
- * Admin Module Registration System
3
- * Declarative API for registering admin pages, menu items, API routes, and widgets
4
- * @module plugins/admin-panel/core/admin-module
5
+ * Format client component code to ensure it assigns to window.__customPages[pageId]
5
6
  */
7
+ function formatClientComponent(pageId, rawCode) {
8
+ let code = (rawCode || '').trim();
9
+ if (!code) return '';
10
+ if (code.includes('window.__customPages')) {
11
+ return code;
12
+ }
13
+ if (code.startsWith('module.exports =')) {
14
+ code = code.replace(/^module\.exports\s*=\s*/, '');
15
+ }
16
+ if (code.endsWith(';')) {
17
+ code = code.slice(0, -1);
18
+ }
19
+ return `window.__customPages = window.__customPages || {};\nwindow.__customPages[${JSON.stringify(pageId)}] = ${code};`;
20
+ }
21
+
22
+ /**
23
+ * Load page definitions and components from a directory structure
24
+ */
25
+ function loadPagesFromDir(dirPath) {
26
+ const resolvedDir = path.resolve(dirPath);
27
+ if (!fs.existsSync(resolvedDir)) {
28
+ throw new Error(`pagesDir directory not found: ${dirPath}`);
29
+ }
30
+ const stat = fs.statSync(resolvedDir);
31
+ if (!stat.isDirectory()) {
32
+ throw new Error(`pagesDir path is not a directory: ${dirPath}`);
33
+ }
34
+
35
+ const entries = fs.readdirSync(resolvedDir, { withFileTypes: true });
36
+ const pages = [];
37
+ const menuItems = [];
38
+
39
+ for (const entry of entries) {
40
+ if (entry.name.startsWith('.')) continue;
41
+
42
+ let pageConfig = null;
43
+ let componentFile = null;
44
+
45
+ if (entry.isDirectory()) {
46
+ const folderPath = path.join(resolvedDir, entry.name);
47
+ const jsonCandidate = path.join(folderPath, 'page.json');
48
+ const configJsonCandidate = path.join(folderPath, 'config.json');
49
+
50
+ if (fs.existsSync(jsonCandidate)) {
51
+ pageConfig = JSON.parse(fs.readFileSync(jsonCandidate, 'utf8'));
52
+ } else if (fs.existsSync(configJsonCandidate)) {
53
+ pageConfig = JSON.parse(fs.readFileSync(configJsonCandidate, 'utf8'));
54
+ } else {
55
+ pageConfig = { id: entry.name, title: entry.name, path: '/' + entry.name };
56
+ }
57
+
58
+ const jsCandidates = ['component.js', 'index.js', 'page.js', `${entry.name}.js`];
59
+ for (const cand of jsCandidates) {
60
+ const fullCandPath = path.join(folderPath, cand);
61
+ if (fs.existsSync(fullCandPath)) {
62
+ componentFile = fullCandPath;
63
+ break;
64
+ }
65
+ }
66
+ } else if (entry.isFile() && entry.name.endsWith('.json')) {
67
+ const jsonPath = path.join(resolvedDir, entry.name);
68
+ pageConfig = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
69
+ const baseName = entry.name.slice(0, -5);
70
+ const jsCandidate = path.join(resolvedDir, baseName + '.js');
71
+ if (fs.existsSync(jsCandidate)) {
72
+ componentFile = jsCandidate;
73
+ }
74
+ }
75
+
76
+ if (pageConfig) {
77
+ if (componentFile && !pageConfig.componentFile) {
78
+ pageConfig.componentFile = componentFile;
79
+ }
80
+ if (pageConfig.menu) {
81
+ if (pageConfig.menu === true) {
82
+ menuItems.push({
83
+ id: pageConfig.id,
84
+ label: pageConfig.title,
85
+ path: pageConfig.path,
86
+ icon: pageConfig.icon,
87
+ });
88
+ } else if (typeof pageConfig.menu === 'object') {
89
+ menuItems.push({
90
+ id: pageConfig.id,
91
+ label: pageConfig.title,
92
+ path: pageConfig.path,
93
+ icon: pageConfig.icon,
94
+ ...pageConfig.menu,
95
+ });
96
+ }
97
+ }
98
+ pages.push(pageConfig);
99
+ }
100
+ }
101
+
102
+ return { pages, menuItems };
103
+ }
6
104
 
7
105
  /**
8
106
  * Register an admin module with all its components in a single declarative call
9
107
  * @param {Object} config - Module configuration
10
108
  * @param {string} config.id - Unique module identifier (required)
11
109
  * @param {Array} [config.pages] - Custom admin pages
110
+ * @param {string} [config.pagesDir] - Directory path containing page folders/configs
12
111
  * @param {Array} [config.menu] - Sidebar menu items
13
112
  * @param {Object} [config.menuGroups] - Menu group definitions
14
113
  * @param {Object} [config.api] - API endpoint definitions
15
114
  * @param {Array} [config.widgets] - Dashboard widgets
16
115
  * @param {Object} deps - Internal dependencies injected by admin-panel
17
- * @param {Object} deps.registry - AdminRegistry instance
18
- * @param {string} deps.adminPath - Admin panel base path
19
- * @param {Object} deps.ctx - Plugin context
20
- * @param {Function} deps.requireAuth - Auth middleware
21
- * @param {Function} deps.optionalAuth - Optional auth middleware
22
- * @param {Function} deps.serveAdminPanel - SPA HTML handler
23
116
  */
24
117
  function registerModule(config, deps) {
25
118
  if (!config || typeof config !== 'object') {
@@ -36,14 +129,36 @@ function registerModule(config, deps) {
36
129
  registerMenuGroups(config.menuGroups, registry);
37
130
  }
38
131
 
39
- if (config.pages) {
40
- registerPages(config.id, config.pages, { registry, adminPath, ctx, optionalAuth, serveAdminPanel });
132
+ if (config.pages !== undefined) {
133
+ if (!Array.isArray(config.pages)) {
134
+ throw new Error(`Module "${config.id}": pages must be an array`);
135
+ }
136
+ }
137
+
138
+ if (config.menu !== undefined) {
139
+ if (!Array.isArray(config.menu)) {
140
+ throw new Error('menu must be an array');
141
+ }
41
142
  }
42
143
 
43
- if (config.menu) {
44
- registerMenuItems(config.menu, registry);
144
+ let pagesList = config.pages ? [...config.pages] : [];
145
+ let menuList = config.menu ? [...config.menu] : [];
146
+
147
+ if (config.pagesDir) {
148
+ const { pages: dirPages, menuItems: dirMenuItems } = loadPagesFromDir(config.pagesDir);
149
+ pagesList = pagesList.concat(dirPages);
150
+ menuList = menuList.concat(dirMenuItems);
45
151
  }
46
152
 
153
+ if (pagesList.length > 0) {
154
+ registerPages(config.id, pagesList, { registry, adminPath, ctx, optionalAuth, serveAdminPanel });
155
+ }
156
+
157
+ if (menuList.length > 0) {
158
+ registerMenuItems(menuList, registry);
159
+ }
160
+
161
+
47
162
  if (config.api) {
48
163
  registerApiRoutes(config.id, config.api, { adminPath, ctx, requireAuth, optionalAuth });
49
164
  }
@@ -53,6 +168,20 @@ function registerModule(config, deps) {
53
168
  }
54
169
  }
55
170
 
171
+ /**
172
+ * Register custom admin pages from a directory directly
173
+ * @param {string} dirPath - Directory path containing custom page folders/configs
174
+ * @param {Object} deps - Dependencies injected by admin-panel
175
+ */
176
+ function registerPageDir(dirPath, deps) {
177
+ const { pages, menuItems } = loadPagesFromDir(dirPath);
178
+ const moduleId = path.basename(dirPath);
179
+ registerPages(moduleId, pages, deps);
180
+ if (menuItems.length > 0) {
181
+ registerMenuItems(menuItems, deps.registry);
182
+ }
183
+ }
184
+
56
185
  function registerPages(moduleId, pages, deps) {
57
186
  if (!Array.isArray(pages)) {
58
187
  throw new Error(`Module "${moduleId}": pages must be an array`);
@@ -73,10 +202,20 @@ function registerPages(moduleId, pages, deps) {
73
202
  icon: page.icon,
74
203
  description: page.description,
75
204
  permission: page.permission,
205
+ layout: page.layout !== undefined ? page.layout : true,
76
206
  });
77
207
 
78
- if (page.component) {
79
- registry.registerClientComponent(pageId, page.component);
208
+ if (page.componentFile) {
209
+ const resolvedPath = path.resolve(page.componentFile);
210
+ if (!fs.existsSync(resolvedPath)) {
211
+ throw new Error(`Module "${moduleId}", page "${pageId}": componentFile not found at ${page.componentFile}`);
212
+ }
213
+ const rawCode = fs.readFileSync(resolvedPath, 'utf8');
214
+ const formattedCode = formatClientComponent(pageId, rawCode);
215
+ registry.registerClientComponent(pageId, formattedCode);
216
+ } else if (page.component) {
217
+ const formattedCode = formatClientComponent(pageId, page.component);
218
+ registry.registerClientComponent(pageId, formattedCode);
80
219
  }
81
220
 
82
221
  ctx.addRoute('get', `${adminPath}${page.path}`, optionalAuth, serveAdminPanel);
@@ -126,7 +265,20 @@ function registerApiRoutes(moduleId, apiConfig, deps) {
126
265
  const useAuth = route.auth !== undefined ? route.auth : defaultAuth;
127
266
  const authMiddleware = useAuth ? requireAuth : optionalAuth;
128
267
 
129
- ctx.addRoute(method, fullPath, authMiddleware, route.handler);
268
+ const safeHandler = async (req, res, next) => {
269
+ try {
270
+ await route.handler(req, res, next);
271
+ } catch (err) {
272
+ if (typeof next === 'function') {
273
+ next(err);
274
+ } else {
275
+ throw err;
276
+ }
277
+ }
278
+ };
279
+ safeHandler._originalHandler = route.handler;
280
+
281
+ ctx.addRoute(method, fullPath, authMiddleware, safeHandler);
130
282
  }
131
283
  }
132
284
 
@@ -143,4 +295,5 @@ function registerWidgets(widgets, registry) {
143
295
  }
144
296
  }
145
297
 
146
- module.exports = { registerModule };
298
+ module.exports = { registerModule, registerPageDir, loadPagesFromDir, formatClientComponent };
299
+
@@ -412,9 +412,12 @@ class AdminRegistry {
412
412
  title: p.title,
413
413
  path: p.path,
414
414
  icon: p.icon,
415
+ description: p.description,
415
416
  permission: p.permission,
417
+ layout: p.layout !== undefined ? p.layout : true,
416
418
  hasClientComponent: this.clientComponents.has(p.id),
417
419
  })),
420
+
418
421
  widgets: this.getWidgets().map(w => ({
419
422
  id: w.id,
420
423
  title: w.title,
@@ -15,7 +15,8 @@ const { registerDashboardWidgets, generateDashboardComponent } = require('./modu
15
15
  const { registerDefaultBulkActions, generateBulkActionsComponent } = require('./modules/bulk-actions');
16
16
  const { registerDefaultPages, createCustomPageApiHandlers, generateCustomPageComponent } = require('./modules/custom-pages');
17
17
  const { registerModelMenuItems, registerSystemMenuItems, generateMenuComponent } = require('./modules/menu');
18
- const { registerModule } = require('./core/admin-module');
18
+ const { registerModule, registerPageDir } = require('./core/admin-module');
19
+
19
20
 
20
21
  /**
21
22
  * Admin Panel Plugin Factory
@@ -105,6 +106,21 @@ function adminPanelPlugin(options = {}) {
105
106
  serveAdminPanel,
106
107
  });
107
108
  },
109
+ registerPageDir(dirPath) {
110
+ const ctx = this.api?._ctx ?? this._ctx;
111
+ if (!ctx) {
112
+ throw new Error('registerPageDir can only be called during or after onRoutesReady');
113
+ }
114
+ return registerPageDir(dirPath, {
115
+ registry,
116
+ adminPath,
117
+ ctx,
118
+ requireAuth,
119
+ optionalAuth,
120
+ serveAdminPanel,
121
+ });
122
+ },
123
+
108
124
  _ctx: null,
109
125
  },
110
126
 
@@ -34,9 +34,11 @@ function generateAuditLogComponent(options = {}) {
34
34
  rows = res.data || [];
35
35
  total = (res.meta && res.meta.total) || 0;
36
36
  loading = false;
37
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
37
38
  }).catch(function(e) {
38
39
  error = e.message || String(e);
39
40
  loading = false;
41
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
40
42
  });
41
43
  }
42
44
 
@@ -33,7 +33,8 @@ There are **three** files in this folder; open the reference that matches the ta
33
33
  | `webspresso dev` | Dev server (watch). |
34
34
  | `webspresso db:migrate` / `db:rollback` / `db:status` | ORM migrations. |
35
35
  | `webspresso doctor` | Environment / project sanity checks. |
36
- | `webspresso skill --preset webspresso` | Copies this skill bundle under `.cursor/skills/webspresso-usage/` (`SKILL.md` + `REFERENCE-*.md`). |
36
+ | `webspresso skill --preset webspresso` | Copies this skill bundle under `.agents/skills/webspresso-usage/` (`SKILL.md` + `REFERENCE-*.md`). |
37
+
37
38
 
38
39
  ## When to use this skill
39
40