Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee94feddfa | |||
| 04517232e8 | |||
| 0ff3596bd8 |
+12
-11
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"npmci": {
|
||||
"npmGlobalTools": [],
|
||||
"npmAccessLevel": "public",
|
||||
"npmRegistryUrl": "registry.npmjs.org"
|
||||
},
|
||||
"gitzone": {
|
||||
"@git.zone/cli": {
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "code.foss.global",
|
||||
@@ -24,13 +19,10 @@
|
||||
"process management",
|
||||
"typescript"
|
||||
]
|
||||
}
|
||||
},
|
||||
"tsdoc": {
|
||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||
},
|
||||
"@git.zone/cli": {
|
||||
"release": {
|
||||
"targets": {
|
||||
"npm": {
|
||||
"registries": [
|
||||
"https://registry.npmjs.org",
|
||||
"https://verdaccio.lossless.digital"
|
||||
@@ -38,4 +30,13 @@
|
||||
"accessLevel": "public"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemaVersion": 2
|
||||
},
|
||||
"@git.zone/tsdoc": {
|
||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||
},
|
||||
"@ship.zone/szci": {
|
||||
"npmGlobalTools": []
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## Pending
|
||||
|
||||
|
||||
## 2026-05-10 - 3.5.0
|
||||
|
||||
### Features
|
||||
|
||||
- Add inherited terminal stdio support to `execSpawn` for trusted interactive CLIs.
|
||||
|
||||
## 2026-05-09 - 3.4.0 - feat(smartshell)
|
||||
add cwd-aware execution options, structured strict-mode errors, and safer process tree termination
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@push.rocks/smartshell",
|
||||
"private": false,
|
||||
"version": "3.4.0",
|
||||
"version": "3.5.0",
|
||||
"description": "A library for executing shell commands using promises.",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
|
||||
@@ -197,6 +197,16 @@ const result = await shell.execSpawn('git', ['status', '--short'], {
|
||||
});
|
||||
```
|
||||
|
||||
For trusted interactive CLIs that need the real terminal while still avoiding shell parsing, pass `stdio: 'inherit'`:
|
||||
|
||||
```typescript
|
||||
await shell.execSpawn('opencode', ['run', '--dir', process.cwd(), prompt], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
```
|
||||
|
||||
Inherited stdio returns an `IExecResult` with the exit code, but stdout and stderr are not captured because the child process writes directly to the terminal.
|
||||
|
||||
### Why Spawn Matters
|
||||
|
||||
```typescript
|
||||
|
||||
@@ -80,6 +80,20 @@ tap.test('execSpawn should properly escape arguments', async () => {
|
||||
expect(result.stdout).toContain('$HOME && ls');
|
||||
});
|
||||
|
||||
tap.test('execSpawn should support inherited stdio for interactive CLIs', async () => {
|
||||
const testSmartshell = new smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
sourceFilePaths: [],
|
||||
});
|
||||
|
||||
const result = await testSmartshell.execSpawn('node', ['-e', 'console.log("inherited stdio works")'], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
expect(result.exitCode).toEqual(0);
|
||||
expect(result.stdout).toEqual('');
|
||||
expect(result.stderr).toEqual('');
|
||||
});
|
||||
|
||||
tap.test('execSpawn streaming should work', async () => {
|
||||
const testSmartshell = new smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartshell',
|
||||
version: '3.4.0',
|
||||
version: '3.5.0',
|
||||
description: 'A library for executing shell commands using promises.'
|
||||
}
|
||||
|
||||
@@ -93,6 +93,11 @@ export interface ISpawnOptions extends IExecRuntimeOptions {
|
||||
passthrough?: boolean;
|
||||
interactiveControl?: boolean;
|
||||
usePty?: boolean;
|
||||
/**
|
||||
* When set to `inherit`, the child process uses the parent terminal directly.
|
||||
* This is useful for trusted interactive CLIs while still avoiding shell parsing.
|
||||
*/
|
||||
stdio?: 'pipe' | 'inherit';
|
||||
}
|
||||
|
||||
export type TExecCommandOptions = IExecRuntimeOptions;
|
||||
@@ -155,17 +160,23 @@ export class Smartshell {
|
||||
let stderrBuffer = '';
|
||||
const maxBuffer = options.maxBuffer || 200 * 1024 * 1024; // Default 200MB
|
||||
let bufferExceeded = false;
|
||||
const inheritStdio = options.stdio === 'inherit';
|
||||
|
||||
// Handle PTY mode if requested
|
||||
if (options.usePty) {
|
||||
throw new Error('PTY mode is not yet supported with execSpawn. Use exec methods with shell:true for PTY.');
|
||||
}
|
||||
|
||||
if (inheritStdio && (options.streaming || options.interactiveControl)) {
|
||||
throw new Error('execSpawn stdio: inherit cannot be combined with streaming or interactiveControl.');
|
||||
}
|
||||
|
||||
const execChildProcess = cp.spawn(options.command, options.args || [], {
|
||||
shell: false, // SECURITY: Never use shell with untrusted input
|
||||
cwd: options.cwd || process.cwd(),
|
||||
env: options.env || process.env,
|
||||
detached: true, // Own process group — immune to terminal SIGINT, managed by smartexit
|
||||
stdio: inheritStdio ? 'inherit' : 'pipe',
|
||||
detached: inheritStdio ? false : true, // Inherited TTY needs normal terminal signal handling.
|
||||
signal: options.signal,
|
||||
});
|
||||
|
||||
@@ -183,7 +194,7 @@ export class Smartshell {
|
||||
}
|
||||
|
||||
// Connect stdin if passthrough is enabled (but not for interactive control)
|
||||
if (options.passthrough && !options.interactiveControl && execChildProcess.stdin) {
|
||||
if (!inheritStdio && options.passthrough && !options.interactiveControl && execChildProcess.stdin) {
|
||||
process.stdin.pipe(execChildProcess.stdin);
|
||||
}
|
||||
|
||||
@@ -192,11 +203,12 @@ export class Smartshell {
|
||||
if (!execChildProcess.stdin) {
|
||||
throw new Error('stdin is not available for this process');
|
||||
}
|
||||
if (execChildProcess.stdin.destroyed || !execChildProcess.stdin.writable) {
|
||||
const childStdin = execChildProcess.stdin;
|
||||
if (childStdin.destroyed || !childStdin.writable) {
|
||||
throw new Error('stdin has been destroyed or is not writable');
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
execChildProcess.stdin.write(input, 'utf8', (error) => {
|
||||
childStdin.write(input, 'utf8', (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
@@ -217,7 +229,7 @@ export class Smartshell {
|
||||
};
|
||||
|
||||
// Capture stdout and stderr output
|
||||
execChildProcess.stdout.on('data', (data) => {
|
||||
execChildProcess.stdout?.on('data', (data) => {
|
||||
if (!options.silent) {
|
||||
shellLogInstance.writeToConsole(data);
|
||||
}
|
||||
@@ -235,7 +247,7 @@ export class Smartshell {
|
||||
}
|
||||
});
|
||||
|
||||
execChildProcess.stderr.on('data', (data) => {
|
||||
execChildProcess.stderr?.on('data', (data) => {
|
||||
if (!options.silent) {
|
||||
shellLogInstance.writeToConsole(data);
|
||||
}
|
||||
@@ -265,7 +277,7 @@ export class Smartshell {
|
||||
this.smartexit.removeProcess(execChildProcess);
|
||||
|
||||
// Safely unpipe stdin when process ends if passthrough was enabled
|
||||
if (options.passthrough && !options.interactiveControl) {
|
||||
if (!inheritStdio && options.passthrough && !options.interactiveControl) {
|
||||
try {
|
||||
if (execChildProcess.stdin && !execChildProcess.stdin.destroyed) {
|
||||
process.stdin.unpipe(execChildProcess.stdin);
|
||||
@@ -302,7 +314,7 @@ export class Smartshell {
|
||||
this.smartexit.removeProcess(execChildProcess);
|
||||
|
||||
// Safely unpipe stdin when process errors if passthrough was enabled
|
||||
if (options.passthrough && !options.interactiveControl && execChildProcess.stdin) {
|
||||
if (!inheritStdio && options.passthrough && !options.interactiveControl && execChildProcess.stdin) {
|
||||
try {
|
||||
if (!execChildProcess.stdin.destroyed) {
|
||||
process.stdin.unpipe(execChildProcess.stdin);
|
||||
|
||||
Reference in New Issue
Block a user