fix(smartcli.helpers): Add robust getUserArgs helper and refactor Smartcli to use it; add MIT license and update documentation

This commit is contained in:
2025-10-28 13:53:34 +00:00
parent 270f75e8e0
commit 2e0b7d5053
5 changed files with 125 additions and 29 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartcli',
version: '4.0.14',
version: '4.0.15',
description: 'A library that simplifies building reactive command-line applications using observables, with robust support for commands, arguments, options, aliases, and asynchronous operation management.'
}

View File

@@ -1,4 +1,5 @@
import * as plugins from './smartcli.plugins.js';
import { getUserArgs } from './smartcli.helpers.js';
// interfaces
export interface ICommandObservableObject {
@@ -91,7 +92,8 @@ export class Smartcli {
* getOption
*/
public getOption(optionNameArg: string) {
const parsedYargs = plugins.yargsParser(process.argv);
const userArgs = getUserArgs(process.argv);
const parsedYargs = plugins.yargsParser(userArgs);
return parsedYargs[optionNameArg];
}
@@ -123,32 +125,10 @@ export class Smartcli {
* start the process of evaluating commands
*/
public startParse(): void {
const parsedYArgs = plugins.yargsParser([...process.argv]);
// lets handle commands
// Filter out runtime executable and script path from arguments
// Node.js: ["/path/to/node", "/path/to/script.js", ...args]
// Deno: ["deno", "/path/to/script.ts", ...args]
// Bun: ["/path/to/bun", "/path/to/script.ts", ...args]
let counter = 0;
let foundCommand = false;
const runtimeNames = ['node', 'deno', 'bun', 'tsx', 'ts-node'];
parsedYArgs._ = parsedYArgs._.filter((commandPartArg) => {
counter++;
if (typeof commandPartArg === 'number') {
return true;
}
if (counter <= 2 && !foundCommand) {
const isPath = commandPartArg.startsWith('/');
const isRuntimeExecutable = runtimeNames.some(name =>
commandPartArg === name || commandPartArg.endsWith(`/${name}`)
);
foundCommand = !isPath && !isRuntimeExecutable;
return foundCommand;
} else {
return true;
}
});
// Get user arguments, properly handling Node.js, Deno (run/compiled), and Bun
// Pass process.argv explicitly to handle test scenarios where it's modified
const userArgs = getUserArgs(process.argv);
const parsedYArgs = plugins.yargsParser(userArgs);
const wantedCommand = parsedYArgs._[0];
// lets handle some standards

80
ts/smartcli.helpers.ts Normal file
View File

@@ -0,0 +1,80 @@
/**
* Return only the user arguments (excluding runtime executable and script path),
* across Node.js, Deno (run/compiled), and Bun.
*
* - Deno: uses Deno.args directly (already user-only in both run and compile).
* - Node/Bun: uses process.execPath's basename to decide if there is a script arg.
* If execPath basename is a known launcher (node/nodejs/bun/deno), skip 2; else skip 1.
*/
export function getUserArgs(argv?: string[]): string[] {
// If argv is explicitly provided, use it instead of Deno.args
// This handles test scenarios where process.argv is manually modified
const useProvidedArgv = argv !== undefined;
// Prefer Deno.args when available and no custom argv provided;
// it's the most reliable for Deno run and compiled.
// deno-lint-ignore no-explicit-any
const g: any = typeof globalThis !== 'undefined' ? globalThis : {};
if (!useProvidedArgv && g.Deno && g.Deno.args && Array.isArray(g.Deno.args)) {
return g.Deno.args.slice();
}
const a =
argv ?? (typeof process !== 'undefined' && Array.isArray(process.argv) ? process.argv : []);
if (!Array.isArray(a) || a.length === 0) return [];
// Determine execPath in Node/Bun (or compat shims)
let execPath = '';
if (typeof process !== 'undefined' && typeof process.execPath === 'string') {
execPath = process.execPath;
} else if (g.Deno && typeof g.Deno.execPath === 'function') {
// Fallback for unusual shims: try Deno.execPath() if present.
try {
execPath = g.Deno.execPath();
} catch {
/* ignore */
}
}
const base = basename(execPath).toLowerCase();
const knownLaunchers = new Set([
'node',
'node.exe',
'nodejs',
'nodejs.exe',
'bun',
'bun.exe',
'deno',
'deno.exe',
'tsx',
'tsx.exe',
'ts-node',
'ts-node.exe',
]);
// Always skip the executable (argv[0]).
let offset = Math.min(1, a.length);
// If the executable is a known runtime launcher, there's almost always a script path in argv[1].
// This handles Node, Bun, and "deno run" (but NOT "deno compile" which won't match 'deno').
if (knownLaunchers.has(base)) {
offset = Math.min(2, a.length);
}
// Safety: if offset would skip all elements and array is not empty, don't skip anything
// This handles edge cases like test environments with unusual argv setups
if (offset >= a.length && a.length > 0) {
offset = 0;
}
// Note: we intentionally avoid path/URL heuristics on argv[1] so we don't
// accidentally drop the first user arg when it's a path-like value in compiled mode.
return a.slice(offset);
}
function basename(p: string): string {
if (!p) return '';
const parts = p.split(/[/\\]/);
return parts[parts.length - 1] || '';
}