webspresso 0.0.56 → 0.0.58

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
@@ -143,7 +143,7 @@ The project includes:
143
143
  - Tailwind CSS with build process
144
144
  - Optimized layout template with navigation and footer
145
145
  - Responsive starter page
146
- - i18n setup (en/tr)
146
+ - i18n setup (e.g. en/de)
147
147
  - Development and production scripts
148
148
 
149
149
  ### `webspresso page`
@@ -191,6 +191,32 @@ webspresso start
191
191
  webspresso start --port 3000
192
192
  ```
193
193
 
194
+ ### `webspresso doctor`
195
+
196
+ Check Node.js version, `package.json` / `engines.node`, typical project files (`server.js`, `pages/`), and whether `webspresso.db.js` or `knexfile.js` exists. Use `--db` to run a quick connection test when a config is present. Warnings alone exit with code `0`; pass `--strict` to fail (exit `1`) on any warning—useful in CI.
197
+
198
+ ```bash
199
+ webspresso doctor
200
+ webspresso doctor --db
201
+ webspresso doctor --strict
202
+ ```
203
+
204
+ ### `webspresso skill`
205
+
206
+ 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.
207
+
208
+ **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.
209
+
210
+ ```bash
211
+ webspresso skill my-workflow
212
+ webspresso skill review-pr --description "Runs PR review checklist. Use when reviewing pull requests."
213
+ webspresso skill deploy-check -g
214
+
215
+ # Install the bundled Webspresso agent reference (same content shipped in templates/skills/)
216
+ webspresso skill --preset webspresso
217
+ webspresso skill -p webspresso --global
218
+ ```
219
+
194
220
  ### `webspresso add tailwind`
195
221
 
196
222
  Add Tailwind CSS to your project with build process.
@@ -277,7 +303,7 @@ my-app/
277
303
  ├── pages/
278
304
  │ ├── locales/ # Global i18n translations
279
305
  │ │ ├── en.json
280
- │ │ └── tr.json
306
+ │ │ └── de.json
281
307
  │ ├── _hooks.js # Global lifecycle hooks
282
308
  │ ├── index.njk # Home page (GET /)
283
309
  │ ├── about/
@@ -482,7 +508,7 @@ const { app } = createApp({
482
508
  hostname: 'https://example.com',
483
509
  exclude: ['/admin/*', '/api/*'],
484
510
  i18n: true,
485
- locales: ['en', 'tr']
511
+ locales: ['en', 'de']
486
512
  }),
487
513
  analyticsPlugin({
488
514
  google: {
@@ -718,6 +744,85 @@ Options:
718
744
 
719
745
  The `analytics_page_views` table is automatically created on first request.
720
746
 
747
+ **Audit log plugin:**
748
+ - Records successful (`2xx`) admin panel model mutations: `create`, `update`, `delete`, `restore` on `${adminPath}/api/models/:model/records…`
749
+ - Actor from `req.session.adminUser` after login; optional IP / user-agent; update metadata stores changed field names only (not full body)
750
+ - `GET ${adminPath}/api/audit-logs` with pagination and filters (`page`, `perPage`, `model`, `action`, `from`, `to`) — use from custom admin pages or the bundled Mithril list (`includeDefaultPage` default `true`)
751
+ - Run `webspresso db:migrate` after adding the migration (see `plugins/audit-log/migration-template.js` or the example under `migrations/`). Prune old rows with the CLI (recommended on a schedule):
752
+
753
+ ```bash
754
+ npx webspresso audit:prune --days 90
755
+ ```
756
+
757
+ ```javascript
758
+ const { adminPanelPlugin, auditLogPlugin } = require('webspresso/plugins');
759
+
760
+ const { app } = createApp({
761
+ pagesDir: './pages',
762
+ plugins: [
763
+ adminPanelPlugin({ db }),
764
+ auditLogPlugin({
765
+ db,
766
+ // adminPath: '/_admin', // must match admin panel `path`
767
+ // tableName: 'audit_logs',
768
+ // includeDefaultPage: true,
769
+ // apiPrefix: '/audit-logs',
770
+ }),
771
+ ],
772
+ });
773
+ ```
774
+
775
+ Programmatic API (other plugins): `ctx.usePlugin('audit-log')` exposes `queryLogs`, `purgeAuditLogs`, and `getMigrationTemplate()`.
776
+
777
+ **reCAPTCHA plugin:**
778
+ - Google reCAPTCHA **v2** (checkbox) or **v3** (score): server verification via `https://www.google.com/recaptcha/api/siteverify` (no extra npm dependency; Node 18+ `fetch`)
779
+ - Registers CSP entries for Google scripts/iframes; Nunjucks helpers: `recaptchaScript`, `recaptchaWidget` (v2), `recaptchaV3Token` (hidden input + execute for v3 — use with `version: 'v3'` and `recaptchaScript` loads `api.js?render=siteKey`)
780
+ - Secret key: `options.secretKey` or env `RECAPTCHA_SECRET_KEY` (never expose to templates)
781
+
782
+ ```javascript
783
+ const {
784
+ recaptchaPlugin,
785
+ createRecaptchaMiddleware,
786
+ resolveRecaptchaMiddlewareParams,
787
+ } = require('webspresso/plugins/recaptcha');
788
+
789
+ const recaptchaConfig = {
790
+ siteKey: process.env.RECAPTCHA_SITE_KEY,
791
+ secretKey: process.env.RECAPTCHA_SECRET_KEY,
792
+ version: 'v2', // or 'v3'
793
+ minScore: 0.5,
794
+ expectedAction: 'contact', // v3 verification only
795
+ defaultV3Action: 'submit', // for recaptchaV3Token helper
796
+ };
797
+
798
+ const { app } = createApp({
799
+ pagesDir: './pages',
800
+ plugins: [recaptchaPlugin(recaptchaConfig)],
801
+ middlewares: {
802
+ recaptcha: createRecaptchaMiddleware({
803
+ ...resolveRecaptchaMiddlewareParams(recaptchaConfig),
804
+ bodyField: 'g-recaptcha-response',
805
+ }),
806
+ },
807
+ });
808
+ ```
809
+
810
+ **File-based API** (`pages/api/...post.js`): use the named middleware instead of duplicating verification in the handler; on success, `req.recaptcha` contains a short summary of Google’s response.
811
+
812
+ ```javascript
813
+ // pages/api/contact.post.js
814
+ module.exports = async function post(req, res) {
815
+ // recaptcha middleware already returned 400 if token invalid
816
+ return res.json({ ok: true, hostname: req.recaptcha.hostname });
817
+ };
818
+
819
+ module.exports.middleware = ['recaptcha'];
820
+ ```
821
+
822
+ Optional: after `createApp`, use `pluginManager.getPluginAPI('recaptcha').createMiddleware({ bodyField: '...' })` to override plugin defaults and attach to the `app.use` chain (after body parsers).
823
+
824
+ Low-level usage (without middleware): import `verifyRecaptcha` / `getRemoteIp` from `webspresso/plugins/recaptcha/verify`.
825
+
721
826
  **SEO Checker Plugin:**
722
827
  - Client-side SEO analysis tool (inspired by django-check-seo)
723
828
  - Integrated with dev toolbar
@@ -1319,6 +1424,10 @@ await UserRepo.query().onlyTrashed().list(); // Only deleted
1319
1424
  await UserRepo.query().forTenant(tenantId).list();
1320
1425
  ```
1321
1426
 
1427
+ `list()`, `first()`, and `paginate()` emit the same `beforeFind` / `afterFind` lifecycle hooks as `findAll` / `findOne` (one `afterFind` per row). `count()` ignores any `.limit()` / `.offset()` on the builder so it returns the full matching row count.
1428
+
1429
+ `query().delete()` runs a SQL `DELETE` for matching rows. On models with soft deletes, use `UserRepo.delete(id)` (or equivalent) to set `deleted_at`; the query builder does not convert deletes to soft deletes.
1430
+
1322
1431
  ### Transactions
1323
1432
 
1324
1433
  ```javascript
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Prune old audit log rows (CLI)
3
+ */
4
+
5
+ const { loadDbConfig, createDbInstance } = require('../utils/db');
6
+ const { purgeAuditLogs } = require('../../plugins/audit-log/purge');
7
+
8
+ function registerCommand(program) {
9
+ program
10
+ .command('audit:prune')
11
+ .description('Delete audit log rows older than the given retention window')
12
+ .option('--days <n>', 'Delete rows older than this many days', '90')
13
+ .option('--table <name>', 'Table name', 'audit_logs')
14
+ .option('-e, --env <environment>', 'Environment (development, production)', 'development')
15
+ .option('-c, --config <path>', 'Path to database config file')
16
+ .action(async (options) => {
17
+ const days = parseInt(options.days, 10);
18
+ if (Number.isNaN(days) || days < 1) {
19
+ console.error('❌ --days must be a positive integer');
20
+ process.exit(1);
21
+ }
22
+
23
+ const { config, path: configPath } = loadDbConfig(options.config);
24
+ console.log(`\n📦 Using config: ${configPath}`);
25
+ console.log(` Environment: ${options.env}\n`);
26
+
27
+ const knex = await createDbInstance(config, options.env);
28
+ const olderThan = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
29
+
30
+ try {
31
+ const deleted = await purgeAuditLogs(knex, {
32
+ tableName: options.table,
33
+ olderThan,
34
+ });
35
+ console.log(`✅ Deleted ${deleted} row(s) with created_at before ${olderThan.toISOString()}.\n`);
36
+ } catch (err) {
37
+ console.error('❌ audit:prune failed:', err.message);
38
+ process.exit(1);
39
+ } finally {
40
+ await knex.destroy();
41
+ }
42
+ });
43
+ }
44
+
45
+ module.exports = { registerCommand };
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Doctor Command — environment and project sanity checks
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { resolveDbConfigIfExists, createDbInstance } = require('../utils/db');
8
+
9
+ /**
10
+ * @param {string} enginesNode - e.g. ">=18.0.0", "^20.1.0"
11
+ * @returns {boolean|null} null if could not interpret
12
+ */
13
+ function nodeEngineOk(enginesNode) {
14
+ const v = process.version;
15
+ const major = parseInt(v.replace(/^v/, '').split('.')[0], 10);
16
+ if (Number.isNaN(major)) return null;
17
+
18
+ const s = String(enginesNode).trim();
19
+ const ge = s.match(/^>=\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
20
+ if (ge) {
21
+ const reqMajor = parseInt(ge[1], 10);
22
+ return major >= reqMajor;
23
+ }
24
+ const gt = s.match(/^>\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
25
+ if (gt) {
26
+ const reqMajor = parseInt(gt[1], 10);
27
+ return major > reqMajor;
28
+ }
29
+ const caret = s.match(/^\^\s*(\d+)/);
30
+ if (caret) {
31
+ const reqMajor = parseInt(caret[1], 10);
32
+ return major === reqMajor;
33
+ }
34
+ const exact = s.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
35
+ if (exact) {
36
+ const reqMajor = parseInt(exact[1], 10);
37
+ return major === reqMajor;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ function registerCommand(program) {
43
+ program
44
+ .command('doctor')
45
+ .description('Check Node version, project layout, and optional database connectivity')
46
+ .option('--db', 'Run a database connection test when webspresso.db.js or knexfile.js exists')
47
+ .option('--strict', 'Exit with code 1 if any warning is reported')
48
+ .option('-e, --env <environment>', 'Environment for DB config (with --db)', 'development')
49
+ .action(async (options) => {
50
+ const cwd = process.cwd();
51
+ let errors = 0;
52
+ let warnings = 0;
53
+
54
+ const line = (icon, msg) => console.log(` ${icon} ${msg}`);
55
+
56
+ console.log('\nWebspresso Doctor');
57
+ console.log('=================\n');
58
+
59
+ console.log('Environment');
60
+ console.log('-----------');
61
+ line('✓', `Node.js ${process.version} (${process.platform}, ${process.arch})`);
62
+
63
+ const pkgPath = path.join(cwd, 'package.json');
64
+ let pkg = null;
65
+ if (!fs.existsSync(pkgPath)) {
66
+ line('✗', 'package.json not found (run this from a project directory)');
67
+ errors += 1;
68
+ } else {
69
+ try {
70
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
71
+ line('✓', `package.json readable (${pkg.name || 'unnamed'})`);
72
+ } catch (e) {
73
+ line('✗', `package.json invalid JSON: ${e.message}`);
74
+ errors += 1;
75
+ }
76
+ }
77
+
78
+ if (pkg && pkg.engines && pkg.engines.node) {
79
+ const ok = nodeEngineOk(pkg.engines.node);
80
+ if (ok === null) {
81
+ line('⚠', `engines.node "${pkg.engines.node}" — could not verify; check manually`);
82
+ warnings += 1;
83
+ } else if (ok) {
84
+ line('✓', `engines.node "${pkg.engines.node}" satisfied`);
85
+ } else {
86
+ line('⚠', `engines.node "${pkg.engines.node}" may not match current Node (adjust nvm / install)`);
87
+ warnings += 1;
88
+ }
89
+ }
90
+
91
+ console.log('\nProject layout');
92
+ console.log('--------------');
93
+
94
+ const serverPath = path.join(cwd, 'server.js');
95
+ if (fs.existsSync(serverPath)) {
96
+ line('✓', 'server.js present');
97
+ } else {
98
+ line('⚠', 'server.js not found (expected for standard Webspresso apps)');
99
+ warnings += 1;
100
+ }
101
+
102
+ const pagesPath = path.join(cwd, 'pages');
103
+ if (fs.existsSync(pagesPath) && fs.statSync(pagesPath).isDirectory()) {
104
+ line('✓', 'pages/ directory present');
105
+ } else {
106
+ line('⚠', 'pages/ not found (file-based routes may be missing)');
107
+ warnings += 1;
108
+ }
109
+
110
+ console.log('\nDatabase config');
111
+ console.log('---------------');
112
+
113
+ let resolved = null;
114
+ let configLoadError = false;
115
+ try {
116
+ resolved = resolveDbConfigIfExists();
117
+ } catch (e) {
118
+ configLoadError = true;
119
+ line('✗', `Database config could not be loaded: ${e.message}`);
120
+ errors += 1;
121
+ }
122
+
123
+ if (resolved) {
124
+ line('✓', `Found ${path.relative(cwd, resolved.path)}`);
125
+ } else if (!configLoadError) {
126
+ line('○', 'No webspresso.db.js or knexfile.js (ORM/CLI DB commands need one)');
127
+ }
128
+
129
+ if (options.db) {
130
+ console.log('\nDatabase connection (--db)');
131
+ console.log('--------------------------');
132
+ if (!resolved) {
133
+ line('○', 'Skipped — no database config file');
134
+ } else {
135
+ let knexMod;
136
+ try {
137
+ knexMod = require('knex');
138
+ } catch {
139
+ line('✗', 'knex is not installed. Run: npm install knex');
140
+ errors += 1;
141
+ }
142
+ if (knexMod) {
143
+ let knex;
144
+ try {
145
+ knex = await createDbInstance(resolved.config, options.env);
146
+ await knex.raw('select 1');
147
+ line('✓', `Connection OK (env: ${options.env})`);
148
+ } catch (e) {
149
+ line('✗', `Connection failed: ${e.message}`);
150
+ errors += 1;
151
+ } finally {
152
+ if (knex && typeof knex.destroy === 'function') {
153
+ try {
154
+ await knex.destroy();
155
+ } catch {
156
+ /* ignore */
157
+ }
158
+ }
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ console.log('');
165
+
166
+ const strict = options.strict === true;
167
+ const failWarnings = strict && warnings > 0;
168
+ const exitCode = errors > 0 || failWarnings ? 1 : 0;
169
+
170
+ if (errors > 0) {
171
+ console.log(`Summary: ${errors} error(s)${warnings ? `, ${warnings} warning(s)` : ''}\n`);
172
+ } else if (warnings > 0) {
173
+ console.log(`Summary: ${warnings} warning(s)${strict ? ' (strict: treating as failure)' : ''}\n`);
174
+ } else {
175
+ console.log('Summary: all checks passed.\n');
176
+ }
177
+
178
+ process.exit(exitCode);
179
+ });
180
+ }
181
+
182
+ module.exports = { registerCommand };
@@ -227,7 +227,7 @@ app.listen(PORT, () => {
227
227
  let envExample = `PORT=3000
228
228
  NODE_ENV=development
229
229
  DEFAULT_LOCALE=en
230
- SUPPORTED_LOCALES=en,tr
230
+ SUPPORTED_LOCALES=en,de
231
231
  BASE_URL=http://localhost:3000
232
232
  `;
233
233
 
@@ -454,23 +454,23 @@ coverage/
454
454
  JSON.stringify(enJson, null, 2) + '\n'
455
455
  );
456
456
 
457
- const trJson = {
457
+ const deJson = {
458
458
  site: {
459
459
  name: 'Webspresso'
460
460
  },
461
461
  nav: {
462
- home: 'Ana Sayfa'
462
+ home: 'Startseite'
463
463
  },
464
464
  footer: {
465
- copyright: '© 2025 Webspresso. Tüm hakları saklıdır.'
465
+ copyright: '© 2025 Webspresso. All rights reserved.'
466
466
  },
467
- welcome: 'Webspresso\'ya Hoş Geldiniz',
468
- description: 'SSR uygulamanızı oluşturmaya başlayın!'
467
+ welcome: 'Willkommen bei Webspresso',
468
+ description: 'Start building your SSR app!'
469
469
  };
470
470
 
471
471
  fs.writeFileSync(
472
- path.join(projectPath, 'pages', 'locales', 'tr.json'),
473
- JSON.stringify(trJson, null, 2) + '\n'
472
+ path.join(projectPath, 'pages', 'locales', 'de.json'),
473
+ JSON.stringify(deJson, null, 2) + '\n'
474
474
  );
475
475
 
476
476
  // Create README
@@ -110,18 +110,18 @@ function registerCommand(program) {
110
110
  JSON.stringify(enContent, null, 2) + '\n'
111
111
  );
112
112
 
113
- const trContent = {
113
+ const deContent = {
114
114
  title: route,
115
- description: 'Sayfa açıklaması',
115
+ description: 'Seitenbeschreibung',
116
116
  meta: {
117
117
  title: `${route} - Webspresso`,
118
- description: 'Sayfa açıklaması'
118
+ description: 'Seitenbeschreibung'
119
119
  }
120
120
  };
121
121
 
122
122
  fs.writeFileSync(
123
- path.join(localesDir, 'tr.json'),
124
- JSON.stringify(trContent, null, 2) + '\n'
123
+ path.join(localesDir, 'de.json'),
124
+ JSON.stringify(deContent, null, 2) + '\n'
125
125
  );
126
126
 
127
127
  console.log(`✅ Created locale files in ${localesDir}`);
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Skill Command — scaffold Agent Skill (SKILL.md) for Cursor / AI tools
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const inquirer = require('inquirer');
9
+
10
+ const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$|^[a-z0-9]$/;
11
+
12
+ function validateSkillName(name) {
13
+ const t = String(name).trim().toLowerCase();
14
+ if (!t) return 'Skill name is required';
15
+ if (t.length > 64) return 'Skill name must be at most 64 characters';
16
+ if (!NAME_PATTERN.test(t)) {
17
+ return 'Use lowercase letters, numbers, and hyphens only (e.g. my-skill-name)';
18
+ }
19
+ return true;
20
+ }
21
+
22
+ function skillDir(base, skillName) {
23
+ return path.join(base, '.cursor', 'skills', skillName);
24
+ }
25
+
26
+ function defaultDescription(skillName) {
27
+ const label = skillName.replace(/-/g, ' ');
28
+ return `Guides the agent through tasks for ${label}. Use when the user works on ${label} or asks about related workflows.`;
29
+ }
30
+
31
+ /** Bundled presets: CLI flag → template path under bin/ */
32
+ const PRESETS = {
33
+ webspresso: {
34
+ skillName: 'webspresso-usage',
35
+ templatePath: path.join(__dirname, '../../templates/skills/webspresso-usage/SKILL.md'),
36
+ },
37
+ };
38
+
39
+ function buildSkillMarkdown(skillName, description) {
40
+ const title = skillName
41
+ .split('-')
42
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
43
+ .join(' ');
44
+ const descYaml = JSON.stringify(description.trim());
45
+
46
+ return `---
47
+ name: ${skillName}
48
+ description: ${descYaml}
49
+ ---
50
+
51
+ # ${title}
52
+
53
+ ## Instructions
54
+
55
+ - Describe step-by-step what the agent should do.
56
+ - Add constraints, file paths, and conventions for this project.
57
+
58
+ ## When to use
59
+
60
+ - List concrete user phrases or tasks that should trigger this skill.
61
+
62
+ ## Examples
63
+
64
+ \`\`\`
65
+ Example prompt or command
66
+ \`\`\`
67
+
68
+ `;
69
+
70
+ }
71
+
72
+ function registerCommand(program) {
73
+ program
74
+ .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/')
77
+ .option('-d, --description <text>', 'Skill description (for YAML frontmatter)')
78
+ .option('-f, --force', 'Overwrite existing SKILL.md')
79
+ .option('-p, --preset <name>', 'Install bundled skill: webspresso → full Webspresso agent reference (SKILL.md)')
80
+ .action(async (nameArg, options) => {
81
+ const presetKey = options.preset ? String(options.preset).trim().toLowerCase() : '';
82
+
83
+ if (presetKey) {
84
+ const preset = PRESETS[presetKey];
85
+ if (!preset) {
86
+ console.error(`❌ Unknown preset "${presetKey}". Available: ${Object.keys(PRESETS).join(', ')}`);
87
+ process.exit(1);
88
+ }
89
+ if (!fs.existsSync(preset.templatePath)) {
90
+ console.error(`❌ Preset template missing: ${preset.templatePath}`);
91
+ process.exit(1);
92
+ }
93
+ const root = options.global ? os.homedir() : process.cwd();
94
+ const dir = skillDir(root, preset.skillName);
95
+ const skillFile = path.join(dir, 'SKILL.md');
96
+
97
+ if (!options.force && fs.existsSync(skillFile)) {
98
+ console.error(`❌ Already exists: ${skillFile}`);
99
+ console.error(' Use --force to overwrite.');
100
+ process.exit(1);
101
+ }
102
+
103
+ fs.mkdirSync(dir, { recursive: true });
104
+ fs.copyFileSync(preset.templatePath, skillFile);
105
+
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');
108
+ return;
109
+ }
110
+
111
+ let skillName = nameArg ? String(nameArg).trim().toLowerCase() : '';
112
+
113
+ if (!skillName) {
114
+ const { name } = await inquirer.prompt([
115
+ {
116
+ type: 'input',
117
+ name: 'name',
118
+ message: 'Skill name (kebab-case, e.g. review-api):',
119
+ validate: validateSkillName,
120
+ },
121
+ ]);
122
+ skillName = name.trim().toLowerCase();
123
+ } else {
124
+ const v = validateSkillName(skillName);
125
+ if (v !== true) {
126
+ console.error(`❌ ${v}`);
127
+ process.exit(1);
128
+ }
129
+ skillName = skillName.trim().toLowerCase();
130
+ }
131
+
132
+ let description = options.description;
133
+ if (!description) {
134
+ const { desc } = await inquirer.prompt([
135
+ {
136
+ type: 'input',
137
+ name: 'desc',
138
+ message: 'Short description (what/when the agent should use this skill):',
139
+ default: defaultDescription(skillName),
140
+ },
141
+ ]);
142
+ description = desc || defaultDescription(skillName);
143
+ }
144
+
145
+ const root = options.global ? os.homedir() : process.cwd();
146
+ const dir = skillDir(root, skillName);
147
+ const skillFile = path.join(dir, 'SKILL.md');
148
+
149
+ if (!options.force && fs.existsSync(skillFile)) {
150
+ console.error(`❌ Already exists: ${skillFile}`);
151
+ console.error(' Use --force to overwrite.');
152
+ process.exit(1);
153
+ }
154
+
155
+ fs.mkdirSync(dir, { recursive: true });
156
+
157
+ const body = buildSkillMarkdown(skillName, description);
158
+ fs.writeFileSync(skillFile, body, 'utf8');
159
+
160
+ console.log(`\n✅ Agent skill created:\n ${skillFile}\n`);
161
+ console.log(' Edit SKILL.md, then restart Cursor or reload the window if needed.\n');
162
+ });
163
+ }
164
+
165
+ module.exports = { registerCommand };
package/bin/utils/db.js CHANGED
@@ -26,6 +26,24 @@ function loadDbConfig(configPath) {
26
26
  process.exit(1);
27
27
  }
28
28
 
29
+ /**
30
+ * Resolve database config if present (no exit when missing; for doctor / tooling)
31
+ * @param {string} [configPath] - Custom config path
32
+ * @returns {{ config: Object, path: string } | null}
33
+ */
34
+ function resolveDbConfigIfExists(configPath) {
35
+ const defaultPaths = ['webspresso.db.js', 'knexfile.js'];
36
+ const paths = configPath ? [configPath, ...defaultPaths] : defaultPaths;
37
+
38
+ for (const p of paths) {
39
+ const fullPath = path.resolve(process.cwd(), p);
40
+ if (fs.existsSync(fullPath)) {
41
+ return { config: require(fullPath), path: fullPath };
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+
29
47
  /**
30
48
  * Create database instance from config
31
49
  * @param {Object} config - Database config
@@ -50,5 +68,6 @@ async function createDbInstance(config, env) {
50
68
 
51
69
  module.exports = {
52
70
  loadDbConfig,
71
+ resolveDbConfigIfExists,
53
72
  createDbInstance
54
73
  };
package/bin/webspresso.js CHANGED
@@ -27,6 +27,9 @@ const { registerCommand: registerSeed } = require('./commands/seed');
27
27
  const { registerCommand: registerAdminSetup } = require('./commands/admin-setup');
28
28
  const { registerCommand: registerAdminPassword } = require('./commands/admin-password');
29
29
  const { registerCommand: registerFaviconGenerate } = require('./commands/favicon-generate');
30
+ const { registerCommand: registerAuditPrune } = require('./commands/audit-prune');
31
+ const { registerCommand: registerDoctor } = require('./commands/doctor');
32
+ const { registerCommand: registerSkill } = require('./commands/skill');
30
33
 
31
34
  registerNew(program);
32
35
  registerPage(program);
@@ -42,6 +45,9 @@ registerSeed(program);
42
45
  registerAdminSetup(program);
43
46
  registerAdminPassword(program);
44
47
  registerFaviconGenerate(program);
48
+ registerAuditPrune(program);
49
+ registerDoctor(program);
50
+ registerSkill(program);
45
51
 
46
52
  // Parse arguments
47
53
  program.parse();
package/core/orm/index.js CHANGED
@@ -186,7 +186,7 @@ function createDatabase(config) {
186
186
  * Get query builder for a model
187
187
  * @param {string} modelName - Model name
188
188
  * @param {import('./types').ScopeContext} [scopeContext] - Scope context
189
- * @returns {import('knex').Knex.QueryBuilder}
189
+ * @returns {import('./query-builder').QueryBuilder}
190
190
  */
191
191
  function query(modelName, scopeContext) {
192
192
  const model = getModelInstance(modelName);