start-command 0.24.5 → 0.24.7
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 +33 -0
- package/package.json +1 -1
- package/src/lib/isolation.js +17 -32
- package/src/lib/shell-utils.js +47 -0
- package/test/regression-91.test.js +296 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.24.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1eef620: fix: display `bash -c "..."` commands with quotes in command line output (issue #91)
|
|
8
|
+
|
|
9
|
+
When a command like `bash -i -c nvm --version` was passed to Docker isolation,
|
|
10
|
+
the displayed command line was missing quotes around the `-c` script argument,
|
|
11
|
+
making the output misleading (showing `bash -i -c nvm --version` instead of
|
|
12
|
+
`bash -i -c "nvm --version"`).
|
|
13
|
+
|
|
14
|
+
A new `buildDisplayCommand()` helper is added in `shell-utils.js` that quotes
|
|
15
|
+
any space-containing `-c` script arguments so the displayed command accurately
|
|
16
|
+
reflects how it was interpreted. Shell command helpers are extracted from
|
|
17
|
+
`isolation.js` into a new `shell-utils.js` module to keep file sizes within limits.
|
|
18
|
+
|
|
19
|
+
## 0.24.6
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- cfc5731: fix: pass `bash -c "..."` style commands directly to Docker without double-wrapping (issue #91)
|
|
24
|
+
|
|
25
|
+
When a command like `bash -i -c "nvm --version"` was passed to Docker isolation,
|
|
26
|
+
it was incorrectly wrapped in an outer shell: `bash -i -c "bash -i -c nvm --version"`.
|
|
27
|
+
This caused two bugs: (1) the quoted argument `"nvm --version"` was split by the outer
|
|
28
|
+
shell, so `--version` became `$0` instead of part of the script; (2) the sudo advisory
|
|
29
|
+
message printed twice due to two nested bash invocations.
|
|
30
|
+
|
|
31
|
+
The fix adds `isShellInvocationWithArgs()` to detect commands that start with a shell
|
|
32
|
+
binary and include `-c`, and `buildShellWithArgsCmdArgs()` to reconstruct the correct
|
|
33
|
+
argv array. Such commands are now passed directly to Docker's exec, just like bare shell
|
|
34
|
+
invocations (issue #84), without any additional wrapping.
|
|
35
|
+
|
|
3
36
|
## 0.24.5
|
|
4
37
|
|
|
5
38
|
### 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,12 +214,12 @@ function getShellInteractiveFlag(shellPath) {
|
|
|
217
214
|
const shellName = shellPath.split('/').pop();
|
|
218
215
|
return shellName === 'bash' || shellName === 'zsh' ? '-i' : null;
|
|
219
216
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
217
|
+
const {
|
|
218
|
+
isInteractiveShellCommand,
|
|
219
|
+
isShellInvocationWithArgs,
|
|
220
|
+
buildShellWithArgsCmdArgs,
|
|
221
|
+
buildDisplayCommand,
|
|
222
|
+
} = require('./shell-utils');
|
|
226
223
|
|
|
227
224
|
/** Returns true if the current process has a TTY attached. */
|
|
228
225
|
function hasTTY() {
|
|
@@ -428,9 +425,7 @@ function runInScreen(command, options = {}) {
|
|
|
428
425
|
let effectiveCommand = wrapCommandWithUser(command, options.user);
|
|
429
426
|
|
|
430
427
|
if (options.detached) {
|
|
431
|
-
// Detached mode: screen -dmS <session> <shell> -c '<command>'
|
|
432
428
|
if (options.keepAlive) {
|
|
433
|
-
// With keep-alive: run command, then keep shell open
|
|
434
429
|
effectiveCommand = `${effectiveCommand}; exec ${shell}`;
|
|
435
430
|
}
|
|
436
431
|
|
|
@@ -466,14 +461,6 @@ function runInScreen(command, options = {}) {
|
|
|
466
461
|
message,
|
|
467
462
|
});
|
|
468
463
|
} 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
464
|
return runScreenWithLogCapture(
|
|
478
465
|
command,
|
|
479
466
|
sessionName,
|
|
@@ -515,9 +502,7 @@ function runInTmux(command, options = {}) {
|
|
|
515
502
|
|
|
516
503
|
try {
|
|
517
504
|
if (options.detached) {
|
|
518
|
-
// Detached mode: tmux new-session -d -s <session> '<command>'
|
|
519
505
|
if (options.keepAlive) {
|
|
520
|
-
// With keep-alive: run command, then keep shell open
|
|
521
506
|
effectiveCommand = `${effectiveCommand}; exec ${shell}`;
|
|
522
507
|
}
|
|
523
508
|
|
|
@@ -549,7 +534,6 @@ function runInTmux(command, options = {}) {
|
|
|
549
534
|
message,
|
|
550
535
|
});
|
|
551
536
|
} else {
|
|
552
|
-
// Attached mode: tmux new-session -s <session> '<command>'
|
|
553
537
|
if (DEBUG) {
|
|
554
538
|
console.log(
|
|
555
539
|
`[DEBUG] Running: tmux new-session -s "${sessionName}" "${effectiveCommand}"`
|
|
@@ -631,7 +615,6 @@ function runInSsh(command, options = {}) {
|
|
|
631
615
|
|
|
632
616
|
try {
|
|
633
617
|
if (options.detached) {
|
|
634
|
-
// Detached mode: Run command in background via nohup; continues after SSH closes
|
|
635
618
|
const remoteShell = useExplicitShell || shellToUse;
|
|
636
619
|
const shellInvocation = shellInteractiveFlag
|
|
637
620
|
? `${remoteShell} ${shellInteractiveFlag}`
|
|
@@ -658,7 +641,6 @@ function runInSsh(command, options = {}) {
|
|
|
658
641
|
message: `Command started in detached SSH session on ${sshTarget}\nSession: ${sessionName}\nView logs: ssh ${sshTarget} "tail -f /tmp/${sessionName}.log"`,
|
|
659
642
|
});
|
|
660
643
|
} else {
|
|
661
|
-
// Attached mode: pass command directly (auto) or wrap with explicit shell + -i flag (bash/zsh).
|
|
662
644
|
const extraFlags = shellInteractiveFlag ? [shellInteractiveFlag] : [];
|
|
663
645
|
const sshArgs = isInteractiveShellCommand(command)
|
|
664
646
|
? [sshTarget, ...command.trim().split(/\s+/)]
|
|
@@ -747,8 +729,6 @@ function runInDocker(command, options = {}) {
|
|
|
747
729
|
}
|
|
748
730
|
|
|
749
731
|
const containerName = options.session || generateSessionName('docker');
|
|
750
|
-
|
|
751
|
-
// Check if image exists locally; if not, pull it as a virtual command
|
|
752
732
|
if (!dockerImageExists(options.image)) {
|
|
753
733
|
const pullResult = dockerPullImage(options.image);
|
|
754
734
|
if (!pullResult.success) {
|
|
@@ -767,14 +747,12 @@ function runInDocker(command, options = {}) {
|
|
|
767
747
|
: detectShellInEnvironment('docker', options, options.shell);
|
|
768
748
|
const shellInteractiveFlag = getShellInteractiveFlag(shellToUse);
|
|
769
749
|
|
|
770
|
-
console.log(outputBlocks.createCommandLine(command));
|
|
750
|
+
console.log(outputBlocks.createCommandLine(buildDisplayCommand(command)));
|
|
771
751
|
console.log();
|
|
772
752
|
|
|
773
753
|
try {
|
|
774
754
|
if (options.detached) {
|
|
775
|
-
// Detached mode: docker run -d --name <name> [--user <user>] <image> <shell> -c '<command>'
|
|
776
755
|
const dockerArgs = ['run', '-d', '--name', containerName];
|
|
777
|
-
// --rm must come before the image name in args
|
|
778
756
|
if (options.autoRemoveDockerContainer) {
|
|
779
757
|
dockerArgs.splice(2, 0, '--rm');
|
|
780
758
|
}
|
|
@@ -791,7 +769,9 @@ function runInDocker(command, options = {}) {
|
|
|
791
769
|
: [shellToUse];
|
|
792
770
|
const cmdArgs = isBareShell
|
|
793
771
|
? command.trim().split(/\s+/)
|
|
794
|
-
:
|
|
772
|
+
: isShellInvocationWithArgs(command)
|
|
773
|
+
? buildShellWithArgsCmdArgs(effectiveCommand)
|
|
774
|
+
: [...shellArgs, '-c', effectiveCommand];
|
|
795
775
|
dockerArgs.push(options.image, ...cmdArgs);
|
|
796
776
|
|
|
797
777
|
if (DEBUG) {
|
|
@@ -829,7 +809,6 @@ function runInDocker(command, options = {}) {
|
|
|
829
809
|
message,
|
|
830
810
|
});
|
|
831
811
|
} else {
|
|
832
|
-
// Attached mode: docker run -it --rm --name <name> [--user <user>] <image> <shell> -c '<cmd>'
|
|
833
812
|
const dockerArgs = ['run', '-it', '--rm', '--name', containerName];
|
|
834
813
|
if (options.user) {
|
|
835
814
|
dockerArgs.push('--user', options.user);
|
|
@@ -841,6 +820,7 @@ function runInDocker(command, options = {}) {
|
|
|
841
820
|
? [shellToUse, shellInteractiveFlag]
|
|
842
821
|
: [shellToUse];
|
|
843
822
|
// Bare shell: pass directly with -i (avoids bash-inside-bash, issue #84; -i ensures interactive).
|
|
823
|
+
// Shell with -c: pass directly as argv (avoids double-wrapping and quote-stripping, issue #91).
|
|
844
824
|
let attachedCmdArgs;
|
|
845
825
|
if (isBareShell) {
|
|
846
826
|
const parts = command.trim().split(/\s+/);
|
|
@@ -849,6 +829,8 @@ function runInDocker(command, options = {}) {
|
|
|
849
829
|
bareFlag && !parts.includes(bareFlag)
|
|
850
830
|
? [parts[0], bareFlag, ...parts.slice(1)]
|
|
851
831
|
: parts;
|
|
832
|
+
} else if (isShellInvocationWithArgs(command)) {
|
|
833
|
+
attachedCmdArgs = buildShellWithArgsCmdArgs(command);
|
|
852
834
|
} else {
|
|
853
835
|
attachedCmdArgs = [...shellCmdArgs, '-c', command];
|
|
854
836
|
}
|
|
@@ -973,6 +955,9 @@ module.exports = {
|
|
|
973
955
|
isCommandAvailable,
|
|
974
956
|
hasTTY,
|
|
975
957
|
isInteractiveShellCommand,
|
|
958
|
+
isShellInvocationWithArgs,
|
|
959
|
+
buildShellWithArgsCmdArgs,
|
|
960
|
+
buildDisplayCommand,
|
|
976
961
|
detectShellInEnvironment,
|
|
977
962
|
runInScreen,
|
|
978
963
|
runInTmux,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Shell command detection and argument-building utilities for start-command */
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const SHELL_NAMES = ['bash', 'zsh', 'sh', 'fish', 'ksh', 'csh', 'tcsh', 'dash'];
|
|
6
|
+
|
|
7
|
+
/** True if command is a bare shell invocation (no -c); avoids bash-inside-bash (issue #84). */
|
|
8
|
+
function isInteractiveShellCommand(command) {
|
|
9
|
+
const parts = command.trim().split(/\s+/);
|
|
10
|
+
return SHELL_NAMES.includes(path.basename(parts[0])) && !parts.includes('-c');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** True if command is a shell invocation with -c (e.g. `bash -i -c "cmd"`); avoids double-wrapping (issue #91). */
|
|
14
|
+
function isShellInvocationWithArgs(command) {
|
|
15
|
+
const parts = command.trim().split(/\s+/);
|
|
16
|
+
return SHELL_NAMES.includes(path.basename(parts[0])) && parts.includes('-c');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Build argv for shell-with-c command; everything after -c is one argument (reverses commandArgs.join(' ')). */
|
|
20
|
+
function buildShellWithArgsCmdArgs(command) {
|
|
21
|
+
const parts = command.trim().split(/\s+/);
|
|
22
|
+
const cIdx = parts.indexOf('-c');
|
|
23
|
+
if (cIdx === -1) {
|
|
24
|
+
return parts;
|
|
25
|
+
}
|
|
26
|
+
const scriptArg = parts.slice(cIdx + 1).join(' ');
|
|
27
|
+
return scriptArg.length > 0
|
|
28
|
+
? [...parts.slice(0, cIdx + 1), scriptArg]
|
|
29
|
+
: parts.slice(0, cIdx + 1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Build a display string for a command, quoting arguments that contain spaces (issue #91). */
|
|
33
|
+
function buildDisplayCommand(command) {
|
|
34
|
+
if (!isShellInvocationWithArgs(command)) {
|
|
35
|
+
return command;
|
|
36
|
+
}
|
|
37
|
+
const argv = buildShellWithArgsCmdArgs(command);
|
|
38
|
+
return argv.map((arg) => (arg.includes(' ') ? `"${arg}"` : arg)).join(' ');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
SHELL_NAMES,
|
|
43
|
+
isInteractiveShellCommand,
|
|
44
|
+
isShellInvocationWithArgs,
|
|
45
|
+
buildShellWithArgsCmdArgs,
|
|
46
|
+
buildDisplayCommand,
|
|
47
|
+
};
|
|
@@ -0,0 +1,296 @@
|
|
|
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
|
+
buildDisplayCommand,
|
|
38
|
+
} = require('../src/lib/isolation');
|
|
39
|
+
|
|
40
|
+
// Helper: mirrors the attached-mode command-args construction logic in runInDocker.
|
|
41
|
+
// Returns the argv array that would be passed to `docker run ... image <argv>`.
|
|
42
|
+
function buildAttachedCmdArgs(command, shellToUse = '/bin/bash') {
|
|
43
|
+
const path = require('path');
|
|
44
|
+
const shellName = shellToUse.split('/').pop();
|
|
45
|
+
const shellInteractiveFlag =
|
|
46
|
+
shellName === 'bash' || shellName === 'zsh' ? '-i' : null;
|
|
47
|
+
const shellCmdArgs = shellInteractiveFlag
|
|
48
|
+
? [shellToUse, shellInteractiveFlag]
|
|
49
|
+
: [shellToUse];
|
|
50
|
+
|
|
51
|
+
if (isInteractiveShellCommand(command)) {
|
|
52
|
+
// Bare shell: pass directly with explicit -i (issue #84 fix)
|
|
53
|
+
const parts = command.trim().split(/\s+/);
|
|
54
|
+
const bareFlag =
|
|
55
|
+
path.basename(parts[0]) === 'bash' || path.basename(parts[0]) === 'zsh'
|
|
56
|
+
? '-i'
|
|
57
|
+
: null;
|
|
58
|
+
if (bareFlag && !parts.includes(bareFlag)) {
|
|
59
|
+
return [parts[0], bareFlag, ...parts.slice(1)];
|
|
60
|
+
}
|
|
61
|
+
return parts;
|
|
62
|
+
} else if (isShellInvocationWithArgs(command)) {
|
|
63
|
+
// Shell with -c: pass directly as argv (issue #91 fix)
|
|
64
|
+
return buildShellWithArgsCmdArgs(command);
|
|
65
|
+
}
|
|
66
|
+
return [...shellCmdArgs, '-c', command];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('isShellInvocationWithArgs (issue #91)', () => {
|
|
70
|
+
it('should return true for "bash -i -c nvm --version"', () => {
|
|
71
|
+
assert.strictEqual(
|
|
72
|
+
isShellInvocationWithArgs('bash -i -c nvm --version'),
|
|
73
|
+
true
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should return true for \'bash -c "echo hello"\'', () => {
|
|
78
|
+
assert.strictEqual(isShellInvocationWithArgs('bash -c "echo hello"'), true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should return true for "bash -c echo hello"', () => {
|
|
82
|
+
assert.strictEqual(isShellInvocationWithArgs('bash -c echo hello'), true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should return true for "zsh -c nvm --version"', () => {
|
|
86
|
+
assert.strictEqual(isShellInvocationWithArgs('zsh -c nvm --version'), true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should return true for "sh -c ls"', () => {
|
|
90
|
+
assert.strictEqual(isShellInvocationWithArgs('sh -c ls'), true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should return true for "/bin/bash -c echo hi"', () => {
|
|
94
|
+
assert.strictEqual(isShellInvocationWithArgs('/bin/bash -c echo hi'), true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should return false for bare "bash" (no -c)', () => {
|
|
98
|
+
assert.strictEqual(isShellInvocationWithArgs('bash'), false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should return false for "bash -i" (no -c)', () => {
|
|
102
|
+
assert.strictEqual(isShellInvocationWithArgs('bash -i'), false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should return false for "bash --norc" (no -c)', () => {
|
|
106
|
+
assert.strictEqual(isShellInvocationWithArgs('bash --norc'), false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should return false for non-shell commands', () => {
|
|
110
|
+
assert.strictEqual(isShellInvocationWithArgs('nvm --version'), false);
|
|
111
|
+
assert.strictEqual(isShellInvocationWithArgs('echo hello'), false);
|
|
112
|
+
assert.strictEqual(isShellInvocationWithArgs('npm test'), false);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('buildShellWithArgsCmdArgs (issue #91)', () => {
|
|
117
|
+
it('should reconstruct "bash -i -c nvm --version" correctly', () => {
|
|
118
|
+
const result = buildShellWithArgsCmdArgs('bash -i -c nvm --version');
|
|
119
|
+
assert.deepStrictEqual(result, ['bash', '-i', '-c', 'nvm --version']);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should reconstruct "bash -c echo hello" correctly', () => {
|
|
123
|
+
const result = buildShellWithArgsCmdArgs('bash -c echo hello');
|
|
124
|
+
assert.deepStrictEqual(result, ['bash', '-c', 'echo hello']);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should handle single-word script "bash -c ls"', () => {
|
|
128
|
+
const result = buildShellWithArgsCmdArgs('bash -c ls');
|
|
129
|
+
assert.deepStrictEqual(result, ['bash', '-c', 'ls']);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should handle zsh with -c', () => {
|
|
133
|
+
const result = buildShellWithArgsCmdArgs('zsh -c nvm --version');
|
|
134
|
+
assert.deepStrictEqual(result, ['zsh', '-c', 'nvm --version']);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should handle /bin/bash -i -c with multi-word script', () => {
|
|
138
|
+
const result = buildShellWithArgsCmdArgs(
|
|
139
|
+
'/bin/bash -i -c node -e process.version'
|
|
140
|
+
);
|
|
141
|
+
assert.deepStrictEqual(result, [
|
|
142
|
+
'/bin/bash',
|
|
143
|
+
'-i',
|
|
144
|
+
'-c',
|
|
145
|
+
'node -e process.version',
|
|
146
|
+
]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should not include -c argument inside script argument (no double -c)', () => {
|
|
150
|
+
const result = buildShellWithArgsCmdArgs('bash -i -c nvm --version');
|
|
151
|
+
// The script arg must be 'nvm --version', not '-c nvm --version'
|
|
152
|
+
assert.strictEqual(result[result.length - 1], 'nvm --version');
|
|
153
|
+
assert.strictEqual(result.indexOf('-c'), 2);
|
|
154
|
+
assert.strictEqual(result.length, 4);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('Regression: No Double-Wrapping for Shell With -c (issue #91)', () => {
|
|
159
|
+
// Each test verifies that `bash -i -c "cmd"` style commands are NOT re-wrapped
|
|
160
|
+
// in another outer shell -c invocation.
|
|
161
|
+
//
|
|
162
|
+
// Before fix: buildAttachedCmdArgs('bash -i -c nvm --version')
|
|
163
|
+
// → ['/bin/bash', '-i', '-c', 'bash -i -c nvm --version'] (WRONG: double-wrap)
|
|
164
|
+
// After fix: buildAttachedCmdArgs('bash -i -c nvm --version')
|
|
165
|
+
// → ['bash', '-i', '-c', 'nvm --version'] (CORRECT: direct pass)
|
|
166
|
+
|
|
167
|
+
it('should pass "bash -i -c nvm --version" directly without outer shell wrapper', () => {
|
|
168
|
+
const args = buildAttachedCmdArgs('bash -i -c nvm --version');
|
|
169
|
+
// Must start with 'bash', not with '/bin/bash' (the outer shellToUse)
|
|
170
|
+
assert.strictEqual(args[0], 'bash');
|
|
171
|
+
// Must not wrap in outer bash -i -c
|
|
172
|
+
assert.ok(
|
|
173
|
+
args.filter((a) => a === '-c').length === 1,
|
|
174
|
+
`Must have exactly one -c flag, got: ${JSON.stringify(args)}`
|
|
175
|
+
);
|
|
176
|
+
// The script argument must be 'nvm --version' as one element
|
|
177
|
+
assert.deepStrictEqual(
|
|
178
|
+
args,
|
|
179
|
+
['bash', '-i', '-c', 'nvm --version'],
|
|
180
|
+
`Expected ['bash', '-i', '-c', 'nvm --version'], got: ${JSON.stringify(args)}`
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should pass "zsh -c nvm --version" directly without outer shell wrapper', () => {
|
|
185
|
+
const args = buildAttachedCmdArgs('zsh -c nvm --version', '/bin/zsh');
|
|
186
|
+
assert.deepStrictEqual(args, ['zsh', '-c', 'nvm --version']);
|
|
187
|
+
assert.ok(
|
|
188
|
+
args.filter((a) => a === '-c').length === 1,
|
|
189
|
+
'Must have exactly one -c flag'
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should pass "bash -c echo hello" directly', () => {
|
|
194
|
+
const args = buildAttachedCmdArgs('bash -c echo hello');
|
|
195
|
+
assert.deepStrictEqual(args, ['bash', '-c', 'echo hello']);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should not introduce a second bash layer (no shell-inside-shell)', () => {
|
|
199
|
+
const args = buildAttachedCmdArgs('bash -i -c nvm --version');
|
|
200
|
+
// The first element of the docker image command args must NOT be the outer shell
|
|
201
|
+
// i.e., must not be ['/bin/bash', '-i', '-c', 'bash ...']
|
|
202
|
+
assert.notStrictEqual(
|
|
203
|
+
args[0],
|
|
204
|
+
'/bin/bash',
|
|
205
|
+
'Must not wrap in outer /bin/bash (shell-inside-shell)'
|
|
206
|
+
);
|
|
207
|
+
// Must not contain the original full command string as an argument
|
|
208
|
+
assert.ok(
|
|
209
|
+
!args.includes('bash -i -c nvm --version'),
|
|
210
|
+
'Must not contain the full command string as a single argument (double-wrap)'
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should still handle bare "bash" with -i (regression guard for issue #84)', () => {
|
|
215
|
+
const args = buildAttachedCmdArgs('bash');
|
|
216
|
+
assert.deepStrictEqual(args, ['bash', '-i']);
|
|
217
|
+
assert.ok(
|
|
218
|
+
!args.includes('-c'),
|
|
219
|
+
'Bare shell must not use -c (issue #84 guard)'
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should still wrap non-shell commands in outer shell -c (guard against over-broad fix)', () => {
|
|
224
|
+
const args = buildAttachedCmdArgs('nvm --version', '/bin/bash');
|
|
225
|
+
assert.deepStrictEqual(args, ['/bin/bash', '-i', '-c', 'nvm --version']);
|
|
226
|
+
assert.ok(
|
|
227
|
+
args.includes('-c'),
|
|
228
|
+
'Non-shell commands must still use -c wrapper'
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should still wrap "npm test" in outer shell -c', () => {
|
|
233
|
+
const args = buildAttachedCmdArgs('npm test', '/bin/bash');
|
|
234
|
+
assert.deepStrictEqual(args, ['/bin/bash', '-i', '-c', 'npm test']);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('isShellInvocationWithArgs is mutually exclusive with isInteractiveShellCommand', () => {
|
|
239
|
+
// A command cannot be both a bare shell AND a shell-with-args; they are disjoint.
|
|
240
|
+
const testCases = [
|
|
241
|
+
'bash',
|
|
242
|
+
'bash -i',
|
|
243
|
+
'bash --norc',
|
|
244
|
+
'bash -i -c nvm --version',
|
|
245
|
+
'bash -c echo hi',
|
|
246
|
+
'zsh -c ls',
|
|
247
|
+
'nvm --version',
|
|
248
|
+
'echo hello',
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
for (const cmd of testCases) {
|
|
252
|
+
it(`"${cmd}" is not both bare-shell and shell-with-args`, () => {
|
|
253
|
+
const bare = isInteractiveShellCommand(cmd);
|
|
254
|
+
const withArgs = isShellInvocationWithArgs(cmd);
|
|
255
|
+
assert.ok(
|
|
256
|
+
!(bare && withArgs),
|
|
257
|
+
`"${cmd}" must not return true from both helpers simultaneously`
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('buildDisplayCommand (issue #91 display fix)', () => {
|
|
264
|
+
it('should quote the -c script argument when it contains spaces', () => {
|
|
265
|
+
assert.strictEqual(
|
|
266
|
+
buildDisplayCommand('bash -i -c nvm --version'),
|
|
267
|
+
'bash -i -c "nvm --version"'
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should quote the -c script argument for plain "bash -c echo hello"', () => {
|
|
272
|
+
assert.strictEqual(
|
|
273
|
+
buildDisplayCommand('bash -c echo hello'),
|
|
274
|
+
'bash -c "echo hello"'
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should not double-quote if script has no spaces', () => {
|
|
279
|
+
assert.strictEqual(buildDisplayCommand('bash -c ls'), 'bash -c ls');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should return command unchanged for non-shell-with-args commands', () => {
|
|
283
|
+
assert.strictEqual(buildDisplayCommand('nvm --version'), 'nvm --version');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should return command unchanged for bare shell invocations', () => {
|
|
287
|
+
assert.strictEqual(buildDisplayCommand('bash -i'), 'bash -i');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should handle zsh -c with spaces', () => {
|
|
291
|
+
assert.strictEqual(
|
|
292
|
+
buildDisplayCommand('zsh -c echo hello world'),
|
|
293
|
+
'zsh -c "echo hello world"'
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
});
|