start-command 0.22.0 → 0.24.0
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 +42 -0
- package/package.json +1 -1
- package/src/bin/cli.js +2 -0
- package/src/lib/isolation.js +35 -34
- package/test/isolation.test.js +71 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.24.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2ea7f43: feat: Use interactive shell mode in isolation environments to source startup files
|
|
8
|
+
|
|
9
|
+
In docker and ssh isolation environments, bash and zsh are now invoked with
|
|
10
|
+
the `-i` (interactive) flag when executing commands. This ensures that startup
|
|
11
|
+
files like `.bashrc` and `.zshrc` are sourced, making environment-dependent
|
|
12
|
+
tools like `nvm`, `rbenv`, `pyenv`, and similar version managers available
|
|
13
|
+
in isolated commands.
|
|
14
|
+
|
|
15
|
+
Previously, even though bash was correctly detected and used over sh, running
|
|
16
|
+
`nvm --version` in a Docker container would fail with "command not found"
|
|
17
|
+
because bash was started in non-interactive mode and did not source `.bashrc`.
|
|
18
|
+
|
|
19
|
+
With this fix:
|
|
20
|
+
- Docker: `docker run <image> bash -i -c "nvm --version"` sources `.bashrc`
|
|
21
|
+
- SSH: `ssh <host> bash -i -c "nvm --version"` sources `.bashrc` on the remote host
|
|
22
|
+
- `zsh` also gets the `-i` flag for the same reason
|
|
23
|
+
- `sh` does not get `-i` as it is used as a fallback for minimal containers
|
|
24
|
+
|
|
25
|
+
Fixes #79
|
|
26
|
+
|
|
27
|
+
## 0.23.0
|
|
28
|
+
|
|
29
|
+
### Minor Changes
|
|
30
|
+
|
|
31
|
+
- b85d3cb: feat: Add shell auto-detection and --shell option for isolation environments
|
|
32
|
+
|
|
33
|
+
In docker and ssh isolation environments, the shell is now automatically
|
|
34
|
+
selected in order of preference: bash, zsh, sh (auto mode). A new `--shell`
|
|
35
|
+
option allows explicitly specifying the shell to use.
|
|
36
|
+
- Auto mode (default): probes the environment for the best available shell
|
|
37
|
+
- `--shell bash/zsh/sh`: forces a specific shell
|
|
38
|
+
- `--shell auto`: explicitly selects auto-detection mode
|
|
39
|
+
|
|
40
|
+
This enables tools like `nvm` to work correctly in Docker containers where
|
|
41
|
+
bash is available but sh does not source the necessary profile scripts.
|
|
42
|
+
|
|
43
|
+
Fixes #79
|
|
44
|
+
|
|
3
45
|
## 0.22.0
|
|
4
46
|
|
|
5
47
|
### Minor Changes
|
package/package.json
CHANGED
package/src/bin/cli.js
CHANGED
|
@@ -331,6 +331,7 @@ Options:
|
|
|
331
331
|
--keep-user Keep isolated user after command completes
|
|
332
332
|
--keep-alive, -k Keep isolation environment alive after command exits
|
|
333
333
|
--auto-remove-docker-container Auto-remove docker container after exit
|
|
334
|
+
--shell <shell> Shell to use in isolation environments: auto, bash, zsh, sh (default: auto)
|
|
334
335
|
--use-command-stream Use command-stream library for execution (experimental)
|
|
335
336
|
--status <uuid> Show status of execution by UUID (--output-format: links-notation|json|text)
|
|
336
337
|
--cleanup Clean up stale "executing" records (crashed/killed processes)
|
|
@@ -575,6 +576,7 @@ async function runWithIsolation(
|
|
|
575
576
|
user: createdUser,
|
|
576
577
|
keepAlive: options.keepAlive,
|
|
577
578
|
autoRemoveDockerContainer: options.autoRemoveDockerContainer,
|
|
579
|
+
shell: options.shell,
|
|
578
580
|
});
|
|
579
581
|
} else if (createdUser) {
|
|
580
582
|
// Run directly as the created user (no isolation environment)
|
package/src/lib/isolation.js
CHANGED
|
@@ -211,6 +211,12 @@ function detectShellInEnvironment(
|
|
|
211
211
|
return 'sh';
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
/** Returns "-i" for bash/zsh (enables .bashrc sourcing), null otherwise. */
|
|
215
|
+
function getShellInteractiveFlag(shellPath) {
|
|
216
|
+
const shellName = shellPath.split('/').pop();
|
|
217
|
+
return shellName === 'bash' || shellName === 'zsh' ? '-i' : null;
|
|
218
|
+
}
|
|
219
|
+
|
|
214
220
|
/**
|
|
215
221
|
* Check if the current process has a TTY attached
|
|
216
222
|
* @returns {boolean} True if TTY is available
|
|
@@ -648,30 +654,31 @@ function runInSsh(command, options = {}) {
|
|
|
648
654
|
const sessionName = options.session || generateSessionName('ssh');
|
|
649
655
|
const sshTarget = options.endpoint;
|
|
650
656
|
|
|
651
|
-
// Detect the shell to use on the remote host
|
|
652
|
-
// In auto mode, detection may fall back to passing command directly to leverage
|
|
653
|
-
// the remote user's default login shell (which may already be bash)
|
|
654
657
|
const shellToUse = detectShellInEnvironment('ssh', options, options.shell);
|
|
655
|
-
//
|
|
658
|
+
// In auto mode, SSH login shells already source startup files (.bashrc etc.);
|
|
659
|
+
// only wrap with an explicit shell when user requests one.
|
|
656
660
|
const useExplicitShell =
|
|
657
661
|
options.shell && options.shell !== 'auto' ? shellToUse : null;
|
|
662
|
+
const shellInteractiveFlag = useExplicitShell
|
|
663
|
+
? getShellInteractiveFlag(useExplicitShell)
|
|
664
|
+
: null;
|
|
658
665
|
|
|
659
666
|
try {
|
|
660
667
|
if (options.detached) {
|
|
661
|
-
// Detached mode: Run command in background
|
|
662
|
-
// The command will continue running even after SSH connection closes
|
|
668
|
+
// Detached mode: Run command in background via nohup; continues after SSH closes
|
|
663
669
|
const remoteShell = useExplicitShell || shellToUse;
|
|
664
|
-
const
|
|
670
|
+
const shellInvocation = shellInteractiveFlag
|
|
671
|
+
? `${remoteShell} ${shellInteractiveFlag}`
|
|
672
|
+
: remoteShell;
|
|
673
|
+
const remoteCommand = `nohup ${shellInvocation} -c ${JSON.stringify(command)} > /tmp/${sessionName}.log 2>&1 &`;
|
|
665
674
|
const sshArgs = [sshTarget, remoteCommand];
|
|
666
675
|
|
|
667
676
|
if (DEBUG) {
|
|
668
677
|
console.log(`[DEBUG] Running: ssh ${sshArgs.join(' ')}`);
|
|
669
|
-
console.log(`[DEBUG] shell: ${
|
|
678
|
+
console.log(`[DEBUG] shell: ${shellInvocation}`);
|
|
670
679
|
}
|
|
671
680
|
|
|
672
|
-
const result = spawnSync('ssh', sshArgs, {
|
|
673
|
-
stdio: 'inherit',
|
|
674
|
-
});
|
|
681
|
+
const result = spawnSync('ssh', sshArgs, { stdio: 'inherit' });
|
|
675
682
|
|
|
676
683
|
if (result.error) {
|
|
677
684
|
throw result.error;
|
|
@@ -683,11 +690,10 @@ function runInSsh(command, options = {}) {
|
|
|
683
690
|
message: `Command started in detached SSH session on ${sshTarget}\nSession: ${sessionName}\nView logs: ssh ${sshTarget} "tail -f /tmp/${sessionName}.log"`,
|
|
684
691
|
});
|
|
685
692
|
} else {
|
|
686
|
-
// Attached mode:
|
|
687
|
-
|
|
688
|
-
// In auto mode, pass the command directly and let the remote's default shell handle it.
|
|
693
|
+
// Attached mode: pass command directly (auto) or wrap with explicit shell + -i flag (bash/zsh).
|
|
694
|
+
const extraFlags = shellInteractiveFlag ? [shellInteractiveFlag] : [];
|
|
689
695
|
const sshArgs = useExplicitShell
|
|
690
|
-
? [sshTarget, useExplicitShell, '-c', command]
|
|
696
|
+
? [sshTarget, useExplicitShell, ...extraFlags, '-c', command]
|
|
691
697
|
: [sshTarget, command];
|
|
692
698
|
|
|
693
699
|
if (DEBUG) {
|
|
@@ -769,8 +775,8 @@ function runInDocker(command, options = {}) {
|
|
|
769
775
|
}
|
|
770
776
|
}
|
|
771
777
|
|
|
772
|
-
// Detect the shell to use in the container
|
|
773
778
|
const shellToUse = detectShellInEnvironment('docker', options, options.shell);
|
|
779
|
+
const shellInteractiveFlag = getShellInteractiveFlag(shellToUse);
|
|
774
780
|
|
|
775
781
|
// Print the user command (this appears after any virtual commands like docker pull)
|
|
776
782
|
const { createCommandLine } = require('./output-blocks');
|
|
@@ -780,20 +786,12 @@ function runInDocker(command, options = {}) {
|
|
|
780
786
|
try {
|
|
781
787
|
if (options.detached) {
|
|
782
788
|
// Detached mode: docker run -d --name <name> [--user <user>] <image> <shell> -c '<command>'
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
if (options.keepAlive) {
|
|
788
|
-
// With keep-alive: run command, then keep shell alive
|
|
789
|
-
effectiveCommand = `${command}; exec ${shellToUse}`;
|
|
790
|
-
}
|
|
791
|
-
// Without keep-alive: container exits naturally when command completes
|
|
792
|
-
|
|
789
|
+
const effectiveCommand = options.keepAlive
|
|
790
|
+
? `${command}; exec ${shellToUse}`
|
|
791
|
+
: command;
|
|
793
792
|
const dockerArgs = ['run', '-d', '--name', containerName];
|
|
794
793
|
|
|
795
|
-
//
|
|
796
|
-
// Note: --rm must come before the image name
|
|
794
|
+
// --rm must come before the image name
|
|
797
795
|
if (options.autoRemoveDockerContainer) {
|
|
798
796
|
dockerArgs.splice(2, 0, '--rm');
|
|
799
797
|
}
|
|
@@ -803,7 +801,10 @@ function runInDocker(command, options = {}) {
|
|
|
803
801
|
dockerArgs.push('--user', options.user);
|
|
804
802
|
}
|
|
805
803
|
|
|
806
|
-
|
|
804
|
+
const shellArgs = shellInteractiveFlag
|
|
805
|
+
? [shellToUse, shellInteractiveFlag]
|
|
806
|
+
: [shellToUse];
|
|
807
|
+
dockerArgs.push(options.image, ...shellArgs, '-c', effectiveCommand);
|
|
807
808
|
|
|
808
809
|
if (DEBUG) {
|
|
809
810
|
console.log(`[DEBUG] Running: docker ${dockerArgs.join(' ')}`);
|
|
@@ -840,19 +841,19 @@ function runInDocker(command, options = {}) {
|
|
|
840
841
|
message,
|
|
841
842
|
});
|
|
842
843
|
} else {
|
|
843
|
-
// Attached mode: docker run -it --name <name> [--user <user>] <image> <shell> -c '<
|
|
844
|
+
// Attached mode: docker run -it --rm --name <name> [--user <user>] <image> <shell> -c '<cmd>'
|
|
844
845
|
const dockerArgs = ['run', '-it', '--rm', '--name', containerName];
|
|
845
|
-
|
|
846
|
-
// Add --user flag if specified
|
|
847
846
|
if (options.user) {
|
|
848
847
|
dockerArgs.push('--user', options.user);
|
|
849
848
|
}
|
|
850
|
-
|
|
851
849
|
if (DEBUG) {
|
|
852
850
|
console.log(`[DEBUG] shell: ${shellToUse}`);
|
|
853
851
|
}
|
|
854
852
|
|
|
855
|
-
|
|
853
|
+
const shellCmdArgs = shellInteractiveFlag
|
|
854
|
+
? [shellToUse, shellInteractiveFlag]
|
|
855
|
+
: [shellToUse];
|
|
856
|
+
dockerArgs.push(options.image, ...shellCmdArgs, '-c', command);
|
|
856
857
|
|
|
857
858
|
if (DEBUG) {
|
|
858
859
|
console.log(`[DEBUG] Running: docker ${dockerArgs.join(' ')}`);
|
package/test/isolation.test.js
CHANGED
|
@@ -824,4 +824,75 @@ describe('detectShellInEnvironment', () => {
|
|
|
824
824
|
);
|
|
825
825
|
console.log(` Detected shell in alpine:latest: ${result}`);
|
|
826
826
|
});
|
|
827
|
+
|
|
828
|
+
it('should read shell preference from options.shell', () => {
|
|
829
|
+
// Test that detectShellInEnvironment respects options.shell field
|
|
830
|
+
const result = detectShellInEnvironment(
|
|
831
|
+
'docker',
|
|
832
|
+
{ image: 'alpine:latest', shell: 'bash' },
|
|
833
|
+
'bash'
|
|
834
|
+
);
|
|
835
|
+
assert.strictEqual(result, 'bash');
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
describe('Shell option forwarding to isolation runners', () => {
|
|
840
|
+
// These tests verify that the shell option is properly forwarded
|
|
841
|
+
// through the call chain: cli.js -> runIsolated -> runInDocker/runInSsh
|
|
842
|
+
|
|
843
|
+
const { runInDocker, runInSsh } = require('../src/lib/isolation');
|
|
844
|
+
|
|
845
|
+
describe('runInDocker shell option', () => {
|
|
846
|
+
it('should accept shell option in options object', async () => {
|
|
847
|
+
if (!isCommandAvailable('docker')) {
|
|
848
|
+
console.log(' Skipping: docker not installed');
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Test with explicit shell option - just verify the option is accepted
|
|
853
|
+
// and the function runs (it may fail if image is not available)
|
|
854
|
+
const result = await runInDocker('echo test', {
|
|
855
|
+
image: 'alpine:latest',
|
|
856
|
+
detached: true,
|
|
857
|
+
shell: 'sh',
|
|
858
|
+
});
|
|
859
|
+
// If docker is available, verify the result
|
|
860
|
+
console.log(` runInDocker with --shell sh result: ${result.success}`);
|
|
861
|
+
// The result can be success or failure depending on docker availability
|
|
862
|
+
// The important thing is no crash due to missing shell option
|
|
863
|
+
assert.ok(typeof result.success === 'boolean');
|
|
864
|
+
});
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
describe('runInSsh shell option', () => {
|
|
868
|
+
it('should accept shell option in options object', async () => {
|
|
869
|
+
// SSH requires endpoint, so without it we get expected error
|
|
870
|
+
const result = await runInSsh('echo test', {
|
|
871
|
+
detached: true,
|
|
872
|
+
shell: 'bash',
|
|
873
|
+
});
|
|
874
|
+
assert.strictEqual(result.success, false);
|
|
875
|
+
// Should fail with endpoint error, not shell option error
|
|
876
|
+
assert.ok(
|
|
877
|
+
result.message.includes('endpoint') ||
|
|
878
|
+
result.message.includes('--endpoint') ||
|
|
879
|
+
result.message.includes('SSH isolation requires')
|
|
880
|
+
);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it('should use auto mode when shell option is auto', async () => {
|
|
884
|
+
// Verify auto mode doesn't crash
|
|
885
|
+
const result = await runInSsh('echo test', {
|
|
886
|
+
detached: true,
|
|
887
|
+
shell: 'auto',
|
|
888
|
+
});
|
|
889
|
+
assert.strictEqual(result.success, false);
|
|
890
|
+
// Should fail with endpoint error in auto mode
|
|
891
|
+
assert.ok(
|
|
892
|
+
result.message.includes('endpoint') ||
|
|
893
|
+
result.message.includes('--endpoint') ||
|
|
894
|
+
result.message.includes('SSH isolation requires')
|
|
895
|
+
);
|
|
896
|
+
});
|
|
897
|
+
});
|
|
827
898
|
});
|