webspresso 0.0.57 → 0.0.60

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
@@ -1,5 +1,8 @@
1
1
  # Webspresso
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/webspresso.svg?style=flat-square)](https://www.npmjs.com/package/webspresso)
4
+ [![vulnerabilities](https://npmx.dev/api/registry/badge/vulnerabilities/webspresso?style=shieldsio)](https://npmx.dev/package/webspresso)
5
+
3
6
  A minimal, file-based SSR framework for Node.js with Nunjucks templating.
4
7
 
5
8
  ## Features
@@ -12,7 +15,7 @@ A minimal, file-based SSR framework for Node.js with Nunjucks templating.
12
15
  - **Lifecycle Hooks**: Global and route-level hooks for request processing
13
16
  - **Template Helpers**: Laravel-inspired helper functions available in templates
14
17
  - **Plugin System**: Extensible architecture with version control and inter-plugin communication
15
- - **Built-in Plugins**: Development dashboard, sitemap generator, SEO checker, analytics integration (Google, Yandex, Bing), self-hosted site analytics
18
+ - **Built-in Plugins**: Development dashboard, sitemap generator, SEO checker, analytics integration (Google, Yandex, Bing), self-hosted site analytics, optional Swagger UI for HTTP APIs, configurable HTTP health probe endpoint
16
19
 
17
20
  ## Installation
18
21
 
@@ -143,7 +146,7 @@ The project includes:
143
146
  - Tailwind CSS with build process
144
147
  - Optimized layout template with navigation and footer
145
148
  - Responsive starter page
146
- - i18n setup (en/tr)
149
+ - i18n setup (e.g. en/de)
147
150
  - Development and production scripts
148
151
 
149
152
  ### `webspresso page`
@@ -191,6 +194,32 @@ webspresso start
191
194
  webspresso start --port 3000
192
195
  ```
193
196
 
197
+ ### `webspresso doctor`
198
+
199
+ 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.
200
+
201
+ ```bash
202
+ webspresso doctor
203
+ webspresso doctor --db
204
+ webspresso doctor --strict
205
+ ```
206
+
207
+ ### `webspresso skill`
208
+
209
+ 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.
210
+
211
+ **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.
212
+
213
+ ```bash
214
+ webspresso skill my-workflow
215
+ webspresso skill review-pr --description "Runs PR review checklist. Use when reviewing pull requests."
216
+ webspresso skill deploy-check -g
217
+
218
+ # Install the bundled Webspresso agent reference (same content shipped in templates/skills/)
219
+ webspresso skill --preset webspresso
220
+ webspresso skill -p webspresso --global
221
+ ```
222
+
194
223
  ### `webspresso add tailwind`
195
224
 
196
225
  Add Tailwind CSS to your project with build process.
@@ -277,7 +306,7 @@ my-app/
277
306
  ├── pages/
278
307
  │ ├── locales/ # Global i18n translations
279
308
  │ │ ├── en.json
280
- │ │ └── tr.json
309
+ │ │ └── de.json
281
310
  │ ├── _hooks.js # Global lifecycle hooks
282
311
  │ ├── index.njk # Home page (GET /)
283
312
  │ ├── about/
@@ -482,7 +511,7 @@ const { app } = createApp({
482
511
  hostname: 'https://example.com',
483
512
  exclude: ['/admin/*', '/api/*'],
484
513
  i18n: true,
485
- locales: ['en', 'tr']
514
+ locales: ['en', 'de']
486
515
  }),
487
516
  analyticsPlugin({
488
517
  google: {
@@ -748,6 +777,55 @@ const { app } = createApp({
748
777
 
749
778
  Programmatic API (other plugins): `ctx.usePlugin('audit-log')` exposes `queryLogs`, `purgeAuditLogs`, and `getMigrationTemplate()`.
750
779
 
780
+ **reCAPTCHA plugin:**
781
+ - Google reCAPTCHA **v2** (checkbox) or **v3** (score): server verification via `https://www.google.com/recaptcha/api/siteverify` (no extra npm dependency; Node 18+ `fetch`)
782
+ - 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`)
783
+ - Secret key: `options.secretKey` or env `RECAPTCHA_SECRET_KEY` (never expose to templates)
784
+
785
+ ```javascript
786
+ const {
787
+ recaptchaPlugin,
788
+ createRecaptchaMiddleware,
789
+ resolveRecaptchaMiddlewareParams,
790
+ } = require('webspresso/plugins/recaptcha');
791
+
792
+ const recaptchaConfig = {
793
+ siteKey: process.env.RECAPTCHA_SITE_KEY,
794
+ secretKey: process.env.RECAPTCHA_SECRET_KEY,
795
+ version: 'v2', // or 'v3'
796
+ minScore: 0.5,
797
+ expectedAction: 'contact', // v3 verification only
798
+ defaultV3Action: 'submit', // for recaptchaV3Token helper
799
+ };
800
+
801
+ const { app } = createApp({
802
+ pagesDir: './pages',
803
+ plugins: [recaptchaPlugin(recaptchaConfig)],
804
+ middlewares: {
805
+ recaptcha: createRecaptchaMiddleware({
806
+ ...resolveRecaptchaMiddlewareParams(recaptchaConfig),
807
+ bodyField: 'g-recaptcha-response',
808
+ }),
809
+ },
810
+ });
811
+ ```
812
+
813
+ **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.
814
+
815
+ ```javascript
816
+ // pages/api/contact.post.js
817
+ module.exports = async function post(req, res) {
818
+ // recaptcha middleware already returned 400 if token invalid
819
+ return res.json({ ok: true, hostname: req.recaptcha.hostname });
820
+ };
821
+
822
+ module.exports.middleware = ['recaptcha'];
823
+ ```
824
+
825
+ Optional: after `createApp`, use `pluginManager.getPluginAPI('recaptcha').createMiddleware({ bodyField: '...' })` to override plugin defaults and attach to the `app.use` chain (after body parsers).
826
+
827
+ Low-level usage (without middleware): import `verifyRecaptcha` / `getRemoteIp` from `webspresso/plugins/recaptcha/verify`.
828
+
751
829
  **SEO Checker Plugin:**
752
830
  - Client-side SEO analysis tool (inspired by django-check-seo)
753
831
  - Integrated with dev toolbar
@@ -1349,6 +1427,10 @@ await UserRepo.query().onlyTrashed().list(); // Only deleted
1349
1427
  await UserRepo.query().forTenant(tenantId).list();
1350
1428
  ```
1351
1429
 
1430
+ `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.
1431
+
1432
+ `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.
1433
+
1352
1434
  ### Transactions
1353
1435
 
1354
1436
  ```javascript
@@ -1697,6 +1779,64 @@ const user = plugin.api.getModel('User'); // Single model
1697
1779
  const names = plugin.api.getModelNames(); // Model names
1698
1780
  ```
1699
1781
 
1782
+ ### Health check plugin
1783
+
1784
+ 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.
1785
+
1786
+ **Setup:**
1787
+
1788
+ ```javascript
1789
+ const { createApp, healthCheckPlugin } = require('webspresso');
1790
+
1791
+ const app = createApp({
1792
+ plugins: [
1793
+ healthCheckPlugin({
1794
+ path: '/health', // default
1795
+ verbose: true, // timestamp, uptime, NODE_ENV, framework name/version
1796
+ authorize: (req) => true, // optional — restrict who can read the endpoint
1797
+ checks: async ({ db }) => {
1798
+ if (db) await db.knex.raw('select 1');
1799
+ return { database: 'ok' };
1800
+ },
1801
+ }),
1802
+ ],
1803
+ });
1804
+ ```
1805
+
1806
+ - **`checks`**: If this function throws, the handler responds with **503** and `{ status: 'unhealthy', error, ... }`. Return a plain object to merge into `checks` on success (e.g. dependency status).
1807
+ - Use a **custom `path`** if your app already serves `GET /health` from `pages/`.
1808
+
1809
+ ### Swagger / OpenAPI plugin
1810
+
1811
+ Serves **OpenAPI 3** for file-based `pages/api` routes and optional [Zod](https://zod.dev) `schema` exports, plus a **Swagger UI** page. Defaults to **development only** (same idea as the schema explorer).
1812
+
1813
+ **Setup:**
1814
+
1815
+ ```javascript
1816
+ const { createApp, swaggerPlugin } = require('webspresso');
1817
+
1818
+ const app = createApp({
1819
+ plugins: [
1820
+ swaggerPlugin({
1821
+ path: '/_swagger', // UI: GET /_swagger, spec: GET /_swagger/openapi.json
1822
+ enabled: true, // default: true in development, false in production
1823
+ title: 'My API', // optional OpenAPI info.title
1824
+ serverUrl: 'https://api.example.com', // optional servers[0].url (else BASE_URL or localhost)
1825
+ includeOrmSchemas: false, // merge ORM model schemas into components.schemas
1826
+ ormExclude: ['Secret'], // when includeOrmSchemas is true
1827
+ authorize: (req) => true, // optional gate for both UI and JSON
1828
+ }),
1829
+ ],
1830
+ });
1831
+ ```
1832
+
1833
+ **Endpoints:**
1834
+
1835
+ - `GET /_swagger/openapi.json` — Full OpenAPI document (`paths` from API routes; request/response shapes from exported `schema({ z })` when present).
1836
+ - `GET /_swagger` — Swagger UI (loads the JSON above; requires network access for CDN assets).
1837
+
1838
+ In production, keep the plugin disabled or protect it with `authorize` / your own middleware.
1839
+
1700
1840
  ## Development
1701
1841
 
1702
1842
  ```bash
@@ -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
@@ -28,6 +28,8 @@ 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
30
  const { registerCommand: registerAuditPrune } = require('./commands/audit-prune');
31
+ const { registerCommand: registerDoctor } = require('./commands/doctor');
32
+ const { registerCommand: registerSkill } = require('./commands/skill');
31
33
 
32
34
  registerNew(program);
33
35
  registerPage(program);
@@ -44,6 +46,8 @@ registerAdminSetup(program);
44
46
  registerAdminPassword(program);
45
47
  registerFaviconGenerate(program);
46
48
  registerAuditPrune(program);
49
+ registerDoctor(program);
50
+ registerSkill(program);
47
51
 
48
52
  // Parse arguments
49
53
  program.parse();