start-command 0.20.4 → 0.22.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 CHANGED
@@ -1,5 +1,70 @@
1
1
  # start-command
2
2
 
3
+ ## 0.22.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 694d85e: feat: Add shell auto-detection and --shell option for isolation environments
8
+
9
+ In docker/ssh and other applicable isolation environments, the shell is now
10
+ automatically detected in order of preference: `bash` → `zsh` → `sh`.
11
+
12
+ Previously, `/bin/sh` was hardcoded in Docker and SSH isolation, which prevented
13
+ access to tools like `nvm` that require bash. Now, the most feature-complete
14
+ available shell is used automatically.
15
+
16
+ Key features:
17
+ - Auto-detect best available shell in Docker containers and SSH hosts (`bash > zsh > sh`)
18
+ - New `--shell` option to force a specific shell (`auto`, `bash`, `zsh`, `sh`)
19
+ - Default mode is `auto` — no need to specify `--shell` for automatic detection
20
+ - `--shell` is passed through in isolation stacking
21
+
22
+ Example usage:
23
+
24
+ ```bash
25
+ # Auto-detect best shell (default behavior, no option needed)
26
+ $ --isolated docker --image node:20 -- nvm use 20
27
+
28
+ # Force bash explicitly
29
+ $ --isolated docker --image ubuntu:22.04 --shell bash -- echo $BASH_VERSION
30
+
31
+ # Use sh specifically
32
+ $ --isolated ssh --endpoint user@host --shell sh -- echo hello
33
+ ```
34
+
35
+ ## 0.21.0
36
+
37
+ ### Minor Changes
38
+
39
+ - bd8fc93: feat: Add isolation stacking support
40
+
41
+ Added support for stacking multiple isolation environments in sequence,
42
+ allowing complex isolation chains like:
43
+
44
+ ```bash
45
+ $ echo hi --isolated "screen ssh tmux docker"
46
+ ```
47
+
48
+ Key features:
49
+ - Space-separated sequences for `--isolated`, `--image`, and `--endpoint` options
50
+ - Underscore (`_`) placeholder for "default/skip" values in option sequences
51
+ - Recursive execution where each level invokes `$` with remaining levels
52
+ - Maximum isolation depth of 7 levels (prevents infinite recursion)
53
+
54
+ Example usage:
55
+
56
+ ```bash
57
+ # SSH to remote host, then run in Docker
58
+ $ cmd --isolated "ssh docker" --endpoint "user@host _" --image "_ node:20"
59
+
60
+ # Create screen session, SSH to host, start tmux, run in Docker
61
+ $ cmd --isolated "screen ssh tmux docker" --endpoint "_ user@host _ _" --image "_ _ _ node:20"
62
+ ```
63
+
64
+ Backward compatible: All existing single-level isolation commands work unchanged.
65
+
66
+ Fixes #77
67
+
3
68
  ## 0.20.4
4
69
 
5
70
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-command",
3
- "version": "0.20.4",
3
+ "version": "0.22.0",
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": {
@@ -16,6 +16,7 @@
16
16
  * --keep-user Keep isolated user after command completes (don't delete)
17
17
  * --keep-alive, -k Keep isolation environment alive after command exits
18
18
  * --auto-remove-docker-container Automatically remove docker container after exit (disabled by default)
19
+ * --shell <shell> Shell to use in isolation environments: auto, bash, zsh, sh (default: auto)
19
20
  * --use-command-stream Use command-stream library for command execution (experimental)
20
21
  * --status <uuid> Show status of a previous command execution by UUID
21
22
  * --output-format <format> Output format for status (links-notation, json, text)
@@ -24,6 +25,7 @@
24
25
  */
25
26
 
26
27
  const { getDefaultDockerImage } = require('./docker-utils');
28
+ const { parseSequence, isSequence } = require('./sequence-parser');
27
29
 
28
30
  // Debug mode from environment
29
31
  const DEBUG =
@@ -34,6 +36,16 @@ const DEBUG =
34
36
  */
35
37
  const VALID_BACKENDS = ['screen', 'tmux', 'docker', 'ssh'];
36
38
 
39
+ /**
40
+ * Valid shell options for --shell
41
+ */
42
+ const VALID_SHELLS = ['auto', 'bash', 'zsh', 'sh'];
43
+
44
+ /**
45
+ * Maximum depth for isolation stacking
46
+ */
47
+ const MAX_ISOLATION_DEPTH = 7;
48
+
37
49
  /**
38
50
  * Valid output formats for --status
39
51
  */
@@ -73,6 +85,63 @@ function generateUUID() {
73
85
  }
74
86
  }
75
87
 
88
+ /**
89
+ * Parse --isolated value, handling both single values and sequences
90
+ * @param {string} value - Isolation value (e.g., "docker" or "screen ssh docker")
91
+ * @param {object} options - Options object to populate
92
+ */
93
+ function parseIsolatedValue(value, options) {
94
+ if (isSequence(value)) {
95
+ // Multi-value sequence (e.g., "screen ssh docker")
96
+ const backends = parseSequence(value).map((v) =>
97
+ v ? v.toLowerCase() : null
98
+ );
99
+ options.isolatedStack = backends;
100
+ options.isolated = backends[0]; // Current level
101
+ } else {
102
+ // Single value (backward compatible)
103
+ const backend = value.toLowerCase();
104
+ options.isolated = backend;
105
+ options.isolatedStack = [backend];
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Parse --image value, handling both single values and sequences
111
+ * @param {string} value - Image value (e.g., "ubuntu:22.04" or "_ _ ubuntu:22.04")
112
+ * @param {object} options - Options object to populate
113
+ */
114
+ function parseImageValue(value, options) {
115
+ if (isSequence(value)) {
116
+ // Multi-value sequence with placeholders
117
+ const images = parseSequence(value);
118
+ options.imageStack = images;
119
+ options.image = images[0]; // Current level
120
+ } else {
121
+ // Single value - will be distributed later during validation
122
+ options.image = value;
123
+ options.imageStack = null; // Will be populated during validation
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Parse --endpoint value, handling both single values and sequences
129
+ * @param {string} value - Endpoint value (e.g., "user@host" or "_ user@host1 _ user@host2")
130
+ * @param {object} options - Options object to populate
131
+ */
132
+ function parseEndpointValue(value, options) {
133
+ if (isSequence(value)) {
134
+ // Multi-value sequence with placeholders
135
+ const endpoints = parseSequence(value);
136
+ options.endpointStack = endpoints;
137
+ options.endpoint = endpoints[0]; // Current level
138
+ } else {
139
+ // Single value - will be distributed later during validation
140
+ options.endpoint = value;
141
+ options.endpointStack = null; // Will be populated during validation
142
+ }
143
+ }
144
+
76
145
  /**
77
146
  * Parse command line arguments into wrapper options and command
78
147
  * @param {string[]} args - Array of command line arguments
@@ -80,18 +149,23 @@ function generateUUID() {
80
149
  */
81
150
  function parseArgs(args) {
82
151
  const wrapperOptions = {
83
- isolated: null, // Isolation environment: screen, tmux, docker, ssh
152
+ isolated: null, // Isolation environment: screen, tmux, docker, ssh (current level)
153
+ isolatedStack: null, // Full isolation stack for multi-level isolation (e.g., ["screen", "ssh", "docker"])
84
154
  attached: false, // Run in attached mode
85
155
  detached: false, // Run in detached mode
86
- session: null, // Session name
156
+ session: null, // Session name (current level)
157
+ sessionStack: null, // Session names for each level
87
158
  sessionId: null, // Session ID (UUID) for tracking - auto-generated if not provided
88
- image: null, // Docker image
89
- endpoint: null, // SSH endpoint (e.g., user@host)
159
+ image: null, // Docker image (current level)
160
+ imageStack: null, // Docker images for each level (with nulls for non-docker levels)
161
+ endpoint: null, // SSH endpoint (current level, e.g., user@host)
162
+ endpointStack: null, // SSH endpoints for each level (with nulls for non-ssh levels)
90
163
  user: false, // Create isolated user
91
164
  userName: null, // Optional custom username for isolated user
92
165
  keepUser: false, // Keep isolated user after command completes (don't delete)
93
166
  keepAlive: false, // Keep environment alive after command exits
94
167
  autoRemoveDockerContainer: false, // Auto-remove docker container after exit
168
+ shell: 'auto', // Shell to use in isolation environments: auto, bash, zsh, sh
95
169
  useCommandStream: false, // Use command-stream library for command execution
96
170
  status: null, // UUID to show status for
97
171
  outputFormat: null, // Output format for status (links-notation, json, text)
@@ -175,18 +249,20 @@ function parseOption(args, index, options) {
175
249
  // --isolated or -i
176
250
  if (arg === '--isolated' || arg === '-i') {
177
251
  if (index + 1 < args.length && !args[index + 1].startsWith('-')) {
178
- options.isolated = args[index + 1].toLowerCase();
252
+ const value = args[index + 1];
253
+ parseIsolatedValue(value, options);
179
254
  return 2;
180
255
  } else {
181
256
  throw new Error(
182
- `Option ${arg} requires a backend argument (screen, tmux, docker)`
257
+ `Option ${arg} requires a backend argument (screen, tmux, docker, ssh)`
183
258
  );
184
259
  }
185
260
  }
186
261
 
187
262
  // --isolated=<value>
188
263
  if (arg.startsWith('--isolated=')) {
189
- options.isolated = arg.split('=')[1].toLowerCase();
264
+ const value = arg.split('=')[1];
265
+ parseIsolatedValue(value, options);
190
266
  return 1;
191
267
  }
192
268
 
@@ -218,10 +294,11 @@ function parseOption(args, index, options) {
218
294
  return 1;
219
295
  }
220
296
 
221
- // --image (for docker)
297
+ // --image (for docker) - supports sequence for stacked isolation
222
298
  if (arg === '--image') {
223
299
  if (index + 1 < args.length && !args[index + 1].startsWith('-')) {
224
- options.image = args[index + 1];
300
+ const value = args[index + 1];
301
+ parseImageValue(value, options);
225
302
  return 2;
226
303
  } else {
227
304
  throw new Error(`Option ${arg} requires an image name argument`);
@@ -230,14 +307,16 @@ function parseOption(args, index, options) {
230
307
 
231
308
  // --image=<value>
232
309
  if (arg.startsWith('--image=')) {
233
- options.image = arg.split('=')[1];
310
+ const value = arg.split('=')[1];
311
+ parseImageValue(value, options);
234
312
  return 1;
235
313
  }
236
314
 
237
- // --endpoint (for ssh)
315
+ // --endpoint (for ssh) - supports sequence for stacked isolation
238
316
  if (arg === '--endpoint') {
239
317
  if (index + 1 < args.length && !args[index + 1].startsWith('-')) {
240
- options.endpoint = args[index + 1];
318
+ const value = args[index + 1];
319
+ parseEndpointValue(value, options);
241
320
  return 2;
242
321
  } else {
243
322
  throw new Error(`Option ${arg} requires an endpoint argument`);
@@ -246,7 +325,8 @@ function parseOption(args, index, options) {
246
325
 
247
326
  // --endpoint=<value>
248
327
  if (arg.startsWith('--endpoint=')) {
249
- options.endpoint = arg.split('=')[1];
328
+ const value = arg.split('=')[1];
329
+ parseEndpointValue(value, options);
250
330
  return 1;
251
331
  }
252
332
 
@@ -291,6 +371,24 @@ function parseOption(args, index, options) {
291
371
  return 1;
292
372
  }
293
373
 
374
+ // --shell <shell>
375
+ if (arg === '--shell') {
376
+ if (index + 1 < args.length && !args[index + 1].startsWith('-')) {
377
+ options.shell = args[index + 1].toLowerCase();
378
+ return 2;
379
+ } else {
380
+ throw new Error(
381
+ `Option ${arg} requires a shell argument (auto, bash, zsh, sh)`
382
+ );
383
+ }
384
+ }
385
+
386
+ // --shell=<value>
387
+ if (arg.startsWith('--shell=')) {
388
+ options.shell = arg.split('=')[1].toLowerCase();
389
+ return 1;
390
+ }
391
+
294
392
  // --use-command-stream
295
393
  if (arg === '--use-command-stream') {
296
394
  options.useCommandStream = true;
@@ -375,23 +473,137 @@ function validateOptions(options) {
375
473
  );
376
474
  }
377
475
 
378
- // Validate isolation environment
476
+ // Validate isolation environment (with stacking support)
379
477
  if (options.isolated !== null) {
380
- if (!VALID_BACKENDS.includes(options.isolated)) {
478
+ const stack = options.isolatedStack || [options.isolated];
479
+ const stackDepth = stack.length;
480
+
481
+ // Check depth limit
482
+ if (stackDepth > MAX_ISOLATION_DEPTH) {
381
483
  throw new Error(
382
- `Invalid isolation environment: "${options.isolated}". Valid options are: ${VALID_BACKENDS.join(', ')}`
484
+ `Isolation stack too deep: ${stackDepth} levels (max: ${MAX_ISOLATION_DEPTH})`
383
485
  );
384
486
  }
385
487
 
386
- // Docker uses --image or defaults to OS-matched image
387
- if (options.isolated === 'docker' && !options.image) {
388
- options.image = getDefaultDockerImage();
488
+ // Validate each backend in the stack
489
+ for (const backend of stack) {
490
+ if (backend && !VALID_BACKENDS.includes(backend)) {
491
+ throw new Error(
492
+ `Invalid isolation environment: "${backend}". Valid options are: ${VALID_BACKENDS.join(', ')}`
493
+ );
494
+ }
495
+ }
496
+
497
+ // Distribute single option values across stack if needed
498
+ if (options.image && !options.imageStack) {
499
+ // Single image value - replicate for all levels
500
+ options.imageStack = Array(stackDepth).fill(options.image);
389
501
  }
390
502
 
391
- // SSH requires --endpoint
392
- if (options.isolated === 'ssh' && !options.endpoint) {
503
+ if (options.endpoint && !options.endpointStack) {
504
+ // Single endpoint value - replicate for all levels
505
+ options.endpointStack = Array(stackDepth).fill(options.endpoint);
506
+ }
507
+
508
+ // Validate stack lengths match
509
+ if (options.imageStack && options.imageStack.length !== stackDepth) {
393
510
  throw new Error(
394
- 'SSH isolation requires --endpoint option to specify the remote server (e.g., user@host)'
511
+ `--image has ${options.imageStack.length} value(s) but isolation stack has ${stackDepth} level(s). ` +
512
+ `Use underscores (_) as placeholders for levels that don't need this option.`
513
+ );
514
+ }
515
+
516
+ if (options.endpointStack && options.endpointStack.length !== stackDepth) {
517
+ throw new Error(
518
+ `--endpoint has ${options.endpointStack.length} value(s) but isolation stack has ${stackDepth} level(s). ` +
519
+ `Use underscores (_) as placeholders for levels that don't need this option.`
520
+ );
521
+ }
522
+
523
+ // Validate each level has required options
524
+ for (let i = 0; i < stackDepth; i++) {
525
+ const backend = stack[i];
526
+
527
+ // Docker uses --image or defaults to OS-matched image
528
+ if (backend === 'docker') {
529
+ const image = options.imageStack
530
+ ? options.imageStack[i]
531
+ : options.image;
532
+ if (!image) {
533
+ // Apply default image
534
+ if (!options.imageStack) {
535
+ options.imageStack = Array(stackDepth).fill(null);
536
+ }
537
+ options.imageStack[i] = getDefaultDockerImage();
538
+ }
539
+ }
540
+
541
+ // SSH requires --endpoint
542
+ if (backend === 'ssh') {
543
+ const endpoint = options.endpointStack
544
+ ? options.endpointStack[i]
545
+ : options.endpoint;
546
+ if (!endpoint) {
547
+ throw new Error(
548
+ `SSH isolation at level ${i + 1} requires --endpoint option. ` +
549
+ `Use a sequence like --endpoint "_ user@host _" to specify endpoints for specific levels.`
550
+ );
551
+ }
552
+ }
553
+ }
554
+
555
+ // Set current level values for backward compatibility
556
+ options.image = options.imageStack ? options.imageStack[0] : options.image;
557
+ options.endpoint = options.endpointStack
558
+ ? options.endpointStack[0]
559
+ : options.endpoint;
560
+
561
+ // Validate option compatibility with current level (for backward compatible error messages)
562
+ const currentBackend = stack[0];
563
+
564
+ // Image is only valid if stack contains docker
565
+ if (options.image && !stack.includes('docker')) {
566
+ throw new Error(
567
+ '--image option is only valid when isolation stack includes docker'
568
+ );
569
+ }
570
+
571
+ // Endpoint is only valid if stack contains ssh
572
+ if (options.endpoint && !stack.includes('ssh')) {
573
+ throw new Error(
574
+ '--endpoint option is only valid when isolation stack includes ssh'
575
+ );
576
+ }
577
+
578
+ // Auto-remove-docker-container is only valid with docker in stack
579
+ if (options.autoRemoveDockerContainer && !stack.includes('docker')) {
580
+ throw new Error(
581
+ '--auto-remove-docker-container option is only valid when isolation stack includes docker'
582
+ );
583
+ }
584
+
585
+ // User isolation is not supported with Docker as first level
586
+ if (options.user && currentBackend === 'docker') {
587
+ throw new Error(
588
+ '--isolated-user is not supported with Docker as the first isolation level. ' +
589
+ 'Docker uses its own user namespace for isolation.'
590
+ );
591
+ }
592
+ } else {
593
+ // Validate options that require isolation when no isolation is specified
594
+ if (options.autoRemoveDockerContainer) {
595
+ throw new Error(
596
+ '--auto-remove-docker-container option is only valid when isolation stack includes docker'
597
+ );
598
+ }
599
+ if (options.image) {
600
+ throw new Error(
601
+ '--image option is only valid when isolation stack includes docker'
602
+ );
603
+ }
604
+ if (options.endpoint) {
605
+ throw new Error(
606
+ '--endpoint option is only valid when isolation stack includes ssh'
395
607
  );
396
608
  }
397
609
  }
@@ -401,36 +613,13 @@ function validateOptions(options) {
401
613
  throw new Error('--session option is only valid with --isolated');
402
614
  }
403
615
 
404
- // Image is only valid with docker
405
- if (options.image && options.isolated !== 'docker') {
406
- throw new Error('--image option is only valid with --isolated docker');
407
- }
408
-
409
- // Endpoint is only valid with ssh
410
- if (options.endpoint && options.isolated !== 'ssh') {
411
- throw new Error('--endpoint option is only valid with --isolated ssh');
412
- }
413
-
414
616
  // Keep-alive is only valid with isolation
415
617
  if (options.keepAlive && !options.isolated) {
416
618
  throw new Error('--keep-alive option is only valid with --isolated');
417
619
  }
418
620
 
419
- // Auto-remove-docker-container is only valid with docker isolation
420
- if (options.autoRemoveDockerContainer && options.isolated !== 'docker') {
421
- throw new Error(
422
- '--auto-remove-docker-container option is only valid with --isolated docker'
423
- );
424
- }
425
-
426
621
  // User isolation validation
427
622
  if (options.user) {
428
- // User isolation is not supported with Docker (Docker has its own user mechanism)
429
- if (options.isolated === 'docker') {
430
- throw new Error(
431
- '--isolated-user is not supported with Docker isolation. Docker uses its own user namespace for isolation.'
432
- );
433
- }
434
623
  // Validate custom username if provided
435
624
  if (options.userName) {
436
625
  if (!/^[a-zA-Z0-9_-]+$/.test(options.userName)) {
@@ -465,6 +654,15 @@ function validateOptions(options) {
465
654
  throw new Error('--output-format option is only valid with --status');
466
655
  }
467
656
 
657
+ // Validate shell option
658
+ if (options.shell !== null && options.shell !== undefined) {
659
+ if (!VALID_SHELLS.includes(options.shell)) {
660
+ throw new Error(
661
+ `Invalid shell: "${options.shell}". Valid options are: ${VALID_SHELLS.join(', ')}`
662
+ );
663
+ }
664
+ }
665
+
468
666
  // Validate session ID is a valid UUID if provided
469
667
  if (options.sessionId !== null && options.sessionId !== undefined) {
470
668
  if (!isValidUUID(options.sessionId)) {
@@ -509,14 +707,26 @@ function getEffectiveMode(options) {
509
707
  return 'attached';
510
708
  }
511
709
 
710
+ /**
711
+ * Check if isolation stack has multiple levels
712
+ * @param {object} options - Parsed wrapper options
713
+ * @returns {boolean} True if multiple isolation levels
714
+ */
715
+ function hasStackedIsolation(options) {
716
+ return options.isolatedStack && options.isolatedStack.length > 1;
717
+ }
718
+
512
719
  module.exports = {
513
720
  parseArgs,
514
721
  validateOptions,
515
722
  generateSessionName,
516
723
  hasIsolation,
724
+ hasStackedIsolation,
517
725
  getEffectiveMode,
518
726
  isValidUUID,
519
727
  generateUUID,
520
728
  VALID_BACKENDS,
521
729
  VALID_OUTPUT_FORMATS,
730
+ VALID_SHELLS,
731
+ MAX_ISOLATION_DEPTH,
522
732
  };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Command Builder for Isolation Stacking
3
+ *
4
+ * Builds the command to execute at each isolation level,
5
+ * including the recursive $ invocation for nested levels.
6
+ */
7
+
8
+ const { formatSequence } = require('./sequence-parser');
9
+
10
+ /**
11
+ * Build command for next isolation level
12
+ * If more levels remain, builds a recursive $ command
13
+ * If this is the last level, returns the actual command
14
+ *
15
+ * @param {object} options - Current wrapper options
16
+ * @param {string} command - User command to execute
17
+ * @returns {string} Command to execute at current level
18
+ */
19
+ function buildNextLevelCommand(options, command) {
20
+ // If no more isolation levels, execute actual command
21
+ if (!options.isolatedStack || options.isolatedStack.length <= 1) {
22
+ return command;
23
+ }
24
+
25
+ // Build recursive $ command for remaining levels
26
+ const parts = ['$'];
27
+
28
+ // Remaining isolation stack (skip first which is current level)
29
+ const remainingStack = options.isolatedStack.slice(1);
30
+ parts.push(`--isolated "${remainingStack.join(' ')}"`);
31
+
32
+ // Shift option values and add if non-empty
33
+ if (options.imageStack && options.imageStack.length > 1) {
34
+ const remainingImages = options.imageStack.slice(1);
35
+ const imageStr = formatSequence(remainingImages);
36
+ if (imageStr && imageStr !== '_'.repeat(remainingImages.length)) {
37
+ parts.push(`--image "${imageStr}"`);
38
+ }
39
+ }
40
+
41
+ if (options.endpointStack && options.endpointStack.length > 1) {
42
+ const remainingEndpoints = options.endpointStack.slice(1);
43
+ const endpointStr = formatSequence(remainingEndpoints);
44
+ if (endpointStr) {
45
+ parts.push(`--endpoint "${endpointStr}"`);
46
+ }
47
+ }
48
+
49
+ if (options.sessionStack && options.sessionStack.length > 1) {
50
+ const remainingSessions = options.sessionStack.slice(1);
51
+ const sessionStr = formatSequence(remainingSessions);
52
+ if (sessionStr && sessionStr !== '_'.repeat(remainingSessions.length)) {
53
+ parts.push(`--session "${sessionStr}"`);
54
+ }
55
+ }
56
+
57
+ // Pass through global flags
58
+ if (options.detached) {
59
+ parts.push('--detached');
60
+ }
61
+
62
+ if (options.keepAlive) {
63
+ parts.push('--keep-alive');
64
+ }
65
+
66
+ if (options.sessionId) {
67
+ parts.push(`--session-id ${options.sessionId}`);
68
+ }
69
+
70
+ if (options.autoRemoveDockerContainer) {
71
+ parts.push('--auto-remove-docker-container');
72
+ }
73
+
74
+ if (options.shell && options.shell !== 'auto') {
75
+ parts.push(`--shell ${options.shell}`);
76
+ }
77
+
78
+ // Separator and command
79
+ parts.push('--');
80
+ parts.push(command);
81
+
82
+ return parts.join(' ');
83
+ }
84
+
85
+ /**
86
+ * Escape a command for safe execution in a shell context
87
+ * @param {string} cmd - Command to escape
88
+ * @returns {string} Escaped command
89
+ */
90
+ function escapeForShell(cmd) {
91
+ // For now, simple escaping - could be enhanced
92
+ return cmd.replace(/'/g, "'\\''");
93
+ }
94
+
95
+ /**
96
+ * Check if we're at the last isolation level
97
+ * @param {object} options - Wrapper options
98
+ * @returns {boolean} True if this is the last level
99
+ */
100
+ function isLastLevel(options) {
101
+ return !options.isolatedStack || options.isolatedStack.length <= 1;
102
+ }
103
+
104
+ /**
105
+ * Get current isolation backend from options
106
+ * @param {object} options - Wrapper options
107
+ * @returns {string|null} Current backend or null
108
+ */
109
+ function getCurrentBackend(options) {
110
+ if (options.isolatedStack && options.isolatedStack.length > 0) {
111
+ return options.isolatedStack[0];
112
+ }
113
+ return options.isolated;
114
+ }
115
+
116
+ /**
117
+ * Get option value for current level
118
+ * @param {(string|null)[]} stack - Option value stack
119
+ * @returns {string|null} Value for current level
120
+ */
121
+ function getCurrentValue(stack) {
122
+ if (Array.isArray(stack) && stack.length > 0) {
123
+ return stack[0];
124
+ }
125
+ return null;
126
+ }
127
+
128
+ module.exports = {
129
+ buildNextLevelCommand,
130
+ escapeForShell,
131
+ isLastLevel,
132
+ getCurrentBackend,
133
+ getCurrentValue,
134
+ };