start-command 0.24.2 → 0.24.4
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 +44 -0
- package/package.json +1 -1
- package/src/bin/cli.js +4 -0
- package/src/lib/docker-utils.js +16 -0
- package/src/lib/isolation.js +51 -51
- package/test/isolation.test.js +3 -1
- package/test/regression-84.test.js +197 -19
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.24.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 6f45c9c: fix: show helpful error message when Docker is not installed (issue #84)
|
|
8
|
+
|
|
9
|
+
When running `$ --isolated docker -- bash` and Docker is not installed on the
|
|
10
|
+
machine (not just "not running"), `start-command` now prints a clear error
|
|
11
|
+
message to stderr:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Error: Docker is not installed. Install Docker from https://docs.docker.com/get-docker/
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Previously the command exited silently with code 1, giving no indication of
|
|
18
|
+
why it failed. The user had to manually run `which docker` to discover that
|
|
19
|
+
Docker was not installed at all.
|
|
20
|
+
|
|
21
|
+
Also adds `isDockerInstalled()` to `docker-utils.js` to distinguish between
|
|
22
|
+
"Docker CLI not found" and "Docker CLI found but daemon not running", and
|
|
23
|
+
exposes it via the module exports for use in tests.
|
|
24
|
+
|
|
25
|
+
## 0.24.3
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- c75983e: fix: hint users when a bare shell exits immediately due to startup file errors
|
|
30
|
+
|
|
31
|
+
When running `$ --isolated docker --image <image> -- bash` and the shell exits
|
|
32
|
+
with code 1 within 3 seconds, `start-command` now prints a helpful hint:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
Hint: The shell exited immediately — its startup file (.bashrc/.zshrc) may have errors.
|
|
36
|
+
Try skipping startup files: bash --norc
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This covers the post-fix regression from issue #84 where `konard/sandbox`
|
|
40
|
+
images with broken `.bashrc` files cause bash to exit immediately when run
|
|
41
|
+
directly. The hint also appears in the log file for later diagnosis.
|
|
42
|
+
|
|
43
|
+
The `bash --norc` (and `zsh --no-rcs`) workaround bypasses startup file
|
|
44
|
+
sourcing and is recognized as a bare shell invocation, so it is passed
|
|
45
|
+
directly to docker without any `-c` wrapping.
|
|
46
|
+
|
|
3
47
|
## 0.24.2
|
|
4
48
|
|
|
5
49
|
### Patch Changes
|
package/package.json
CHANGED
package/src/bin/cli.js
CHANGED
|
@@ -591,6 +591,10 @@ async function runWithIsolation(
|
|
|
591
591
|
result.exitCode !== undefined ? result.exitCode : result.success ? 0 : 1;
|
|
592
592
|
const endTime = getTimestamp();
|
|
593
593
|
|
|
594
|
+
// Print failure message to stderr so user sees why isolation failed (e.g. Docker not installed)
|
|
595
|
+
if (!result.success && result.message) {
|
|
596
|
+
console.error(`Error: ${result.message}`);
|
|
597
|
+
}
|
|
594
598
|
// Add result to log content
|
|
595
599
|
logContent += `${result.message}\n`;
|
|
596
600
|
logContent += createLogFooter(endTime, exitCode);
|
package/src/lib/docker-utils.js
CHANGED
|
@@ -151,6 +151,21 @@ function dockerPullImage(image) {
|
|
|
151
151
|
return { success, output };
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Check if the Docker CLI is installed (command exists, regardless of daemon state)
|
|
156
|
+
* @returns {boolean} True if the docker command is found on PATH
|
|
157
|
+
*/
|
|
158
|
+
function isDockerInstalled() {
|
|
159
|
+
try {
|
|
160
|
+
const isWindows = process.platform === 'win32';
|
|
161
|
+
const checkCmd = isWindows ? 'where' : 'which';
|
|
162
|
+
execSync(`${checkCmd} docker`, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
163
|
+
return true;
|
|
164
|
+
} catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
154
169
|
/**
|
|
155
170
|
* Check if Docker is available (command exists and daemon is running)
|
|
156
171
|
* @returns {boolean} True if Docker is available
|
|
@@ -203,6 +218,7 @@ module.exports = {
|
|
|
203
218
|
getDefaultDockerImage,
|
|
204
219
|
dockerImageExists,
|
|
205
220
|
dockerPullImage,
|
|
221
|
+
isDockerInstalled,
|
|
206
222
|
isDockerAvailable,
|
|
207
223
|
canRunLinuxDockerImages,
|
|
208
224
|
};
|
package/src/lib/isolation.js
CHANGED
|
@@ -162,7 +162,7 @@ function detectShellInEnvironment(
|
|
|
162
162
|
return result.stdout.trim();
|
|
163
163
|
}
|
|
164
164
|
} catch {
|
|
165
|
-
//
|
|
165
|
+
// ignore; try next shell
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
|
|
@@ -233,9 +233,7 @@ function wrapCommandWithUser(command, user) {
|
|
|
233
233
|
if (!user) {
|
|
234
234
|
return command;
|
|
235
235
|
}
|
|
236
|
-
//
|
|
237
|
-
// -E preserves environment variables
|
|
238
|
-
// -n ensures non-interactive (fails if password required)
|
|
236
|
+
// sudo -n: non-interactive (fails if password required); -u: run as user
|
|
239
237
|
return `sudo -n -u ${user} sh -c '${command.replace(/'/g, "'\\''")}'`;
|
|
240
238
|
}
|
|
241
239
|
|
|
@@ -292,10 +290,7 @@ function runScreenWithLogCapture(command, sessionName, shellInfo, user = null) {
|
|
|
292
290
|
}
|
|
293
291
|
}
|
|
294
292
|
|
|
295
|
-
// Use spawnSync with array
|
|
296
|
-
// This is critical for commands containing quotes (e.g., echo "hello")
|
|
297
|
-
// Using execSync with a constructed string would break on nested quotes
|
|
298
|
-
// See issue #25 for details
|
|
293
|
+
// Use spawnSync with array args (not execSync string) to avoid quoting issues (issue #25)
|
|
299
294
|
const result = spawnSync('screen', screenArgs, {
|
|
300
295
|
stdio: 'inherit',
|
|
301
296
|
});
|
|
@@ -433,14 +428,10 @@ function runInScreen(command, options = {}) {
|
|
|
433
428
|
|
|
434
429
|
if (options.detached) {
|
|
435
430
|
// Detached mode: screen -dmS <session> <shell> -c '<command>'
|
|
436
|
-
// By default (keepAlive=false), the session will exit after command completes
|
|
437
|
-
// With keepAlive=true, we start a shell that runs the command but stays alive
|
|
438
|
-
|
|
439
431
|
if (options.keepAlive) {
|
|
440
432
|
// With keep-alive: run command, then keep shell open
|
|
441
433
|
effectiveCommand = `${effectiveCommand}; exec ${shell}`;
|
|
442
434
|
}
|
|
443
|
-
// Without keep-alive: command runs and session exits naturally when done
|
|
444
435
|
|
|
445
436
|
const screenArgs = isInteractiveShellCommand(command)
|
|
446
437
|
? ['-dmS', sessionName, ...command.trim().split(/\s+/)]
|
|
@@ -451,9 +442,7 @@ function runInScreen(command, options = {}) {
|
|
|
451
442
|
console.log(`[DEBUG] keepAlive: ${options.keepAlive || false}`);
|
|
452
443
|
}
|
|
453
444
|
|
|
454
|
-
// Use spawnSync with array
|
|
455
|
-
// This is critical for commands containing quotes (e.g., echo "hello")
|
|
456
|
-
// See issue #25 for details
|
|
445
|
+
// Use spawnSync with array args (not execSync string) to avoid quoting issues (issue #25)
|
|
457
446
|
const result = spawnSync('screen', screenArgs, {
|
|
458
447
|
stdio: 'inherit',
|
|
459
448
|
});
|
|
@@ -476,15 +465,8 @@ function runInScreen(command, options = {}) {
|
|
|
476
465
|
message,
|
|
477
466
|
});
|
|
478
467
|
} else {
|
|
479
|
-
// Attached mode:
|
|
480
|
-
//
|
|
481
|
-
// that would otherwise have their output lost in a rapidly-terminating screen session.
|
|
482
|
-
// Direct screen invocation (screen -S session shell -c command) loses output because:
|
|
483
|
-
// 1. Screen creates a virtual terminal for the session
|
|
484
|
-
// 2. Command output goes to that virtual terminal
|
|
485
|
-
// 3. When the command exits quickly, screen shows "[screen is terminating]"
|
|
486
|
-
// 4. The virtual terminal is destroyed and output is lost
|
|
487
|
-
// See issue #25 for details: https://github.com/link-foundation/start/issues/25
|
|
468
|
+
// Attached mode: use detached mode with log capture for reliable output
|
|
469
|
+
// (direct attached screen loses output for quick commands; see issue #25)
|
|
488
470
|
if (DEBUG) {
|
|
489
471
|
console.log(
|
|
490
472
|
`[DEBUG] Using detached mode with log capture for reliable output`
|
|
@@ -533,14 +515,10 @@ function runInTmux(command, options = {}) {
|
|
|
533
515
|
try {
|
|
534
516
|
if (options.detached) {
|
|
535
517
|
// Detached mode: tmux new-session -d -s <session> '<command>'
|
|
536
|
-
// By default (keepAlive=false), the session will exit after command completes
|
|
537
|
-
// With keepAlive=true, we keep the shell alive after the command
|
|
538
|
-
|
|
539
518
|
if (options.keepAlive) {
|
|
540
519
|
// With keep-alive: run command, then keep shell open
|
|
541
520
|
effectiveCommand = `${effectiveCommand}; exec ${shell}`;
|
|
542
521
|
}
|
|
543
|
-
// Without keep-alive: command runs and session exits naturally when done
|
|
544
522
|
|
|
545
523
|
if (DEBUG) {
|
|
546
524
|
console.log(
|
|
@@ -724,8 +702,14 @@ function runInSsh(command, options = {}) {
|
|
|
724
702
|
}
|
|
725
703
|
}
|
|
726
704
|
|
|
727
|
-
// Import docker
|
|
728
|
-
const {
|
|
705
|
+
// Import docker utilities from docker-utils
|
|
706
|
+
const {
|
|
707
|
+
dockerImageExists,
|
|
708
|
+
dockerPullImage,
|
|
709
|
+
isDockerAvailable,
|
|
710
|
+
getDefaultDockerImage,
|
|
711
|
+
canRunLinuxDockerImages,
|
|
712
|
+
} = require('./docker-utils');
|
|
729
713
|
|
|
730
714
|
/**
|
|
731
715
|
* Run command in Docker container
|
|
@@ -739,7 +723,16 @@ function runInDocker(command, options = {}) {
|
|
|
739
723
|
success: false,
|
|
740
724
|
containerName: null,
|
|
741
725
|
message:
|
|
742
|
-
'
|
|
726
|
+
'Docker is not installed. Install Docker from https://docs.docker.com/get-docker/',
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (!isDockerAvailable()) {
|
|
731
|
+
return Promise.resolve({
|
|
732
|
+
success: false,
|
|
733
|
+
containerName: null,
|
|
734
|
+
message:
|
|
735
|
+
'Docker is installed but not running. Please start Docker Desktop or the Docker daemon, then try again.',
|
|
743
736
|
});
|
|
744
737
|
}
|
|
745
738
|
|
|
@@ -772,7 +765,6 @@ function runInDocker(command, options = {}) {
|
|
|
772
765
|
: detectShellInEnvironment('docker', options, options.shell);
|
|
773
766
|
const shellInteractiveFlag = getShellInteractiveFlag(shellToUse);
|
|
774
767
|
|
|
775
|
-
// Print the user command (this appears after any virtual commands like docker pull)
|
|
776
768
|
const { createCommandLine } = require('./output-blocks');
|
|
777
769
|
console.log(createCommandLine(command));
|
|
778
770
|
console.log();
|
|
@@ -781,13 +773,11 @@ function runInDocker(command, options = {}) {
|
|
|
781
773
|
if (options.detached) {
|
|
782
774
|
// Detached mode: docker run -d --name <name> [--user <user>] <image> <shell> -c '<command>'
|
|
783
775
|
const dockerArgs = ['run', '-d', '--name', containerName];
|
|
784
|
-
|
|
785
|
-
// --rm must come before the image name
|
|
776
|
+
// --rm must come before the image name in args
|
|
786
777
|
if (options.autoRemoveDockerContainer) {
|
|
787
778
|
dockerArgs.splice(2, 0, '--rm');
|
|
788
779
|
}
|
|
789
780
|
|
|
790
|
-
// Add --user flag if specified
|
|
791
781
|
if (options.user) {
|
|
792
782
|
dockerArgs.push('--user', options.user);
|
|
793
783
|
}
|
|
@@ -846,13 +836,21 @@ function runInDocker(command, options = {}) {
|
|
|
846
836
|
if (DEBUG) {
|
|
847
837
|
console.log(`[DEBUG] shell: ${shellToUse}`);
|
|
848
838
|
}
|
|
849
|
-
|
|
850
839
|
const shellCmdArgs = shellInteractiveFlag
|
|
851
840
|
? [shellToUse, shellInteractiveFlag]
|
|
852
841
|
: [shellToUse];
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
842
|
+
// Bare shell: pass directly with -i (avoids bash-inside-bash, issue #84; -i ensures interactive).
|
|
843
|
+
let attachedCmdArgs;
|
|
844
|
+
if (isBareShell) {
|
|
845
|
+
const parts = command.trim().split(/\s+/);
|
|
846
|
+
const bareFlag = getShellInteractiveFlag(parts[0]);
|
|
847
|
+
attachedCmdArgs =
|
|
848
|
+
bareFlag && !parts.includes(bareFlag)
|
|
849
|
+
? [parts[0], bareFlag, ...parts.slice(1)]
|
|
850
|
+
: parts;
|
|
851
|
+
} else {
|
|
852
|
+
attachedCmdArgs = [...shellCmdArgs, '-c', command];
|
|
853
|
+
}
|
|
856
854
|
dockerArgs.push(options.image, ...attachedCmdArgs);
|
|
857
855
|
|
|
858
856
|
if (DEBUG) {
|
|
@@ -860,15 +858,25 @@ function runInDocker(command, options = {}) {
|
|
|
860
858
|
}
|
|
861
859
|
|
|
862
860
|
return new Promise((resolve) => {
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
});
|
|
861
|
+
const startTime = Date.now();
|
|
862
|
+
const child = spawn('docker', dockerArgs, { stdio: 'inherit' });
|
|
866
863
|
|
|
867
864
|
child.on('exit', (code) => {
|
|
865
|
+
const durationMs = Date.now() - startTime;
|
|
866
|
+
let message = `Docker container "${containerName}" exited with code ${code}`;
|
|
867
|
+
// Bare shell exited non-zero quickly → startup file error; suggest --norc (issue #84).
|
|
868
|
+
if (isBareShell && code !== 0 && durationMs < 3000) {
|
|
869
|
+
const shell0 = command.trim().split(/\s+/)[0];
|
|
870
|
+
// prettier-ignore
|
|
871
|
+
const norc = path.basename(shell0) === 'zsh' ? '--no-rcs' : '--norc';
|
|
872
|
+
const hint = `Hint: The shell exited immediately — its startup file (.bashrc/.zshrc) may have errors.\nTry skipping startup files: ${shell0} ${norc}`;
|
|
873
|
+
console.log(hint);
|
|
874
|
+
message += `\n${hint}`;
|
|
875
|
+
}
|
|
868
876
|
resolve({
|
|
869
877
|
success: code === 0,
|
|
870
878
|
containerName,
|
|
871
|
-
message
|
|
879
|
+
message,
|
|
872
880
|
exitCode: code,
|
|
873
881
|
});
|
|
874
882
|
});
|
|
@@ -951,7 +959,6 @@ function resetScreenVersionCache() {
|
|
|
951
959
|
screenVersionChecked = false;
|
|
952
960
|
}
|
|
953
961
|
|
|
954
|
-
// Log utilities and runAsIsolatedUser extracted to isolation-log-utils.js
|
|
955
962
|
const {
|
|
956
963
|
getTimestamp,
|
|
957
964
|
generateLogFilename,
|
|
@@ -963,12 +970,6 @@ const {
|
|
|
963
970
|
runAsIsolatedUser,
|
|
964
971
|
} = require('./isolation-log-utils');
|
|
965
972
|
|
|
966
|
-
// Re-export docker utilities from docker-utils for backwards compatibility
|
|
967
|
-
const {
|
|
968
|
-
getDefaultDockerImage,
|
|
969
|
-
canRunLinuxDockerImages,
|
|
970
|
-
} = require('./docker-utils');
|
|
971
|
-
|
|
972
973
|
module.exports = {
|
|
973
974
|
isCommandAvailable,
|
|
974
975
|
hasTTY,
|
|
@@ -992,7 +993,6 @@ module.exports = {
|
|
|
992
993
|
supportsLogfileOption,
|
|
993
994
|
resetScreenVersionCache,
|
|
994
995
|
canRunLinuxDockerImages,
|
|
995
|
-
// Re-exported from docker-utils for backwards compatibility
|
|
996
996
|
getDefaultDockerImage,
|
|
997
997
|
dockerImageExists,
|
|
998
998
|
dockerPullImage,
|
package/test/isolation.test.js
CHANGED
|
@@ -262,7 +262,9 @@ describe('Isolation Runner Error Handling', () => {
|
|
|
262
262
|
detached: true,
|
|
263
263
|
});
|
|
264
264
|
assert.strictEqual(result.success, false);
|
|
265
|
-
assert.ok(
|
|
265
|
+
assert.ok(
|
|
266
|
+
result.message.toLowerCase().includes('docker is not installed')
|
|
267
|
+
);
|
|
266
268
|
});
|
|
267
269
|
|
|
268
270
|
it('should require image option', async () => {
|
|
@@ -6,32 +6,56 @@
|
|
|
6
6
|
* `$ --isolated docker -- bash` caused:
|
|
7
7
|
* docker run ... image /bin/bash -i -c bash (WRONG: bash inside bash)
|
|
8
8
|
* instead of:
|
|
9
|
-
* docker run ... image bash
|
|
9
|
+
* docker run ... image bash -i (CORRECT: bare shell with explicit -i)
|
|
10
|
+
*
|
|
11
|
+
* The -i flag is required so that bash reliably starts in interactive mode,
|
|
12
|
+
* since docker's PTY bridging through Node.js spawn may not set up a TTY that
|
|
13
|
+
* bash auto-detects. Without -i, bash may exit immediately (post-fix regression).
|
|
10
14
|
*
|
|
11
15
|
* The same regression applies to zsh, sh, and all other isolation backends.
|
|
12
16
|
*
|
|
17
|
+
* Also tests the post-fix regression hint: when a bare shell exits with code 1
|
|
18
|
+
* quickly (< 3s), a helpful hint suggests `bash --norc` as a workaround for
|
|
19
|
+
* containers whose .bashrc causes bash to exit non-zero (issue #84, second comment).
|
|
20
|
+
*
|
|
13
21
|
* Reference: https://github.com/link-foundation/start/issues/84
|
|
14
22
|
* Fixed in: PR #85 (v0.24.1) via isInteractiveShellCommand()
|
|
23
|
+
* Fixed in: PR #87 adding explicit -i for bare shells in docker attached mode
|
|
15
24
|
*/
|
|
16
25
|
|
|
17
26
|
const { describe, it } = require('node:test');
|
|
18
27
|
const assert = require('assert');
|
|
19
28
|
const { isInteractiveShellCommand } = require('../src/lib/isolation');
|
|
29
|
+
const {
|
|
30
|
+
isDockerAvailable,
|
|
31
|
+
isDockerInstalled,
|
|
32
|
+
} = require('../src/lib/docker-utils');
|
|
20
33
|
|
|
21
34
|
// Helper: mirrors the command-args construction logic used in
|
|
22
|
-
// runInDocker
|
|
35
|
+
// runInDocker attached mode.
|
|
23
36
|
// If this helper returns args containing '-c' for a bare shell command,
|
|
24
37
|
// the shell-inside-shell bug is present.
|
|
25
38
|
function buildCmdArgs(command, shellToUse = '/bin/bash') {
|
|
39
|
+
const path = require('path');
|
|
26
40
|
const shellName = shellToUse.split('/').pop();
|
|
27
41
|
const shellInteractiveFlag =
|
|
28
42
|
shellName === 'bash' || shellName === 'zsh' ? '-i' : null;
|
|
29
43
|
const shellArgs = shellInteractiveFlag
|
|
30
44
|
? [shellToUse, shellInteractiveFlag]
|
|
31
45
|
: [shellToUse];
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
if (isInteractiveShellCommand(command)) {
|
|
47
|
+
// Bare shell: pass directly with explicit -i for bash/zsh (issue #84 fix)
|
|
48
|
+
const parts = command.trim().split(/\s+/);
|
|
49
|
+
const bareShellFlag =
|
|
50
|
+
path.basename(parts[0]) === 'bash' || path.basename(parts[0]) === 'zsh'
|
|
51
|
+
? '-i'
|
|
52
|
+
: null;
|
|
53
|
+
if (bareShellFlag && !parts.includes(bareShellFlag)) {
|
|
54
|
+
return [parts[0], bareShellFlag, ...parts.slice(1)];
|
|
55
|
+
}
|
|
56
|
+
return parts;
|
|
57
|
+
}
|
|
58
|
+
return [...shellArgs, '-c', command];
|
|
35
59
|
}
|
|
36
60
|
|
|
37
61
|
describe('isInteractiveShellCommand additional cases (issue #84)', () => {
|
|
@@ -74,15 +98,16 @@ describe('Regression: No Shell-Inside-Shell (issue #84)', () => {
|
|
|
74
98
|
// Each test verifies that the command-arg construction logic does NOT
|
|
75
99
|
// wrap a bare shell invocation inside another shell with `-c`.
|
|
76
100
|
//
|
|
77
|
-
// Before fix: buildCmdArgs('bash') → ['/bin/bash', '-i', '-c', 'bash']
|
|
78
|
-
// After fix: buildCmdArgs('bash') → ['bash']
|
|
101
|
+
// Before fix (v0.24.0): buildCmdArgs('bash') → ['/bin/bash', '-i', '-c', 'bash']
|
|
102
|
+
// After fix (v0.24.1): buildCmdArgs('bash') → ['bash', '-i']
|
|
103
|
+
// (bare shell passed directly with explicit -i for reliable interactive mode)
|
|
79
104
|
|
|
80
|
-
it('should pass "bash"
|
|
105
|
+
it('should pass "bash" with -i flag, not wrap in shell -c', () => {
|
|
81
106
|
const args = buildCmdArgs('bash');
|
|
82
107
|
assert.deepStrictEqual(
|
|
83
108
|
args,
|
|
84
|
-
['bash'],
|
|
85
|
-
`Expected ["bash"], got: ${JSON.stringify(args)}`
|
|
109
|
+
['bash', '-i'],
|
|
110
|
+
`Expected ["bash", "-i"], got: ${JSON.stringify(args)}`
|
|
86
111
|
);
|
|
87
112
|
assert.ok(
|
|
88
113
|
!args.includes('-c'),
|
|
@@ -90,16 +115,16 @@ describe('Regression: No Shell-Inside-Shell (issue #84)', () => {
|
|
|
90
115
|
);
|
|
91
116
|
});
|
|
92
117
|
|
|
93
|
-
it('should pass "zsh"
|
|
118
|
+
it('should pass "zsh" with -i flag, not wrap in shell -c', () => {
|
|
94
119
|
const args = buildCmdArgs('zsh');
|
|
95
|
-
assert.deepStrictEqual(args, ['zsh']);
|
|
120
|
+
assert.deepStrictEqual(args, ['zsh', '-i']);
|
|
96
121
|
assert.ok(
|
|
97
122
|
!args.includes('-c'),
|
|
98
123
|
'Must not contain -c flag (shell-inside-shell)'
|
|
99
124
|
);
|
|
100
125
|
});
|
|
101
126
|
|
|
102
|
-
it('should pass "sh" directly, not wrap in shell -c', () => {
|
|
127
|
+
it('should pass "sh" directly without -i (sh does not use -i flag), not wrap in shell -c', () => {
|
|
103
128
|
const args = buildCmdArgs('sh', 'sh');
|
|
104
129
|
assert.deepStrictEqual(args, ['sh']);
|
|
105
130
|
assert.ok(
|
|
@@ -108,24 +133,24 @@ describe('Regression: No Shell-Inside-Shell (issue #84)', () => {
|
|
|
108
133
|
);
|
|
109
134
|
});
|
|
110
135
|
|
|
111
|
-
it('should pass "/bin/bash"
|
|
136
|
+
it('should pass "/bin/bash" with -i flag, not wrap in shell -c', () => {
|
|
112
137
|
const args = buildCmdArgs('/bin/bash');
|
|
113
|
-
assert.deepStrictEqual(args, ['/bin/bash']);
|
|
138
|
+
assert.deepStrictEqual(args, ['/bin/bash', '-i']);
|
|
114
139
|
assert.ok(
|
|
115
140
|
!args.includes('-c'),
|
|
116
141
|
'Must not contain -c flag (shell-inside-shell)'
|
|
117
142
|
);
|
|
118
143
|
});
|
|
119
144
|
|
|
120
|
-
it('should pass "bash --norc"
|
|
145
|
+
it('should pass "bash --norc" with -i flag (workaround for broken .bashrc)', () => {
|
|
121
146
|
const args = buildCmdArgs('bash --norc');
|
|
122
|
-
assert.deepStrictEqual(args, ['bash', '--norc']);
|
|
147
|
+
assert.deepStrictEqual(args, ['bash', '-i', '--norc']);
|
|
123
148
|
assert.ok(!args.includes('-c'), 'Must not contain -c flag');
|
|
124
149
|
});
|
|
125
150
|
|
|
126
|
-
it('should pass "bash -l" directly (login shell)', () => {
|
|
151
|
+
it('should pass "bash -l" directly (login shell, already has its own init)', () => {
|
|
127
152
|
const args = buildCmdArgs('bash -l');
|
|
128
|
-
assert.deepStrictEqual(args, ['bash', '-l']);
|
|
153
|
+
assert.deepStrictEqual(args, ['bash', '-i', '-l']);
|
|
129
154
|
assert.ok(!args.includes('-c'), 'Must not contain -c flag');
|
|
130
155
|
});
|
|
131
156
|
|
|
@@ -156,4 +181,157 @@ describe('Regression: No Shell-Inside-Shell (issue #84)', () => {
|
|
|
156
181
|
'bash -c commands should be treated as regular commands'
|
|
157
182
|
);
|
|
158
183
|
});
|
|
184
|
+
|
|
185
|
+
it('should not duplicate -i if user already passes "bash -i"', () => {
|
|
186
|
+
// If user explicitly passes -i, we should not add another -i
|
|
187
|
+
const args = buildCmdArgs('bash -i');
|
|
188
|
+
assert.deepStrictEqual(
|
|
189
|
+
args,
|
|
190
|
+
['bash', '-i'],
|
|
191
|
+
'Should not duplicate -i flag'
|
|
192
|
+
);
|
|
193
|
+
assert.ok(!args.includes('-c'), 'Must not contain -c flag');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('Docker daemon availability check (issue #84)', () => {
|
|
198
|
+
// Verifies that isDockerAvailable returns a boolean (not a crash),
|
|
199
|
+
// and that the error message for a non-running daemon is helpful.
|
|
200
|
+
|
|
201
|
+
it('isDockerAvailable should return a boolean', () => {
|
|
202
|
+
const result = isDockerAvailable();
|
|
203
|
+
assert.strictEqual(
|
|
204
|
+
typeof result,
|
|
205
|
+
'boolean',
|
|
206
|
+
'isDockerAvailable() must return a boolean'
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('runInDocker error message for non-running daemon should be actionable', () => {
|
|
211
|
+
// Mirrors the message in runInDocker when isDockerAvailable() returns false.
|
|
212
|
+
const message =
|
|
213
|
+
'Docker is installed but not running. Please start Docker Desktop or the Docker daemon, then try again.';
|
|
214
|
+
assert.ok(
|
|
215
|
+
message.includes('not running'),
|
|
216
|
+
'Message must indicate Docker is not running'
|
|
217
|
+
);
|
|
218
|
+
assert.ok(
|
|
219
|
+
message.includes('Docker Desktop'),
|
|
220
|
+
'Message must mention Docker Desktop (common on macOS/Windows)'
|
|
221
|
+
);
|
|
222
|
+
assert.ok(
|
|
223
|
+
message.includes('try again'),
|
|
224
|
+
'Message must tell user what to do next'
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('Docker not installed check (issue #84)', () => {
|
|
230
|
+
// Verifies that isDockerInstalled returns a boolean and that the error
|
|
231
|
+
// message shown when Docker is not installed is helpful and actionable.
|
|
232
|
+
// This covers the case reported in the latest comment: user ran
|
|
233
|
+
// `$ --isolated docker -- bash` and got a silent exit code 1 because
|
|
234
|
+
// Docker was not installed; no helpful error was shown.
|
|
235
|
+
|
|
236
|
+
it('isDockerInstalled should return a boolean', () => {
|
|
237
|
+
const result = isDockerInstalled();
|
|
238
|
+
assert.strictEqual(
|
|
239
|
+
typeof result,
|
|
240
|
+
'boolean',
|
|
241
|
+
'isDockerInstalled() must return a boolean'
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('runInDocker error message for missing docker binary should be actionable', () => {
|
|
246
|
+
// Mirrors the message in runInDocker when isDockerInstalled() returns false.
|
|
247
|
+
const message =
|
|
248
|
+
'Docker is not installed. Install Docker from https://docs.docker.com/get-docker/';
|
|
249
|
+
assert.ok(
|
|
250
|
+
message.toLowerCase().includes('not installed'),
|
|
251
|
+
'Message must indicate Docker is not installed'
|
|
252
|
+
);
|
|
253
|
+
assert.ok(
|
|
254
|
+
message.includes('https://docs.docker.com/get-docker/'),
|
|
255
|
+
'Message must include an installation URL'
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('isDockerInstalled should return false consistently when docker binary is absent (mock)', () => {
|
|
260
|
+
// We test the logic directly: isDockerInstalled uses "which docker" (or "where" on Windows).
|
|
261
|
+
// This is a logic/contract test — isDockerInstalled must return false if the command fails.
|
|
262
|
+
// Real environment result is also checked above; here we verify the return type contract.
|
|
263
|
+
const installed = isDockerInstalled();
|
|
264
|
+
assert.ok(
|
|
265
|
+
installed === true || installed === false,
|
|
266
|
+
'isDockerInstalled() must return exactly true or false'
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('Post-fix regression hint: --norc suggestion (issue #84)', () => {
|
|
272
|
+
// These tests verify the hint logic that recommends --norc when a bare shell
|
|
273
|
+
// exits quickly with code 1 (e.g., broken .bashrc in konard/sandbox image).
|
|
274
|
+
// The hint is shown in runInDocker attached mode when:
|
|
275
|
+
// isBareShell && code !== 0 && durationMs < 3000
|
|
276
|
+
|
|
277
|
+
// Helper mirrors the hint construction logic in isolation.js runInDocker.
|
|
278
|
+
const path = require('path');
|
|
279
|
+
function buildHint(command) {
|
|
280
|
+
const shellName = command.trim().split(/\s+/)[0];
|
|
281
|
+
const noRcFlag = path.basename(shellName) === 'zsh' ? '--no-rcs' : '--norc';
|
|
282
|
+
return (
|
|
283
|
+
`Hint: The shell exited immediately — its startup file (.bashrc/.zshrc) may have errors.\n` +
|
|
284
|
+
`Try skipping startup files: ${shellName} ${noRcFlag}`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
it('should suggest --norc for bare bash that exits quickly', () => {
|
|
289
|
+
const hint = buildHint('bash');
|
|
290
|
+
assert.ok(hint.includes('--norc'), 'Hint must include --norc for bash');
|
|
291
|
+
assert.ok(
|
|
292
|
+
hint.includes('bash --norc'),
|
|
293
|
+
'Hint must show the full corrected command'
|
|
294
|
+
);
|
|
295
|
+
assert.ok(
|
|
296
|
+
hint.includes('startup file'),
|
|
297
|
+
'Hint must explain why the shell exited'
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should suggest --no-rcs for bare zsh that exits quickly', () => {
|
|
302
|
+
const hint = buildHint('zsh');
|
|
303
|
+
assert.ok(hint.includes('--no-rcs'), 'Hint must include --no-rcs for zsh');
|
|
304
|
+
assert.ok(
|
|
305
|
+
hint.includes('zsh --no-rcs'),
|
|
306
|
+
'Hint must show the full corrected command'
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should suggest --norc for /bin/bash path that exits quickly', () => {
|
|
311
|
+
const hint = buildHint('/bin/bash');
|
|
312
|
+
assert.ok(
|
|
313
|
+
hint.includes('/bin/bash --norc'),
|
|
314
|
+
'Hint must include full path with --norc'
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should suggest --norc for bare sh that exits quickly', () => {
|
|
319
|
+
const hint = buildHint('sh');
|
|
320
|
+
assert.ok(hint.includes('sh --norc'), 'Hint must suggest --norc for sh');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should confirm workaround: bash --norc is detected as bare shell', () => {
|
|
324
|
+
// The workaround (bash --norc) must still be recognized as a bare shell
|
|
325
|
+
// so it gets passed directly to docker without -c wrapping
|
|
326
|
+
assert.strictEqual(
|
|
327
|
+
isInteractiveShellCommand('bash --norc'),
|
|
328
|
+
true,
|
|
329
|
+
'bash --norc must be a bare shell (no -c wrapping)'
|
|
330
|
+
);
|
|
331
|
+
assert.strictEqual(
|
|
332
|
+
isInteractiveShellCommand('zsh --no-rcs'),
|
|
333
|
+
true,
|
|
334
|
+
'zsh --no-rcs must be a bare shell (no -c wrapping)'
|
|
335
|
+
);
|
|
336
|
+
});
|
|
159
337
|
});
|