webspresso 0.0.96 → 0.0.98
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 +153 -5
- package/.agents/ARCHITECTURE.md +10 -0
- package/.agents/CLI.md +195 -1
- package/.agents/PLUGINS.md +153 -9
- package/.agents/QUEUE.md +43 -0
- package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +122 -0
- package/.agents/skills/webspresso-framework/SKILL.md +56 -0
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +119 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +114 -0
- package/.agents/skills/webspresso-services/SKILL.md +101 -0
- package/bin/commands/add-esbuild.js +80 -0
- package/bin/commands/add-tailwind.js +76 -31
- package/bin/commands/admin-password.js +2 -1
- package/bin/commands/admin-setup.js +1 -1
- package/bin/commands/agents-init.js +51 -11
- package/bin/commands/analytics-prune.js +57 -0
- package/bin/commands/api.js +207 -15
- package/bin/commands/audit-prune.js +1 -1
- package/bin/commands/build.js +99 -0
- package/bin/commands/dev.js +64 -35
- package/bin/commands/doctor.js +26 -0
- package/bin/commands/email-prune.js +47 -0
- package/bin/commands/favicon-generate.js +1 -1
- package/bin/commands/make-job.js +86 -0
- package/bin/commands/make-model.js +250 -0
- package/bin/commands/new.js +45 -28
- package/bin/commands/openapi.js +102 -0
- package/bin/commands/orm-map.js +5 -4
- package/bin/commands/page.js +6 -3
- package/bin/commands/polar-migrate.js +1 -1
- package/bin/commands/queue-prune.js +59 -0
- package/bin/commands/routes.js +52 -0
- package/bin/commands/seed.js +1 -1
- package/bin/commands/service.js +6 -3
- package/bin/commands/typegen.js +106 -0
- package/bin/commands/upload-prune.js +79 -0
- package/bin/webspresso.js +22 -0
- package/core/applySchema.js +13 -6
- package/core/auth/hash.js +31 -17
- package/core/compression/middleware.js +23 -17
- package/core/openapi/build-from-api-routes.js +160 -29
- package/core/orm/cache/fingerprint.js +15 -1
- package/core/orm/cache/memory-provider.js +27 -2
- package/core/orm/index.js +12 -7
- package/core/orm/model.js +49 -7
- package/core/queue/adapters/database.js +94 -0
- package/core/queue/adapters/memory.js +62 -0
- package/core/queue/adapters/redis.js +80 -0
- package/core/queue/manager.js +123 -0
- package/core/ssr/stream.js +11 -0
- package/index.d.ts +660 -10
- package/index.js +44 -96
- package/package.json +32 -12
- package/plugins/admin-panel/api.js +279 -17
- package/plugins/admin-panel/app.js +124 -31
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
- package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +10 -5
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
- package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
- package/plugins/admin-panel/client/parts/08-record-list.js +243 -23
- package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
- package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
- package/plugins/admin-panel/core/admin-module.js +13 -7
- package/plugins/admin-panel/core/api-extensions.js +34 -2
- package/plugins/admin-panel/core/registry.js +96 -16
- package/plugins/admin-panel/index.js +96 -0
- package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
- package/plugins/admin-panel/modules/admin-users.js +16 -1
- package/plugins/admin-panel/modules/bulk-actions.js +13 -5
- package/plugins/admin-panel/modules/custom-pages.js +492 -3
- package/plugins/admin-panel/modules/dashboard.js +283 -0
- package/plugins/admin-panel/modules/menu.js +56 -18
- package/plugins/admin-panel/modules/user-management.js +24 -6
- package/plugins/audit-log/admin-component.js +11 -0
- package/plugins/audit-log/api-handlers.js +34 -0
- package/plugins/audit-log/index.js +129 -3
- package/plugins/audit-log/purge.js +60 -10
- package/plugins/basic-auth/index.js +3 -1
- package/plugins/content/client/inline-edit.js +12 -12
- package/plugins/cors.js +6 -7
- package/plugins/dashboard/app.js +152 -27
- package/plugins/dashboard/index.js +143 -39
- package/plugins/dashboard/styles.js +78 -0
- package/plugins/data-exchange/export-xlsx.js +9 -2
- package/plugins/data-exchange/import.js +9 -2
- package/plugins/data-exchange/index.js +6 -3
- package/plugins/email/index.js +132 -2
- package/plugins/email/purge.js +60 -10
- package/plugins/file-manager/core.js +1 -1
- package/plugins/file-manager/security.js +33 -23
- package/plugins/index.js +49 -87
- package/plugins/mcp/transports/sse.js +4 -2
- package/plugins/orm-cache-admin/index.js +4 -2
- package/plugins/queue/index.js +11 -0
- package/plugins/schema-explorer/admin-component.js +714 -0
- package/plugins/schema-explorer/relations-graph.js +205 -0
- package/plugins/schema-explorer.js +248 -69
- package/plugins/site-analytics/admin-component.js +30 -8
- package/plugins/site-analytics/api-handlers.js +42 -0
- package/plugins/site-analytics/index.js +147 -2
- package/plugins/site-analytics/purge.js +168 -0
- package/plugins/swagger.js +261 -13
- package/plugins/upload/index.js +26 -0
- package/plugins/upload/orphan-pruner.js +292 -0
- package/src/api/api-loader.js +3 -1
- package/src/bundle/index.js +517 -0
- package/src/bundle/manifest.js +165 -0
- package/src/dev/banner.js +296 -0
- package/src/dev/colors.js +70 -0
- package/src/dev/index.js +62 -0
- package/src/dev/profiler.js +354 -0
- package/src/dev/typegen.js +497 -0
- package/src/dev-reload/index.js +611 -0
- package/src/file-router.js +213 -63
- package/src/helpers.js +958 -98
- package/src/njk-frontmatter.js +12 -0
- package/src/pages/page-loader.js +61 -11
- package/src/server.js +629 -63
- package/src/services/discovery.js +6 -1
- package/src/services/index.js +6 -13
package/bin/commands/new.js
CHANGED
|
@@ -46,6 +46,8 @@ function registerCommand(program) {
|
|
|
46
46
|
.description('Create a new Webspresso project')
|
|
47
47
|
.option('-t, --template <template>', 'Template to use (minimal, full)', 'minimal')
|
|
48
48
|
.option('--no-tailwind', 'Skip Tailwind CSS setup')
|
|
49
|
+
.option('--tailwind-v4', 'Use Tailwind CSS v4 (Oxide engine, zero-config)')
|
|
50
|
+
.option('--tailwind-version <version>', 'Tailwind version (3 or 4)', '3')
|
|
49
51
|
.option('--no-agents', 'Skip AI Agent guidelines setup (.agents/, AGENTS.md, CLAUDE.md, .cursorrules)')
|
|
50
52
|
.option('-i, --install', 'Auto install dependencies and build CSS')
|
|
51
53
|
.option('-y, --yes', 'Non-interactive: no database, skip install unless -i/--install, skip dev server')
|
|
@@ -657,20 +659,43 @@ Visit http://localhost:3000
|
|
|
657
659
|
|
|
658
660
|
// Add Tailwind if requested (default: true)
|
|
659
661
|
if (useTailwind) {
|
|
660
|
-
|
|
662
|
+
const isV4 = Boolean(options.tailwindV4 || options.tailwindVersion === '4');
|
|
663
|
+
console.log(`\n🎨 Setting up Tailwind CSS ${isV4 ? 'v4 (Oxide engine, zero-config)' : 'v3'}...\n`);
|
|
661
664
|
|
|
662
665
|
// Create src directory
|
|
663
666
|
fs.mkdirSync(path.join(projectPath, 'src'), { recursive: true });
|
|
664
667
|
|
|
665
|
-
// Create
|
|
666
|
-
|
|
668
|
+
// Create public/css directory
|
|
669
|
+
fs.mkdirSync(path.join(projectPath, 'public', 'css'), { recursive: true });
|
|
670
|
+
fs.writeFileSync(path.join(projectPath, 'public', 'css', 'style.css'), '/* Run npm run build:css */\n');
|
|
671
|
+
|
|
672
|
+
// Update package.json
|
|
673
|
+
const updatedPackageJson = JSON.parse(fs.readFileSync(path.join(projectPath, 'package.json'), 'utf-8'));
|
|
674
|
+
updatedPackageJson.devDependencies = updatedPackageJson.devDependencies || {};
|
|
675
|
+
updatedPackageJson.scripts = updatedPackageJson.scripts || {};
|
|
676
|
+
|
|
677
|
+
if (isV4) {
|
|
678
|
+
const inputCss = `@import "tailwindcss";
|
|
679
|
+
|
|
680
|
+
@source "../pages/**/*.{njk,js}";
|
|
681
|
+
@source "../views/**/*.njk";
|
|
682
|
+
@source "../src/**/*.js";
|
|
683
|
+
`;
|
|
684
|
+
fs.writeFileSync(path.join(projectPath, 'src', 'input.css'), inputCss);
|
|
685
|
+
|
|
686
|
+
updatedPackageJson.devDependencies['@tailwindcss/cli'] = '^4.0.0';
|
|
687
|
+
updatedPackageJson.devDependencies['tailwindcss'] = '^4.0.0';
|
|
688
|
+
|
|
689
|
+
updatedPackageJson.scripts['build:css'] = '@tailwindcss/cli -i ./src/input.css -o ./public/css/style.css --minify';
|
|
690
|
+
updatedPackageJson.scripts['watch:css'] = '@tailwindcss/cli -i ./src/input.css -o ./public/css/style.css --watch';
|
|
691
|
+
} else {
|
|
692
|
+
const inputCss = `@tailwind base;
|
|
667
693
|
@tailwind components;
|
|
668
694
|
@tailwind utilities;
|
|
669
695
|
`;
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
|
|
696
|
+
fs.writeFileSync(path.join(projectPath, 'src', 'input.css'), inputCss);
|
|
697
|
+
|
|
698
|
+
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
|
|
674
699
|
module.exports = {
|
|
675
700
|
content: [
|
|
676
701
|
'./pages/**/*.{njk,js}',
|
|
@@ -683,33 +708,25 @@ module.exports = {
|
|
|
683
708
|
plugins: [],
|
|
684
709
|
}
|
|
685
710
|
`;
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const postcssConfig = `module.exports = {
|
|
711
|
+
fs.writeFileSync(path.join(projectPath, 'tailwind.config.js'), tailwindConfig);
|
|
712
|
+
|
|
713
|
+
const postcssConfig = `module.exports = {
|
|
690
714
|
plugins: {
|
|
691
715
|
tailwindcss: {},
|
|
692
716
|
autoprefixer: {},
|
|
693
717
|
},
|
|
694
718
|
}
|
|
695
719
|
`;
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const updatedPackageJson = JSON.parse(fs.readFileSync(path.join(projectPath, 'package.json'), 'utf-8'));
|
|
706
|
-
updatedPackageJson.devDependencies = updatedPackageJson.devDependencies || {};
|
|
707
|
-
updatedPackageJson.devDependencies['tailwindcss'] = '^3.4.1';
|
|
708
|
-
updatedPackageJson.devDependencies['postcss'] = '^8.4.35';
|
|
709
|
-
updatedPackageJson.devDependencies['autoprefixer'] = '^10.4.17';
|
|
720
|
+
fs.writeFileSync(path.join(projectPath, 'postcss.config.js'), postcssConfig);
|
|
721
|
+
|
|
722
|
+
updatedPackageJson.devDependencies['tailwindcss'] = '^3.4.1';
|
|
723
|
+
updatedPackageJson.devDependencies['postcss'] = '^8.4.35';
|
|
724
|
+
updatedPackageJson.devDependencies['autoprefixer'] = '^10.4.17';
|
|
725
|
+
|
|
726
|
+
updatedPackageJson.scripts['build:css'] = 'tailwindcss -i ./src/input.css -o ./public/css/style.css --minify';
|
|
727
|
+
updatedPackageJson.scripts['watch:css'] = 'tailwindcss -i ./src/input.css -o ./public/css/style.css --watch';
|
|
728
|
+
}
|
|
710
729
|
|
|
711
|
-
updatedPackageJson.scripts['build:css'] = 'tailwindcss -i ./src/input.css -o ./public/css/style.css --minify';
|
|
712
|
-
updatedPackageJson.scripts['watch:css'] = 'tailwindcss -i ./src/input.css -o ./public/css/style.css --watch';
|
|
713
730
|
updatedPackageJson.scripts.dev = 'webspresso dev';
|
|
714
731
|
updatedPackageJson.scripts.start = 'npm run build:css && NODE_ENV=production node server.js';
|
|
715
732
|
|
|
@@ -718,7 +735,7 @@ module.exports = {
|
|
|
718
735
|
JSON.stringify(updatedPackageJson, null, 2) + '\n'
|
|
719
736
|
);
|
|
720
737
|
|
|
721
|
-
console.log(
|
|
738
|
+
console.log(`✅ Tailwind CSS ${isV4 ? 'v4' : 'v3'} setup complete!`);
|
|
722
739
|
}
|
|
723
740
|
|
|
724
741
|
// Scaffold AI Agent guidelines unless --no-agents is specified
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - openapi / api:docs Command
|
|
5
|
+
* Generate and export OpenAPI 3 specification from application routes
|
|
6
|
+
* @module bin/commands/openapi
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { scanRoutes } = require('../../src/discovery/scan-routes');
|
|
12
|
+
const { compileRouteTable } = require('../../src/routing/route-table');
|
|
13
|
+
const { buildOpenApiDocument } = require('../../core/openapi/build-from-api-routes');
|
|
14
|
+
|
|
15
|
+
function registerCommand(program) {
|
|
16
|
+
program
|
|
17
|
+
.command('openapi')
|
|
18
|
+
.alias('api:docs')
|
|
19
|
+
.description('Generate and export OpenAPI 3 specification from application routes')
|
|
20
|
+
.option('-o, --out <file>', 'Output JSON file path (e.g. openapi.json or public/openapi.json)')
|
|
21
|
+
.option('-r, --root <dir>', 'Root directory of the project', process.cwd())
|
|
22
|
+
.option('--pages <dir>', 'Pages directory relative to root', 'pages')
|
|
23
|
+
.option('--title <title>', 'OpenAPI specification title override')
|
|
24
|
+
.option('--api-version <version>', 'OpenAPI specification version override')
|
|
25
|
+
.option('--description <desc>', 'OpenAPI specification description override')
|
|
26
|
+
.option('--server <url>', 'Default server URL in spec', 'http://localhost:3000')
|
|
27
|
+
.option('--include-orm', 'Include ORM model schemas in components', false)
|
|
28
|
+
.option('--indent <spaces>', 'JSON indentation spaces', '2')
|
|
29
|
+
.option('-p, --print', 'Print specification JSON to stdout', false)
|
|
30
|
+
.action((options) => {
|
|
31
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
32
|
+
const pagesRelDir = options.pages || 'pages';
|
|
33
|
+
const pagesDir = path.resolve(rootDir, pagesRelDir);
|
|
34
|
+
const apiDir = path.resolve(pagesDir, 'api');
|
|
35
|
+
|
|
36
|
+
if (!fs.existsSync(pagesDir) && !fs.existsSync(apiDir)) {
|
|
37
|
+
console.warn(`[webspresso] Warning: Pages directory not found at: ${pagesDir}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Discover routes
|
|
41
|
+
const descriptors = scanRoutes({
|
|
42
|
+
rootDir,
|
|
43
|
+
pages: fs.existsSync(pagesDir) ? { dir: pagesRelDir } : true,
|
|
44
|
+
api: fs.existsSync(apiDir) ? { dir: path.join(pagesRelDir, 'api') } : true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const routeTable = compileRouteTable(descriptors, { isDev: true });
|
|
48
|
+
const routes = routeTable.list();
|
|
49
|
+
|
|
50
|
+
// Read package.json metadata if available
|
|
51
|
+
let pkg = {};
|
|
52
|
+
const pkgPath = path.join(rootDir, 'package.json');
|
|
53
|
+
if (fs.existsSync(pkgPath)) {
|
|
54
|
+
try {
|
|
55
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
56
|
+
} catch {
|
|
57
|
+
// ignore invalid json
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const docTitle = options.title || pkg.name || 'Webspresso API';
|
|
62
|
+
const docVersion = options.apiVersion || pkg.version || '1.0.0';
|
|
63
|
+
const docDescription =
|
|
64
|
+
options.description ||
|
|
65
|
+
pkg.description ||
|
|
66
|
+
'OpenAPI 3.0 specification generated from Webspresso file-based API routes.';
|
|
67
|
+
|
|
68
|
+
const doc = buildOpenApiDocument({
|
|
69
|
+
routes,
|
|
70
|
+
pagesDir,
|
|
71
|
+
includeOrmSchemas: Boolean(options.includeOrm),
|
|
72
|
+
info: {
|
|
73
|
+
title: docTitle,
|
|
74
|
+
version: docVersion,
|
|
75
|
+
description: docDescription,
|
|
76
|
+
},
|
|
77
|
+
servers: [{ url: String(options.server).replace(/\/$/, '') }],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const indent = parseInt(options.indent, 10) || 2;
|
|
81
|
+
const formattedJson = JSON.stringify(doc, null, indent);
|
|
82
|
+
|
|
83
|
+
if (options.out) {
|
|
84
|
+
const targetPath = path.resolve(rootDir, options.out);
|
|
85
|
+
const targetDir = path.dirname(targetPath);
|
|
86
|
+
if (!fs.existsSync(targetDir)) {
|
|
87
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
fs.writeFileSync(targetPath, formattedJson, 'utf8');
|
|
90
|
+
|
|
91
|
+
const relPath = path.relative(rootDir, targetPath);
|
|
92
|
+
const pathCount = Object.keys(doc.paths || {}).length;
|
|
93
|
+
console.log(`\x1b[32m✔\x1b[0m Generated OpenAPI 3 specification with ${pathCount} path(s) -> \x1b[1m${relPath}\x1b[0m`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!options.out || options.print) {
|
|
97
|
+
console.log(formattedJson);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { registerCommand };
|
package/bin/commands/orm-map.js
CHANGED
|
@@ -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,
|
package/bin/commands/page.js
CHANGED
|
@@ -19,14 +19,17 @@ const path = require('path');
|
|
|
19
19
|
function registerCommand(program) {
|
|
20
20
|
program
|
|
21
21
|
.command('page [route]')
|
|
22
|
+
.alias('make:page')
|
|
23
|
+
.alias('page:make')
|
|
22
24
|
.description('Add a new SSR page (Nunjucks template + companion load() loader)')
|
|
23
25
|
.option('--no-loader', 'Skip creating companion .js loader file')
|
|
24
26
|
.option('--locales', 'Create page-level locale files')
|
|
27
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
25
28
|
.action(async (routeArg, options) => {
|
|
26
|
-
const
|
|
29
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
30
|
+
const pagesDir = path.join(rootDir, 'pages');
|
|
27
31
|
if (!fs.existsSync(pagesDir)) {
|
|
28
|
-
|
|
29
|
-
process.exit(1);
|
|
32
|
+
fs.mkdirSync(pagesDir, { recursive: true });
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
let route = routeArg ? String(routeArg).trim() : '';
|
|
@@ -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}`);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prune old background queue jobs (CLI)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const { loadDbConfig, createDbInstance } = require('../utils/db');
|
|
8
|
+
|
|
9
|
+
function registerCommand(program) {
|
|
10
|
+
program
|
|
11
|
+
.command('queue:prune')
|
|
12
|
+
.description('Delete completed or failed queue jobs older than the given retention window')
|
|
13
|
+
.option('--days <n>', 'Delete jobs older than this many days', '7')
|
|
14
|
+
.option('--status <statuses>', 'Comma-separated statuses to delete (e.g. completed, failed)', 'completed')
|
|
15
|
+
.option('--table <name>', 'Queue table name', 'webspresso_jobs')
|
|
16
|
+
.option('-e, --env <environment>', 'Environment (development, production)', 'development')
|
|
17
|
+
.option('-c, --config <path>', 'Path to database config file')
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
const days = parseInt(options.days, 10);
|
|
20
|
+
if (Number.isNaN(days) || days < 0) {
|
|
21
|
+
console.error('❌ --days must be a non-negative integer');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const statuses = (options.status || 'completed')
|
|
26
|
+
.split(',')
|
|
27
|
+
.map((s) => s.trim())
|
|
28
|
+
.filter(Boolean);
|
|
29
|
+
|
|
30
|
+
const { config, path: configPath } = loadDbConfig(options.config);
|
|
31
|
+
console.log(`\n📦 Using config: ${configPath}`);
|
|
32
|
+
console.log(` Environment: ${options.env}\n`);
|
|
33
|
+
|
|
34
|
+
const knex = await createDbInstance(config, options.env);
|
|
35
|
+
const olderThan = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const { DatabaseQueueAdapter } = require('../../core/queue/adapters/database');
|
|
39
|
+
const adapter = new DatabaseQueueAdapter({
|
|
40
|
+
knex,
|
|
41
|
+
tableName: options.table,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const deleted = await adapter.clean({
|
|
45
|
+
status: statuses,
|
|
46
|
+
olderThan,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
console.log(`✅ Deleted ${deleted} job(s) [status: ${statuses.join(', ')}] with timestamp before ${olderThan.toISOString()}.\n`);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error('❌ queue:prune failed:', err.message);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
} finally {
|
|
54
|
+
await knex.destroy();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { registerCommand };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - routes Command
|
|
5
|
+
* Inspect and display compiled application routes
|
|
6
|
+
* @module bin/commands/routes
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const { scanRoutes } = require('../../src/discovery/scan-routes');
|
|
12
|
+
const { compileRouteTable } = require('../../src/routing/route-table');
|
|
13
|
+
const { formatRouteTable } = require('../../src/dev/banner');
|
|
14
|
+
|
|
15
|
+
function registerCommand(program) {
|
|
16
|
+
program
|
|
17
|
+
.command('routes')
|
|
18
|
+
.description('List and inspect all discovered application routes')
|
|
19
|
+
.option('-a, --api', 'Filter to only API endpoints')
|
|
20
|
+
.option('-p, --pages', 'Filter to only SSR page routes')
|
|
21
|
+
.option('-j, --json', 'Output routes as JSON')
|
|
22
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
23
|
+
.action((options) => {
|
|
24
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
25
|
+
|
|
26
|
+
const descriptors = scanRoutes({
|
|
27
|
+
rootDir,
|
|
28
|
+
pages: fs.existsSync(path.join(rootDir, 'pages')) ? { dir: 'pages' } : true,
|
|
29
|
+
api: fs.existsSync(path.join(rootDir, 'pages', 'api')) ? { dir: 'pages/api' } : true,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const routeTable = compileRouteTable(descriptors, { isDev: true });
|
|
33
|
+
let routes = routeTable.list();
|
|
34
|
+
|
|
35
|
+
if (options.api) {
|
|
36
|
+
routes = routes.filter((r) => r.type === 'api' || (r.path && r.path.startsWith('/api')));
|
|
37
|
+
} else if (options.pages) {
|
|
38
|
+
routes = routes.filter((r) => r.type === 'page' && (!r.path || !r.path.startsWith('/api')));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (options.json) {
|
|
42
|
+
console.log(JSON.stringify(routes, null, 2));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log(formatRouteTable(routes, { limit: 200 }));
|
|
48
|
+
console.log('');
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { registerCommand };
|
package/bin/commands/seed.js
CHANGED
|
@@ -14,7 +14,6 @@ const fs = require('fs');
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const { execSync } = require('child_process');
|
|
16
16
|
const { loadDbConfig } = require('../utils/db');
|
|
17
|
-
const { getSeedFileTemplate } = require('../utils/seed');
|
|
18
17
|
|
|
19
18
|
function registerCommand(program) {
|
|
20
19
|
program
|
|
@@ -136,6 +135,7 @@ function registerCommand(program) {
|
|
|
136
135
|
}
|
|
137
136
|
|
|
138
137
|
// Create seed index file
|
|
138
|
+
const { getSeedFileTemplate } = require('../utils/seed');
|
|
139
139
|
const seedIndex = getSeedFileTemplate();
|
|
140
140
|
|
|
141
141
|
fs.writeFileSync(seedIndexPath, seedIndex);
|
package/bin/commands/service.js
CHANGED
|
@@ -199,6 +199,7 @@ function registerCommand(program) {
|
|
|
199
199
|
.description('Scaffold a new service in services/<domain>/<action>.js')
|
|
200
200
|
.option('-t, --transaction', 'Enable ACID transaction wrapper (transaction: true)')
|
|
201
201
|
.option('-a, --auth <role>', 'Require authentication or specific role')
|
|
202
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
202
203
|
.action(async (nameArg, options) => {
|
|
203
204
|
const fs = require('fs');
|
|
204
205
|
let serviceName = nameArg ? String(nameArg).trim() : '';
|
|
@@ -223,7 +224,8 @@ function registerCommand(program) {
|
|
|
223
224
|
const actionName = parts.pop();
|
|
224
225
|
const domainPath = parts.join(path.sep);
|
|
225
226
|
|
|
226
|
-
const
|
|
227
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
228
|
+
const servicesDir = path.resolve(rootDir, 'services');
|
|
227
229
|
const targetDir = path.join(servicesDir, domainPath);
|
|
228
230
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
229
231
|
|
|
@@ -241,13 +243,14 @@ function registerCommand(program) {
|
|
|
241
243
|
|
|
242
244
|
const content = `'use strict';
|
|
243
245
|
|
|
246
|
+
const { defineService } = require('webspresso');
|
|
244
247
|
const { z } = require('zod');
|
|
245
248
|
|
|
246
249
|
/**
|
|
247
250
|
* Service: ${serviceName}
|
|
248
251
|
* @type {import('webspresso').ServiceDefinition}
|
|
249
252
|
*/
|
|
250
|
-
module.exports = {
|
|
253
|
+
module.exports = defineService({
|
|
251
254
|
// Input validation schema (Zod)
|
|
252
255
|
schema: z.object({
|
|
253
256
|
// Define input properties here
|
|
@@ -262,7 +265,7 @@ module.exports = {
|
|
|
262
265
|
data: input,
|
|
263
266
|
};
|
|
264
267
|
},
|
|
265
|
-
};
|
|
268
|
+
});
|
|
266
269
|
`;
|
|
267
270
|
|
|
268
271
|
fs.writeFileSync(serviceFile, content, 'utf8');
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - typegen / types Command
|
|
5
|
+
* Generate TypeScript type definitions and IDE auto-complete for models, services, and routes
|
|
6
|
+
* @module bin/commands/typegen
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { generateTypes } = require('../../src/dev/typegen');
|
|
12
|
+
|
|
13
|
+
function registerCommand(program) {
|
|
14
|
+
program
|
|
15
|
+
.command('typegen')
|
|
16
|
+
.alias('types')
|
|
17
|
+
.alias('generate:types')
|
|
18
|
+
.description('Generate TypeScript type definitions and IDE auto-complete for models, services, and routes')
|
|
19
|
+
.option('-o, --out <file>', 'Output declaration file path (default: .webspresso/types.d.ts)')
|
|
20
|
+
.option('-r, --root <dir>', 'Root directory of the project', process.cwd())
|
|
21
|
+
.option('--models <dir>', 'Models directory relative to root')
|
|
22
|
+
.option('--services <dir>', 'Services directory relative to root')
|
|
23
|
+
.option('--pages <dir>', 'Pages directory relative to root', 'pages')
|
|
24
|
+
.option('--no-env-file', 'Skip generating webspresso-env.d.ts at project root')
|
|
25
|
+
.option('-w, --watch', 'Watch models, services, and pages directories for changes')
|
|
26
|
+
.option('-s, --silent', 'Suppress console output', false)
|
|
27
|
+
.action((options) => {
|
|
28
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
29
|
+
|
|
30
|
+
const runOnce = () => {
|
|
31
|
+
const startTime = Date.now();
|
|
32
|
+
const result = generateTypes({
|
|
33
|
+
rootDir,
|
|
34
|
+
modelsDir: options.models,
|
|
35
|
+
servicesDir: options.services,
|
|
36
|
+
pagesDir: options.pages,
|
|
37
|
+
outFile: options.out,
|
|
38
|
+
createEnvFile: options.envFile !== false,
|
|
39
|
+
silent: true,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const elapsed = Date.now() - startTime;
|
|
43
|
+
if (!options.silent) {
|
|
44
|
+
const relOut = path.relative(rootDir, result.outFile);
|
|
45
|
+
console.log(`\n⚡ [Webspresso TypeGen] Completed in ${elapsed}ms`);
|
|
46
|
+
console.log(` ✓ Models: ${result.modelsCount}`);
|
|
47
|
+
console.log(` ✓ Services: ${result.servicesCount}`);
|
|
48
|
+
console.log(` ✓ Routes: ${result.routesCount}`);
|
|
49
|
+
console.log(` → Output: ${relOut}`);
|
|
50
|
+
if (result.envFile) {
|
|
51
|
+
console.log(` → Env Ref: ${path.relative(rootDir, result.envFile)}`);
|
|
52
|
+
}
|
|
53
|
+
console.log('');
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
runOnce();
|
|
59
|
+
|
|
60
|
+
// Watch mode
|
|
61
|
+
if (options.watch) {
|
|
62
|
+
if (!options.silent) {
|
|
63
|
+
console.log('👀 Watching models, services, and pages for changes... (Press Ctrl+C to stop)\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const watchDirs = [
|
|
67
|
+
options.models || 'models',
|
|
68
|
+
options.models || 'src/models',
|
|
69
|
+
options.services || 'services',
|
|
70
|
+
options.services || 'src/services',
|
|
71
|
+
options.pages || 'pages',
|
|
72
|
+
options.pages || 'src/pages',
|
|
73
|
+
]
|
|
74
|
+
.map((d) => path.resolve(rootDir, d))
|
|
75
|
+
.filter((d) => fs.existsSync(d));
|
|
76
|
+
|
|
77
|
+
const uniqueDirs = Array.from(new Set(watchDirs));
|
|
78
|
+
let debounceTimer = null;
|
|
79
|
+
|
|
80
|
+
for (const dir of uniqueDirs) {
|
|
81
|
+
try {
|
|
82
|
+
fs.watch(dir, { recursive: true }, (eventType, filename) => {
|
|
83
|
+
if (
|
|
84
|
+
filename &&
|
|
85
|
+
(filename.endsWith('.js') || filename.endsWith('.ts') || filename.endsWith('.njk'))
|
|
86
|
+
) {
|
|
87
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
88
|
+
debounceTimer = setTimeout(() => {
|
|
89
|
+
if (!options.silent) {
|
|
90
|
+
console.log(`🔄 Detected change in ${filename}, regenerating types...`);
|
|
91
|
+
}
|
|
92
|
+
runOnce();
|
|
93
|
+
}, 150);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
} catch {
|
|
97
|
+
// Watch unsupported on platform or directory
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
registerCommand,
|
|
106
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prune unreferenced / orphaned uploaded files (CLI)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { loadDbConfig, createDbInstance } = require('../utils/db');
|
|
9
|
+
const { pruneOrphanedUploads } = require('../../plugins/upload/orphan-pruner');
|
|
10
|
+
|
|
11
|
+
function registerCommand(program) {
|
|
12
|
+
program
|
|
13
|
+
.command('upload:prune')
|
|
14
|
+
.description('Scan and delete orphaned files in the uploads directory not referenced by any database model')
|
|
15
|
+
.option('--dir <path>', 'Custom uploads directory path (default: public/uploads)')
|
|
16
|
+
.option('--url-base <path>', 'Public URL base path (default: /uploads)', '/uploads')
|
|
17
|
+
.option('--dry-run', 'Scan and list orphaned files without deleting them', false)
|
|
18
|
+
.option('-e, --env <environment>', 'Environment (development, production)', 'development')
|
|
19
|
+
.option('-c, --config <path>', 'Path to database config file')
|
|
20
|
+
.action(async (options) => {
|
|
21
|
+
const { config, path: configPath } = loadDbConfig(options.config);
|
|
22
|
+
console.log(`\n📦 Using config: ${configPath}`);
|
|
23
|
+
console.log(` Environment: ${options.env}\n`);
|
|
24
|
+
|
|
25
|
+
const knex = await createDbInstance(config, options.env);
|
|
26
|
+
const db = { knex };
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const destDir = options.dir
|
|
30
|
+
? path.resolve(process.cwd(), options.dir)
|
|
31
|
+
: path.resolve(process.cwd(), 'public', 'uploads');
|
|
32
|
+
|
|
33
|
+
console.log(`🔍 Scanning directory: ${destDir}`);
|
|
34
|
+
console.log(` Public URL base: ${options.urlBase}`);
|
|
35
|
+
if (options.dryRun) {
|
|
36
|
+
console.log(` Mode: DRY RUN (no files will be deleted)\n`);
|
|
37
|
+
} else {
|
|
38
|
+
console.log(` Mode: PRUNE & DELETE\n`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const result = await pruneOrphanedUploads({
|
|
42
|
+
db,
|
|
43
|
+
destDir,
|
|
44
|
+
publicBasePath: options.urlBase,
|
|
45
|
+
dryRun: options.dryRun,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log(`📊 Scan Summary:`);
|
|
49
|
+
console.log(` Total files on disk: ${result.totalFiles} (${result.totalBytesFormatted})`);
|
|
50
|
+
console.log(` Referenced by DB: ${result.referencedCount}`);
|
|
51
|
+
console.log(` Orphaned files: ${result.orphanedCount} (${result.orphanedBytesFormatted})\n`);
|
|
52
|
+
|
|
53
|
+
if (result.orphanedCount === 0) {
|
|
54
|
+
console.log(`✅ No orphaned files found. Storage is clean!\n`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (options.dryRun) {
|
|
59
|
+
console.log(`ℹ️ [DRY RUN] The following ${result.orphanedCount} file(s) would be deleted:`);
|
|
60
|
+
for (const file of result.orphanedFiles.slice(0, 20)) {
|
|
61
|
+
console.log(` - ${file.relativePath} (${file.publicUrl})`);
|
|
62
|
+
}
|
|
63
|
+
if (result.orphanedFiles.length > 20) {
|
|
64
|
+
console.log(` ... and ${result.orphanedFiles.length - 20} more file(s)`);
|
|
65
|
+
}
|
|
66
|
+
console.log(`\n💡 To permanently delete these files, run without --dry-run.\n`);
|
|
67
|
+
} else {
|
|
68
|
+
console.log(`🗑️ Successfully deleted ${result.deletedCount} orphaned file(s) (${result.deletedBytesFormatted} reclaimed).\n`);
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error('❌ upload:prune failed:', err.message);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
} finally {
|
|
74
|
+
await knex.destroy();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { registerCommand };
|