feat: add service field support

- service: true/false is now a required field in manyplug.json
- init --service creates background service template
- list shows ● for services, ○ for standard plugins
- validate checks service field and warns on mismatches
- templates document service vs non-service behavior

Services run regardless of isPluginRunning lock, but can
choose to respect it. Non-services are blocked automatically.
This commit is contained in:
synt-xerror
2026-04-09 01:24:41 -03:00
parent 649c7bb05a
commit fc54170171
4 changed files with 89 additions and 10 deletions

View File

@@ -22,6 +22,7 @@ program
.command('init <name>') .command('init <name>')
.description('Create a new plugin boilerplate') .description('Create a new plugin boilerplate')
.option('-c, --category <cat>', 'Plugin category', 'utility') .option('-c, --category <cat>', 'Plugin category', 'utility')
.option('-s, --service', 'Create as a background service', false)
.action(initCommand); .action(initCommand);
program program

View File

@@ -4,6 +4,11 @@ import chalk from 'chalk';
const PLUGIN_TEMPLATE = `/** const PLUGIN_TEMPLATE = `/**
* {{name}} plugin * {{name}} plugin
*
* SERVICE BEHAVIOR:
* - If service: true in manyplug.json, this plugin runs as a background service.
* - Services can choose whether to respect api.isPluginRunning(chatId) or not.
* - If service: false, the plugin is BLOCKED when isPluginRunning(chatId) is true.
*/ */
/** /**
@@ -11,6 +16,18 @@ const PLUGIN_TEMPLATE = `/**
* @param {Object} ctx - { msg, chat, api } * @param {Object} ctx - { msg, chat, api }
*/ */
export default async function {{name}}Plugin({ msg, chat, api }) { export default async function {{name}}Plugin({ msg, chat, api }) {
// Check if another plugin is running in this chat
// Only works if service: false in manyplug.json
if (!api.isService && api.isPluginRunning && api.isPluginRunning(msg.from)) {
// This plugin is blocked because another plugin is running
return false;
}
// For services (service: true), you can choose to respect or ignore:
// if (!api.ignoreIsRunning && api.isPluginRunning?.(msg.from)) {
// return false; // Service choosing to respect the lock
// }
// Your plugin logic here // Your plugin logic here
// Return true to stop further processing // Return true to stop further processing
return false; return false;
@@ -22,6 +39,7 @@ export default async function {{name}}Plugin({ msg, chat, api }) {
*/ */
export async function setup(api) { export async function setup(api) {
// Initialize databases, schedules, etc. // Initialize databases, schedules, etc.
// Services can use api.schedule() for background tasks
} }
/** /**
@@ -32,6 +50,47 @@ export const api = {
}; };
`; `;
const SERVICE_TEMPLATE = `/**
* {{name}} service
*
* This is a BACKGROUND SERVICE that can run regardless of isPluginRunning state.
* Services can optionally respect the lock by checking api.isPluginRunning(chatId).
*/
/**
* Service function - called on every message
* Services run even when other plugins have the lock
* @param {Object} ctx - { msg, chat, api }
*/
export default async function {{name}}Service({ msg, chat, api }) {
// This service runs even when isPluginRunning is true for this chat
// You can optionally check and respect the lock:
const isLocked = api.isPluginRunning?.(msg.from);
if (isLocked && !api.config.ignoreLock) {
// Service choosing not to run while another plugin is active
return false;
}
// Service logic here
return false;
}
/**
* Setup runs once when bot connects
* Good place to start background tasks
*/
export async function setup(api) {
// Services can schedule background tasks
// api.schedule({ ... })
}
export const api = {
// Expose service methods
};
`;
export async function initCommand(name, options) { export async function initCommand(name, options) {
const pluginDir = path.resolve(name); const pluginDir = path.resolve(name);
@@ -44,12 +103,14 @@ export async function initCommand(name, options) {
await fs.ensureDir(pluginDir); await fs.ensureDir(pluginDir);
const isService = options.category === 'service' || options.service;
// Create manyplug.json // Create manyplug.json
const manifest = { const manifest = {
name, name,
version: '1.0.0', version: '1.0.0',
category: options.category || 'utility', category: isService ? 'service' : (options.category || 'utility'),
service: false, service: isService,
description: '', description: '',
author: '', author: '',
dependencies: {} dependencies: {}
@@ -57,8 +118,9 @@ export async function initCommand(name, options) {
await fs.writeJson(path.join(pluginDir, 'manyplug.json'), manifest, { spaces: 2 }); await fs.writeJson(path.join(pluginDir, 'manyplug.json'), manifest, { spaces: 2 });
// Create index.js // Create index.js with appropriate template
const indexContent = PLUGIN_TEMPLATE.replace(/\{\{name\}\}/g, name); const template = isService ? SERVICE_TEMPLATE : PLUGIN_TEMPLATE;
const indexContent = template.replace(/\{\{name\}\}/g, name);
await fs.writeFile(path.join(pluginDir, 'index.js'), indexContent); await fs.writeFile(path.join(pluginDir, 'index.js'), indexContent);
// Create locale directory // Create locale directory
@@ -69,7 +131,8 @@ export async function initCommand(name, options) {
{ spaces: 2 } { spaces: 2 }
); );
console.log(chalk.green(`Plugin "${name}" created at ./${name}`)); console.log(chalk.green(`${isService ? 'Service' : 'Plugin'} "${name}" created at ./${name}`));
console.log(chalk.gray(` Category: ${options.category}`)); console.log(chalk.gray(` Category: ${manifest.category}`));
console.log(chalk.gray(` Service: ${manifest.service ? 'yes (background)' : 'no (respects isPluginRunning)'}`));
console.log(chalk.gray(` Edit manyplug.json to add dependencies`)); console.log(chalk.gray(` Edit manyplug.json to add dependencies`));
} }

View File

@@ -41,6 +41,7 @@ export async function listCommand(options) {
name: manifest.name || entry.name, name: manifest.name || entry.name,
version: manifest.version || '-', version: manifest.version || '-',
category: manifest.category || '-', category: manifest.category || '-',
service: manifest.service === true,
status: hasEntry ? chalk.green('✓') : chalk.red('✗'), status: hasEntry ? chalk.green('✓') : chalk.red('✗'),
path: path.join('src/plugins', entry.name) path: path.join('src/plugins', entry.name)
}); });
@@ -52,12 +53,14 @@ export async function listCommand(options) {
} }
console.log(chalk.bold('\nInstalled Plugins:\n')); console.log(chalk.bold('\nInstalled Plugins:\n'));
console.log(`${chalk.gray('NAME'.padEnd(20))} ${chalk.gray('VERSION'.padEnd(10))} ${chalk.gray('CATEGORY'.padEnd(12))} STATUS`); console.log(`${chalk.gray('NAME'.padEnd(18))} ${chalk.gray('VERSION'.padEnd(10))} ${chalk.gray('CATEGORY'.padEnd(12))} ${chalk.gray('SRV')} STATUS`);
console.log(chalk.gray('─'.repeat(55))); console.log(chalk.gray('─'.repeat(60)));
for (const p of plugins) { for (const p of plugins) {
console.log(`${p.name.padEnd(20)} ${p.version.padEnd(10)} ${p.category.padEnd(12)} ${p.status}`); const serviceIcon = p.service ? chalk.cyan('●') : chalk.gray('○');
console.log(`${p.name.padEnd(18)} ${p.version.padEnd(10)} ${p.category.padEnd(12)} ${serviceIcon} ${p.status}`);
} }
console.log(chalk.gray(`\nTotal: ${plugins.length} plugin(s)`)); console.log(chalk.gray(`\nTotal: ${plugins.length} plugin(s)`));
console.log(chalk.gray(`Legend: ${chalk.cyan('●')} service (background) ${chalk.gray('○')} standard (respects isPluginRunning)`));
} }

View File

@@ -2,8 +2,9 @@ import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import chalk from 'chalk'; import chalk from 'chalk';
const REQUIRED_FIELDS = ['name', 'version', 'category']; const REQUIRED_FIELDS = ['name', 'version', 'category', 'service'];
const VALID_CATEGORIES = ['games', 'media', 'utility', 'service', 'admin', 'fun']; const VALID_CATEGORIES = ['games', 'media', 'utility', 'service', 'admin', 'fun'];
const VALID_SERVICES = [true, false];
export async function validateCommand(pluginPath) { export async function validateCommand(pluginPath) {
const targetPath = pluginPath ? path.resolve(pluginPath) : process.cwd(); const targetPath = pluginPath ? path.resolve(pluginPath) : process.cwd();
@@ -49,12 +50,23 @@ export async function validateCommand(pluginPath) {
errors.push('"dependencies" must be an object'); errors.push('"dependencies" must be an object');
} }
// Validate service field
if (manifest.service !== undefined && !VALID_SERVICES.includes(manifest.service)) {
errors.push('"service" must be a boolean (true or false)');
}
// Warn if service is true but no category hints
if (manifest.service === true && manifest.category !== 'service') {
warnings.push('Plugin marked as service=true but category is not "service"');
}
// Report // Report
if (errors.length === 0 && warnings.length === 0) { if (errors.length === 0 && warnings.length === 0) {
console.log(chalk.green('✅ Valid manyplug.json')); console.log(chalk.green('✅ Valid manyplug.json'));
console.log(chalk.gray(` Name: ${manifest.name}`)); console.log(chalk.gray(` Name: ${manifest.name}`));
console.log(chalk.gray(` Version: ${manifest.version}`)); console.log(chalk.gray(` Version: ${manifest.version}`));
console.log(chalk.gray(` Category: ${manifest.category}`)); console.log(chalk.gray(` Category: ${manifest.category}`));
console.log(chalk.gray(` Service: ${manifest.service === true ? 'yes (background)' : 'no (respects isPluginRunning)'}`));
return; return;
} }