webspresso 0.0.96 → 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.
@@ -0,0 +1,122 @@
1
+ ---
2
+ name: webspresso-admin-and-plugins
3
+ description: >-
4
+ Extend the Mithril.js Admin Panel SPA and configure official plugins in Webspresso.
5
+ Use when creating custom admin pages (component.js, page.json), custom field renderers,
6
+ dashboard widgets, actions, or configuring plugins like fileManager, xlsx, csv, polar, realtime, csrf, cors, and basicAuth.
7
+ ---
8
+
9
+ # Webspresso Admin Panel & Plugins Skill
10
+
11
+ The Admin Panel is a modular Mithril.js SPA mounted at `/_admin` powered by `adminPanelPlugin({ db })`.
12
+
13
+ ---
14
+
15
+ ## 1. Custom Admin Pages (`pagesDir` or `registerPageDir`)
16
+
17
+ Folder structure for auto-discovered admin pages:
18
+ ```text
19
+ admin/pages/
20
+ └── system-stats/
21
+ ├── page.json
22
+ └── component.js
23
+ ```
24
+
25
+ ### `page.json`
26
+ ```json
27
+ {
28
+ "id": "system-stats",
29
+ "label": "System Statistics",
30
+ "icon": "activity",
31
+ "order": 10,
32
+ "layout": true,
33
+ "permission": "admin"
34
+ }
35
+ ```
36
+
37
+ ### `component.js` (Clean Mithril Component)
38
+ ```javascript
39
+ module.exports = {
40
+ oninit(vnode) {
41
+ vnode.state.stats = null;
42
+ vnode.state.loading = true;
43
+ m.request({ method: 'GET', url: '/_admin/api/custom/stats' }).then((res) => {
44
+ vnode.state.stats = res;
45
+ vnode.state.loading = false;
46
+ });
47
+ },
48
+ view(vnode) {
49
+ if (vnode.state.loading) return m('.p-6', 'Loading stats...');
50
+ return m('.space-y-4', [
51
+ m('h2.text-xl.font-bold', 'System Overview'),
52
+ m('.grid.grid-cols-3.gap-4', [
53
+ m('.card.p-4.border.rounded', [
54
+ m('.text-sm.text-gray-500', 'Total Users'),
55
+ m('.text-2xl.font-semibold', vnode.state.stats.usersCount),
56
+ ]),
57
+ ]),
58
+ ]);
59
+ },
60
+ };
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 2. Admin Panel Extension Points
66
+
67
+ ```javascript
68
+ // Register custom widget in dashboard
69
+ registry.registerWidget('recent-signups', {
70
+ label: 'Recent Signups',
71
+ component: require('./widgets/recent-signups'),
72
+ width: 'half', // 'full' | 'half' | 'third'
73
+ });
74
+
75
+ // Register single record action button
76
+ registry.registerAction('User', {
77
+ name: 'impersonate',
78
+ label: 'Impersonate',
79
+ icon: 'user-check',
80
+ handler: async (record) => { ... },
81
+ });
82
+
83
+ // Register custom field display/edit renderer
84
+ registry.registerFieldRenderer('color-picker', {
85
+ display: (val) => m('.w-6.h-6.rounded', { style: { backgroundColor: val } }),
86
+ edit: (val, onChange) => m('input[type=color]', { value: val, onchange: (e) => onChange(e.target.value) }),
87
+ });
88
+ ```
89
+
90
+ ---
91
+
92
+ ## 3. Official Plugin Catalog & Usage
93
+
94
+ In `createApp({ plugins: [...] })`:
95
+
96
+ ```javascript
97
+ const {
98
+ adminPanelPlugin,
99
+ fileManagerPlugin,
100
+ xlsxPlugin,
101
+ csvPlugin,
102
+ basicAuthPlugin,
103
+ corsPlugin,
104
+ csrfPlugin,
105
+ realtimePlugin,
106
+ polarPlugin,
107
+ } = require('webspresso/plugins');
108
+
109
+ createApp({
110
+ plugins: [
111
+ adminPanelPlugin({ db }),
112
+ fileManagerPlugin({ baseDir: './public/uploads', publicBasePath: '/uploads' }),
113
+ xlsxPlugin(),
114
+ csvPlugin(),
115
+ basicAuthPlugin({ users: { admin: process.env.ADMIN_BASIC_AUTH_PASS }, routes: ['/metrics'] }),
116
+ corsPlugin({ origin: ['https://example.com'], credentials: true }),
117
+ csrfPlugin({ cookie: true }),
118
+ realtimePlugin({ adapter: 'websocket' }),
119
+ polarPlugin({ apiKey: process.env.POLAR_ACCESS_TOKEN }),
120
+ ],
121
+ });
122
+ ```
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: webspresso-framework
3
+ description: >-
4
+ Core workflow and guide for Webspresso, a lightweight, zero-dependency-sprawl full-stack
5
+ Node.js SSR and API framework. Use when scaffolding, architecting, building, or debugging
6
+ Webspresso applications, configuring plugins, dual auth, response compression, graceful shutdown,
7
+ and error boundaries.
8
+ ---
9
+
10
+ # Webspresso Framework Master Guide
11
+
12
+ Webspresso is a lightweight, zero-dependency-sprawl full-stack Node.js SSR & API framework featuring Next.js/Nuxt-style file-based routing, an intuitive Knex-based ORM, a modular plugin ecosystem, and a customizable Mithril.js SPA Admin Panel.
13
+
14
+ ## 🛡️ 4-Step Mandatory Agent Workflow Protocol
15
+
16
+ 1. **Discover & Triage**: Identify relevant topic guides (`.agents/`) and inspect existing files in `pages/api/`, `models/`, `services/`.
17
+ 2. **Contract Adherence**: Export `{ schema, middleware, handler }` for APIs, `defineModel` for ORM, `defineService` for services.
18
+ 3. **Zero External Dependencies**: Core functionality relies strictly on native Node.js modules (`crypto`, `path`, `fs`, `zlib`, `events`). NEVER install ad-hoc packages for JWT, passport, express-session, or custom routers.
19
+ 4. **Targeted Verification**: Run specific Vitest unit tests (`npx vitest run tests/...`) to ensure zero regressions.
20
+
21
+ ---
22
+
23
+ ## ⚖️ Golden Rules & Anti-Patterns
24
+
25
+ | Category | ❌ NEVER DO THIS (Anti-Pattern) | ✅ ALWAYS DO THIS (Webspresso Way) |
26
+ | :--- | :--- | :--- |
27
+ | **Routing** | `routes/index.js`, `express.Router()`, `app.get('/api/users')` | `pages/api/users.get.js` exporting `{ schema, handler }` |
28
+ | **Database** | `db('users').where({ id }).select()` (Raw Knex) | `db.getRepository('User').findById(id)` |
29
+ | **Business Logic** | `controllers/UserController.js` | `services/user/get.js` → `ctx.service('user.get', input)` |
30
+ | **Auth** | `npm i jsonwebtoken passport express-session` | Built-in `req.auth`, `core/auth`, `middleware: ['jwt']` |
31
+ | **SSR Rendering**| `res.render('profile.njk', data)` inside handlers | Companion loader `pages/profile.js` exporting `async function load()` |
32
+ | **Exceptions** | `res.status(404).json({ error: 'Not found' })` | `throw new NotFoundError('User not found')` |
33
+ | **Background** | `setTimeout(...)`, `npm i bullmq` | `jobs/user/welcome.js` → `req.queue.dispatch('user.welcome', data)` |
34
+
35
+ ---
36
+
37
+ ## 🚀 CLI Commands
38
+
39
+ ```bash
40
+ webspresso dev # Start dev server with hot reload
41
+ webspresso start # Start production server
42
+ webspresso doctor # Run system sanity checks
43
+ webspresso db:migrate # Run pending Knex migrations
44
+ webspresso db:seed # Run database seeders
45
+ webspresso agents:init # Scaffold AI Agent rules and skills
46
+ ```
47
+
48
+ ---
49
+
50
+ ## 🏗️ Core Architecture Overview
51
+
52
+ - **`pages/` & `pages/api/`**: File-based SSR templates (`.njk`), companion data loaders (`.js`), and method-suffixed API endpoints (`.get.js`, `.post.js`).
53
+ - **`services/`**: Auto-discovered domain services mapping to dot-separated names (`'user.create'`).
54
+ - **`models/`**: Schema-driven ORM models with `zdb` definitions, repositories, relations, scopes, and query cache.
55
+ - **`plugins/`**: Modular extension ecosystem (Admin Panel, Media Library, Polar Billing, CSV/XLSX, Realtime, CSRF, CORS, Basic Auth).
56
+ - **`views/`**: Nunjucks layouts, partials, and `fsy` template helpers.
@@ -0,0 +1,119 @@
1
+ ---
2
+ name: webspresso-orm-and-database
3
+ description: >-
4
+ Define models, run queries, handle transactions, manage relations, and scaffold migrations in
5
+ Webspresso ORM. Use when writing database models (defineModel), using zdb schema builders,
6
+ querying repositories (db.getRepository), configuring query cache, soft deletes, and ambient transactions.
7
+ ---
8
+
9
+ # Webspresso ORM & Database Skill
10
+
11
+ ⚠️ **CRITICAL ANTI-PATTERN:** Never write raw Knex table queries (e.g. `db('users')` or `knex('users')`) in application code. Always use Repositories (`db.getRepository('ModelName')`).
12
+
13
+ ---
14
+
15
+ ## 1. Defining Models (`models/*.js`)
16
+
17
+ ```javascript
18
+ const { defineModel, zdb } = require('webspresso');
19
+
20
+ module.exports = defineModel({
21
+ name: 'Product',
22
+ table: 'products',
23
+ primaryKey: 'id',
24
+
25
+ schema: zdb.object({
26
+ id: zdb.id(),
27
+ name: zdb.string().min(1).max(255),
28
+ slug: zdb.string().unique(),
29
+ price: zdb.decimal({ precision: 10, scale: 2 }),
30
+ in_stock: zdb.boolean().default(true),
31
+ metadata: zdb.json().optional(),
32
+ user_id: zdb.foreignKey('users.id'),
33
+ created_at: zdb.timestamp().autoCreate(),
34
+ updated_at: zdb.timestamp().autoUpdate(),
35
+ deleted_at: zdb.timestamp().nullable(),
36
+ }),
37
+
38
+ relations: {
39
+ user: {
40
+ type: 'belongsTo',
41
+ model: 'User',
42
+ foreignKey: 'user_id',
43
+ },
44
+ reviews: {
45
+ type: 'hasMany',
46
+ model: 'Review',
47
+ foreignKey: 'product_id',
48
+ },
49
+ },
50
+
51
+ scopes: {
52
+ softDelete: true, // enables deleted_at filtering automatically
53
+ active: (query) => query.where('in_stock', true),
54
+ },
55
+
56
+ hidden: ['metadata.internal_notes'], // stripped from output
57
+
58
+ cache: 'auto', // query caching ('auto' | 'smart' | true)
59
+
60
+ admin: {
61
+ enabled: true,
62
+ label: 'Products',
63
+ icon: 'package',
64
+ columns: ['name', 'price', 'in_stock', 'created_at'],
65
+ search: ['name', 'slug'],
66
+ filters: ['in_stock', 'user_id'],
67
+ },
68
+ });
69
+ ```
70
+
71
+ ---
72
+
73
+ ## 2. Repositories API
74
+
75
+ ```javascript
76
+ const repo = db.getRepository('Product');
77
+
78
+ // Single Record Queries
79
+ const item = await repo.findById(id);
80
+ const item = await repo.findOne({ slug: 'espresso-blend' });
81
+
82
+ // Multiple Records & Filtering
83
+ const activeItems = await repo.find({ in_stock: true });
84
+
85
+ // Insert & Update (auto-validates with schema & triggers lifecycle hooks)
86
+ const created = await repo.create({
87
+ name: 'Espresso Blend',
88
+ slug: 'espresso-blend',
89
+ price: 14.99,
90
+ user_id: user.id,
91
+ });
92
+
93
+ const updated = await repo.update(created.id, { price: 16.99 });
94
+
95
+ // Deletion (respects softDelete scope)
96
+ await repo.delete(id);
97
+
98
+ // Fluent Query Builder with Eager Loading & Pagination
99
+ const results = await repo.query()
100
+ .where('price', '>=', 10)
101
+ .include('user', 'reviews')
102
+ .orderBy('created_at', 'desc')
103
+ .paginate({ page: 1, limit: 20 });
104
+ // Returns: { data: [...], pagination: { total, page, limit, totalPages } }
105
+ ```
106
+
107
+ ---
108
+
109
+ ## 3. Ambient Transactions (`AsyncLocalStorage`)
110
+
111
+ Inside `db.transaction()` or services declared with `transaction: true`, all repository operations automatically bind to the ambient transaction without manual `trx` passing:
112
+
113
+ ```javascript
114
+ await db.transaction(async () => {
115
+ const user = await db.getRepository('User').create(userData);
116
+ const account = await db.getRepository('Account').create({ userId: user.id, balance: 100 });
117
+ // Automatically commits if promise resolves, or rolls back if an exception is thrown
118
+ });
119
+ ```
@@ -0,0 +1,114 @@
1
+ ---
2
+ name: webspresso-routing-and-api
3
+ description: >-
4
+ Build and declare file-based API endpoints and SSR pages in Webspresso using defineApi, definePage,
5
+ and defineModule. Use when writing route handlers, data loaders (load()), parameter brackets ([id]),
6
+ Zod input validation, response streaming, and route middleware.
7
+ ---
8
+
9
+ # Webspresso Routing & API Skill
10
+
11
+ Webspresso uses strictly file-based routing. Never use `express.Router()`, `app.get()`, or a `routes/` directory.
12
+
13
+ ---
14
+
15
+ ## 1. File-Based API Endpoints (`pages/api/...`)
16
+
17
+ All API routes live in `pages/api/` and use HTTP method extensions:
18
+ - `pages/api/notes.get.js` → `GET /api/notes`
19
+ - `pages/api/notes.post.js` → `POST /api/notes`
20
+ - `pages/api/notes/[id].get.js` → `GET /api/notes/:id`
21
+ - `pages/api/notes/[id].delete.js` → `DELETE /api/notes/:id`
22
+
23
+ ### Standard `defineApi` Contract
24
+
25
+ ```javascript
26
+ const { defineApi } = require('webspresso');
27
+
28
+ module.exports = defineApi({
29
+ schema: ({ z }) => ({
30
+ body: z.object({
31
+ title: z.string().min(1, 'Title is required'),
32
+ content: z.string().optional(),
33
+ }),
34
+ params: z.object({
35
+ id: z.string().optional(),
36
+ }),
37
+ query: z.object({
38
+ filter: z.string().optional(),
39
+ }),
40
+ }),
41
+ middleware: ['auth'], // optional named middleware string or array of middlewares
42
+ handler: async (req, res, ctx) => {
43
+ // 1. Validated input is automatically available in req.input
44
+ const { title, content } = req.input.body;
45
+
46
+ // 2. Repositories and Services are injected
47
+ const note = await req.db.getRepository('Note').create({ title, content });
48
+
49
+ // 3. Return response
50
+ return res.status(201).json(note);
51
+ },
52
+ });
53
+ ```
54
+
55
+ ---
56
+
57
+ ## 2. SSR HTML Pages & Data Loaders (`pages/...`)
58
+
59
+ - **Template**: `pages/products/[id].njk`
60
+ - **Companion Loader**: `pages/products/[id].js`
61
+
62
+ ### Companion Loader (`definePage`)
63
+
64
+ ```javascript
65
+ const { definePage } = require('webspresso');
66
+
67
+ module.exports = definePage({
68
+ middleware: ['auth'], // optional
69
+ head: {
70
+ title: 'Product Details',
71
+ },
72
+ load: async ({ req, res, db, ctx }) => {
73
+ const product = await db.getRepository('Product').findById(req.params.id);
74
+ if (!product) {
75
+ throw new ctx.errors.NotFoundError('Product not found');
76
+ }
77
+ return { product, title: product.name };
78
+ },
79
+ });
80
+ ```
81
+
82
+ ### Template (`.njk`)
83
+
84
+ ```nunjucks
85
+ {% extends "layout.njk" %}
86
+
87
+ {% block content %}
88
+ <div class="product-page">
89
+ <h1>{{ product.name }}</h1>
90
+ <p class="price">{{ fsy.formatCurrency(product.price) }}</p>
91
+ <p>{{ product.description }}</p>
92
+ </div>
93
+ {% endblock %}
94
+ ```
95
+
96
+ ---
97
+
98
+ ## 3. Modular Domain Architecture (`defineModule`)
99
+
100
+ Encapsulate feature domains inside `src/modules/` or `modules/`:
101
+
102
+ ```javascript
103
+ const { defineModule } = require('webspresso');
104
+
105
+ module.exports = defineModule({
106
+ name: 'billing',
107
+ pages: './pages', // auto-discovers pages under /billing/*
108
+ api: './api', // auto-discovers API under /api/billing/*
109
+ services: './services', // auto-discovers services under 'billing.*'
110
+ middlewares: {
111
+ paidOnly: (req, res, next) => { ... },
112
+ },
113
+ });
114
+ ```
@@ -0,0 +1,101 @@
1
+ ---
2
+ name: webspresso-services
3
+ description: >-
4
+ Encapsulate business logic, validations, transactions, and authorizations in Webspresso services.
5
+ Use when writing service files (services/domain/action.js), defining schemas (defineService),
6
+ configuring transaction: true, calling ctx.service() or req.service(), and adding RBAC guards.
7
+ ---
8
+
9
+ # Webspresso Services Layer Skill
10
+
11
+ ⚠️ **CRITICAL ANTI-PATTERN:** Never create a `controllers/` folder or controller classes. Encapsulate multi-step business logic in `services/` and invoke via `ctx.service()` or `req.service()`.
12
+
13
+ ---
14
+
15
+ ## 1. File-Based Auto-Discovery (`services/`)
16
+
17
+ Files in `services/` map directly to dot-separated service names:
18
+ - `services/user/create.js` → `'user.create'`
19
+ - `services/order/checkout.js` → `'order.checkout'`
20
+ - `services/billing/process-refund.js` → `'billing.process-refund'` (and `'billing.processRefund'`)
21
+
22
+ ---
23
+
24
+ ## 2. Defining Services (`defineService`)
25
+
26
+ ```javascript
27
+ const { defineService } = require('webspresso');
28
+
29
+ module.exports = defineService({
30
+ schema: ({ z }) => z.object({
31
+ userId: z.number().int().positive(),
32
+ amount: z.number().positive(),
33
+ reason: z.string().min(3),
34
+ }),
35
+
36
+ // Optional Declarative Auth / Role Guard
37
+ auth: ['admin', 'manager'], // or boolean true, or (ctx, input) => boolean
38
+
39
+ // Automatic ACID Transaction Propagation
40
+ transaction: true,
41
+
42
+ // Optional Service Result Cache
43
+ cache: {
44
+ ttl: 300, // seconds
45
+ key: (input) => `refund:${input.userId}:${input.amount}`,
46
+ },
47
+
48
+ // Execution Handler
49
+ handler: async (input, ctx) => {
50
+ // ctx.db, ctx.service, ctx.user, ctx.logger are automatically injected
51
+ const userRepo = ctx.db.getRepository('User');
52
+ const accountRepo = ctx.db.getRepository('Account');
53
+
54
+ const user = await userRepo.findById(input.userId);
55
+ if (!user) {
56
+ throw new ctx.errors.NotFoundError('User not found');
57
+ }
58
+
59
+ const account = await accountRepo.findOne({ userId: user.id });
60
+ const updatedAccount = await accountRepo.update(account.id, {
61
+ balance: account.balance + input.amount,
62
+ });
63
+
64
+ // Nested service call shares the same ambient transaction automatically
65
+ await ctx.service('audit.log', {
66
+ action: 'refund.processed',
67
+ userId: user.id,
68
+ amount: input.amount,
69
+ });
70
+
71
+ return {
72
+ success: true,
73
+ balance: updatedAccount.balance,
74
+ };
75
+ },
76
+ });
77
+ ```
78
+
79
+ ---
80
+
81
+ ## 3. Invoking Services
82
+
83
+ From SSR Companion Loaders:
84
+ ```javascript
85
+ module.exports = definePage({
86
+ load: async ({ ctx }) => {
87
+ const data = await ctx.service('order.summary', { userId: ctx.user.id });
88
+ return { data };
89
+ },
90
+ });
91
+ ```
92
+
93
+ From API Route Handlers:
94
+ ```javascript
95
+ module.exports = defineApi({
96
+ handler: async (req, res) => {
97
+ const result = await req.service('order.checkout', req.input.body);
98
+ return res.status(200).json(result);
99
+ },
100
+ });
101
+ ```
@@ -92,7 +92,8 @@ function registerCommand(program) {
92
92
 
93
93
  // Hash the password (same rounds as admin panel setup)
94
94
  const { hash } = require('../../core/auth/hash');
95
- const hashedPassword = await hash(password, 10);
95
+ const rounds = process.env.NODE_ENV === 'test' ? 4 : 10;
96
+ const hashedPassword = await hash(password, rounds);
96
97
 
97
98
  // Update the password
98
99
  await db('admin_users')
@@ -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
@@ -39,6 +39,40 @@ function copyFileSafe(src, dest, force = false) {
39
39
  return destExists ? 'overwritten' : 'created';
40
40
  }
41
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
+
42
76
  /**
43
77
  * Scaffolds AI Agent rules and guidebook files into target project directory.
44
78
  * @param {string} targetDir
@@ -98,23 +132,29 @@ function scaffoldAgentsFiles(targetDir, options = {}) {
98
132
  }
99
133
  }
100
134
 
101
- // 4. .agents/*.md Guidebook directory
135
+ // 4. .agents/*.md Guidebook and skills/ directory
102
136
  if (includeGuides && fs.existsSync(SOURCE_AGENTS_DIR)) {
103
137
  const destAgentsDir = path.join(targetDir, '.agents');
104
138
  if (!fs.existsSync(destAgentsDir)) {
105
139
  fs.mkdirSync(destAgentsDir, { recursive: true });
106
140
  }
107
141
 
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);
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
+ }
118
158
  }
119
159
  }
120
160
  }
@@ -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,
@@ -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
  }
@@ -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,
@@ -7,7 +7,6 @@
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
9
  const { loadDbConfig } = require('../utils/db');
10
- const { generatePolarMigration } = require('../../plugins/polar/src/migration');
11
10
 
12
11
  function registerCommand(program) {
13
12
  program
@@ -40,6 +39,7 @@ function registerCommand(program) {
40
39
  filepath = path.join(migrationDir, `${timestamp}_polar_billing.js`);
41
40
  }
42
41
 
42
+ const { generatePolarMigration } = require('../../plugins/polar/src/migration');
43
43
  const content = generatePolarMigration({ tableName: options.table });
44
44
  fs.writeFileSync(filepath, content, 'utf8');
45
45
  console.log(`✅ Polar migration written: ${filepath}`);