start-command 0.9.0 → 0.10.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.
@@ -131,6 +131,22 @@ function hasTTY() {
131
131
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
132
132
  }
133
133
 
134
+ /**
135
+ * Wrap command with sudo -u if user option is specified
136
+ * @param {string} command - Original command
137
+ * @param {string|null} user - Username to run as (or null)
138
+ * @returns {string} Wrapped command
139
+ */
140
+ function wrapCommandWithUser(command, user) {
141
+ if (!user) {
142
+ return command;
143
+ }
144
+ // Use sudo -u to run command as specified user
145
+ // -E preserves environment variables
146
+ // -n ensures non-interactive (fails if password required)
147
+ return `sudo -n -u ${user} sh -c '${command.replace(/'/g, "'\\''")}'`;
148
+ }
149
+
134
150
  /**
135
151
  * Run command in GNU Screen using detached mode with log capture
136
152
  * This is a workaround for environments without TTY
@@ -142,9 +158,10 @@ function hasTTY() {
142
158
  * @param {string} command - Command to execute
143
159
  * @param {string} sessionName - Session name
144
160
  * @param {object} shellInfo - Shell info from getShell()
161
+ * @param {string|null} user - Username to run command as (optional)
145
162
  * @returns {Promise<{success: boolean, sessionName: string, message: string, output: string}>}
146
163
  */
147
- function runScreenWithLogCapture(command, sessionName, shellInfo) {
164
+ function runScreenWithLogCapture(command, sessionName, shellInfo, user = null) {
148
165
  const { shell, shellArg } = shellInfo;
149
166
  const logFile = path.join(os.tmpdir(), `screen-output-${sessionName}.log`);
150
167
 
@@ -154,7 +171,8 @@ function runScreenWithLogCapture(command, sessionName, shellInfo) {
154
171
  return new Promise((resolve) => {
155
172
  try {
156
173
  let screenArgs;
157
- let effectiveCommand = command;
174
+ // Wrap command with user switch if specified
175
+ let effectiveCommand = wrapCommandWithUser(command, user);
158
176
 
159
177
  if (useNativeLogging) {
160
178
  // Modern screen (>= 4.5.1): Use -L -Logfile option for native log capture
@@ -167,7 +185,7 @@ function runScreenWithLogCapture(command, sessionName, shellInfo) {
167
185
  logFile,
168
186
  shell,
169
187
  shellArg,
170
- command,
188
+ effectiveCommand,
171
189
  ];
172
190
 
173
191
  if (DEBUG) {
@@ -179,7 +197,7 @@ function runScreenWithLogCapture(command, sessionName, shellInfo) {
179
197
  // Older screen (< 4.5.1, e.g., macOS bundled 4.0.3): Use tee fallback
180
198
  // Wrap the command to capture output using tee
181
199
  // The parentheses ensure proper grouping of the command and its stderr
182
- effectiveCommand = `(${command}) 2>&1 | tee "${logFile}"`;
200
+ effectiveCommand = `(${effectiveCommand}) 2>&1 | tee "${logFile}"`;
183
201
  screenArgs = ['-dmS', sessionName, shell, shellArg, effectiveCommand];
184
202
 
185
203
  if (DEBUG) {
@@ -299,7 +317,7 @@ function runScreenWithLogCapture(command, sessionName, shellInfo) {
299
317
  /**
300
318
  * Run command in GNU Screen
301
319
  * @param {string} command - Command to execute
302
- * @param {object} options - Options (session, detached, keepAlive)
320
+ * @param {object} options - Options (session, detached, user, keepAlive)
303
321
  * @returns {Promise<{success: boolean, sessionName: string, message: string}>}
304
322
  */
305
323
  function runInScreen(command, options = {}) {
@@ -317,16 +335,17 @@ function runInScreen(command, options = {}) {
317
335
  const { shell, shellArg } = shellInfo;
318
336
 
319
337
  try {
338
+ // Wrap command with user switch if specified
339
+ let effectiveCommand = wrapCommandWithUser(command, options.user);
340
+
320
341
  if (options.detached) {
321
342
  // Detached mode: screen -dmS <session> <shell> -c '<command>'
322
343
  // By default (keepAlive=false), the session will exit after command completes
323
344
  // With keepAlive=true, we start a shell that runs the command but stays alive
324
- let effectiveCommand = command;
325
345
 
326
346
  if (options.keepAlive) {
327
347
  // With keep-alive: run command, then keep shell open
328
- // Use exec to replace the shell, but first run command
329
- effectiveCommand = `${command}; exec ${shell}`;
348
+ effectiveCommand = `${effectiveCommand}; exec ${shell}`;
330
349
  }
331
350
  // Without keep-alive: command runs and session exits naturally when done
332
351
 
@@ -383,7 +402,12 @@ function runInScreen(command, options = {}) {
383
402
  );
384
403
  }
385
404
 
386
- return runScreenWithLogCapture(command, sessionName, shellInfo);
405
+ return runScreenWithLogCapture(
406
+ command,
407
+ sessionName,
408
+ shellInfo,
409
+ options.user
410
+ );
387
411
  }
388
412
  } catch (err) {
389
413
  return Promise.resolve({
@@ -397,7 +421,7 @@ function runInScreen(command, options = {}) {
397
421
  /**
398
422
  * Run command in tmux
399
423
  * @param {string} command - Command to execute
400
- * @param {object} options - Options (session, detached, keepAlive)
424
+ * @param {object} options - Options (session, detached, user, keepAlive)
401
425
  * @returns {Promise<{success: boolean, sessionName: string, message: string}>}
402
426
  */
403
427
  function runInTmux(command, options = {}) {
@@ -414,16 +438,18 @@ function runInTmux(command, options = {}) {
414
438
  const shellInfo = getShell();
415
439
  const { shell } = shellInfo;
416
440
 
441
+ // Wrap command with user switch if specified
442
+ let effectiveCommand = wrapCommandWithUser(command, options.user);
443
+
417
444
  try {
418
445
  if (options.detached) {
419
446
  // Detached mode: tmux new-session -d -s <session> '<command>'
420
447
  // By default (keepAlive=false), the session will exit after command completes
421
448
  // With keepAlive=true, we keep the shell alive after the command
422
- let effectiveCommand = command;
423
449
 
424
450
  if (options.keepAlive) {
425
451
  // With keep-alive: run command, then keep shell open
426
- effectiveCommand = `${command}; exec ${shell}`;
452
+ effectiveCommand = `${effectiveCommand}; exec ${shell}`;
427
453
  }
428
454
  // Without keep-alive: command runs and session exits naturally when done
429
455
 
@@ -458,14 +484,14 @@ function runInTmux(command, options = {}) {
458
484
  // Attached mode: tmux new-session -s <session> '<command>'
459
485
  if (DEBUG) {
460
486
  console.log(
461
- `[DEBUG] Running: tmux new-session -s "${sessionName}" "${command}"`
487
+ `[DEBUG] Running: tmux new-session -s "${sessionName}" "${effectiveCommand}"`
462
488
  );
463
489
  }
464
490
 
465
491
  return new Promise((resolve) => {
466
492
  const child = spawn(
467
493
  'tmux',
468
- ['new-session', '-s', sessionName, command],
494
+ ['new-session', '-s', sessionName, effectiveCommand],
469
495
  {
470
496
  stdio: 'inherit',
471
497
  }
@@ -501,7 +527,7 @@ function runInTmux(command, options = {}) {
501
527
  /**
502
528
  * Run command in Docker container
503
529
  * @param {string} command - Command to execute
504
- * @param {object} options - Options (image, session/name, detached, keepAlive, autoRemoveDockerContainer)
530
+ * @param {object} options - Options (image, session/name, detached, user, keepAlive, autoRemoveDockerContainer)
505
531
  * @returns {Promise<{success: boolean, containerName: string, message: string}>}
506
532
  */
507
533
  function runInDocker(command, options = {}) {
@@ -526,7 +552,7 @@ function runInDocker(command, options = {}) {
526
552
 
527
553
  try {
528
554
  if (options.detached) {
529
- // Detached mode: docker run -d --name <name> <image> <shell> -c '<command>'
555
+ // Detached mode: docker run -d --name <name> [--user <user>] <image> <shell> -c '<command>'
530
556
  // By default (keepAlive=false), the container exits after command completes
531
557
  // With keepAlive=true, we keep the container running with a shell
532
558
  let effectiveCommand = command;
@@ -537,16 +563,7 @@ function runInDocker(command, options = {}) {
537
563
  }
538
564
  // Without keep-alive: container exits naturally when command completes
539
565
 
540
- const dockerArgs = [
541
- 'run',
542
- '-d',
543
- '--name',
544
- containerName,
545
- options.image,
546
- '/bin/sh',
547
- '-c',
548
- effectiveCommand,
549
- ];
566
+ const dockerArgs = ['run', '-d', '--name', containerName];
550
567
 
551
568
  // Add --rm flag if autoRemoveDockerContainer is true
552
569
  // Note: --rm must come before the image name
@@ -554,6 +571,13 @@ function runInDocker(command, options = {}) {
554
571
  dockerArgs.splice(2, 0, '--rm');
555
572
  }
556
573
 
574
+ // Add --user flag if specified
575
+ if (options.user) {
576
+ dockerArgs.push('--user', options.user);
577
+ }
578
+
579
+ dockerArgs.push(options.image, '/bin/sh', '-c', effectiveCommand);
580
+
557
581
  if (DEBUG) {
558
582
  console.log(`[DEBUG] Running: docker ${dockerArgs.join(' ')}`);
559
583
  console.log(`[DEBUG] keepAlive: ${options.keepAlive || false}`);
@@ -588,18 +612,15 @@ function runInDocker(command, options = {}) {
588
612
  message,
589
613
  });
590
614
  } else {
591
- // Attached mode: docker run -it --name <name> <image> <shell> -c '<command>'
592
- const dockerArgs = [
593
- 'run',
594
- '-it',
595
- '--rm',
596
- '--name',
597
- containerName,
598
- options.image,
599
- '/bin/sh',
600
- '-c',
601
- command,
602
- ];
615
+ // Attached mode: docker run -it --name <name> [--user <user>] <image> <shell> -c '<command>'
616
+ const dockerArgs = ['run', '-it', '--rm', '--name', containerName];
617
+
618
+ // Add --user flag if specified
619
+ if (options.user) {
620
+ dockerArgs.push('--user', options.user);
621
+ }
622
+
623
+ dockerArgs.push(options.image, '/bin/sh', '-c', command);
603
624
 
604
625
  if (DEBUG) {
605
626
  console.log(`[DEBUG] Running: docker ${dockerArgs.join(' ')}`);
@@ -687,6 +708,7 @@ function generateLogFilename(environment) {
687
708
  * @param {string} params.mode - attached or detached
688
709
  * @param {string} params.sessionName - Session/container name
689
710
  * @param {string} [params.image] - Docker image (for docker environment)
711
+ * @param {string} [params.user] - User to run command as (optional)
690
712
  * @param {string} params.startTime - Start timestamp
691
713
  * @returns {string} Log header content
692
714
  */
@@ -700,6 +722,9 @@ function createLogHeader(params) {
700
722
  if (params.image) {
701
723
  content += `Image: ${params.image}\n`;
702
724
  }
725
+ if (params.user) {
726
+ content += `User: ${params.user}\n`;
727
+ }
703
728
  content += `Platform: ${process.platform}\n`;
704
729
  content += `Node Version: ${process.version}\n`;
705
730
  content += `Working Directory: ${process.cwd()}\n`;
@@ -763,6 +788,37 @@ function resetScreenVersionCache() {
763
788
  screenVersionChecked = false;
764
789
  }
765
790
 
791
+ /**
792
+ * Run command as an isolated user (without isolation backend)
793
+ * Uses sudo -u to switch users
794
+ * @param {string} cmd - Command to execute
795
+ * @param {string} username - User to run as
796
+ * @returns {Promise<{success: boolean, message: string, exitCode: number}>}
797
+ */
798
+ function runAsIsolatedUser(cmd, username) {
799
+ return new Promise((resolve) => {
800
+ const child = spawn('sudo', ['-n', '-u', username, 'sh', '-c', cmd], {
801
+ stdio: 'inherit',
802
+ });
803
+
804
+ child.on('exit', (code) => {
805
+ resolve({
806
+ success: code === 0,
807
+ message: `Command completed as user "${username}" with exit code ${code}`,
808
+ exitCode: code || 0,
809
+ });
810
+ });
811
+
812
+ child.on('error', (err) => {
813
+ resolve({
814
+ success: false,
815
+ message: `Failed to run as user "${username}": ${err.message}`,
816
+ exitCode: 1,
817
+ });
818
+ });
819
+ });
820
+ }
821
+
766
822
  module.exports = {
767
823
  isCommandAvailable,
768
824
  hasTTY,
@@ -770,7 +826,8 @@ module.exports = {
770
826
  runInTmux,
771
827
  runInDocker,
772
828
  runIsolated,
773
- // Export logging utilities for unified experience
829
+ runAsIsolatedUser,
830
+ wrapCommandWithUser,
774
831
  getTimestamp,
775
832
  generateLogFilename,
776
833
  createLogHeader,
@@ -778,7 +835,6 @@ module.exports = {
778
835
  writeLogFile,
779
836
  getLogDir,
780
837
  createLogPath,
781
- // Export screen version utilities for testing and debugging
782
838
  getScreenVersion,
783
839
  supportsLogfileOption,
784
840
  resetScreenVersionCache,