start-command 0.24.5 → 0.24.6
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 +28 -27
- package/test/regression-91.test.js +260 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.24.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- cfc5731: fix: pass `bash -c "..."` style commands directly to Docker without double-wrapping (issue #91)
|
|
8
|
+
|
|
9
|
+
When a command like `bash -i -c "nvm --version"` was passed to Docker isolation,
|
|
10
|
+
it was incorrectly wrapped in an outer shell: `bash -i -c "bash -i -c nvm --version"`.
|
|
11
|
+
This caused two bugs: (1) the quoted argument `"nvm --version"` was split by the outer
|
|
12
|
+
shell, so `--version` became `$0` instead of part of the script; (2) the sudo advisory
|
|
13
|
+
message printed twice due to two nested bash invocations.
|
|
14
|
+
|
|
15
|
+
The fix adds `isShellInvocationWithArgs()` to detect commands that start with a shell
|
|
16
|
+
binary and include `-c`, and `buildShellWithArgsCmdArgs()` to reconstruct the correct
|
|
17
|
+
argv array. Such commands are now passed directly to Docker's exec, just like bare shell
|
|
18
|
+
invocations (issue #84), without any additional wrapping.
|
|
19
|
+
|
|
3
20
|
## 0.24.5
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/package.json
CHANGED
package/src/lib/isolation.js
CHANGED
|
@@ -106,10 +106,7 @@ function isCommandAvailable(command) {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/**
|
|
110
|
-
* Get the shell to use for command execution
|
|
111
|
-
* @returns {{shell: string, shellArgs: string[]}} Shell path and args
|
|
112
|
-
*/
|
|
109
|
+
/** Get the shell to use for command execution. */
|
|
113
110
|
function getShell() {
|
|
114
111
|
const isWindows = process.platform === 'win32';
|
|
115
112
|
const shell = isWindows ? 'cmd.exe' : process.env.SHELL || '/bin/sh';
|
|
@@ -217,11 +214,28 @@ function getShellInteractiveFlag(shellPath) {
|
|
|
217
214
|
const shellName = shellPath.split('/').pop();
|
|
218
215
|
return shellName === 'bash' || shellName === 'zsh' ? '-i' : null;
|
|
219
216
|
}
|
|
217
|
+
const SHELL_NAMES = ['bash', 'zsh', 'sh', 'fish', 'ksh', 'csh', 'tcsh', 'dash'];
|
|
220
218
|
/** True if command is a bare shell invocation (no -c); avoids bash-inside-bash (issue #84). */
|
|
221
219
|
function isInteractiveShellCommand(command) {
|
|
222
220
|
const parts = command.trim().split(/\s+/);
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
return SHELL_NAMES.includes(path.basename(parts[0])) && !parts.includes('-c');
|
|
222
|
+
}
|
|
223
|
+
/** True if command is a shell invocation with -c (e.g. `bash -i -c "cmd"`); avoids double-wrapping (issue #91). */
|
|
224
|
+
function isShellInvocationWithArgs(command) {
|
|
225
|
+
const parts = command.trim().split(/\s+/);
|
|
226
|
+
return SHELL_NAMES.includes(path.basename(parts[0])) && parts.includes('-c');
|
|
227
|
+
}
|
|
228
|
+
/** Build argv for shell-with-c command; everything after -c is one argument (reverses commandArgs.join(' ')). */
|
|
229
|
+
function buildShellWithArgsCmdArgs(command) {
|
|
230
|
+
const parts = command.trim().split(/\s+/);
|
|
231
|
+
const cIdx = parts.indexOf('-c');
|
|
232
|
+
if (cIdx === -1) {
|
|
233
|
+
return parts;
|
|
234
|
+
}
|
|
235
|
+
const scriptArg = parts.slice(cIdx + 1).join(' ');
|
|
236
|
+
return scriptArg.length > 0
|
|
237
|
+
? [...parts.slice(0, cIdx + 1), scriptArg]
|
|
238
|
+
: parts.slice(0, cIdx + 1);
|
|
225
239
|
}
|
|
226
240
|
|
|
227
241
|
/** Returns true if the current process has a TTY attached. */
|
|
@@ -428,9 +442,7 @@ function runInScreen(command, options = {}) {
|
|
|
428
442
|
let effectiveCommand = wrapCommandWithUser(command, options.user);
|
|
429
443
|
|
|
430
444
|
if (options.detached) {
|
|
431
|
-
// Detached mode: screen -dmS <session> <shell> -c '<command>'
|
|
432
445
|
if (options.keepAlive) {
|
|
433
|
-
// With keep-alive: run command, then keep shell open
|
|
434
446
|
effectiveCommand = `${effectiveCommand}; exec ${shell}`;
|
|
435
447
|
}
|
|
436
448
|
|
|
@@ -466,14 +478,6 @@ function runInScreen(command, options = {}) {
|
|
|
466
478
|
message,
|
|
467
479
|
});
|
|
468
480
|
} else {
|
|
469
|
-
// Attached mode: use detached mode with log capture for reliable output
|
|
470
|
-
// (direct attached screen loses output for quick commands; see issue #25)
|
|
471
|
-
if (DEBUG) {
|
|
472
|
-
console.log(
|
|
473
|
-
`[DEBUG] Using detached mode with log capture for reliable output`
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
481
|
return runScreenWithLogCapture(
|
|
478
482
|
command,
|
|
479
483
|
sessionName,
|
|
@@ -515,9 +519,7 @@ function runInTmux(command, options = {}) {
|
|
|
515
519
|
|
|
516
520
|
try {
|
|
517
521
|
if (options.detached) {
|
|
518
|
-
// Detached mode: tmux new-session -d -s <session> '<command>'
|
|
519
522
|
if (options.keepAlive) {
|
|
520
|
-
// With keep-alive: run command, then keep shell open
|
|
521
523
|
effectiveCommand = `${effectiveCommand}; exec ${shell}`;
|
|
522
524
|
}
|
|
523
525
|
|
|
@@ -549,7 +551,6 @@ function runInTmux(command, options = {}) {
|
|
|
549
551
|
message,
|
|
550
552
|
});
|
|
551
553
|
} else {
|
|
552
|
-
// Attached mode: tmux new-session -s <session> '<command>'
|
|
553
554
|
if (DEBUG) {
|
|
554
555
|
console.log(
|
|
555
556
|
`[DEBUG] Running: tmux new-session -s "${sessionName}" "${effectiveCommand}"`
|
|
@@ -631,7 +632,6 @@ function runInSsh(command, options = {}) {
|
|
|
631
632
|
|
|
632
633
|
try {
|
|
633
634
|
if (options.detached) {
|
|
634
|
-
// Detached mode: Run command in background via nohup; continues after SSH closes
|
|
635
635
|
const remoteShell = useExplicitShell || shellToUse;
|
|
636
636
|
const shellInvocation = shellInteractiveFlag
|
|
637
637
|
? `${remoteShell} ${shellInteractiveFlag}`
|
|
@@ -658,7 +658,6 @@ function runInSsh(command, options = {}) {
|
|
|
658
658
|
message: `Command started in detached SSH session on ${sshTarget}\nSession: ${sessionName}\nView logs: ssh ${sshTarget} "tail -f /tmp/${sessionName}.log"`,
|
|
659
659
|
});
|
|
660
660
|
} else {
|
|
661
|
-
// Attached mode: pass command directly (auto) or wrap with explicit shell + -i flag (bash/zsh).
|
|
662
661
|
const extraFlags = shellInteractiveFlag ? [shellInteractiveFlag] : [];
|
|
663
662
|
const sshArgs = isInteractiveShellCommand(command)
|
|
664
663
|
? [sshTarget, ...command.trim().split(/\s+/)]
|
|
@@ -747,8 +746,6 @@ function runInDocker(command, options = {}) {
|
|
|
747
746
|
}
|
|
748
747
|
|
|
749
748
|
const containerName = options.session || generateSessionName('docker');
|
|
750
|
-
|
|
751
|
-
// Check if image exists locally; if not, pull it as a virtual command
|
|
752
749
|
if (!dockerImageExists(options.image)) {
|
|
753
750
|
const pullResult = dockerPullImage(options.image);
|
|
754
751
|
if (!pullResult.success) {
|
|
@@ -772,9 +769,7 @@ function runInDocker(command, options = {}) {
|
|
|
772
769
|
|
|
773
770
|
try {
|
|
774
771
|
if (options.detached) {
|
|
775
|
-
// Detached mode: docker run -d --name <name> [--user <user>] <image> <shell> -c '<command>'
|
|
776
772
|
const dockerArgs = ['run', '-d', '--name', containerName];
|
|
777
|
-
// --rm must come before the image name in args
|
|
778
773
|
if (options.autoRemoveDockerContainer) {
|
|
779
774
|
dockerArgs.splice(2, 0, '--rm');
|
|
780
775
|
}
|
|
@@ -791,7 +786,9 @@ function runInDocker(command, options = {}) {
|
|
|
791
786
|
: [shellToUse];
|
|
792
787
|
const cmdArgs = isBareShell
|
|
793
788
|
? command.trim().split(/\s+/)
|
|
794
|
-
:
|
|
789
|
+
: isShellInvocationWithArgs(command)
|
|
790
|
+
? buildShellWithArgsCmdArgs(effectiveCommand)
|
|
791
|
+
: [...shellArgs, '-c', effectiveCommand];
|
|
795
792
|
dockerArgs.push(options.image, ...cmdArgs);
|
|
796
793
|
|
|
797
794
|
if (DEBUG) {
|
|
@@ -829,7 +826,6 @@ function runInDocker(command, options = {}) {
|
|
|
829
826
|
message,
|
|
830
827
|
});
|
|
831
828
|
} else {
|
|
832
|
-
// Attached mode: docker run -it --rm --name <name> [--user <user>] <image> <shell> -c '<cmd>'
|
|
833
829
|
const dockerArgs = ['run', '-it', '--rm', '--name', containerName];
|
|
834
830
|
if (options.user) {
|
|
835
831
|
dockerArgs.push('--user', options.user);
|
|
@@ -841,6 +837,7 @@ function runInDocker(command, options = {}) {
|
|
|
841
837
|
? [shellToUse, shellInteractiveFlag]
|
|
842
838
|
: [shellToUse];
|
|
843
839
|
// Bare shell: pass directly with -i (avoids bash-inside-bash, issue #84; -i ensures interactive).
|
|
840
|
+
// Shell with -c: pass directly as argv (avoids double-wrapping and quote-stripping, issue #91).
|
|
844
841
|
let attachedCmdArgs;
|
|
845
842
|
if (isBareShell) {
|
|
846
843
|
const parts = command.trim().split(/\s+/);
|
|
@@ -849,6 +846,8 @@ function runInDocker(command, options = {}) {
|
|
|
849
846
|
bareFlag && !parts.includes(bareFlag)
|
|
850
847
|
? [parts[0], bareFlag, ...parts.slice(1)]
|
|
851
848
|
: parts;
|
|
849
|
+
} else if (isShellInvocationWithArgs(command)) {
|
|
850
|
+
attachedCmdArgs = buildShellWithArgsCmdArgs(command);
|
|
852
851
|
} else {
|
|
853
852
|
attachedCmdArgs = [...shellCmdArgs, '-c', command];
|
|
854
853
|
}
|
|
@@ -973,6 +972,8 @@ module.exports = {
|
|
|
973
972
|
isCommandAvailable,
|
|
974
973
|
hasTTY,
|
|
975
974
|
isInteractiveShellCommand,
|
|
975
|
+
isShellInvocationWithArgs,
|
|
976
|
+
buildShellWithArgsCmdArgs,
|
|
976
977
|
detectShellInEnvironment,
|
|
977
978
|
runInScreen,
|
|
978
979
|
runInTmux,
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Regression tests for issue #91:
|
|
4
|
+
* "`bash -i -c "nvm --version"` was interpreted as `bash -i -c nvm --version`,
|
|
5
|
+
* and executed inside bash, instead of directly"
|
|
6
|
+
*
|
|
7
|
+
* Two bugs were reported:
|
|
8
|
+
*
|
|
9
|
+
* Bug 1 — Quote stripping / wrong interpretation:
|
|
10
|
+
* `bash -i -c "nvm --version"` was treated as `bash -i -c nvm` with `--version`
|
|
11
|
+
* as $0. Caused by join(' ') collapsing the quoted argument, then the command
|
|
12
|
+
* being re-wrapped in an outer shell: `bash -i -c "bash -i -c nvm --version"`.
|
|
13
|
+
* The outer bash then parsed `nvm --version` as two separate words.
|
|
14
|
+
*
|
|
15
|
+
* Bug 2 — Executed inside bash (double-wrapping):
|
|
16
|
+
* Because isInteractiveShellCommand() returns false for any command with -c,
|
|
17
|
+
* the code wrapped `bash -i -c "nvm --version"` inside another `bash -i -c "..."`.
|
|
18
|
+
* This caused the sudo advisory message to print twice (one per bash invocation).
|
|
19
|
+
*
|
|
20
|
+
* Root cause:
|
|
21
|
+
* isInteractiveShellCommand() only returns true for bare shell invocations
|
|
22
|
+
* (no -c flag). Commands that ARE a shell invocation but include -c fell into the
|
|
23
|
+
* else-branch: `[shellToUse, shellInteractiveFlag, '-c', command]`, creating a
|
|
24
|
+
* shell-inside-shell. The fix adds isShellInvocationWithArgs() + buildShellWithArgsCmdArgs()
|
|
25
|
+
* to detect and pass such commands directly to Docker without additional wrapping.
|
|
26
|
+
*
|
|
27
|
+
* Reference: https://github.com/link-foundation/start/issues/91
|
|
28
|
+
* Fixed in: PR #92
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const { describe, it } = require('node:test');
|
|
32
|
+
const assert = require('assert');
|
|
33
|
+
const {
|
|
34
|
+
isInteractiveShellCommand,
|
|
35
|
+
isShellInvocationWithArgs,
|
|
36
|
+
buildShellWithArgsCmdArgs,
|
|
37
|
+
} = require('../src/lib/isolation');
|
|
38
|
+
|
|
39
|
+
// Helper: mirrors the attached-mode command-args construction logic in runInDocker.
|
|
40
|
+
// Returns the argv array that would be passed to `docker run ... image <argv>`.
|
|
41
|
+
function buildAttachedCmdArgs(command, shellToUse = '/bin/bash') {
|
|
42
|
+
const path = require('path');
|
|
43
|
+
const shellName = shellToUse.split('/').pop();
|
|
44
|
+
const shellInteractiveFlag =
|
|
45
|
+
shellName === 'bash' || shellName === 'zsh' ? '-i' : null;
|
|
46
|
+
const shellCmdArgs = shellInteractiveFlag
|
|
47
|
+
? [shellToUse, shellInteractiveFlag]
|
|
48
|
+
: [shellToUse];
|
|
49
|
+
|
|
50
|
+
if (isInteractiveShellCommand(command)) {
|
|
51
|
+
// Bare shell: pass directly with explicit -i (issue #84 fix)
|
|
52
|
+
const parts = command.trim().split(/\s+/);
|
|
53
|
+
const bareFlag =
|
|
54
|
+
path.basename(parts[0]) === 'bash' || path.basename(parts[0]) === 'zsh'
|
|
55
|
+
? '-i'
|
|
56
|
+
: null;
|
|
57
|
+
if (bareFlag && !parts.includes(bareFlag)) {
|
|
58
|
+
return [parts[0], bareFlag, ...parts.slice(1)];
|
|
59
|
+
}
|
|
60
|
+
return parts;
|
|
61
|
+
} else if (isShellInvocationWithArgs(command)) {
|
|
62
|
+
// Shell with -c: pass directly as argv (issue #91 fix)
|
|
63
|
+
return buildShellWithArgsCmdArgs(command);
|
|
64
|
+
}
|
|
65
|
+
return [...shellCmdArgs, '-c', command];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('isShellInvocationWithArgs (issue #91)', () => {
|
|
69
|
+
it('should return true for "bash -i -c nvm --version"', () => {
|
|
70
|
+
assert.strictEqual(
|
|
71
|
+
isShellInvocationWithArgs('bash -i -c nvm --version'),
|
|
72
|
+
true
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should return true for \'bash -c "echo hello"\'', () => {
|
|
77
|
+
assert.strictEqual(isShellInvocationWithArgs('bash -c "echo hello"'), true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should return true for "bash -c echo hello"', () => {
|
|
81
|
+
assert.strictEqual(isShellInvocationWithArgs('bash -c echo hello'), true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should return true for "zsh -c nvm --version"', () => {
|
|
85
|
+
assert.strictEqual(isShellInvocationWithArgs('zsh -c nvm --version'), true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should return true for "sh -c ls"', () => {
|
|
89
|
+
assert.strictEqual(isShellInvocationWithArgs('sh -c ls'), true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should return true for "/bin/bash -c echo hi"', () => {
|
|
93
|
+
assert.strictEqual(isShellInvocationWithArgs('/bin/bash -c echo hi'), true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should return false for bare "bash" (no -c)', () => {
|
|
97
|
+
assert.strictEqual(isShellInvocationWithArgs('bash'), false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should return false for "bash -i" (no -c)', () => {
|
|
101
|
+
assert.strictEqual(isShellInvocationWithArgs('bash -i'), false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should return false for "bash --norc" (no -c)', () => {
|
|
105
|
+
assert.strictEqual(isShellInvocationWithArgs('bash --norc'), false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return false for non-shell commands', () => {
|
|
109
|
+
assert.strictEqual(isShellInvocationWithArgs('nvm --version'), false);
|
|
110
|
+
assert.strictEqual(isShellInvocationWithArgs('echo hello'), false);
|
|
111
|
+
assert.strictEqual(isShellInvocationWithArgs('npm test'), false);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('buildShellWithArgsCmdArgs (issue #91)', () => {
|
|
116
|
+
it('should reconstruct "bash -i -c nvm --version" correctly', () => {
|
|
117
|
+
const result = buildShellWithArgsCmdArgs('bash -i -c nvm --version');
|
|
118
|
+
assert.deepStrictEqual(result, ['bash', '-i', '-c', 'nvm --version']);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should reconstruct "bash -c echo hello" correctly', () => {
|
|
122
|
+
const result = buildShellWithArgsCmdArgs('bash -c echo hello');
|
|
123
|
+
assert.deepStrictEqual(result, ['bash', '-c', 'echo hello']);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should handle single-word script "bash -c ls"', () => {
|
|
127
|
+
const result = buildShellWithArgsCmdArgs('bash -c ls');
|
|
128
|
+
assert.deepStrictEqual(result, ['bash', '-c', 'ls']);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should handle zsh with -c', () => {
|
|
132
|
+
const result = buildShellWithArgsCmdArgs('zsh -c nvm --version');
|
|
133
|
+
assert.deepStrictEqual(result, ['zsh', '-c', 'nvm --version']);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should handle /bin/bash -i -c with multi-word script', () => {
|
|
137
|
+
const result = buildShellWithArgsCmdArgs(
|
|
138
|
+
'/bin/bash -i -c node -e process.version'
|
|
139
|
+
);
|
|
140
|
+
assert.deepStrictEqual(result, [
|
|
141
|
+
'/bin/bash',
|
|
142
|
+
'-i',
|
|
143
|
+
'-c',
|
|
144
|
+
'node -e process.version',
|
|
145
|
+
]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should not include -c argument inside script argument (no double -c)', () => {
|
|
149
|
+
const result = buildShellWithArgsCmdArgs('bash -i -c nvm --version');
|
|
150
|
+
// The script arg must be 'nvm --version', not '-c nvm --version'
|
|
151
|
+
assert.strictEqual(result[result.length - 1], 'nvm --version');
|
|
152
|
+
assert.strictEqual(result.indexOf('-c'), 2);
|
|
153
|
+
assert.strictEqual(result.length, 4);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('Regression: No Double-Wrapping for Shell With -c (issue #91)', () => {
|
|
158
|
+
// Each test verifies that `bash -i -c "cmd"` style commands are NOT re-wrapped
|
|
159
|
+
// in another outer shell -c invocation.
|
|
160
|
+
//
|
|
161
|
+
// Before fix: buildAttachedCmdArgs('bash -i -c nvm --version')
|
|
162
|
+
// → ['/bin/bash', '-i', '-c', 'bash -i -c nvm --version'] (WRONG: double-wrap)
|
|
163
|
+
// After fix: buildAttachedCmdArgs('bash -i -c nvm --version')
|
|
164
|
+
// → ['bash', '-i', '-c', 'nvm --version'] (CORRECT: direct pass)
|
|
165
|
+
|
|
166
|
+
it('should pass "bash -i -c nvm --version" directly without outer shell wrapper', () => {
|
|
167
|
+
const args = buildAttachedCmdArgs('bash -i -c nvm --version');
|
|
168
|
+
// Must start with 'bash', not with '/bin/bash' (the outer shellToUse)
|
|
169
|
+
assert.strictEqual(args[0], 'bash');
|
|
170
|
+
// Must not wrap in outer bash -i -c
|
|
171
|
+
assert.ok(
|
|
172
|
+
args.filter((a) => a === '-c').length === 1,
|
|
173
|
+
`Must have exactly one -c flag, got: ${JSON.stringify(args)}`
|
|
174
|
+
);
|
|
175
|
+
// The script argument must be 'nvm --version' as one element
|
|
176
|
+
assert.deepStrictEqual(
|
|
177
|
+
args,
|
|
178
|
+
['bash', '-i', '-c', 'nvm --version'],
|
|
179
|
+
`Expected ['bash', '-i', '-c', 'nvm --version'], got: ${JSON.stringify(args)}`
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should pass "zsh -c nvm --version" directly without outer shell wrapper', () => {
|
|
184
|
+
const args = buildAttachedCmdArgs('zsh -c nvm --version', '/bin/zsh');
|
|
185
|
+
assert.deepStrictEqual(args, ['zsh', '-c', 'nvm --version']);
|
|
186
|
+
assert.ok(
|
|
187
|
+
args.filter((a) => a === '-c').length === 1,
|
|
188
|
+
'Must have exactly one -c flag'
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should pass "bash -c echo hello" directly', () => {
|
|
193
|
+
const args = buildAttachedCmdArgs('bash -c echo hello');
|
|
194
|
+
assert.deepStrictEqual(args, ['bash', '-c', 'echo hello']);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should not introduce a second bash layer (no shell-inside-shell)', () => {
|
|
198
|
+
const args = buildAttachedCmdArgs('bash -i -c nvm --version');
|
|
199
|
+
// The first element of the docker image command args must NOT be the outer shell
|
|
200
|
+
// i.e., must not be ['/bin/bash', '-i', '-c', 'bash ...']
|
|
201
|
+
assert.notStrictEqual(
|
|
202
|
+
args[0],
|
|
203
|
+
'/bin/bash',
|
|
204
|
+
'Must not wrap in outer /bin/bash (shell-inside-shell)'
|
|
205
|
+
);
|
|
206
|
+
// Must not contain the original full command string as an argument
|
|
207
|
+
assert.ok(
|
|
208
|
+
!args.includes('bash -i -c nvm --version'),
|
|
209
|
+
'Must not contain the full command string as a single argument (double-wrap)'
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should still handle bare "bash" with -i (regression guard for issue #84)', () => {
|
|
214
|
+
const args = buildAttachedCmdArgs('bash');
|
|
215
|
+
assert.deepStrictEqual(args, ['bash', '-i']);
|
|
216
|
+
assert.ok(
|
|
217
|
+
!args.includes('-c'),
|
|
218
|
+
'Bare shell must not use -c (issue #84 guard)'
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should still wrap non-shell commands in outer shell -c (guard against over-broad fix)', () => {
|
|
223
|
+
const args = buildAttachedCmdArgs('nvm --version', '/bin/bash');
|
|
224
|
+
assert.deepStrictEqual(args, ['/bin/bash', '-i', '-c', 'nvm --version']);
|
|
225
|
+
assert.ok(
|
|
226
|
+
args.includes('-c'),
|
|
227
|
+
'Non-shell commands must still use -c wrapper'
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should still wrap "npm test" in outer shell -c', () => {
|
|
232
|
+
const args = buildAttachedCmdArgs('npm test', '/bin/bash');
|
|
233
|
+
assert.deepStrictEqual(args, ['/bin/bash', '-i', '-c', 'npm test']);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('isShellInvocationWithArgs is mutually exclusive with isInteractiveShellCommand', () => {
|
|
238
|
+
// A command cannot be both a bare shell AND a shell-with-args; they are disjoint.
|
|
239
|
+
const testCases = [
|
|
240
|
+
'bash',
|
|
241
|
+
'bash -i',
|
|
242
|
+
'bash --norc',
|
|
243
|
+
'bash -i -c nvm --version',
|
|
244
|
+
'bash -c echo hi',
|
|
245
|
+
'zsh -c ls',
|
|
246
|
+
'nvm --version',
|
|
247
|
+
'echo hello',
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
for (const cmd of testCases) {
|
|
251
|
+
it(`"${cmd}" is not both bare-shell and shell-with-args`, () => {
|
|
252
|
+
const bare = isInteractiveShellCommand(cmd);
|
|
253
|
+
const withArgs = isShellInvocationWithArgs(cmd);
|
|
254
|
+
assert.ok(
|
|
255
|
+
!(bare && withArgs),
|
|
256
|
+
`"${cmd}" must not return true from both helpers simultaneously`
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
});
|