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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-command",
3
- "version": "0.24.1",
3
+ "version": "0.24.2",
4
4
  "description": "Gamification of coding, execute any command with ability to auto-report issues on GitHub",
5
5
  "main": "src/bin/cli.js",
6
6
  "exports": {
@@ -766,7 +766,10 @@ function runInDocker(command, options = {}) {
766
766
  }
767
767
  }
768
768
 
769
- const shellToUse = detectShellInEnvironment('docker', options, options.shell);
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 = isInteractiveShellCommand(command)
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 = isInteractiveShellCommand(command)
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
+ });