start-command 0.24.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/package.json +1 -1
- package/src/lib/isolation.js +41 -43
- package/test/isolation.test.js +56 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.24.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2cb0101: fix: avoid running shell inside shell when command is a bare shell invocation
|
|
8
|
+
|
|
9
|
+
When running `$ --isolated docker --image <image> -- bash`, the tool was
|
|
10
|
+
wrapping the bare `bash` command inside `bash -i -c bash`, causing `.bashrc`
|
|
11
|
+
to be sourced twice and printing errors twice. Same issue affected `zsh`, `sh`,
|
|
12
|
+
and other shells.
|
|
13
|
+
|
|
14
|
+
Added `isInteractiveShellCommand()` helper that detects when the user's command
|
|
15
|
+
is a bare interactive shell invocation (e.g. `bash`, `/bin/zsh`, `bash -l`).
|
|
16
|
+
When detected, the command is passed directly to the isolation environment
|
|
17
|
+
instead of being wrapped in another shell. Applies to Docker (attached and
|
|
18
|
+
detached), SSH (attached and detached), and Screen (attached and detached).
|
|
19
|
+
|
|
3
20
|
## 0.24.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
package/package.json
CHANGED
package/src/lib/isolation.js
CHANGED
|
@@ -216,21 +216,19 @@ function getShellInteractiveFlag(shellPath) {
|
|
|
216
216
|
const shellName = shellPath.split('/').pop();
|
|
217
217
|
return shellName === 'bash' || shellName === 'zsh' ? '-i' : null;
|
|
218
218
|
}
|
|
219
|
+
/** True if command is a bare shell invocation (no -c); avoids bash-inside-bash (issue #84). */
|
|
220
|
+
function isInteractiveShellCommand(command) {
|
|
221
|
+
const parts = command.trim().split(/\s+/);
|
|
222
|
+
const shells = ['bash', 'zsh', 'sh', 'fish', 'ksh', 'csh', 'tcsh', 'dash'];
|
|
223
|
+
return shells.includes(path.basename(parts[0])) && !parts.includes('-c');
|
|
224
|
+
}
|
|
219
225
|
|
|
220
|
-
/**
|
|
221
|
-
* Check if the current process has a TTY attached
|
|
222
|
-
* @returns {boolean} True if TTY is available
|
|
223
|
-
*/
|
|
226
|
+
/** Returns true if the current process has a TTY attached. */
|
|
224
227
|
function hasTTY() {
|
|
225
228
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
226
229
|
}
|
|
227
230
|
|
|
228
|
-
/**
|
|
229
|
-
* Wrap command with sudo -u if user option is specified
|
|
230
|
-
* @param {string} command - Original command
|
|
231
|
-
* @param {string|null} user - Username to run as (or null)
|
|
232
|
-
* @returns {string} Wrapped command
|
|
233
|
-
*/
|
|
231
|
+
/** Wraps command with sudo -u if user is specified; returns command unchanged otherwise. */
|
|
234
232
|
function wrapCommandWithUser(command, user) {
|
|
235
233
|
if (!user) {
|
|
236
234
|
return command;
|
|
@@ -242,13 +240,8 @@ function wrapCommandWithUser(command, user) {
|
|
|
242
240
|
}
|
|
243
241
|
|
|
244
242
|
/**
|
|
245
|
-
* Run command in GNU Screen using detached mode with log capture
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* Supports two methods based on screen version:
|
|
249
|
-
* - screen >= 4.5.1: Uses -L -Logfile option for native log capture
|
|
250
|
-
* - screen < 4.5.1: Uses tee command within the wrapped command for output capture
|
|
251
|
-
*
|
|
243
|
+
* Run command in GNU Screen using detached mode with log capture.
|
|
244
|
+
* Supports screen >= 4.5.1 (native -Logfile) and older versions (tee fallback).
|
|
252
245
|
* @param {string} command - Command to execute
|
|
253
246
|
* @param {string} sessionName - Session name
|
|
254
247
|
* @param {object} shellInfo - Shell info from getShell()
|
|
@@ -271,16 +264,10 @@ function runScreenWithLogCapture(command, sessionName, shellInfo, user = null) {
|
|
|
271
264
|
if (useNativeLogging) {
|
|
272
265
|
// Modern screen (>= 4.5.1): Use -L -Logfile option for native log capture
|
|
273
266
|
// screen -dmS <session> -L -Logfile <logfile> <shell> -c '<command>'
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
'-Logfile',
|
|
279
|
-
logFile,
|
|
280
|
-
shell,
|
|
281
|
-
shellArg,
|
|
282
|
-
effectiveCommand,
|
|
283
|
-
];
|
|
267
|
+
const logArgs = ['-dmS', sessionName, '-L', '-Logfile', logFile];
|
|
268
|
+
screenArgs = isInteractiveShellCommand(command)
|
|
269
|
+
? [...logArgs, ...command.trim().split(/\s+/)]
|
|
270
|
+
: [...logArgs, shell, shellArg, effectiveCommand];
|
|
284
271
|
|
|
285
272
|
if (DEBUG) {
|
|
286
273
|
console.log(
|
|
@@ -289,10 +276,14 @@ function runScreenWithLogCapture(command, sessionName, shellInfo, user = null) {
|
|
|
289
276
|
}
|
|
290
277
|
} else {
|
|
291
278
|
// Older screen (< 4.5.1, e.g., macOS bundled 4.0.3): Use tee fallback
|
|
292
|
-
// Wrap the command to capture output using tee
|
|
293
279
|
// The parentheses ensure proper grouping of the command and its stderr
|
|
294
|
-
|
|
295
|
-
|
|
280
|
+
const isBareShell = isInteractiveShellCommand(command);
|
|
281
|
+
if (!isBareShell) {
|
|
282
|
+
effectiveCommand = `(${effectiveCommand}) 2>&1 | tee "${logFile}"`;
|
|
283
|
+
}
|
|
284
|
+
screenArgs = isBareShell
|
|
285
|
+
? ['-dmS', sessionName, ...command.trim().split(/\s+/)]
|
|
286
|
+
: ['-dmS', sessionName, shell, shellArg, effectiveCommand];
|
|
296
287
|
|
|
297
288
|
if (DEBUG) {
|
|
298
289
|
console.log(
|
|
@@ -451,13 +442,9 @@ function runInScreen(command, options = {}) {
|
|
|
451
442
|
}
|
|
452
443
|
// Without keep-alive: command runs and session exits naturally when done
|
|
453
444
|
|
|
454
|
-
const screenArgs =
|
|
455
|
-
'-dmS',
|
|
456
|
-
sessionName,
|
|
457
|
-
shell,
|
|
458
|
-
shellArg,
|
|
459
|
-
effectiveCommand,
|
|
460
|
-
];
|
|
445
|
+
const screenArgs = isInteractiveShellCommand(command)
|
|
446
|
+
? ['-dmS', sessionName, ...command.trim().split(/\s+/)]
|
|
447
|
+
: ['-dmS', sessionName, shell, shellArg, effectiveCommand];
|
|
461
448
|
|
|
462
449
|
if (DEBUG) {
|
|
463
450
|
console.log(`[DEBUG] Running: screen ${screenArgs.join(' ')}`);
|
|
@@ -670,7 +657,9 @@ function runInSsh(command, options = {}) {
|
|
|
670
657
|
const shellInvocation = shellInteractiveFlag
|
|
671
658
|
? `${remoteShell} ${shellInteractiveFlag}`
|
|
672
659
|
: remoteShell;
|
|
673
|
-
const remoteCommand =
|
|
660
|
+
const remoteCommand = isInteractiveShellCommand(command)
|
|
661
|
+
? `nohup ${command} > /tmp/${sessionName}.log 2>&1 &`
|
|
662
|
+
: `nohup ${shellInvocation} -c ${JSON.stringify(command)} > /tmp/${sessionName}.log 2>&1 &`;
|
|
674
663
|
const sshArgs = [sshTarget, remoteCommand];
|
|
675
664
|
|
|
676
665
|
if (DEBUG) {
|
|
@@ -692,9 +681,11 @@ function runInSsh(command, options = {}) {
|
|
|
692
681
|
} else {
|
|
693
682
|
// Attached mode: pass command directly (auto) or wrap with explicit shell + -i flag (bash/zsh).
|
|
694
683
|
const extraFlags = shellInteractiveFlag ? [shellInteractiveFlag] : [];
|
|
695
|
-
const sshArgs =
|
|
696
|
-
? [sshTarget,
|
|
697
|
-
:
|
|
684
|
+
const sshArgs = isInteractiveShellCommand(command)
|
|
685
|
+
? [sshTarget, ...command.trim().split(/\s+/)]
|
|
686
|
+
: useExplicitShell
|
|
687
|
+
? [sshTarget, useExplicitShell, ...extraFlags, '-c', command]
|
|
688
|
+
: [sshTarget, command];
|
|
698
689
|
|
|
699
690
|
if (DEBUG) {
|
|
700
691
|
console.log(`[DEBUG] Running: ssh ${sshArgs.join(' ')}`);
|
|
@@ -804,7 +795,10 @@ function runInDocker(command, options = {}) {
|
|
|
804
795
|
const shellArgs = shellInteractiveFlag
|
|
805
796
|
? [shellToUse, shellInteractiveFlag]
|
|
806
797
|
: [shellToUse];
|
|
807
|
-
|
|
798
|
+
const cmdArgs = isInteractiveShellCommand(command)
|
|
799
|
+
? command.trim().split(/\s+/)
|
|
800
|
+
: [...shellArgs, '-c', effectiveCommand];
|
|
801
|
+
dockerArgs.push(options.image, ...cmdArgs);
|
|
808
802
|
|
|
809
803
|
if (DEBUG) {
|
|
810
804
|
console.log(`[DEBUG] Running: docker ${dockerArgs.join(' ')}`);
|
|
@@ -853,7 +847,10 @@ function runInDocker(command, options = {}) {
|
|
|
853
847
|
const shellCmdArgs = shellInteractiveFlag
|
|
854
848
|
? [shellToUse, shellInteractiveFlag]
|
|
855
849
|
: [shellToUse];
|
|
856
|
-
|
|
850
|
+
const attachedCmdArgs = isInteractiveShellCommand(command)
|
|
851
|
+
? command.trim().split(/\s+/)
|
|
852
|
+
: [...shellCmdArgs, '-c', command];
|
|
853
|
+
dockerArgs.push(options.image, ...attachedCmdArgs);
|
|
857
854
|
|
|
858
855
|
if (DEBUG) {
|
|
859
856
|
console.log(`[DEBUG] Running: docker ${dockerArgs.join(' ')}`);
|
|
@@ -972,6 +969,7 @@ const {
|
|
|
972
969
|
module.exports = {
|
|
973
970
|
isCommandAvailable,
|
|
974
971
|
hasTTY,
|
|
972
|
+
isInteractiveShellCommand,
|
|
975
973
|
detectShellInEnvironment,
|
|
976
974
|
runInScreen,
|
|
977
975
|
runInTmux,
|
package/test/isolation.test.js
CHANGED
|
@@ -896,3 +896,59 @@ describe('Shell option forwarding to isolation runners', () => {
|
|
|
896
896
|
});
|
|
897
897
|
});
|
|
898
898
|
});
|
|
899
|
+
|
|
900
|
+
describe('isInteractiveShellCommand (issue #84)', () => {
|
|
901
|
+
const { isInteractiveShellCommand } = require('../src/lib/isolation');
|
|
902
|
+
|
|
903
|
+
it('should return true for "bash"', () => {
|
|
904
|
+
assert.strictEqual(isInteractiveShellCommand('bash'), true);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
it('should return true for "zsh"', () => {
|
|
908
|
+
assert.strictEqual(isInteractiveShellCommand('zsh'), true);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it('should return true for "sh"', () => {
|
|
912
|
+
assert.strictEqual(isInteractiveShellCommand('sh'), true);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
it('should return true for "/bin/bash"', () => {
|
|
916
|
+
assert.strictEqual(isInteractiveShellCommand('/bin/bash'), true);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
it('should return true for "/usr/bin/zsh"', () => {
|
|
920
|
+
assert.strictEqual(isInteractiveShellCommand('/usr/bin/zsh'), true);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it('should return true for "bash -l" (login flag, no -c)', () => {
|
|
924
|
+
assert.strictEqual(isInteractiveShellCommand('bash -l'), true);
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
it('should return false for "bash -c echo"', () => {
|
|
928
|
+
assert.strictEqual(isInteractiveShellCommand('bash -c echo'), false);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
it('should return false for "bash -c \'echo hi\'"', () => {
|
|
932
|
+
assert.strictEqual(isInteractiveShellCommand("bash -c 'echo hi'"), false);
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
it('should return false for "npm test"', () => {
|
|
936
|
+
assert.strictEqual(isInteractiveShellCommand('npm test'), false);
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
it('should return false for "echo hello"', () => {
|
|
940
|
+
assert.strictEqual(isInteractiveShellCommand('echo hello'), false);
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
it('should return false for "ls -la"', () => {
|
|
944
|
+
assert.strictEqual(isInteractiveShellCommand('ls -la'), false);
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
it('should return false for empty string', () => {
|
|
948
|
+
assert.strictEqual(isInteractiveShellCommand(''), false);
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it('should return false for " "', () => {
|
|
952
|
+
assert.strictEqual(isInteractiveShellCommand(' '), false);
|
|
953
|
+
});
|
|
954
|
+
});
|