puter-deploy / src /backend /src /services /CommandService.js
gionuibk's picture
Upload folder using huggingface_hub
61d39e2 verified
/*
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const { Context } = require('../util/context');
const BaseService = require('./BaseService');
/**
* Represents a Command class that encapsulates command execution functionality.
* Each Command instance contains a specification (spec) that defines its ID,
* name, description, handler function, and optional argument completer.
* The class provides methods for executing commands and handling command
* argument completion.
*/
class Command {
constructor (spec) {
this.spec_ = spec;
}
/**
* Gets the unique identifier for this command
* @returns {string} The command's ID as specified in the constructor
*/
get id () {
return this.spec_.id;
}
/**
* Executes the command with given arguments and logging
* @param {Array} args - Command arguments to pass to the handler
* @param {Object} [log=console] - Logger object for output, defaults to console
* @returns {Promise<void>}
* @throws {Error} Logs any errors that occur during command execution
*/
async execute (args, log) {
log = log ?? console;
const { id, name, description, handler } = this.spec_;
try {
await handler(args, log);
} catch ( err ) {
log.error(`command ${name ?? id} failed: ${err.message}`);
log.error(err.stack);
}
}
completeArgument (args) {
const completer = this.spec_.completer;
if ( completer )
{
return completer(args);
}
return [];
}
}
/**
* CommandService class manages the registration, execution, and handling of commands in the Puter system.
* Extends BaseService to provide command-line interface functionality. Maintains a collection of Command
* objects, supports command registration with namespaces, command execution with arguments, and provides
* command lookup capabilities. Includes built-in help command functionality.
* @extends BaseService
*/
class CommandService extends BaseService {
/**
* Initializes the command service's internal state
* Called during service construction to set up the empty commands array
*/
async _construct () {
this.commands_ = [];
}
/**
* Add the help command to the list of commands on init
*/
async _init () {
this.commands_.push(new Command({
id: 'help',
description: 'show this help',
handler: (args, log) => {
log.log('available commands:');
for ( const command of this.commands_ ) {
log.log(`- ${command.spec_.id}: ${command.spec_.description}`);
}
},
}));
}
async ['__on_boot.consolidation'] () {
const svc_event = this.services.get('event');
const svc_command = this;
const event = {
createCommand (name, command) {
const serviceName = Context.get('extension_name') ?? '%missing%';
const commandSpec = typeof command === 'function'
? { handler: command }
: command;
if ( typeof commandSpec !== 'object' ) {
throw new Error('command must be either a function or an object');
}
if ( ! (typeof command.handler === 'function') ) {
throw new Error('command should have a handler function');
}
svc_command.registerCommands(serviceName, [{
id: name,
...commandSpec,
}]);
},
};
svc_event.emit('create.commands', event);
}
registerCommands (serviceName, commands) {
if ( ! this.log ) {
/* eslint-disable */
console.error(
'CommandService.registerCommands was called before a logger ' +
'was initialied. This happens when calling registerCommands ' +
'in the "construct" phase instead of the "init" phase. If ' +
'you are migrating a legacy service that does not extend ' +
'BaseService, maybe the _construct hook is calling init()'
);
/* eslint-enable */
process.exit(1);
}
for ( const command of commands ) {
this.log.debug(`registering command ${serviceName}:${command.id}`);
this.commands_.push(new Command({
...command,
id: `${serviceName}:${command.id}`,
}));
}
}
/**
* Executes a command with the given arguments and logging context
* @param {string[]} args - Array of command arguments where first element is command name
* @param {Object} log - Logger object for output (defaults to console if not provided)
* @returns {Promise<void>}
* @throws {Error} If command execution fails
*/
async executeCommand (args, log) {
const [commandName, ...commandArgs] = args;
const command = this.commands_.find(c => c.spec_.id === commandName);
if ( ! command ) {
log.error(`unknown command: ${commandName}`);
return;
}
/**
* Executes a command with the given arguments in a global context
* @param {string[]} args - Array of command arguments where first element is command name
* @param {Object} log - Logger object for output
* @returns {Promise<void>}
* @throws {Error} If command execution fails
*/
await globalThis.root_context.sub({
injected_logger: log,
}).arun(async () => {
await command.execute(commandArgs, log);
});
}
/**
* Executes a raw command string by splitting it into arguments and executing the command
* @param {string} text - Raw command string to execute
* @param {object} log - Logger object for output (defaults to console if not provided)
* @returns {Promise<void>}
* @todo Replace basic whitespace splitting with proper tokenizer (obvious-json)
*/
async executeRawCommand (text, log) {
// TODO: add obvious-json as a tokenizer
const args = text.split(/\s+/);
await this.executeCommand(args, log);
}
/**
* Gets a list of all registered command names/IDs
* @returns {string[]} Array of command identifier strings
*/
get commandNames () {
return this.commands_.map(command => command.id);
}
getCommand (id) {
return this.commands_.find(command => command.id === id);
}
}
module.exports = {
CommandService,
};