Spaces:
Sleeping
Sleeping
| /* | |
| * 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, | |
| }; | |