Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d28939986 | |||
| 01623eab2a | |||
| 5c65c43589 | |||
| 72109e478f | |||
| 53d9956735 | |||
| 913f8556d0 | |||
| e905af4b21 | |||
| 2e0b7d5053 | |||
| 270f75e8e0 | |||
| b9ec1e2be6 |
37
changelog.md
37
changelog.md
@@ -1,5 +1,42 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-10-28 - 4.0.18 - fix(smartcli)
|
||||||
|
Allow passing argv to startParse and improve getUserArgs Deno/runtime handling; update tests and add license
|
||||||
|
|
||||||
|
- Smartcli.startParse now accepts an optional testArgv parameter to bypass automatic runtime detection (makes testing deterministic).
|
||||||
|
- getUserArgs logic refined: always prefer Deno.args when available (handles Deno run and compiled executables reliably) and improve execPath fallback and slicing behavior for Node/Bun/other launchers.
|
||||||
|
- Tests updated: test/test.node+deno+bun.ts now passes process.argv explicitly to startParse to avoid Deno.args interference in test environments.
|
||||||
|
- Added MIT LICENSE file and a local .claude/settings.local.json for environment/permission settings.
|
||||||
|
|
||||||
|
## 2025-10-28 - 4.0.17 - fix(license)
|
||||||
|
Add MIT license and local Claude settings
|
||||||
|
|
||||||
|
- Add LICENSE file (MIT) to repository
|
||||||
|
- Add .claude/settings.local.json with local permissions for tooling
|
||||||
|
|
||||||
|
## 2025-10-28 - 4.0.16 - fix(smartcli.helpers)
|
||||||
|
Improve CLI argument parsing and Deno runtime detection; use getUserArgs consistently
|
||||||
|
|
||||||
|
- Enhance getUserArgs() to prefer Deno.args but detect when process.argv was manipulated (e.g. in tests) and fallback to manual parsing
|
||||||
|
- Add robust handling of process.execPath / execPath basename and compute correct argv offset for known launchers vs. compiled executables
|
||||||
|
- Call getUserArgs() (no explicit process.argv) from Smartcli.getOption and Smartcli.startParse to ensure consistent cross-runtime behavior
|
||||||
|
- Expand readme.hints.md with detailed cross-runtime examples and explanation of Deno.args vs process.argv for compiled executables
|
||||||
|
- Add local claude settings file for tooling configuration
|
||||||
|
|
||||||
|
## 2025-10-28 - 4.0.15 - fix(smartcli.helpers)
|
||||||
|
Add robust getUserArgs helper and refactor Smartcli to use it; add MIT license and update documentation
|
||||||
|
|
||||||
|
- Add ts/smartcli.helpers.ts: getUserArgs to normalize user arguments across Node.js, Deno (run/compiled), and Bun, with safety checks for test environments
|
||||||
|
- Refactor Smartcli (ts/smartcli.classes.smartcli.ts) to use getUserArgs in startParse and getOption for correct argument parsing and improved test compatibility
|
||||||
|
- Update readme.hints.md with detailed cross-runtime CLI argument parsing guidance
|
||||||
|
- Add LICENSE (MIT) file
|
||||||
|
- Add .claude/settings.local.json (local settings)
|
||||||
|
|
||||||
|
## 2025-10-28 - 4.0.14 - fix(license)
|
||||||
|
Add MIT license file
|
||||||
|
|
||||||
|
- Add MIT License file to repository to clarify licensing and copyright (Push.Rocks 2015).
|
||||||
|
|
||||||
## 2025-10-27 - 4.0.13 - fix(smartcli)
|
## 2025-10-27 - 4.0.13 - fix(smartcli)
|
||||||
Improve CLI argument parsing, update deps and tests
|
Improve CLI argument parsing, update deps and tests
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartcli",
|
"name": "@push.rocks/smartcli",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "4.0.13",
|
"version": "4.0.18",
|
||||||
"description": "A library that simplifies building reactive command-line applications using observables, with robust support for commands, arguments, options, aliases, and asynchronous operation management.",
|
"description": "A library that simplifies building reactive command-line applications using observables, with robust support for commands, arguments, options, aliases, and asynchronous operation management.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
"typings": "dist_ts/index.d.ts",
|
"typings": "dist_ts/index.d.ts",
|
||||||
|
|||||||
@@ -1 +1,42 @@
|
|||||||
No specific hints.
|
## Cross-Runtime Compatibility
|
||||||
|
|
||||||
|
### CLI Argument Parsing
|
||||||
|
The module uses a robust cross-runtime approach for parsing command-line arguments through the `getUserArgs()` utility in `ts/smartcli.helpers.ts`.
|
||||||
|
|
||||||
|
**Runtime-Specific Implementations:**
|
||||||
|
|
||||||
|
| Runtime | process.argv Structure | Preferred API | Reason |
|
||||||
|
|---------|------------------------|---------------|---------|
|
||||||
|
| **Node.js** | `["/path/to/node", "/path/to/script.js", ...userArgs]` | Manual parsing | No native user-args API |
|
||||||
|
| **Deno run** | `["deno", "/path/to/script.ts", ...userArgs]` | `Deno.args` ✅ | Pre-filtered by runtime |
|
||||||
|
| **Deno compiled** | `["/path/to/binary", "/tmp/deno-compile-.../mod.ts", ...userArgs]` | `Deno.args` ✅ | Filters internal bundle path |
|
||||||
|
| **Bun** | `["/path/to/bun", "/path/to/script.ts", ...userArgs]` | Manual parsing | Bun.argv not pre-filtered |
|
||||||
|
|
||||||
|
**Why Deno.args is Critical for Compiled Executables:**
|
||||||
|
|
||||||
|
Deno compiled executables insert an internal bundle path at `argv[1]`:
|
||||||
|
```javascript
|
||||||
|
process.argv = [
|
||||||
|
"/usr/local/bin/moxytool", // argv[0] - executable
|
||||||
|
"/tmp/deno-compile-moxytool/mod.ts", // argv[1] - INTERNAL bundle path
|
||||||
|
"scripts", // argv[2] - actual user command
|
||||||
|
"--option" // argv[3+] - user args
|
||||||
|
]
|
||||||
|
|
||||||
|
Deno.args = ["scripts", "--option"] // ✓ Correctly filtered by Deno runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
**getUserArgs() Logic:**
|
||||||
|
|
||||||
|
1. **Prefer Deno.args** when available (unless process.argv appears manipulated for testing)
|
||||||
|
2. **Fallback to manual parsing** for Node.js and Bun:
|
||||||
|
- Check `process.execPath` basename
|
||||||
|
- Known launchers (node, deno, bun, tsx, ts-node) → skip 2 args
|
||||||
|
- Unknown (compiled executables) → skip 1 arg
|
||||||
|
3. **Test detection**: If `process.argv.length > 2` in Deno, use manual parsing (handles test manipulation)
|
||||||
|
|
||||||
|
**Key Benefits:**
|
||||||
|
- ✅ Works with custom-named compiled executables
|
||||||
|
- ✅ Handles Deno's internal bundle path automatically
|
||||||
|
- ✅ Compatible with test environments
|
||||||
|
- ✅ No heuristics needed for Deno (runtime does the work)
|
||||||
@@ -19,7 +19,8 @@ tap.test('should add an command', async (toolsArg) => {
|
|||||||
console.log(process.argv);
|
console.log(process.argv);
|
||||||
process.argv.splice(2, 0, 'awesome');
|
process.argv.splice(2, 0, 'awesome');
|
||||||
console.log(process.argv);
|
console.log(process.argv);
|
||||||
smartCliTestObject.startParse();
|
// Pass process.argv explicitly for testing (bypasses Deno.args in Deno environments)
|
||||||
|
smartCliTestObject.startParse(process.argv);
|
||||||
await done.promise;
|
await done.promise;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartcli',
|
name: '@push.rocks/smartcli',
|
||||||
version: '4.0.13',
|
version: '4.0.18',
|
||||||
description: 'A library that simplifies building reactive command-line applications using observables, with robust support for commands, arguments, options, aliases, and asynchronous operation management.'
|
description: 'A library that simplifies building reactive command-line applications using observables, with robust support for commands, arguments, options, aliases, and asynchronous operation management.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as plugins from './smartcli.plugins.js';
|
import * as plugins from './smartcli.plugins.js';
|
||||||
|
import { getUserArgs } from './smartcli.helpers.js';
|
||||||
|
|
||||||
// interfaces
|
// interfaces
|
||||||
export interface ICommandObservableObject {
|
export interface ICommandObservableObject {
|
||||||
@@ -91,7 +92,8 @@ export class Smartcli {
|
|||||||
* getOption
|
* getOption
|
||||||
*/
|
*/
|
||||||
public getOption(optionNameArg: string) {
|
public getOption(optionNameArg: string) {
|
||||||
const parsedYargs = plugins.yargsParser(process.argv);
|
const userArgs = getUserArgs();
|
||||||
|
const parsedYargs = plugins.yargsParser(userArgs);
|
||||||
return parsedYargs[optionNameArg];
|
return parsedYargs[optionNameArg];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,34 +123,12 @@ export class Smartcli {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* start the process of evaluating commands
|
* start the process of evaluating commands
|
||||||
|
* @param testArgv - Optional argv override for testing (bypasses automatic runtime detection)
|
||||||
*/
|
*/
|
||||||
public startParse(): void {
|
public startParse(testArgv?: string[]): void {
|
||||||
const parsedYArgs = plugins.yargsParser([...process.argv]);
|
// Get user arguments, properly handling Node.js, Deno (run/compiled), and Bun
|
||||||
|
const userArgs = testArgv ? getUserArgs(testArgv) : getUserArgs();
|
||||||
// lets handle commands
|
const parsedYArgs = plugins.yargsParser(userArgs);
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const wantedCommand = parsedYArgs._[0];
|
const wantedCommand = parsedYArgs._[0];
|
||||||
|
|
||||||
// lets handle some standards
|
// lets handle some standards
|
||||||
|
|||||||
81
ts/smartcli.helpers.ts
Normal file
81
ts/smartcli.helpers.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* 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.args is ALWAYS correct in Deno environments - it handles the internal bundle path automatically.
|
||||||
|
// 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] || '';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user