webspresso 0.0.95 → 0.0.97

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.
Files changed (94) hide show
  1. package/.agents/ADMIN.md +225 -0
  2. package/.agents/AGENTS.md +72 -0
  3. package/.agents/ARCHITECTURE.md +119 -0
  4. package/.agents/AUTH.md +103 -0
  5. package/.agents/CLI.md +115 -0
  6. package/.agents/COMPRESSION.md +59 -0
  7. package/.agents/CONVENTIONS.md +46 -0
  8. package/.agents/EMAIL.md +141 -0
  9. package/.agents/ERRORS.md +201 -0
  10. package/.agents/MCP.md +103 -0
  11. package/.agents/ORM.md +230 -0
  12. package/.agents/PLUGINS.md +419 -0
  13. package/.agents/QUEUE.md +113 -0
  14. package/.agents/REALTIME.md +213 -0
  15. package/.agents/ROUTING.md +292 -0
  16. package/.agents/SERVICES.md +341 -0
  17. package/.agents/SHUTDOWN.md +67 -0
  18. package/.agents/STREAMING.md +73 -0
  19. package/.agents/WORKFLOW.md +27 -0
  20. package/.agents/internal/DOCUMENTATION_REVIEW.md +485 -0
  21. package/.agents/internal/TECHNICAL_REVIEW.md +428 -0
  22. package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +122 -0
  23. package/.agents/skills/webspresso-framework/SKILL.md +56 -0
  24. package/.agents/skills/webspresso-orm-and-database/SKILL.md +119 -0
  25. package/.agents/skills/webspresso-routing-and-api/SKILL.md +114 -0
  26. package/.agents/skills/webspresso-services/SKILL.md +101 -0
  27. package/.cursorrules +14 -0
  28. package/AGENTS.md +175 -0
  29. package/CLAUDE.md +54 -0
  30. package/README.md +18 -14
  31. package/bin/commands/admin-password.js +2 -1
  32. package/bin/commands/admin-setup.js +1 -1
  33. package/bin/commands/agents-init.js +209 -0
  34. package/bin/commands/api.js +114 -48
  35. package/bin/commands/audit-prune.js +1 -1
  36. package/bin/commands/doctor.js +35 -0
  37. package/bin/commands/favicon-generate.js +1 -1
  38. package/bin/commands/new.js +13 -2
  39. package/bin/commands/orm-map.js +5 -4
  40. package/bin/commands/page.js +75 -89
  41. package/bin/commands/polar-migrate.js +1 -1
  42. package/bin/commands/seed.js +1 -1
  43. package/bin/commands/service.js +77 -0
  44. package/bin/webspresso.js +2 -2
  45. package/core/auth/hash.js +10 -5
  46. package/core/compileSchema.js +1 -1
  47. package/core/errors/middleware.js +22 -2
  48. package/core/errors/normalize.js +29 -2
  49. package/{utils/schemaCache.js → core/schema-cache.js} +2 -3
  50. package/index.d.ts +306 -0
  51. package/index.js +35 -1
  52. package/package.json +36 -14
  53. package/plugins/admin-panel/app.js +10 -9
  54. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +22 -1
  55. package/plugins/admin-panel/core/api-extensions.js +4 -0
  56. package/plugins/basic-auth/index.js +3 -1
  57. package/plugins/content/client/inline-edit.js +12 -12
  58. package/plugins/cors.js +6 -7
  59. package/plugins/csrf/index.js +3 -2
  60. package/plugins/csv/engine.js +447 -0
  61. package/plugins/csv/index.js +94 -0
  62. package/plugins/csv/middleware.js +80 -0
  63. package/plugins/csv/services.js +138 -0
  64. package/plugins/file-manager/admin-component.js +613 -0
  65. package/plugins/file-manager/api-handlers.js +261 -0
  66. package/plugins/file-manager/core.js +601 -0
  67. package/plugins/file-manager/index.js +139 -0
  68. package/plugins/file-manager/security.js +217 -0
  69. package/plugins/index.js +45 -79
  70. package/plugins/mcp/transports/sse.js +4 -2
  71. package/plugins/site-analytics/admin-component.js +7 -8
  72. package/plugins/xlsx/engine.js +397 -0
  73. package/plugins/xlsx/index.js +91 -0
  74. package/plugins/xlsx/middleware.js +87 -0
  75. package/plugins/xlsx/services.js +118 -0
  76. package/src/api/api-loader.js +233 -0
  77. package/src/api/define-api.js +35 -0
  78. package/src/discovery/file-route-parser.js +201 -0
  79. package/src/discovery/scan-routes.js +265 -0
  80. package/src/file-router.js +9 -1
  81. package/src/modules/define-module.js +99 -0
  82. package/src/modules/module-discovery.js +117 -0
  83. package/src/pages/define-page.js +36 -0
  84. package/src/pages/page-loader.js +214 -0
  85. package/src/routing/mount-discovered-routes.js +78 -0
  86. package/src/routing/route-table.js +191 -0
  87. package/src/server.js +112 -15
  88. package/src/services/builtins/file-manager.js +178 -0
  89. package/src/services/discovery.js +6 -1
  90. package/src/services/index.js +6 -11
  91. package/bin/commands/skill.js +0 -187
  92. package/templates/skills/webspresso-usage/REFERENCE-framework.md +0 -289
  93. package/templates/skills/webspresso-usage/REFERENCE-kernel.md +0 -93
  94. package/templates/skills/webspresso-usage/SKILL.md +0 -48
@@ -6,7 +6,6 @@
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
  const { loadDbConfig } = require('../utils/db');
9
- const { generateAdminUsersMigration } = require('../../plugins/admin-panel/migration-template');
10
9
 
11
10
  function registerCommand(program) {
12
11
  program
@@ -53,6 +52,7 @@ function registerCommand(program) {
53
52
  const filepath = path.join(migrationDir, filename);
54
53
 
55
54
  // Generate migration content
55
+ const { generateAdminUsersMigration } = require('../../plugins/admin-panel/migration-template');
56
56
  const content = generateAdminUsersMigration();
57
57
 
58
58
  // Write migration file
@@ -0,0 +1,209 @@
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
+ * Recursively copies a directory safely.
44
+ * @param {string} srcDir
45
+ * @param {string} destDir
46
+ * @param {boolean} force
47
+ * @param {string[]} installed
48
+ * @param {string[]} skipped
49
+ * @param {string} baseRel
50
+ */
51
+ function copyDirSafe(srcDir, destDir, force = false, installed = [], skipped = [], baseRel = '') {
52
+ if (!fs.existsSync(srcDir)) return;
53
+ if (!fs.existsSync(destDir)) {
54
+ fs.mkdirSync(destDir, { recursive: true });
55
+ }
56
+
57
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
58
+ for (const entry of entries) {
59
+ const srcPath = path.join(srcDir, entry.name);
60
+ const destPath = path.join(destDir, entry.name);
61
+ const relPath = path.join(baseRel, entry.name);
62
+
63
+ if (entry.isDirectory()) {
64
+ copyDirSafe(srcPath, destPath, force, installed, skipped, relPath);
65
+ } else {
66
+ const status = copyFileSafe(srcPath, destPath, force);
67
+ if (status === 'created' || status === 'overwritten') {
68
+ installed.push(relPath);
69
+ } else {
70
+ skipped.push(relPath);
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Scaffolds AI Agent rules and guidebook files into target project directory.
78
+ * @param {string} targetDir
79
+ * @param {Object} [options]
80
+ * @param {boolean} [options.force=false]
81
+ * @param {boolean} [options.claude=true]
82
+ * @param {boolean} [options.cursor=true]
83
+ * @param {boolean} [options.guides=true]
84
+ * @returns {{ installed: string[], skipped: string[], errors: string[] }}
85
+ */
86
+ function scaffoldAgentsFiles(targetDir, options = {}) {
87
+ const force = options.force === true;
88
+ const includeClaude = options.claude !== false;
89
+ const includeCursor = options.cursor !== false;
90
+ const includeGuides = options.guides !== false;
91
+
92
+ const result = {
93
+ installed: [],
94
+ skipped: [],
95
+ errors: [],
96
+ };
97
+
98
+ if (!fs.existsSync(targetDir)) {
99
+ fs.mkdirSync(targetDir, { recursive: true });
100
+ }
101
+
102
+ // 1. Root AGENTS.md
103
+ if (fs.existsSync(SOURCE_AGENTS_MD)) {
104
+ const destAgentsMd = path.join(targetDir, 'AGENTS.md');
105
+ const status = copyFileSafe(SOURCE_AGENTS_MD, destAgentsMd, force);
106
+ if (status === 'created' || status === 'overwritten') {
107
+ result.installed.push('AGENTS.md');
108
+ } else {
109
+ result.skipped.push('AGENTS.md');
110
+ }
111
+ }
112
+
113
+ // 2. CLAUDE.md
114
+ if (includeClaude && fs.existsSync(SOURCE_CLAUDE_MD)) {
115
+ const destClaudeMd = path.join(targetDir, 'CLAUDE.md');
116
+ const status = copyFileSafe(SOURCE_CLAUDE_MD, destClaudeMd, force);
117
+ if (status === 'created' || status === 'overwritten') {
118
+ result.installed.push('CLAUDE.md');
119
+ } else {
120
+ result.skipped.push('CLAUDE.md');
121
+ }
122
+ }
123
+
124
+ // 3. .cursorrules
125
+ if (includeCursor && fs.existsSync(SOURCE_CURSORRULES)) {
126
+ const destCursor = path.join(targetDir, '.cursorrules');
127
+ const status = copyFileSafe(SOURCE_CURSORRULES, destCursor, force);
128
+ if (status === 'created' || status === 'overwritten') {
129
+ result.installed.push('.cursorrules');
130
+ } else {
131
+ result.skipped.push('.cursorrules');
132
+ }
133
+ }
134
+
135
+ // 4. .agents/*.md Guidebook and skills/ directory
136
+ if (includeGuides && fs.existsSync(SOURCE_AGENTS_DIR)) {
137
+ const destAgentsDir = path.join(targetDir, '.agents');
138
+ if (!fs.existsSync(destAgentsDir)) {
139
+ fs.mkdirSync(destAgentsDir, { recursive: true });
140
+ }
141
+
142
+ const entries = fs.readdirSync(SOURCE_AGENTS_DIR, { withFileTypes: true });
143
+ for (const entry of entries) {
144
+ if (entry.name === 'internal') continue; // exclude internal reviews from user projects
145
+ const srcPath = path.join(SOURCE_AGENTS_DIR, entry.name);
146
+ const destPath = path.join(destAgentsDir, entry.name);
147
+ const relPath = path.join('.agents', entry.name);
148
+
149
+ if (entry.isDirectory()) {
150
+ copyDirSafe(srcPath, destPath, force, result.installed, result.skipped, relPath);
151
+ } else if (entry.name.endsWith('.md')) {
152
+ const status = copyFileSafe(srcPath, destPath, force);
153
+ if (status === 'created' || status === 'overwritten') {
154
+ result.installed.push(relPath);
155
+ } else {
156
+ result.skipped.push(relPath);
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ return result;
163
+ }
164
+
165
+ function registerCommand(program) {
166
+ program
167
+ .command('agents:init')
168
+ .alias('agents')
169
+ .alias('agents:setup')
170
+ .alias('ai:init')
171
+ .description('Scaffold AI Agent guidelines (.agents/, AGENTS.md, CLAUDE.md, .cursorrules)')
172
+ .option('-p, --path <path>', 'Target project directory', process.cwd())
173
+ .option('-f, --force', 'Overwrite existing agent guideline files', false)
174
+ .option('--no-claude', 'Skip generating CLAUDE.md')
175
+ .option('--no-cursor', 'Skip generating .cursorrules')
176
+ .option('--no-guides', 'Skip generating .agents/ topic guide bundle')
177
+ .action(async (options) => {
178
+ const targetDir = path.resolve(options.path || process.cwd());
179
+ console.log(`\n🤖 Initializing AI Agent Guidelines in: ${targetDir}\n`);
180
+
181
+ const result = scaffoldAgentsFiles(targetDir, {
182
+ force: options.force,
183
+ claude: options.claude,
184
+ cursor: options.cursor,
185
+ guides: options.guides,
186
+ });
187
+
188
+ if (result.installed.length > 0) {
189
+ console.log('✅ Installed/Updated:');
190
+ for (const file of result.installed) {
191
+ console.log(` + ${file}`);
192
+ }
193
+ }
194
+
195
+ if (result.skipped.length > 0) {
196
+ console.log('\n⚠️ Skipped (already exists, use --force to overwrite):');
197
+ for (const file of result.skipped) {
198
+ console.log(` - ${file}`);
199
+ }
200
+ }
201
+
202
+ console.log('\n🎉 AI Agent Guidelines ready! Agents (Cursor, Claude Code, Antigravity) will follow strict Webspresso standards.\n');
203
+ });
204
+ }
205
+
206
+ module.exports = {
207
+ registerCommand,
208
+ scaffoldAgentsFiles,
209
+ };
@@ -1,6 +1,9 @@
1
+ 'use strict';
2
+
1
3
  /**
2
4
  * API Command
3
- * Add a new API endpoint to the current project
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 the current project')
20
- .action(async () => {
21
- if (!fs.existsSync('pages')) {
22
- console.error('❌ Not a Webspresso project! Run this command in your project directory.');
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
- const answers = await inquirer.prompt([
27
- {
28
- type: 'input',
29
- name: 'route',
30
- message: 'API route path (e.g., /api/users or /api/users/[id]):',
31
- validate: (input) => {
32
- if (!input.startsWith('/api/')) {
33
- return 'API route must start with /api/';
34
- }
35
- return true;
36
- }
37
- },
38
- {
39
- type: 'list',
40
- name: 'method',
41
- message: 'HTTP method:',
42
- choices: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
43
- default: 'GET'
44
- }
45
- ]);
46
-
47
- const route = answers.route.replace(/^\/api\//, '');
48
- const routePath = path.join('pages', 'api', route);
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
- // Create API file
56
- const apiFile = path.join(dirPath, `${fileName}.${answers.method.toLowerCase()}.js`);
57
-
58
- const apiContent = `/**
59
- * ${answers.method} ${answers.route}
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
- module.exports = async function handler(req, res) {
63
- res.json({
64
- message: 'Hello from ${answers.route}',
65
- method: '${answers.method}',
66
- timestamp: new Date().toISOString()
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 };
@@ -3,7 +3,6 @@
3
3
  */
4
4
 
5
5
  const { loadDbConfig, createDbInstance } = require('../utils/db');
6
- const { purgeAuditLogs } = require('../../plugins/audit-log/purge');
7
6
 
8
7
  function registerCommand(program) {
9
8
  program
@@ -28,6 +27,7 @@ function registerCommand(program) {
28
27
  const olderThan = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
29
28
 
30
29
  try {
30
+ const { purgeAuditLogs } = require('../../plugins/audit-log/purge');
31
31
  const deleted = await purgeAuditLogs(knex, {
32
32
  tableName: options.table,
33
33
  olderThan,
@@ -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
 
@@ -6,7 +6,7 @@
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
  function getSharp() {
9
- return process.env.NODE_ENV === 'test'
9
+ return (process.env.NODE_ENV === 'test' || process.env.VITEST)
10
10
  ? require('../../tests/mocks/sharp')
11
11
  : require('sharp');
12
12
  }
@@ -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);
@@ -6,10 +6,6 @@ const fs = require('fs');
6
6
  const os = require('os');
7
7
  const path = require('path');
8
8
  const { execFileSync } = require('child_process');
9
- const { getWebspressoOrmForProject } = require('../utils/resolve-webspresso-orm');
10
- const { loadProjectModels } = require('../utils/orm-map-load');
11
- const { buildSnapshot, buildMermaidErDiagram } = require('../utils/orm-map-snapshot');
12
- const { buildOrmMapHtml, readPackageName } = require('../utils/orm-map-html');
13
9
 
14
10
  function ensureOpenInsideTrustedRoots(absFile, cwd) {
15
11
  if (!fs.existsSync(absFile)) {
@@ -72,6 +68,11 @@ function registerCommand(program) {
72
68
  .option('-e, --env <environment>', 'Config environment', 'development')
73
69
  .option('-m, --models <dir>', 'Models directory (overrides config default ./models)')
74
70
  .action((options) => {
71
+ const { getWebspressoOrmForProject } = require('../utils/resolve-webspresso-orm');
72
+ const { loadProjectModels } = require('../utils/orm-map-load');
73
+ const { buildSnapshot, buildMermaidErDiagram } = require('../utils/orm-map-snapshot');
74
+ const { buildOrmMapHtml, readPackageName } = require('../utils/orm-map-html');
75
+
75
76
  const cwd = process.cwd();
76
77
  const { modelsDir, loaded, errors } = loadProjectModels(cwd, {
77
78
  modelsOverride: options.models,