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.
Files changed (121) hide show
  1. package/.agents/ADMIN.md +153 -5
  2. package/.agents/ARCHITECTURE.md +10 -0
  3. package/.agents/CLI.md +195 -1
  4. package/.agents/PLUGINS.md +153 -9
  5. package/.agents/QUEUE.md +43 -0
  6. package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +122 -0
  7. package/.agents/skills/webspresso-framework/SKILL.md +56 -0
  8. package/.agents/skills/webspresso-orm-and-database/SKILL.md +119 -0
  9. package/.agents/skills/webspresso-routing-and-api/SKILL.md +114 -0
  10. package/.agents/skills/webspresso-services/SKILL.md +101 -0
  11. package/bin/commands/add-esbuild.js +80 -0
  12. package/bin/commands/add-tailwind.js +76 -31
  13. package/bin/commands/admin-password.js +2 -1
  14. package/bin/commands/admin-setup.js +1 -1
  15. package/bin/commands/agents-init.js +51 -11
  16. package/bin/commands/analytics-prune.js +57 -0
  17. package/bin/commands/api.js +207 -15
  18. package/bin/commands/audit-prune.js +1 -1
  19. package/bin/commands/build.js +99 -0
  20. package/bin/commands/dev.js +64 -35
  21. package/bin/commands/doctor.js +26 -0
  22. package/bin/commands/email-prune.js +47 -0
  23. package/bin/commands/favicon-generate.js +1 -1
  24. package/bin/commands/make-job.js +86 -0
  25. package/bin/commands/make-model.js +250 -0
  26. package/bin/commands/new.js +45 -28
  27. package/bin/commands/openapi.js +102 -0
  28. package/bin/commands/orm-map.js +5 -4
  29. package/bin/commands/page.js +6 -3
  30. package/bin/commands/polar-migrate.js +1 -1
  31. package/bin/commands/queue-prune.js +59 -0
  32. package/bin/commands/routes.js +52 -0
  33. package/bin/commands/seed.js +1 -1
  34. package/bin/commands/service.js +6 -3
  35. package/bin/commands/typegen.js +106 -0
  36. package/bin/commands/upload-prune.js +79 -0
  37. package/bin/webspresso.js +22 -0
  38. package/core/applySchema.js +13 -6
  39. package/core/auth/hash.js +31 -17
  40. package/core/compression/middleware.js +23 -17
  41. package/core/openapi/build-from-api-routes.js +160 -29
  42. package/core/orm/cache/fingerprint.js +15 -1
  43. package/core/orm/cache/memory-provider.js +27 -2
  44. package/core/orm/index.js +12 -7
  45. package/core/orm/model.js +49 -7
  46. package/core/queue/adapters/database.js +94 -0
  47. package/core/queue/adapters/memory.js +62 -0
  48. package/core/queue/adapters/redis.js +80 -0
  49. package/core/queue/manager.js +123 -0
  50. package/core/ssr/stream.js +11 -0
  51. package/index.d.ts +660 -10
  52. package/index.js +44 -96
  53. package/package.json +32 -12
  54. package/plugins/admin-panel/api.js +279 -17
  55. package/plugins/admin-panel/app.js +124 -31
  56. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
  57. package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
  58. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +10 -5
  59. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
  60. package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
  61. package/plugins/admin-panel/client/parts/08-record-list.js +243 -23
  62. package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
  63. package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
  64. package/plugins/admin-panel/core/admin-module.js +13 -7
  65. package/plugins/admin-panel/core/api-extensions.js +34 -2
  66. package/plugins/admin-panel/core/registry.js +96 -16
  67. package/plugins/admin-panel/index.js +96 -0
  68. package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
  69. package/plugins/admin-panel/modules/admin-users.js +16 -1
  70. package/plugins/admin-panel/modules/bulk-actions.js +13 -5
  71. package/plugins/admin-panel/modules/custom-pages.js +492 -3
  72. package/plugins/admin-panel/modules/dashboard.js +283 -0
  73. package/plugins/admin-panel/modules/menu.js +56 -18
  74. package/plugins/admin-panel/modules/user-management.js +24 -6
  75. package/plugins/audit-log/admin-component.js +11 -0
  76. package/plugins/audit-log/api-handlers.js +34 -0
  77. package/plugins/audit-log/index.js +129 -3
  78. package/plugins/audit-log/purge.js +60 -10
  79. package/plugins/basic-auth/index.js +3 -1
  80. package/plugins/content/client/inline-edit.js +12 -12
  81. package/plugins/cors.js +6 -7
  82. package/plugins/dashboard/app.js +152 -27
  83. package/plugins/dashboard/index.js +143 -39
  84. package/plugins/dashboard/styles.js +78 -0
  85. package/plugins/data-exchange/export-xlsx.js +9 -2
  86. package/plugins/data-exchange/import.js +9 -2
  87. package/plugins/data-exchange/index.js +6 -3
  88. package/plugins/email/index.js +132 -2
  89. package/plugins/email/purge.js +60 -10
  90. package/plugins/file-manager/core.js +1 -1
  91. package/plugins/file-manager/security.js +33 -23
  92. package/plugins/index.js +49 -87
  93. package/plugins/mcp/transports/sse.js +4 -2
  94. package/plugins/orm-cache-admin/index.js +4 -2
  95. package/plugins/queue/index.js +11 -0
  96. package/plugins/schema-explorer/admin-component.js +714 -0
  97. package/plugins/schema-explorer/relations-graph.js +205 -0
  98. package/plugins/schema-explorer.js +248 -69
  99. package/plugins/site-analytics/admin-component.js +30 -8
  100. package/plugins/site-analytics/api-handlers.js +42 -0
  101. package/plugins/site-analytics/index.js +147 -2
  102. package/plugins/site-analytics/purge.js +168 -0
  103. package/plugins/swagger.js +261 -13
  104. package/plugins/upload/index.js +26 -0
  105. package/plugins/upload/orphan-pruner.js +292 -0
  106. package/src/api/api-loader.js +3 -1
  107. package/src/bundle/index.js +517 -0
  108. package/src/bundle/manifest.js +165 -0
  109. package/src/dev/banner.js +296 -0
  110. package/src/dev/colors.js +70 -0
  111. package/src/dev/index.js +62 -0
  112. package/src/dev/profiler.js +354 -0
  113. package/src/dev/typegen.js +497 -0
  114. package/src/dev-reload/index.js +611 -0
  115. package/src/file-router.js +213 -63
  116. package/src/helpers.js +958 -98
  117. package/src/njk-frontmatter.js +12 -0
  118. package/src/pages/page-loader.js +61 -11
  119. package/src/server.js +629 -63
  120. package/src/services/discovery.js +6 -1
  121. package/src/services/index.js +6 -13
@@ -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
- console.log('\n🎨 Setting up Tailwind CSS...\n');
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 input.css
666
- const inputCss = `@tailwind base;
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
- fs.writeFileSync(path.join(projectPath, 'src', 'input.css'), inputCss);
671
-
672
- // Create tailwind.config.js
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
- fs.writeFileSync(path.join(projectPath, 'tailwind.config.js'), tailwindConfig);
687
-
688
- // Create postcss.config.js
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
- fs.writeFileSync(path.join(projectPath, 'postcss.config.js'), postcssConfig);
697
-
698
- // Create public/css directory
699
- fs.mkdirSync(path.join(projectPath, 'public', 'css'), { recursive: true });
700
-
701
- // Create placeholder CSS (will be replaced by build)
702
- fs.writeFileSync(path.join(projectPath, 'public', 'css', 'style.css'), '/* Run npm run build:css */\n');
703
-
704
- // Update package.json
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('✅ Tailwind CSS setup complete!');
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 };
@@ -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,
@@ -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 pagesDir = path.resolve(process.cwd(), 'pages');
29
+ const rootDir = path.resolve(options.root || process.cwd());
30
+ const pagesDir = path.join(rootDir, 'pages');
27
31
  if (!fs.existsSync(pagesDir)) {
28
- console.error('❌ Not a Webspresso project! (pages/ directory not found)');
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 };
@@ -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);
@@ -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 servicesDir = path.resolve(process.cwd(), 'services');
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 };