Compare commits

...

3 Commits

Author SHA1 Message Date
jkunz ee94feddfa v3.5.0 2026-05-10 14:33:40 +00:00
jkunz 04517232e8 chore(config): migrate gitzone smartconfig 2026-05-10 14:33:14 +00:00
jkunz 0ff3596bd8 feat(spawn): support inherited stdio 2026-05-10 14:32:07 +00:00
7 changed files with 72 additions and 26 deletions
+12 -11
View File
@@ -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": []
}
}
+9
View File
@@ -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
View File
@@ -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",
+10
View File
@@ -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
+14
View File
@@ -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',
+1 -1
View File
@@ -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.'
}
+20 -8
View File
@@ -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);