webspresso 0.0.95 → 0.0.96
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/.agents/ADMIN.md +225 -0
- package/.agents/AGENTS.md +72 -0
- package/.agents/ARCHITECTURE.md +119 -0
- package/.agents/AUTH.md +103 -0
- package/.agents/CLI.md +115 -0
- package/.agents/COMPRESSION.md +59 -0
- package/.agents/CONVENTIONS.md +46 -0
- package/.agents/EMAIL.md +141 -0
- package/.agents/ERRORS.md +201 -0
- package/.agents/MCP.md +103 -0
- package/.agents/ORM.md +230 -0
- package/.agents/PLUGINS.md +419 -0
- package/.agents/QUEUE.md +113 -0
- package/.agents/REALTIME.md +213 -0
- package/.agents/ROUTING.md +292 -0
- package/.agents/SERVICES.md +341 -0
- package/.agents/SHUTDOWN.md +67 -0
- package/.agents/STREAMING.md +73 -0
- package/.agents/WORKFLOW.md +27 -0
- package/.agents/internal/DOCUMENTATION_REVIEW.md +485 -0
- package/.agents/internal/TECHNICAL_REVIEW.md +428 -0
- package/.cursorrules +14 -0
- package/AGENTS.md +175 -0
- package/CLAUDE.md +54 -0
- package/README.md +18 -14
- package/bin/commands/agents-init.js +169 -0
- package/bin/commands/api.js +114 -48
- package/bin/commands/doctor.js +35 -0
- package/bin/commands/new.js +13 -2
- package/bin/commands/page.js +75 -89
- package/bin/commands/service.js +77 -0
- package/bin/webspresso.js +2 -2
- package/core/compileSchema.js +1 -1
- package/core/errors/middleware.js +22 -2
- package/core/errors/normalize.js +29 -2
- package/{utils/schemaCache.js → core/schema-cache.js} +2 -3
- package/index.d.ts +306 -0
- package/index.js +35 -1
- package/package.json +5 -3
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +22 -1
- package/plugins/csrf/index.js +3 -2
- package/plugins/csv/engine.js +447 -0
- package/plugins/csv/index.js +94 -0
- package/plugins/csv/middleware.js +80 -0
- package/plugins/csv/services.js +138 -0
- package/plugins/file-manager/admin-component.js +613 -0
- package/plugins/file-manager/api-handlers.js +261 -0
- package/plugins/file-manager/core.js +601 -0
- package/plugins/file-manager/index.js +139 -0
- package/plugins/file-manager/security.js +207 -0
- package/plugins/index.js +8 -0
- package/plugins/xlsx/engine.js +397 -0
- package/plugins/xlsx/index.js +91 -0
- package/plugins/xlsx/middleware.js +87 -0
- package/plugins/xlsx/services.js +118 -0
- package/src/api/api-loader.js +233 -0
- package/src/api/define-api.js +35 -0
- package/src/discovery/file-route-parser.js +201 -0
- package/src/discovery/scan-routes.js +265 -0
- package/src/file-router.js +6 -0
- package/src/modules/define-module.js +99 -0
- package/src/modules/module-discovery.js +117 -0
- package/src/pages/define-page.js +36 -0
- package/src/pages/page-loader.js +214 -0
- package/src/routing/mount-discovered-routes.js +78 -0
- package/src/routing/route-table.js +191 -0
- package/src/server.js +112 -15
- package/src/services/builtins/file-manager.js +178 -0
- package/src/services/index.js +2 -0
- package/bin/commands/skill.js +0 -187
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +0 -289
- package/templates/skills/webspresso-usage/REFERENCE-kernel.md +0 -93
- package/templates/skills/webspresso-usage/SKILL.md +0 -48
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Webspresso — Claude Code Assistant Guide
|
|
2
|
+
|
|
3
|
+
Webspresso is a lightweight, zero-dependency-sprawl full-stack Node.js SSR & API framework.
|
|
4
|
+
|
|
5
|
+
> [!IMPORTANT]
|
|
6
|
+
> **Authoritative Rules & Architecture**: Always follow [AGENTS.md](AGENTS.md) and the modular topic guides in [.agents/](.agents/AGENTS.md).
|
|
7
|
+
> **API & Types Ground Truth**: Inspect [index.d.ts](index.d.ts) for exact method signatures.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## ⚡ Essential Commands
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Run test suite (Node >= 20 required)
|
|
15
|
+
npm test
|
|
16
|
+
npx vitest run tests/unit/
|
|
17
|
+
npx vitest run tests/integration/
|
|
18
|
+
npx vitest run tests/security/
|
|
19
|
+
|
|
20
|
+
# Development & diagnostics
|
|
21
|
+
npm run dev
|
|
22
|
+
npx webspresso doctor
|
|
23
|
+
npx webspresso db:migrate
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 🛡️ Critical Architecture Rules
|
|
29
|
+
|
|
30
|
+
1. **100% File-Based Routing (NO `routes/` or `app.get()`)**:
|
|
31
|
+
- All endpoints live in `pages/api/...` with HTTP method extensions:
|
|
32
|
+
- `pages/api/notes.get.js` → `GET /api/notes`
|
|
33
|
+
- `pages/api/notes.post.js` → `POST /api/notes`
|
|
34
|
+
- `pages/api/notes/[id].get.js` → `GET /api/notes/:id`
|
|
35
|
+
- `pages/api/notes/[id].delete.js` → `DELETE /api/notes/:id`
|
|
36
|
+
- Contract: `module.exports = { schema: ({ z }) => ({ body, query, params }), middleware: ['auth'], handler: async (req, res) => ... }`
|
|
37
|
+
|
|
38
|
+
2. **Strict Repository Pattern (NO Raw `db('table')`)**:
|
|
39
|
+
- Never run raw Knex queries (`req.db('table')`).
|
|
40
|
+
- Always use `req.db.getRepository('ModelName')` to ensure schema validation, hooks, scopes, and query caching.
|
|
41
|
+
|
|
42
|
+
3. **Services Layer (NO `controllers/`)**:
|
|
43
|
+
- Place business logic in `services/domain/action.js` and invoke via `ctx.service('domain.action', input)`.
|
|
44
|
+
|
|
45
|
+
4. **Zero-Dependency Native Dual Auth**:
|
|
46
|
+
- Never install `jsonwebtoken`, `passport`, or `express-session`.
|
|
47
|
+
- Use built-in `req.auth` and `webspresso/core/auth`.
|
|
48
|
+
|
|
49
|
+
5. **Semantic Exceptions & Page Loaders**:
|
|
50
|
+
- Throw `NotFoundError`, `ValidationError`, `UnauthorizedError`, `ForbiddenError`.
|
|
51
|
+
- For SSR pages (`pages/name.njk`), use companion loaders `pages/name.js` exporting `async function load({ req, res, db, ctx })`. Never call `res.render()` manually.
|
|
52
|
+
|
|
53
|
+
6. **Zero Regression Policy**:
|
|
54
|
+
- Always run the relevant Vitest tests before declaring any task complete.
|
package/README.md
CHANGED
|
@@ -28,7 +28,9 @@ Open **`http://localhost:3000`** in your browser.
|
|
|
28
28
|
## ✨ Why Webspresso?
|
|
29
29
|
|
|
30
30
|
- **Zero-Dependency Sprawl**: Core framework features (Dual Auth, JWT signing, Error handling, Streaming Compression, Graceful Shutdown, Template Helpers) rely strictly on native Node.js standard modules (`crypto`, `path`, `fs`, `events`, `zlib`, `async_hooks`) without bloated dependency trees.
|
|
31
|
-
- **File-Based Routing**:
|
|
31
|
+
- **File-Based Routing & API Suffixes**: Pages and APIs automatically map to clean URLs with dynamic params (`[id]`), wildcards (`[...slug]`), and method suffixes (`.get.js`, `.post.js`, `.delete.js`, `.patch.js`, `.put.js`).
|
|
32
|
+
- **Feature Modules (`defineModule`)**: Domain-driven modular organization for large projects (`modules/{name}/pages`, `api`, `services`, `middleware`).
|
|
33
|
+
- **Modern Page & API Helpers (`definePage`, `defineApi`)**: Declarative Zod request schemas, unified execution context, and automatic JSON response serialization.
|
|
32
34
|
- **Server Data Prefetching (`load()`)**: Page routes export `async function load({ req, res, db, ctx })` to fetch data server-side before template compilation.
|
|
33
35
|
- **Services Layer**: File-based auto-discovery in `services/` with declarative Zod validation, role/predicate authorization guards, and memoization.
|
|
34
36
|
- **Ambient Transactions**: Knex database transactions automatically bind to `AsyncLocalStorage` (`core/orm/transaction.js`). Scoped repositories seamlessly participate in active transactions without manual `trx` passing.
|
|
@@ -78,33 +80,35 @@ app.listen(PORT, () => {
|
|
|
78
80
|
|
|
79
81
|
```javascript
|
|
80
82
|
// pages/index.js
|
|
81
|
-
|
|
83
|
+
const { definePage } = require('webspresso');
|
|
84
|
+
|
|
85
|
+
module.exports = definePage({
|
|
82
86
|
async load({ req, ctx }) {
|
|
83
87
|
return {
|
|
84
88
|
title: 'Home Page',
|
|
85
89
|
user: req.user || null,
|
|
86
90
|
};
|
|
87
91
|
},
|
|
88
|
-
};
|
|
92
|
+
});
|
|
89
93
|
```
|
|
90
94
|
|
|
91
95
|
### 3. Validated JSON API Endpoint (`pages/api/greet.post.js`)
|
|
92
96
|
```javascript
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
97
|
+
const { defineApi, z } = require('webspresso');
|
|
98
|
+
|
|
99
|
+
module.exports = defineApi({
|
|
100
|
+
schema: {
|
|
101
|
+
body: z.object({
|
|
102
|
+
name: z.string().min(2),
|
|
103
|
+
}),
|
|
100
104
|
},
|
|
101
105
|
|
|
102
|
-
async handler(req, res) {
|
|
106
|
+
async handler(req, res, ctx) {
|
|
103
107
|
// req.input is typed and validated by Zod
|
|
104
|
-
const result = await
|
|
105
|
-
|
|
108
|
+
const result = await ctx.service('user.greet', req.input.body);
|
|
109
|
+
return result; // Automatically serialized as JSON (HTTP 200)
|
|
106
110
|
},
|
|
107
|
-
};
|
|
111
|
+
});
|
|
108
112
|
```
|
|
109
113
|
|
|
110
114
|
---
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agents Init Command
|
|
5
|
+
* Scaffold and synchronize AI Agent guidelines (.agents/, AGENTS.md, CLAUDE.md, .cursorrules)
|
|
6
|
+
* @module bin/commands/agents-init
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const SOURCE_ROOT = path.resolve(__dirname, '../../');
|
|
13
|
+
const SOURCE_AGENTS_DIR = path.join(SOURCE_ROOT, '.agents');
|
|
14
|
+
const SOURCE_AGENTS_MD = path.join(SOURCE_ROOT, 'AGENTS.md');
|
|
15
|
+
const SOURCE_CLAUDE_MD = path.join(SOURCE_ROOT, 'CLAUDE.md');
|
|
16
|
+
const SOURCE_CURSORRULES = path.join(SOURCE_ROOT, '.cursorrules');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Copies a file if force is true or if target file does not exist.
|
|
20
|
+
* @param {string} src
|
|
21
|
+
* @param {string} dest
|
|
22
|
+
* @param {boolean} force
|
|
23
|
+
* @returns {'created'|'overwritten'|'skipped'}
|
|
24
|
+
*/
|
|
25
|
+
function copyFileSafe(src, dest, force = false) {
|
|
26
|
+
if (!fs.existsSync(src)) return 'skipped';
|
|
27
|
+
const destExists = fs.existsSync(dest);
|
|
28
|
+
|
|
29
|
+
if (destExists && !force) {
|
|
30
|
+
return 'skipped';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const destDir = path.dirname(dest);
|
|
34
|
+
if (!fs.existsSync(destDir)) {
|
|
35
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fs.copyFileSync(src, dest);
|
|
39
|
+
return destExists ? 'overwritten' : 'created';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Scaffolds AI Agent rules and guidebook files into target project directory.
|
|
44
|
+
* @param {string} targetDir
|
|
45
|
+
* @param {Object} [options]
|
|
46
|
+
* @param {boolean} [options.force=false]
|
|
47
|
+
* @param {boolean} [options.claude=true]
|
|
48
|
+
* @param {boolean} [options.cursor=true]
|
|
49
|
+
* @param {boolean} [options.guides=true]
|
|
50
|
+
* @returns {{ installed: string[], skipped: string[], errors: string[] }}
|
|
51
|
+
*/
|
|
52
|
+
function scaffoldAgentsFiles(targetDir, options = {}) {
|
|
53
|
+
const force = options.force === true;
|
|
54
|
+
const includeClaude = options.claude !== false;
|
|
55
|
+
const includeCursor = options.cursor !== false;
|
|
56
|
+
const includeGuides = options.guides !== false;
|
|
57
|
+
|
|
58
|
+
const result = {
|
|
59
|
+
installed: [],
|
|
60
|
+
skipped: [],
|
|
61
|
+
errors: [],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (!fs.existsSync(targetDir)) {
|
|
65
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 1. Root AGENTS.md
|
|
69
|
+
if (fs.existsSync(SOURCE_AGENTS_MD)) {
|
|
70
|
+
const destAgentsMd = path.join(targetDir, 'AGENTS.md');
|
|
71
|
+
const status = copyFileSafe(SOURCE_AGENTS_MD, destAgentsMd, force);
|
|
72
|
+
if (status === 'created' || status === 'overwritten') {
|
|
73
|
+
result.installed.push('AGENTS.md');
|
|
74
|
+
} else {
|
|
75
|
+
result.skipped.push('AGENTS.md');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 2. CLAUDE.md
|
|
80
|
+
if (includeClaude && fs.existsSync(SOURCE_CLAUDE_MD)) {
|
|
81
|
+
const destClaudeMd = path.join(targetDir, 'CLAUDE.md');
|
|
82
|
+
const status = copyFileSafe(SOURCE_CLAUDE_MD, destClaudeMd, force);
|
|
83
|
+
if (status === 'created' || status === 'overwritten') {
|
|
84
|
+
result.installed.push('CLAUDE.md');
|
|
85
|
+
} else {
|
|
86
|
+
result.skipped.push('CLAUDE.md');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 3. .cursorrules
|
|
91
|
+
if (includeCursor && fs.existsSync(SOURCE_CURSORRULES)) {
|
|
92
|
+
const destCursor = path.join(targetDir, '.cursorrules');
|
|
93
|
+
const status = copyFileSafe(SOURCE_CURSORRULES, destCursor, force);
|
|
94
|
+
if (status === 'created' || status === 'overwritten') {
|
|
95
|
+
result.installed.push('.cursorrules');
|
|
96
|
+
} else {
|
|
97
|
+
result.skipped.push('.cursorrules');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 4. .agents/*.md Guidebook directory
|
|
102
|
+
if (includeGuides && fs.existsSync(SOURCE_AGENTS_DIR)) {
|
|
103
|
+
const destAgentsDir = path.join(targetDir, '.agents');
|
|
104
|
+
if (!fs.existsSync(destAgentsDir)) {
|
|
105
|
+
fs.mkdirSync(destAgentsDir, { recursive: true });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const files = fs.readdirSync(SOURCE_AGENTS_DIR).filter((f) => f.endsWith('.md'));
|
|
109
|
+
for (const file of files) {
|
|
110
|
+
const srcFile = path.join(SOURCE_AGENTS_DIR, file);
|
|
111
|
+
const destFile = path.join(destAgentsDir, file);
|
|
112
|
+
const relPath = path.join('.agents', file);
|
|
113
|
+
const status = copyFileSafe(srcFile, destFile, force);
|
|
114
|
+
if (status === 'created' || status === 'overwritten') {
|
|
115
|
+
result.installed.push(relPath);
|
|
116
|
+
} else {
|
|
117
|
+
result.skipped.push(relPath);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function registerCommand(program) {
|
|
126
|
+
program
|
|
127
|
+
.command('agents:init')
|
|
128
|
+
.alias('agents')
|
|
129
|
+
.alias('agents:setup')
|
|
130
|
+
.alias('ai:init')
|
|
131
|
+
.description('Scaffold AI Agent guidelines (.agents/, AGENTS.md, CLAUDE.md, .cursorrules)')
|
|
132
|
+
.option('-p, --path <path>', 'Target project directory', process.cwd())
|
|
133
|
+
.option('-f, --force', 'Overwrite existing agent guideline files', false)
|
|
134
|
+
.option('--no-claude', 'Skip generating CLAUDE.md')
|
|
135
|
+
.option('--no-cursor', 'Skip generating .cursorrules')
|
|
136
|
+
.option('--no-guides', 'Skip generating .agents/ topic guide bundle')
|
|
137
|
+
.action(async (options) => {
|
|
138
|
+
const targetDir = path.resolve(options.path || process.cwd());
|
|
139
|
+
console.log(`\n🤖 Initializing AI Agent Guidelines in: ${targetDir}\n`);
|
|
140
|
+
|
|
141
|
+
const result = scaffoldAgentsFiles(targetDir, {
|
|
142
|
+
force: options.force,
|
|
143
|
+
claude: options.claude,
|
|
144
|
+
cursor: options.cursor,
|
|
145
|
+
guides: options.guides,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (result.installed.length > 0) {
|
|
149
|
+
console.log('✅ Installed/Updated:');
|
|
150
|
+
for (const file of result.installed) {
|
|
151
|
+
console.log(` + ${file}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (result.skipped.length > 0) {
|
|
156
|
+
console.log('\n⚠️ Skipped (already exists, use --force to overwrite):');
|
|
157
|
+
for (const file of result.skipped) {
|
|
158
|
+
console.log(` - ${file}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log('\n🎉 AI Agent Guidelines ready! Agents (Cursor, Claude Code, Antigravity) will follow strict Webspresso standards.\n');
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
registerCommand,
|
|
168
|
+
scaffoldAgentsFiles,
|
|
169
|
+
};
|
package/bin/commands/api.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* API Command
|
|
3
|
-
* Add a new API endpoint
|
|
5
|
+
* Add a new API endpoint with modern Webspresso schema & handler contract
|
|
6
|
+
* @module bin/commands/api
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
9
|
let _inquirer;
|
|
@@ -13,64 +16,127 @@ const inquirer = {
|
|
|
13
16
|
const fs = require('fs');
|
|
14
17
|
const path = require('path');
|
|
15
18
|
|
|
19
|
+
const VALID_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
20
|
+
|
|
21
|
+
function parseRouteAndMethod(rawInput, optionMethod) {
|
|
22
|
+
let route = String(rawInput || '').trim();
|
|
23
|
+
let method = optionMethod ? String(optionMethod).toUpperCase() : null;
|
|
24
|
+
|
|
25
|
+
// Normalize /api/ prefix if present
|
|
26
|
+
route = route.replace(/^\/?api\/?/, '');
|
|
27
|
+
|
|
28
|
+
// Check if route has method suffix (e.g. notes.post or notes.post.js or [id].get)
|
|
29
|
+
for (const m of VALID_METHODS) {
|
|
30
|
+
const lower = m.toLowerCase();
|
|
31
|
+
if (route.endsWith(`.${lower}.js`)) {
|
|
32
|
+
method = m;
|
|
33
|
+
route = route.slice(0, -(lower.length + 4));
|
|
34
|
+
break;
|
|
35
|
+
} else if (route.endsWith(`.${lower}`)) {
|
|
36
|
+
method = m;
|
|
37
|
+
route = route.slice(0, -(lower.length + 1));
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
route: route.replace(/^\/+|\/+$/g, ''),
|
|
44
|
+
method: method || 'GET',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
16
48
|
function registerCommand(program) {
|
|
17
49
|
program
|
|
18
|
-
.command('api')
|
|
19
|
-
.description('Add a new API endpoint to
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
-
|
|
50
|
+
.command('api [route]')
|
|
51
|
+
.description('Add a new API endpoint to pages/api/ with schema validation and handler')
|
|
52
|
+
.option('-m, --method <method>', 'HTTP method (GET, POST, PUT, PATCH, DELETE)', 'GET')
|
|
53
|
+
.option('--auth', 'Protect with session auth middleware')
|
|
54
|
+
.option('--jwt', 'Protect with JWT bearer middleware')
|
|
55
|
+
.action(async (routeArg, options) => {
|
|
56
|
+
const pagesDir = path.resolve(process.cwd(), 'pages');
|
|
57
|
+
if (!fs.existsSync(pagesDir)) {
|
|
58
|
+
console.error('❌ Not a Webspresso project! (pages/ directory not found)');
|
|
23
59
|
process.exit(1);
|
|
24
60
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
61
|
+
|
|
62
|
+
let route;
|
|
63
|
+
let method;
|
|
64
|
+
|
|
65
|
+
if (routeArg) {
|
|
66
|
+
const parsed = parseRouteAndMethod(routeArg, options.method);
|
|
67
|
+
route = parsed.route;
|
|
68
|
+
method = parsed.method;
|
|
69
|
+
} else {
|
|
70
|
+
const answers = await inquirer.prompt([
|
|
71
|
+
{
|
|
72
|
+
type: 'input',
|
|
73
|
+
name: 'route',
|
|
74
|
+
message: 'API route path (e.g., users or users/[id] or /api/notes):',
|
|
75
|
+
validate: (input) => {
|
|
76
|
+
if (!input || !input.trim()) return 'API route is required';
|
|
77
|
+
return true;
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: 'list',
|
|
82
|
+
name: 'method',
|
|
83
|
+
message: 'HTTP method:',
|
|
84
|
+
choices: VALID_METHODS,
|
|
85
|
+
default: options.method ? options.method.toUpperCase() : 'GET',
|
|
86
|
+
},
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
const parsed = parseRouteAndMethod(answers.route, answers.method);
|
|
90
|
+
route = parsed.route;
|
|
91
|
+
method = parsed.method;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const methodUpper = String(method).toUpperCase();
|
|
95
|
+
const methodLower = methodUpper.toLowerCase();
|
|
96
|
+
const routePath = path.join(pagesDir, 'api', route);
|
|
49
97
|
const dirPath = path.dirname(routePath);
|
|
50
98
|
const fileName = path.basename(routePath);
|
|
51
|
-
|
|
52
|
-
// Create directory
|
|
99
|
+
|
|
53
100
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
101
|
+
|
|
102
|
+
const apiFile = path.join(dirPath, `${fileName}.${methodLower}.js`);
|
|
103
|
+
|
|
104
|
+
const middlewares = [];
|
|
105
|
+
if (options.auth) middlewares.push("'auth'");
|
|
106
|
+
if (options.jwt) middlewares.push("'jwt'");
|
|
107
|
+
const middlewareStr = middlewares.length > 0 ? `\n middleware: [${middlewares.join(', ')}],` : '';
|
|
108
|
+
|
|
109
|
+
const hasParams = route.includes('[');
|
|
110
|
+
const hasBody = ['POST', 'PUT', 'PATCH'].includes(methodUpper);
|
|
111
|
+
|
|
112
|
+
const apiContent = `'use strict';
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* ${methodUpper} /api/${route}
|
|
60
116
|
*/
|
|
117
|
+
module.exports = {
|
|
118
|
+
// Input validation schema (Zod)
|
|
119
|
+
schema: ({ z }) => ({
|
|
120
|
+
${hasParams ? 'params: z.object({ id: z.string().min(1) }),\n ' : ''}${hasBody ? 'body: z.object({\n // Define body schema here\n }),\n ' : ''}query: z.object({\n // Define query params here\n }),
|
|
121
|
+
}),${middlewareStr}
|
|
61
122
|
|
|
62
|
-
|
|
63
|
-
res
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
123
|
+
// Request Handler
|
|
124
|
+
handler: async (req, res) => {
|
|
125
|
+
// req.db.getRepository('ModelName') -> Access ORM models
|
|
126
|
+
// req.service('service.name', payload) -> Invoke service
|
|
127
|
+
// req.input -> Type-safe validated body, query, params
|
|
128
|
+
return res.json({
|
|
129
|
+
success: true,
|
|
130
|
+
message: 'Hello from /api/${route}',
|
|
131
|
+
timestamp: new Date().toISOString(),
|
|
132
|
+
});
|
|
133
|
+
},
|
|
68
134
|
};
|
|
69
135
|
`;
|
|
70
|
-
|
|
71
|
-
fs.writeFileSync(apiFile, apiContent);
|
|
72
|
-
console.log(`\n✅ Created ${apiFile}\n`);
|
|
136
|
+
|
|
137
|
+
fs.writeFileSync(apiFile, apiContent, 'utf8');
|
|
138
|
+
console.log(`\n✅ Created API endpoint:\n ${path.relative(process.cwd(), apiFile)}\n`);
|
|
73
139
|
});
|
|
74
140
|
}
|
|
75
141
|
|
|
76
|
-
module.exports = { registerCommand };
|
|
142
|
+
module.exports = { registerCommand, parseRouteAndMethod };
|
package/bin/commands/doctor.js
CHANGED
|
@@ -99,6 +99,23 @@ function registerCommand(program) {
|
|
|
99
99
|
warnings += 1;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// Check anti-pattern directories
|
|
103
|
+
const forbiddenRoutes = [path.join(cwd, 'routes'), path.join(cwd, 'src', 'routes')];
|
|
104
|
+
for (const p of forbiddenRoutes) {
|
|
105
|
+
if (fs.existsSync(p) && fs.statSync(p).isDirectory()) {
|
|
106
|
+
line('⚠', `Anti-pattern detected: "${path.relative(cwd, p)}" folder found. Webspresso uses 100% file-based routing; move endpoints to "pages/api/".`);
|
|
107
|
+
warnings += 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const forbiddenControllers = [path.join(cwd, 'controllers'), path.join(cwd, 'src', 'controllers')];
|
|
112
|
+
for (const p of forbiddenControllers) {
|
|
113
|
+
if (fs.existsSync(p) && fs.statSync(p).isDirectory()) {
|
|
114
|
+
line('⚠', `Anti-pattern detected: "${path.relative(cwd, p)}" folder found. Encapsulate business logic in "services/<domain>/<action>.js" instead of controllers.`);
|
|
115
|
+
warnings += 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
102
119
|
const pagesPath = path.join(cwd, 'pages');
|
|
103
120
|
if (fs.existsSync(pagesPath) && fs.statSync(pagesPath).isDirectory()) {
|
|
104
121
|
line('✓', 'pages/ directory present');
|
|
@@ -119,6 +136,8 @@ function registerCommand(program) {
|
|
|
119
136
|
return name;
|
|
120
137
|
}
|
|
121
138
|
|
|
139
|
+
const VALID_API_METHODS = ['.get.js', '.post.js', '.put.js', '.patch.js', '.delete.js'];
|
|
140
|
+
|
|
122
141
|
function scanRoutes(dirPath, relativeDir = '', isApi = false) {
|
|
123
142
|
const children = fs.readdirSync(dirPath);
|
|
124
143
|
const kept = [];
|
|
@@ -148,6 +167,12 @@ function registerCommand(program) {
|
|
|
148
167
|
const ext = path.extname(child);
|
|
149
168
|
if (isApi) {
|
|
150
169
|
if (ext === '.js') {
|
|
170
|
+
const hasMethod = VALID_API_METHODS.some((m) => child.endsWith(m));
|
|
171
|
+
if (!hasMethod && !child.startsWith('_')) {
|
|
172
|
+
line('⚠', `API route "pages/${relPath}" is missing an HTTP method suffix (e.g. .get.js, .post.js).`);
|
|
173
|
+
warnings += 1;
|
|
174
|
+
routingChecksPassed = false;
|
|
175
|
+
}
|
|
151
176
|
const base = path.basename(child, ext);
|
|
152
177
|
kept.push({ name: base, isDir: false, original: child });
|
|
153
178
|
}
|
|
@@ -190,6 +215,16 @@ function registerCommand(program) {
|
|
|
190
215
|
warnings += 1;
|
|
191
216
|
}
|
|
192
217
|
|
|
218
|
+
console.log('\nAI Agent Guidelines');
|
|
219
|
+
console.log('-------------------');
|
|
220
|
+
const agentsMdPath = path.join(cwd, 'AGENTS.md');
|
|
221
|
+
const agentsDirPath = path.join(cwd, '.agents');
|
|
222
|
+
if (fs.existsSync(agentsMdPath) && fs.existsSync(agentsDirPath)) {
|
|
223
|
+
line('✓', 'AGENTS.md and .agents/ topic guides present');
|
|
224
|
+
} else {
|
|
225
|
+
line('○', 'No AGENTS.md / .agents/ found. Tip: run "npx webspresso agents:init" to scaffold AI agent guidelines.');
|
|
226
|
+
}
|
|
227
|
+
|
|
193
228
|
console.log('\nEnvironment files');
|
|
194
229
|
console.log('-----------------');
|
|
195
230
|
|
package/bin/commands/new.js
CHANGED
|
@@ -14,6 +14,7 @@ const fs = require('fs');
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const { runInstallation, startDevServer } = require('../utils/project');
|
|
16
16
|
const { getSeedFileTemplate } = require('../utils/seed');
|
|
17
|
+
const { scaffoldAgentsFiles } = require('./agents-init');
|
|
17
18
|
|
|
18
19
|
/** `.` / `./` mean scaffold into the current working directory */
|
|
19
20
|
function isCurrentDirAlias(name) {
|
|
@@ -45,6 +46,7 @@ function registerCommand(program) {
|
|
|
45
46
|
.description('Create a new Webspresso project')
|
|
46
47
|
.option('-t, --template <template>', 'Template to use (minimal, full)', 'minimal')
|
|
47
48
|
.option('--no-tailwind', 'Skip Tailwind CSS setup')
|
|
49
|
+
.option('--no-agents', 'Skip AI Agent guidelines setup (.agents/, AGENTS.md, CLAUDE.md, .cursorrules)')
|
|
48
50
|
.option('-i, --install', 'Auto install dependencies and build CSS')
|
|
49
51
|
.option('-y, --yes', 'Non-interactive: no database, skip install unless -i/--install, skip dev server')
|
|
50
52
|
.action(async (projectNameArg, options) => {
|
|
@@ -718,8 +720,17 @@ module.exports = {
|
|
|
718
720
|
|
|
719
721
|
console.log('✅ Tailwind CSS setup complete!');
|
|
720
722
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
+
|
|
724
|
+
// Scaffold AI Agent guidelines unless --no-agents is specified
|
|
725
|
+
if (options.agents !== false) {
|
|
726
|
+
try {
|
|
727
|
+
console.log('\n🤖 Setting up AI Agent guidelines (.agents/, AGENTS.md, CLAUDE.md, .cursorrules)...\n');
|
|
728
|
+
scaffoldAgentsFiles(projectPath, { force: true, claude: true, cursor: true, guides: true });
|
|
729
|
+
} catch (e) {
|
|
730
|
+
// Non-fatal
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
723
734
|
// Auto install if requested or ask interactively
|
|
724
735
|
if (autoInstall) {
|
|
725
736
|
await runInstallation(projectPath, useTailwind);
|