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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-command",
3
- "version": "0.24.0",
3
+ "version": "0.24.1",
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": {
@@ -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
- * This is a workaround for environments without TTY
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
- screenArgs = [
275
- '-dmS',
276
- sessionName,
277
- '-L',
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
- effectiveCommand = `(${effectiveCommand}) 2>&1 | tee "${logFile}"`;
295
- screenArgs = ['-dmS', sessionName, shell, shellArg, effectiveCommand];
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 = `nohup ${shellInvocation} -c ${JSON.stringify(command)} > /tmp/${sessionName}.log 2>&1 &`;
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 = useExplicitShell
696
- ? [sshTarget, useExplicitShell, ...extraFlags, '-c', command]
697
- : [sshTarget, command];
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
- dockerArgs.push(options.image, ...shellArgs, '-c', effectiveCommand);
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
- dockerArgs.push(options.image, ...shellCmdArgs, '-c', command);
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,
@@ -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
+ });