webspresso 0.0.100 → 0.0.102
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 +47 -4
- package/.agents/AGENTS.md +3 -1
- package/.agents/ARCHITECTURE.md +11 -0
- package/.agents/AUTH.md +63 -0
- package/.agents/CLI.md +81 -0
- package/.agents/CONVENTIONS.md +4 -0
- package/.agents/EMAIL.md +84 -2
- package/.agents/EVENTS.md +179 -0
- package/.agents/ORM.md +59 -0
- package/.agents/PLUGINS.md +32 -6
- package/.agents/ROUTING.md +163 -3
- package/.agents/skills/webspresso-framework/SKILL.md +32 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +4 -4
- package/AGENTS.md +7 -0
- package/README.md +18 -5
- package/bin/commands/add-tailwind.js +10 -0
- package/bin/commands/build.js +34 -0
- package/bin/commands/db-makemigrations.js +83 -0
- package/bin/commands/list.js +96 -0
- package/bin/commands/make-command.js +93 -0
- package/bin/commands/make-event.js +80 -0
- package/bin/commands/make-factory.js +72 -0
- package/bin/commands/make-mail.js +93 -0
- package/bin/commands/make-policy.js +106 -0
- package/bin/commands/routes.js +8 -0
- package/bin/commands/run.js +195 -0
- package/bin/webspresso.js +17 -0
- package/core/applySchema.js +18 -1
- package/core/auth/bouncer.js +196 -0
- package/core/auth/index.js +10 -0
- package/core/auth/middleware.js +15 -1
- package/core/commands/define-command.js +48 -0
- package/core/commands/index.js +18 -0
- package/core/commands/registry.js +156 -0
- package/core/commands/runner.js +76 -0
- package/core/compression/compressible.js +1 -1
- package/core/compression/middleware.js +35 -251
- package/core/content/service.js +8 -2
- package/core/events/discovery.js +142 -0
- package/core/events/dispatcher.js +552 -0
- package/core/events/index.js +23 -0
- package/core/events/signal.js +92 -0
- package/core/forms/flash.js +234 -0
- package/core/forms/form-builder.js +214 -0
- package/core/forms/index.js +16 -0
- package/core/mail/index.js +23 -0
- package/core/mail/mailable.js +250 -0
- package/core/mail/mailer.js +184 -0
- package/core/mail/transports.js +238 -0
- package/core/orm/factory.js +250 -0
- package/core/orm/index.js +6 -0
- package/core/orm/migrations/diff.js +185 -0
- package/core/orm/migrations/index.js +22 -4
- package/core/orm/model.js +4 -0
- package/index.d.ts +422 -1
- package/index.js +111 -16
- package/package.json +9 -5
- package/plugins/admin-panel/api.js +509 -3
- package/plugins/admin-panel/index.js +29 -0
- package/plugins/content/models/content-entry.js +4 -3
- package/plugins/content/models/content-type.js +4 -3
- package/plugins/cors.js +2 -2
- package/plugins/csrf/index.js +1 -1
- package/plugins/email/index.js +48 -0
- package/plugins/email/preview-handler.js +555 -0
- package/plugins/email/service.js +23 -12
- package/plugins/email/transport.js +30 -11
- package/plugins/file-manager/index.js +13 -0
- package/plugins/index.js +2 -0
- package/plugins/lazy.js +258 -0
- package/plugins/redirect/index.js +1 -1
- package/src/api/api-loader.js +18 -2
- package/src/api/validation-i18n.js +231 -0
- package/src/app-context.js +48 -6
- package/src/dev-reload/index.js +9 -1
- package/src/discovery/route-manifest.js +197 -0
- package/src/discovery/scan-routes.js +80 -5
- package/src/file-router.js +153 -91
- package/src/helpers.js +143 -0
- package/src/njk-frontmatter.js +17 -1
- package/src/pages/page-loader.js +1 -1
- package/src/plugin-manager.js +12 -0
- package/src/routing/cascading.js +14 -10
- package/src/routing/route-table.js +1 -1
- package/src/server.js +243 -34
- package/src/services/builtins/system.js +4 -6
- package/src/services/registry.js +12 -2
- package/src/template/precompile.js +247 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - make:command Command
|
|
5
|
+
* Scaffolds a custom management command in commands/<name>.js
|
|
6
|
+
* @module bin/commands/make-command
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
function generateCommandContent(commandName, description = '') {
|
|
13
|
+
return `'use strict';
|
|
14
|
+
|
|
15
|
+
const { defineCommand } = require('webspresso');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Custom Management Command: ${commandName}
|
|
19
|
+
* Run via: npx webspresso run ${commandName}
|
|
20
|
+
*/
|
|
21
|
+
module.exports = defineCommand({
|
|
22
|
+
name: '${commandName}',
|
|
23
|
+
description: '${description || `Execute ${commandName} task`}',
|
|
24
|
+
options: [
|
|
25
|
+
{ flags: '-d, --dry-run', description: 'Run without making persistent changes', defaultValue: false },
|
|
26
|
+
],
|
|
27
|
+
handle: async ({ db, queue, services, mailer, events, logger, args, flags }) => {
|
|
28
|
+
logger.log('Executing "${commandName}"...');
|
|
29
|
+
if (flags.dryRun) {
|
|
30
|
+
logger.log('[Dry Run] No actual changes will be committed.');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Business logic goes here:
|
|
34
|
+
// e.g. const users = await db.getRepository('User').find({ active: false });
|
|
35
|
+
|
|
36
|
+
return { success: true };
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function registerCommand(program) {
|
|
43
|
+
program
|
|
44
|
+
.command('make:command <name>')
|
|
45
|
+
.alias('command:make')
|
|
46
|
+
.description('Scaffold a custom management command in commands/<name>.js')
|
|
47
|
+
.option('-d, --desc <description>', 'Command description', '')
|
|
48
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
49
|
+
.action((nameArg, options) => {
|
|
50
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
51
|
+
const commandsDir = path.join(rootDir, 'commands');
|
|
52
|
+
|
|
53
|
+
if (!fs.existsSync(commandsDir)) {
|
|
54
|
+
fs.mkdirSync(commandsDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const commandName = String(nameArg || '').trim();
|
|
58
|
+
if (!commandName) {
|
|
59
|
+
console.error('❌ Command name is required.');
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Convert colons or slashes to subdirectories or file path
|
|
65
|
+
// e.g. "users:prune" -> commands/users/prune.js or commands/users-prune.js
|
|
66
|
+
const relPath = commandName.includes(':')
|
|
67
|
+
? commandName.replace(/:/g, '/') + '.js'
|
|
68
|
+
: `${commandName}.js`;
|
|
69
|
+
|
|
70
|
+
const targetFilePath = path.join(commandsDir, relPath);
|
|
71
|
+
const targetDir = path.dirname(targetFilePath);
|
|
72
|
+
|
|
73
|
+
if (!fs.existsSync(targetDir)) {
|
|
74
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (fs.existsSync(targetFilePath)) {
|
|
78
|
+
console.error(`❌ Command file already exists: ${path.relative(rootDir, targetFilePath)}`);
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const content = generateCommandContent(commandName, options.desc);
|
|
84
|
+
fs.writeFileSync(targetFilePath, content, 'utf-8');
|
|
85
|
+
console.log(`\n✅ Created custom management command: ${path.relative(rootDir, targetFilePath)}`);
|
|
86
|
+
console.log(` Execute it with: npx webspresso run ${commandName}\n`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
registerCommand,
|
|
92
|
+
generateCommandContent,
|
|
93
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - make:event Command
|
|
5
|
+
* Scaffolds an event / signal listener in listeners/<name>.js
|
|
6
|
+
* @module bin/commands/make-event
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
function generateEventContent(eventName) {
|
|
13
|
+
return `'use strict';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Event Listener: ${eventName}
|
|
17
|
+
* Triggered by: app.events.emit('${eventName}', payload)
|
|
18
|
+
*/
|
|
19
|
+
module.exports = {
|
|
20
|
+
event: '${eventName}',
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Handle incoming event
|
|
24
|
+
* @param {Object} payload - Event payload
|
|
25
|
+
* @param {Object} ctx - Injected context { events, db, queue, mailer, logger }
|
|
26
|
+
*/
|
|
27
|
+
async handle(payload, ctx = {}) {
|
|
28
|
+
const { logger, mailer, queue } = ctx;
|
|
29
|
+
if (logger) {
|
|
30
|
+
logger.log('[Event:${eventName}] Received event payload:', payload);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Example: Dispatch background job or send notification email
|
|
34
|
+
// if (queue) await queue.dispatch('notify', payload);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function registerCommand(program) {
|
|
41
|
+
program
|
|
42
|
+
.command('make:event <name>')
|
|
43
|
+
.alias('event:make')
|
|
44
|
+
.description('Scaffold an event listener in listeners/<name>.js')
|
|
45
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
46
|
+
.action((nameArg, options) => {
|
|
47
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
48
|
+
const listenersDir = path.join(rootDir, 'listeners');
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(listenersDir)) {
|
|
51
|
+
fs.mkdirSync(listenersDir, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rawName = String(nameArg || '').trim();
|
|
55
|
+
if (!rawName) {
|
|
56
|
+
console.error('❌ Event name is required.');
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const eventName = rawName.replace(/\.js$/, '');
|
|
62
|
+
const fileName = eventName.replace(/:/g, '-');
|
|
63
|
+
const targetFilePath = path.join(listenersDir, `${fileName}.js`);
|
|
64
|
+
|
|
65
|
+
if (fs.existsSync(targetFilePath)) {
|
|
66
|
+
console.error(`❌ Listener file already exists: ${path.relative(rootDir, targetFilePath)}`);
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const content = generateEventContent(eventName);
|
|
72
|
+
fs.writeFileSync(targetFilePath, content, 'utf-8');
|
|
73
|
+
console.log(`\n✅ Created event listener: ${path.relative(rootDir, targetFilePath)} (event: "${eventName}")\n`);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
registerCommand,
|
|
79
|
+
generateEventContent,
|
|
80
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - make:factory Command
|
|
5
|
+
* Scaffolds an ORM model factory in factories/<name>.factory.js
|
|
6
|
+
* @module bin/commands/make-factory
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
function generateFactoryContent(modelName) {
|
|
13
|
+
const cleanName = modelName.charAt(0).toUpperCase() + modelName.slice(1);
|
|
14
|
+
|
|
15
|
+
return `'use strict';
|
|
16
|
+
|
|
17
|
+
const { defineFactory } = require('webspresso');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Model Factory: ${cleanName}
|
|
21
|
+
* Used in tests and seeders:
|
|
22
|
+
* const ${cleanName.toLowerCase()} = await ${cleanName}Factory.create({ status: 'active' });
|
|
23
|
+
*/
|
|
24
|
+
module.exports = defineFactory('${cleanName}', ({ faker, seq }) => ({
|
|
25
|
+
title: faker ? faker.lorem.words(3) : \`${cleanName} \${seq}\`,
|
|
26
|
+
status: 'active',
|
|
27
|
+
createdAt: new Date(),
|
|
28
|
+
updatedAt: new Date(),
|
|
29
|
+
}));
|
|
30
|
+
`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function registerCommand(program) {
|
|
34
|
+
program
|
|
35
|
+
.command('make:factory <name>')
|
|
36
|
+
.alias('factory:make')
|
|
37
|
+
.description('Scaffold an ORM model factory in factories/<name>.factory.js')
|
|
38
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
39
|
+
.action((nameArg, options) => {
|
|
40
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
41
|
+
const factoriesDir = path.join(rootDir, 'factories');
|
|
42
|
+
|
|
43
|
+
if (!fs.existsSync(factoriesDir)) {
|
|
44
|
+
fs.mkdirSync(factoriesDir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const rawName = String(nameArg || '').trim();
|
|
48
|
+
if (!rawName) {
|
|
49
|
+
console.error('❌ Model name is required for factory.');
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const cleanName = rawName.replace(/\.factory(\.js)?$/, '').replace(/\.js$/, '');
|
|
55
|
+
const targetFilePath = path.join(factoriesDir, `${cleanName.toLowerCase()}.factory.js`);
|
|
56
|
+
|
|
57
|
+
if (fs.existsSync(targetFilePath)) {
|
|
58
|
+
console.error(`❌ Factory file already exists: ${path.relative(rootDir, targetFilePath)}`);
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const content = generateFactoryContent(cleanName);
|
|
64
|
+
fs.writeFileSync(targetFilePath, content, 'utf-8');
|
|
65
|
+
console.log(`\n✅ Created model factory: ${path.relative(rootDir, targetFilePath)}\n`);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
registerCommand,
|
|
71
|
+
generateFactoryContent,
|
|
72
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - make:mail Command
|
|
5
|
+
* Scaffolds a declarative Mailable class in emails/<name>.js
|
|
6
|
+
* @module bin/commands/make-mail
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
function generateMailContent(rawName) {
|
|
13
|
+
const baseName = rawName.replace(/email$/i, '').replace(/mail$/i, '').trim();
|
|
14
|
+
const cleanBase = baseName.charAt(0).toUpperCase() + baseName.slice(1);
|
|
15
|
+
const className = `${cleanBase}Email`;
|
|
16
|
+
const tplName = cleanBase.toLowerCase();
|
|
17
|
+
|
|
18
|
+
return `'use strict';
|
|
19
|
+
|
|
20
|
+
const { Mailable } = require('webspresso');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Mailable: ${className}
|
|
24
|
+
* Send via: req.mailer.send(new ${className}(user))
|
|
25
|
+
*/
|
|
26
|
+
class ${className} extends Mailable {
|
|
27
|
+
constructor(recipient, data = {}) {
|
|
28
|
+
super();
|
|
29
|
+
this.recipient = recipient;
|
|
30
|
+
this.data = data;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build email definition
|
|
35
|
+
*/
|
|
36
|
+
build() {
|
|
37
|
+
const toAddress = typeof this.recipient === 'string' ? this.recipient : this.recipient?.email;
|
|
38
|
+
const toName = typeof this.recipient === 'object' ? this.recipient?.name : undefined;
|
|
39
|
+
|
|
40
|
+
this.to(toAddress, toName)
|
|
41
|
+
.subject('Hello from Webspresso')
|
|
42
|
+
.view('${tplName}', this.data);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
${className},
|
|
48
|
+
};
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function registerCommand(program) {
|
|
53
|
+
program
|
|
54
|
+
.command('make:mail <name>')
|
|
55
|
+
.alias('mail:make')
|
|
56
|
+
.alias('make:mailable')
|
|
57
|
+
.description('Scaffold a declarative Mailable class in emails/<name>.js')
|
|
58
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
59
|
+
.action((nameArg, options) => {
|
|
60
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
61
|
+
const emailsDir = path.join(rootDir, 'emails');
|
|
62
|
+
|
|
63
|
+
if (!fs.existsSync(emailsDir)) {
|
|
64
|
+
fs.mkdirSync(emailsDir, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const rawName = String(nameArg || '').trim();
|
|
68
|
+
if (!rawName) {
|
|
69
|
+
console.error('❌ Mailable name is required.');
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const cleanName = rawName.replace(/\.js$/, '');
|
|
75
|
+
const fileName = cleanName.endsWith('Email') ? cleanName : `${cleanName}Email`;
|
|
76
|
+
const targetFilePath = path.join(emailsDir, `${fileName}.js`);
|
|
77
|
+
|
|
78
|
+
if (fs.existsSync(targetFilePath)) {
|
|
79
|
+
console.error(`❌ Mailable file already exists: ${path.relative(rootDir, targetFilePath)}`);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const content = generateMailContent(cleanName);
|
|
85
|
+
fs.writeFileSync(targetFilePath, content, 'utf-8');
|
|
86
|
+
console.log(`\n✅ Created Mailable class: ${path.relative(rootDir, targetFilePath)}\n`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
registerCommand,
|
|
92
|
+
generateMailContent,
|
|
93
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - make:policy Command
|
|
5
|
+
* Scaffolds an authorization policy in policies/<name>.policy.js
|
|
6
|
+
* @module bin/commands/make-policy
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
function generatePolicyContent(policyName) {
|
|
13
|
+
const modelName = policyName.replace(/policy$/i, '').trim();
|
|
14
|
+
const cleanName = modelName.charAt(0).toUpperCase() + modelName.slice(1);
|
|
15
|
+
|
|
16
|
+
return `'use strict';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Authorization Policy: ${cleanName}
|
|
20
|
+
* Invoked via req.bouncer.authorize('${cleanName.toLowerCase()}.update', record)
|
|
21
|
+
* or {% if fsy.can('${cleanName.toLowerCase()}.update', record) %}
|
|
22
|
+
*/
|
|
23
|
+
module.exports = {
|
|
24
|
+
name: '${cleanName.toLowerCase()}',
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Determine whether the user can view any records
|
|
28
|
+
*/
|
|
29
|
+
async viewAny(user) {
|
|
30
|
+
return true;
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Determine whether the user can view the specific record
|
|
35
|
+
*/
|
|
36
|
+
async view(user, record) {
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Determine whether the user can create records
|
|
42
|
+
*/
|
|
43
|
+
async create(user) {
|
|
44
|
+
return Boolean(user);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Determine whether the user can update the record
|
|
49
|
+
*/
|
|
50
|
+
async update(user, record) {
|
|
51
|
+
if (!user) return false;
|
|
52
|
+
if (user.isAdmin || user.role === 'admin') return true;
|
|
53
|
+
return record && (record.userId === user.id || record.user_id === user.id);
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Determine whether the user can delete the record
|
|
58
|
+
*/
|
|
59
|
+
async delete(user, record) {
|
|
60
|
+
if (!user) return false;
|
|
61
|
+
return Boolean(user.isAdmin || user.role === 'admin');
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function registerCommand(program) {
|
|
68
|
+
program
|
|
69
|
+
.command('make:policy <name>')
|
|
70
|
+
.alias('policy:make')
|
|
71
|
+
.description('Scaffold an authorization policy in policies/<name>.policy.js')
|
|
72
|
+
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
73
|
+
.action((nameArg, options) => {
|
|
74
|
+
const rootDir = path.resolve(options.root || process.cwd());
|
|
75
|
+
const policiesDir = path.join(rootDir, 'policies');
|
|
76
|
+
|
|
77
|
+
if (!fs.existsSync(policiesDir)) {
|
|
78
|
+
fs.mkdirSync(policiesDir, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const rawName = String(nameArg || '').trim();
|
|
82
|
+
if (!rawName) {
|
|
83
|
+
console.error('❌ Policy name is required.');
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const policyBase = rawName.replace(/\.policy(\.js)?$/, '').replace(/\.js$/, '');
|
|
89
|
+
const targetFilePath = path.join(policiesDir, `${policyBase}.policy.js`);
|
|
90
|
+
|
|
91
|
+
if (fs.existsSync(targetFilePath)) {
|
|
92
|
+
console.error(`❌ Policy file already exists: ${path.relative(rootDir, targetFilePath)}`);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const content = generatePolicyContent(policyBase);
|
|
98
|
+
fs.writeFileSync(targetFilePath, content, 'utf-8');
|
|
99
|
+
console.log(`\n✅ Created authorization policy: ${path.relative(rootDir, targetFilePath)}\n`);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
registerCommand,
|
|
105
|
+
generatePolicyContent,
|
|
106
|
+
};
|
package/bin/commands/routes.js
CHANGED
|
@@ -19,10 +19,18 @@ function registerCommand(program) {
|
|
|
19
19
|
.option('-a, --api', 'Filter to only API endpoints')
|
|
20
20
|
.option('-p, --pages', 'Filter to only SSR page routes')
|
|
21
21
|
.option('-j, --json', 'Output routes as JSON')
|
|
22
|
+
.option('-m, --manifest', 'Generate or update .webspresso/routes.manifest.json')
|
|
22
23
|
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
23
24
|
.action((options) => {
|
|
24
25
|
const rootDir = path.resolve(options.root || process.cwd());
|
|
25
26
|
|
|
27
|
+
if (options.manifest) {
|
|
28
|
+
const { generateRouteManifest } = require('../../src/discovery/route-manifest');
|
|
29
|
+
const result = generateRouteManifest({ rootDir });
|
|
30
|
+
console.log(`✅ AOT route manifest written to: ${path.relative(rootDir, result.manifestPath)} (${result.routeCount} routes)`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
const descriptors = scanRoutes({
|
|
27
35
|
rootDir,
|
|
28
36
|
pages: fs.existsSync(path.join(rootDir, 'pages')) ? { dir: 'pages' } : true,
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso CLI - run Command
|
|
5
|
+
* Executes custom management commands with injected framework context
|
|
6
|
+
* @module bin/commands/run
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const { createCommandRegistry, executeCommand } = require('../../core/commands');
|
|
12
|
+
const { createEventDispatcher } = require('../../core/events');
|
|
13
|
+
const { createMailer } = require('../../core/mail');
|
|
14
|
+
const { resolveDbConfigIfExists, createDbInstance } = require('../utils/db');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Helper to parse unknown CLI options into a flags object
|
|
18
|
+
* e.g. ['--dry-run', '--limit', '50', 'arg1'] -> flags: { dryRun: true, limit: 50 }, args: ['arg1']
|
|
19
|
+
* @param {string[]} rawArgs
|
|
20
|
+
* @returns {{ flags: Object, args: string[] }}
|
|
21
|
+
*/
|
|
22
|
+
function parseRawArgs(rawArgs = []) {
|
|
23
|
+
const flags = {};
|
|
24
|
+
const args = [];
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
27
|
+
const item = rawArgs[i];
|
|
28
|
+
if (item.startsWith('--')) {
|
|
29
|
+
const withoutDashes = item.slice(2);
|
|
30
|
+
if (withoutDashes.includes('=')) {
|
|
31
|
+
const [k, v] = withoutDashes.split('=');
|
|
32
|
+
flags[camelCase(k)] = parseVal(v);
|
|
33
|
+
} else {
|
|
34
|
+
const next = rawArgs[i + 1];
|
|
35
|
+
if (next && !next.startsWith('-')) {
|
|
36
|
+
flags[camelCase(withoutDashes)] = parseVal(next);
|
|
37
|
+
i++;
|
|
38
|
+
} else {
|
|
39
|
+
flags[camelCase(withoutDashes)] = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} else if (item.startsWith('-') && item.length === 2) {
|
|
43
|
+
const flagChar = item.slice(1);
|
|
44
|
+
const next = rawArgs[i + 1];
|
|
45
|
+
if (next && !next.startsWith('-')) {
|
|
46
|
+
flags[flagChar] = parseVal(next);
|
|
47
|
+
i++;
|
|
48
|
+
} else {
|
|
49
|
+
flags[flagChar] = true;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
args.push(item);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { flags, args };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function camelCase(str) {
|
|
60
|
+
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseVal(val) {
|
|
64
|
+
if (val === 'true') return true;
|
|
65
|
+
if (val === 'false') return false;
|
|
66
|
+
if (val === 'null') return null;
|
|
67
|
+
if (!Number.isNaN(Number(val)) && val.trim() !== '') return Number(val);
|
|
68
|
+
return val;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function registerCommand(program) {
|
|
72
|
+
program
|
|
73
|
+
.command('run [commandName] [args...]')
|
|
74
|
+
.alias('command:run')
|
|
75
|
+
.description('Run a custom management command from commands/ with injected framework context')
|
|
76
|
+
.option('-d, --dir <path>', 'Custom commands directory (default: ./commands)')
|
|
77
|
+
.option('-c, --config <path>', 'Path to database config file (optional)')
|
|
78
|
+
.option('-E, --env <environment>', 'Environment name (default: development)')
|
|
79
|
+
.allowUnknownOption(true)
|
|
80
|
+
.action(async (commandName, rawArgs = [], options = {}) => {
|
|
81
|
+
const rootDir = process.cwd();
|
|
82
|
+
const commandsDir = options.dir
|
|
83
|
+
? path.resolve(rootDir, options.dir)
|
|
84
|
+
: path.resolve(rootDir, 'commands');
|
|
85
|
+
|
|
86
|
+
const registry = createCommandRegistry({ commandsDir });
|
|
87
|
+
const available = registry.list();
|
|
88
|
+
|
|
89
|
+
if (!commandName) {
|
|
90
|
+
if (available.length === 0) {
|
|
91
|
+
console.log(`\n⚠️ No custom commands found in "${path.relative(rootDir, commandsDir) || 'commands/'}".`);
|
|
92
|
+
console.log(' Create your first custom command with:');
|
|
93
|
+
console.log(' npx webspresso make:command <name>\n');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(`\n⚡ Available Custom Management Commands (${available.length}):\n`);
|
|
98
|
+
const maxLen = Math.max(...available.map((c) => c.name.length), 10);
|
|
99
|
+
for (const cmd of available) {
|
|
100
|
+
console.log(` webspresso run ${cmd.name.padEnd(maxLen + 2)} ${cmd.description ? `# ${cmd.description}` : ''}`);
|
|
101
|
+
}
|
|
102
|
+
console.log('');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!registry.has(commandName)) {
|
|
107
|
+
console.error(`\n❌ Error: Command "${commandName}" not found in ${path.relative(rootDir, commandsDir) || 'commands/'}.`);
|
|
108
|
+
if (available.length > 0) {
|
|
109
|
+
console.log(` Available commands: ${registry.names().join(', ')}\n`);
|
|
110
|
+
} else {
|
|
111
|
+
console.log(' Use `npx webspresso make:command <name>` to create it.\n');
|
|
112
|
+
}
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const cmdDef = registry.get(commandName);
|
|
118
|
+
const { flags, args } = parseRawArgs(rawArgs);
|
|
119
|
+
|
|
120
|
+
// Boot Context
|
|
121
|
+
let knexInstance = null;
|
|
122
|
+
let db = null;
|
|
123
|
+
let services = null;
|
|
124
|
+
const events = createEventDispatcher();
|
|
125
|
+
const mailer = createMailer({ backend: 'console' });
|
|
126
|
+
|
|
127
|
+
// 1. Database
|
|
128
|
+
try {
|
|
129
|
+
const dbInfo = resolveDbConfigIfExists(options.config);
|
|
130
|
+
if (dbInfo) {
|
|
131
|
+
knexInstance = await createDbInstance(dbInfo.config, options.env);
|
|
132
|
+
const { createDatabase } = require('../../core/orm');
|
|
133
|
+
db = createDatabase({ knex: knexInstance });
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.warn('⚠️ Could not initialize database for command:', err.message);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 2. Services
|
|
140
|
+
try {
|
|
141
|
+
const servicesDir = path.resolve(rootDir, 'services');
|
|
142
|
+
if (fs.existsSync(servicesDir)) {
|
|
143
|
+
const { createServiceRegistry } = require('../../src/services');
|
|
144
|
+
services = createServiceRegistry({ servicesDir, isDev: true });
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.warn('⚠️ Could not initialize service registry for command:', err.message);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 3. Queue
|
|
151
|
+
let queue = null;
|
|
152
|
+
try {
|
|
153
|
+
const { createQueueDispatcher } = require('../../core/queue');
|
|
154
|
+
queue = createQueueDispatcher({ db: knexInstance });
|
|
155
|
+
} catch (_) {
|
|
156
|
+
queue = {
|
|
157
|
+
dispatch: async () => {},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log(`\n🚀 [webspresso run] Executing "${commandName}"...`);
|
|
162
|
+
const executionResult = await executeCommand(cmdDef, {
|
|
163
|
+
args,
|
|
164
|
+
flags,
|
|
165
|
+
db,
|
|
166
|
+
queue,
|
|
167
|
+
services,
|
|
168
|
+
mailer,
|
|
169
|
+
events,
|
|
170
|
+
logger: console,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Cleanup
|
|
174
|
+
if (knexInstance && typeof knexInstance.destroy === 'function') {
|
|
175
|
+
try {
|
|
176
|
+
await knexInstance.destroy();
|
|
177
|
+
} catch (_) {}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (executionResult.success) {
|
|
181
|
+
console.log(`\n✨ Command "${commandName}" completed successfully in ${executionResult.durationMs}ms.\n`);
|
|
182
|
+
} else {
|
|
183
|
+
console.error(`\n❌ Command "${commandName}" failed after ${executionResult.durationMs}ms:`);
|
|
184
|
+
if (executionResult.error) {
|
|
185
|
+
console.error(executionResult.error.stack || executionResult.error.message);
|
|
186
|
+
}
|
|
187
|
+
process.exitCode = 1;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = {
|
|
193
|
+
registerCommand,
|
|
194
|
+
parseRawArgs,
|
|
195
|
+
};
|