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>')
.description('Create a new plugin boilerplate')
.option('-c, --category <cat>', 'Plugin category', 'utility')
.option('-s, --service', 'Create as a background service', false)
.action(initCommand);
program

View File

@@ -4,6 +4,11 @@ import chalk from 'chalk';
const PLUGIN_TEMPLATE = `/**
* {{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 }
*/
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
// Return true to stop further processing
return false;
@@ -22,6 +39,7 @@ export default async function {{name}}Plugin({ msg, chat, api }) {
*/
export async function setup(api) {
// 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) {
const pluginDir = path.resolve(name);
@@ -44,12 +103,14 @@ export async function initCommand(name, options) {
await fs.ensureDir(pluginDir);
const isService = options.category === 'service' || options.service;
// Create manyplug.json
const manifest = {
name,
version: '1.0.0',
category: options.category || 'utility',
service: false,
category: isService ? 'service' : (options.category || 'utility'),
service: isService,
description: '',
author: '',
dependencies: {}
@@ -57,8 +118,9 @@ export async function initCommand(name, options) {
await fs.writeJson(path.join(pluginDir, 'manyplug.json'), manifest, { spaces: 2 });
// Create index.js
const indexContent = PLUGIN_TEMPLATE.replace(/\{\{name\}\}/g, name);
// Create index.js with appropriate template
const template = isService ? SERVICE_TEMPLATE : PLUGIN_TEMPLATE;
const indexContent = template.replace(/\{\{name\}\}/g, name);
await fs.writeFile(path.join(pluginDir, 'index.js'), indexContent);
// Create locale directory
@@ -69,7 +131,8 @@ export async function initCommand(name, options) {
{ spaces: 2 }
);
console.log(chalk.green(`Plugin "${name}" created at ./${name}`));
console.log(chalk.gray(` Category: ${options.category}`));
console.log(chalk.green(`${isService ? 'Service' : 'Plugin'} "${name}" created at ./${name}`));
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`));
}

View File

@@ -41,6 +41,7 @@ export async function listCommand(options) {
name: manifest.name || entry.name,
version: manifest.version || '-',
category: manifest.category || '-',
service: manifest.service === true,
status: hasEntry ? chalk.green('✓') : chalk.red('✗'),
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.gray('NAME'.padEnd(20))} ${chalk.gray('VERSION'.padEnd(10))} ${chalk.gray('CATEGORY'.padEnd(12))} STATUS`);
console.log(chalk.gray('─'.repeat(55)));
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(60)));
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(`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 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_SERVICES = [true, false];
export async function validateCommand(pluginPath) {
const targetPath = pluginPath ? path.resolve(pluginPath) : process.cwd();
@@ -49,12 +50,23 @@ export async function validateCommand(pluginPath) {
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
if (errors.length === 0 && warnings.length === 0) {
console.log(chalk.green('✅ Valid manyplug.json'));
console.log(chalk.gray(` Name: ${manifest.name}`));
console.log(chalk.gray(` Version: ${manifest.version}`));
console.log(chalk.gray(` Category: ${manifest.category}`));
console.log(chalk.gray(` Service: ${manifest.service === true ? 'yes (background)' : 'no (respects isPluginRunning)'}`));
return;
}