start-command 0.24.1 → 0.24.2
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 +18 -0
- package/package.json +1 -1
- package/src/lib/isolation.js +9 -6
- package/test/regression-84.test.js +159 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.24.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f870146: perf: skip shell detection probe when command is a bare shell invocation
|
|
8
|
+
|
|
9
|
+
When running `$ --isolated docker -- bash`, the tool previously ran a probe
|
|
10
|
+
container (`docker run --rm image sh -c 'command -v bash'`) to detect which
|
|
11
|
+
shell to use, even though the result was unused for bare shell commands.
|
|
12
|
+
|
|
13
|
+
Now `isInteractiveShellCommand(command)` is evaluated first, and
|
|
14
|
+
`detectShellInEnvironment` is only called when the command is not a bare shell.
|
|
15
|
+
This avoids up to three unnecessary container starts per invocation and eliminates
|
|
16
|
+
spurious failures when the probe itself fails on images with complex entrypoints.
|
|
17
|
+
|
|
18
|
+
Also caches the `isInteractiveShellCommand(command)` result in `isBareShell` to
|
|
19
|
+
avoid redundant calls in both attached and detached code paths.
|
|
20
|
+
|
|
3
21
|
## 0.24.1
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/package.json
CHANGED
package/src/lib/isolation.js
CHANGED
|
@@ -766,7 +766,10 @@ function runInDocker(command, options = {}) {
|
|
|
766
766
|
}
|
|
767
767
|
}
|
|
768
768
|
|
|
769
|
-
const
|
|
769
|
+
const isBareShell = isInteractiveShellCommand(command);
|
|
770
|
+
const shellToUse = isBareShell
|
|
771
|
+
? 'sh'
|
|
772
|
+
: detectShellInEnvironment('docker', options, options.shell);
|
|
770
773
|
const shellInteractiveFlag = getShellInteractiveFlag(shellToUse);
|
|
771
774
|
|
|
772
775
|
// Print the user command (this appears after any virtual commands like docker pull)
|
|
@@ -777,9 +780,6 @@ function runInDocker(command, options = {}) {
|
|
|
777
780
|
try {
|
|
778
781
|
if (options.detached) {
|
|
779
782
|
// Detached mode: docker run -d --name <name> [--user <user>] <image> <shell> -c '<command>'
|
|
780
|
-
const effectiveCommand = options.keepAlive
|
|
781
|
-
? `${command}; exec ${shellToUse}`
|
|
782
|
-
: command;
|
|
783
783
|
const dockerArgs = ['run', '-d', '--name', containerName];
|
|
784
784
|
|
|
785
785
|
// --rm must come before the image name
|
|
@@ -792,10 +792,13 @@ function runInDocker(command, options = {}) {
|
|
|
792
792
|
dockerArgs.push('--user', options.user);
|
|
793
793
|
}
|
|
794
794
|
|
|
795
|
+
const effectiveCommand = options.keepAlive
|
|
796
|
+
? `${command}; exec ${shellToUse}`
|
|
797
|
+
: command;
|
|
795
798
|
const shellArgs = shellInteractiveFlag
|
|
796
799
|
? [shellToUse, shellInteractiveFlag]
|
|
797
800
|
: [shellToUse];
|
|
798
|
-
const cmdArgs =
|
|
801
|
+
const cmdArgs = isBareShell
|
|
799
802
|
? command.trim().split(/\s+/)
|
|
800
803
|
: [...shellArgs, '-c', effectiveCommand];
|
|
801
804
|
dockerArgs.push(options.image, ...cmdArgs);
|
|
@@ -847,7 +850,7 @@ function runInDocker(command, options = {}) {
|
|
|
847
850
|
const shellCmdArgs = shellInteractiveFlag
|
|
848
851
|
? [shellToUse, shellInteractiveFlag]
|
|
849
852
|
: [shellToUse];
|
|
850
|
-
const attachedCmdArgs =
|
|
853
|
+
const attachedCmdArgs = isBareShell
|
|
851
854
|
? command.trim().split(/\s+/)
|
|
852
855
|
: [...shellCmdArgs, '-c', command];
|
|
853
856
|
dockerArgs.push(options.image, ...attachedCmdArgs);
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Regression tests for issue #84: "We should not run bash inside bash"
|
|
4
|
+
*
|
|
5
|
+
* These tests guard against the shell-inside-shell regression where
|
|
6
|
+
* `$ --isolated docker -- bash` caused:
|
|
7
|
+
* docker run ... image /bin/bash -i -c bash (WRONG: bash inside bash)
|
|
8
|
+
* instead of:
|
|
9
|
+
* docker run ... image bash (CORRECT: bare shell)
|
|
10
|
+
*
|
|
11
|
+
* The same regression applies to zsh, sh, and all other isolation backends.
|
|
12
|
+
*
|
|
13
|
+
* Reference: https://github.com/link-foundation/start/issues/84
|
|
14
|
+
* Fixed in: PR #85 (v0.24.1) via isInteractiveShellCommand()
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { describe, it } = require('node:test');
|
|
18
|
+
const assert = require('assert');
|
|
19
|
+
const { isInteractiveShellCommand } = require('../src/lib/isolation');
|
|
20
|
+
|
|
21
|
+
// Helper: mirrors the command-args construction logic used in
|
|
22
|
+
// runInDocker (attached + detached), runInScreen, and runInSsh.
|
|
23
|
+
// If this helper returns args containing '-c' for a bare shell command,
|
|
24
|
+
// the shell-inside-shell bug is present.
|
|
25
|
+
function buildCmdArgs(command, shellToUse = '/bin/bash') {
|
|
26
|
+
const shellName = shellToUse.split('/').pop();
|
|
27
|
+
const shellInteractiveFlag =
|
|
28
|
+
shellName === 'bash' || shellName === 'zsh' ? '-i' : null;
|
|
29
|
+
const shellArgs = shellInteractiveFlag
|
|
30
|
+
? [shellToUse, shellInteractiveFlag]
|
|
31
|
+
: [shellToUse];
|
|
32
|
+
return isInteractiveShellCommand(command)
|
|
33
|
+
? command.trim().split(/\s+/)
|
|
34
|
+
: [...shellArgs, '-c', command];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('isInteractiveShellCommand additional cases (issue #84)', () => {
|
|
38
|
+
// These cover edge cases not in the base isInteractiveShellCommand test suite.
|
|
39
|
+
|
|
40
|
+
// Workaround: bash --norc skips .bashrc sourcing (post-fix regression workaround)
|
|
41
|
+
it('should return true for "bash --norc"', () => {
|
|
42
|
+
assert.strictEqual(isInteractiveShellCommand('bash --norc'), true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return true for "zsh --no-rcs"', () => {
|
|
46
|
+
assert.strictEqual(isInteractiveShellCommand('zsh --no-rcs'), true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return true for "bash -i" (interactive flag, no -c)', () => {
|
|
50
|
+
assert.strictEqual(isInteractiveShellCommand('bash -i'), true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return true for "fish"', () => {
|
|
54
|
+
assert.strictEqual(isInteractiveShellCommand('fish'), true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should return true for "dash"', () => {
|
|
58
|
+
assert.strictEqual(isInteractiveShellCommand('dash'), true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should return true for "/usr/local/bin/bash"', () => {
|
|
62
|
+
assert.strictEqual(isInteractiveShellCommand('/usr/local/bin/bash'), true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should return false for \'bash -c "echo hello"\'', () => {
|
|
66
|
+
assert.strictEqual(
|
|
67
|
+
isInteractiveShellCommand('bash -c "echo hello"'),
|
|
68
|
+
false
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('Regression: No Shell-Inside-Shell (issue #84)', () => {
|
|
74
|
+
// Each test verifies that the command-arg construction logic does NOT
|
|
75
|
+
// wrap a bare shell invocation inside another shell with `-c`.
|
|
76
|
+
//
|
|
77
|
+
// Before fix: buildCmdArgs('bash') → ['/bin/bash', '-i', '-c', 'bash']
|
|
78
|
+
// After fix: buildCmdArgs('bash') → ['bash']
|
|
79
|
+
|
|
80
|
+
it('should pass "bash" directly, not wrap in shell -c', () => {
|
|
81
|
+
const args = buildCmdArgs('bash');
|
|
82
|
+
assert.deepStrictEqual(
|
|
83
|
+
args,
|
|
84
|
+
['bash'],
|
|
85
|
+
`Expected ["bash"], got: ${JSON.stringify(args)}`
|
|
86
|
+
);
|
|
87
|
+
assert.ok(
|
|
88
|
+
!args.includes('-c'),
|
|
89
|
+
'Must not contain -c flag (shell-inside-shell)'
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should pass "zsh" directly, not wrap in shell -c', () => {
|
|
94
|
+
const args = buildCmdArgs('zsh');
|
|
95
|
+
assert.deepStrictEqual(args, ['zsh']);
|
|
96
|
+
assert.ok(
|
|
97
|
+
!args.includes('-c'),
|
|
98
|
+
'Must not contain -c flag (shell-inside-shell)'
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should pass "sh" directly, not wrap in shell -c', () => {
|
|
103
|
+
const args = buildCmdArgs('sh', 'sh');
|
|
104
|
+
assert.deepStrictEqual(args, ['sh']);
|
|
105
|
+
assert.ok(
|
|
106
|
+
!args.includes('-c'),
|
|
107
|
+
'Must not contain -c flag (shell-inside-shell)'
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should pass "/bin/bash" directly, not wrap in shell -c', () => {
|
|
112
|
+
const args = buildCmdArgs('/bin/bash');
|
|
113
|
+
assert.deepStrictEqual(args, ['/bin/bash']);
|
|
114
|
+
assert.ok(
|
|
115
|
+
!args.includes('-c'),
|
|
116
|
+
'Must not contain -c flag (shell-inside-shell)'
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should pass "bash --norc" directly (workaround for broken .bashrc)', () => {
|
|
121
|
+
const args = buildCmdArgs('bash --norc');
|
|
122
|
+
assert.deepStrictEqual(args, ['bash', '--norc']);
|
|
123
|
+
assert.ok(!args.includes('-c'), 'Must not contain -c flag');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should pass "bash -l" directly (login shell)', () => {
|
|
127
|
+
const args = buildCmdArgs('bash -l');
|
|
128
|
+
assert.deepStrictEqual(args, ['bash', '-l']);
|
|
129
|
+
assert.ok(!args.includes('-c'), 'Must not contain -c flag');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should still wrap non-shell commands in shell -c (guard against over-broad fix)', () => {
|
|
133
|
+
const args = buildCmdArgs('echo hello', '/bin/bash');
|
|
134
|
+
assert.deepStrictEqual(args, ['/bin/bash', '-i', '-c', 'echo hello']);
|
|
135
|
+
assert.ok(
|
|
136
|
+
args.includes('-c'),
|
|
137
|
+
'Non-shell commands must still use -c wrapper'
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should still wrap "npm test" in shell -c (guard against over-broad fix)', () => {
|
|
142
|
+
const args = buildCmdArgs('npm test', '/bin/bash');
|
|
143
|
+
assert.deepStrictEqual(args, ['/bin/bash', '-i', '-c', 'npm test']);
|
|
144
|
+
assert.ok(
|
|
145
|
+
args.includes('-c'),
|
|
146
|
+
'Non-shell commands must still use -c wrapper'
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should not treat "bash -c something" as bare shell', () => {
|
|
151
|
+
// bash -c ... has -c, so isInteractiveShellCommand returns false
|
|
152
|
+
// The command gets wrapped: ['/bin/bash', '-i', '-c', 'bash -c "echo hi"']
|
|
153
|
+
const args = buildCmdArgs('bash -c "echo hi"', '/bin/bash');
|
|
154
|
+
assert.ok(
|
|
155
|
+
args.includes('-c'),
|
|
156
|
+
'bash -c commands should be treated as regular commands'
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
});
|