@nu-art/commando
Version:
Shell command execution framework with interactive sessions, CLI parameter resolution, and plugin system for building and executing shell scripts programmatically
140 lines (139 loc) • 5.33 kB
JavaScript
/*
* commando provides shell command execution framework with interactive sessions and plugin system
*
* Copyright (C) 2020 Adam van der Kruk aka TacB0sS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ThisShouldNotHappenException } from '@nu-art/ts-common';
import { SimpleShell } from './SimpleShell.js';
import { BaseCommando } from '../core/BaseCommando.js';
import { CliError } from '../core/CliError.js';
/**
* Simple shell command executor extending BaseCommando.
*
* Provides a straightforward way to build and execute shell commands
* using SimpleShell. Supports file execution and remote file execution.
*
* **Features**:
* - Command building via BaseCommando fluent API
* - File execution with optional interpreter
* - Remote file execution via curl
* - Error handling with CliError
* - UID tagging for log identification
*
* **Error Handling**:
* - Catches CliError and calls callback with error details
* - Throws ThisShouldNotHappenException for unexpected errors
* - Always calls callback (even on error) if provided
*/
export class Commando extends BaseCommando {
/** Optional unique identifier for log tagging */
uid;
/**
* Creates a Commando instance with plugins.
*
* @template T - Array of plugin constructor types
* @param plugins - Plugin classes to merge
* @returns Merged Commando instance with plugins
*/
static create(...plugins) {
const _commando = BaseCommando._create(Commando, ...plugins);
return _commando;
}
constructor() {
super();
}
/**
* Sets a unique identifier for this commando instance.
*
* Used for log tagging to identify which commando instance
* generated log messages.
*
* @param uid - Unique identifier string
* @returns This instance for method chaining
*/
setUID(uid) {
this.uid = uid;
return this;
}
/**
* Executes a local file with an optional interpreter.
*
* If an interpreter is provided, prefixes the file path with it.
* Otherwise executes the file directly (must be executable).
*
* @param filePath - Path to file to execute
* @param interpreter - Optional interpreter (e.g., 'node', 'python3')
* @returns Promise resolving to command output
*/
executeFile(filePath, interpreter) {
let command = filePath;
// If an interpreter is provided, prefix the command with it.
if (interpreter) {
command = `${interpreter} ${filePath}`;
}
return new SimpleShell().execute(command);
}
/**
* Executes a remote file by downloading and piping to interpreter.
*
* Uses curl to download the file and pipes it directly to the interpreter.
* Does not save the file locally.
*
* **Security Note**: Executes remote code without verification.
*
* @param pathToFile - URL to remote file
* @param interpreter - Interpreter to execute the file (e.g., 'bash', 'node')
* @returns Promise resolving to command output
*/
executeRemoteFile(pathToFile, interpreter) {
const command = `curl -o- "${pathToFile}" | ${interpreter}`;
return new SimpleShell().execute(command);
}
/**
* Executes the accumulated commands and optionally processes output.
*
* **Behavior**:
* - Resets the command builder (gets accumulated command)
* - Creates a new SimpleShell instance with debug mode
* - Executes the command
* - On success: calls callback with stdout, stderr, exitCode=0
* - On error: catches CliError, calls callback with error details, returns result
* - On unexpected error: throws ThisShouldNotHappenException
*
* **Note**: The callback is always called if provided, even on error.
* This allows handling errors without try/catch.
*
* @template T - Return type of callback
* @param callback - Optional function to process command output
* @returns Promise resolving to callback result or void
*/
async execute(callback) {
const command = this.builder.reset();
const simpleShell = new SimpleShell().debug(this._debug);
try {
if (this.uid)
simpleShell.setUID(this.uid);
const { stdout, stderr } = await simpleShell.execute(command);
return callback?.(stdout, stderr, 0);
}
catch (_error) {
simpleShell.logError(_error);
const cliError = _error;
if ('isInstanceOf' in cliError && cliError.isInstanceOf(CliError))
return callback?.(cliError.stdout, cliError.stderr, cliError.cause.code ?? -1);
throw new ThisShouldNotHappenException('Unhandled error', _error);
}
}
}