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 +65 -0
- package/package.json +1 -1
- package/src/lib/args-parser.js +255 -45
- package/src/lib/command-builder.js +134 -0
- package/src/lib/isolation-log-utils.js +147 -0
- package/src/lib/isolation.js +166 -139
- package/src/lib/sequence-parser.js +231 -0
- package/test/args-parser-shell.test.js +165 -0
- package/test/args-parser.test.js +8 -6
- package/test/isolation-stacking.test.js +366 -0
- package/test/isolation.test.js +64 -0
- package/test/sequence-parser.test.js +237 -0
- package/test/user-manager.test.js +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/** Log utility functions for isolation runners */
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate timestamp for logging
|
|
10
|
+
* @returns {string} ISO timestamp without 'T' and 'Z'
|
|
11
|
+
*/
|
|
12
|
+
function getTimestamp() {
|
|
13
|
+
return new Date().toISOString().replace('T', ' ').replace('Z', '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate unique log filename
|
|
18
|
+
* @param {string} environment - The isolation environment name
|
|
19
|
+
* @returns {string} Log filename
|
|
20
|
+
*/
|
|
21
|
+
function generateLogFilename(environment) {
|
|
22
|
+
const timestamp = Date.now();
|
|
23
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
24
|
+
return `start-command-${environment}-${timestamp}-${random}.log`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create log content header
|
|
29
|
+
* @param {object} params - Log parameters
|
|
30
|
+
* @param {string} params.command - The command being executed
|
|
31
|
+
* @param {string} params.environment - The isolation environment
|
|
32
|
+
* @param {string} params.mode - attached or detached
|
|
33
|
+
* @param {string} params.sessionName - Session/container name
|
|
34
|
+
* @param {string} [params.image] - Docker image (for docker environment)
|
|
35
|
+
* @param {string} [params.user] - User to run command as (optional)
|
|
36
|
+
* @param {string} params.startTime - Start timestamp
|
|
37
|
+
* @returns {string} Log header content
|
|
38
|
+
*/
|
|
39
|
+
function createLogHeader(params) {
|
|
40
|
+
let content = `=== Start Command Log ===\n`;
|
|
41
|
+
content += `Timestamp: ${params.startTime}\n`;
|
|
42
|
+
content += `Command: ${params.command}\n`;
|
|
43
|
+
content += `Environment: ${params.environment}\n`;
|
|
44
|
+
content += `Mode: ${params.mode}\n`;
|
|
45
|
+
content += `Session: ${params.sessionName}\n`;
|
|
46
|
+
if (params.image) {
|
|
47
|
+
content += `Image: ${params.image}\n`;
|
|
48
|
+
}
|
|
49
|
+
if (params.user) {
|
|
50
|
+
content += `User: ${params.user}\n`;
|
|
51
|
+
}
|
|
52
|
+
content += `Platform: ${process.platform}\n`;
|
|
53
|
+
content += `Node Version: ${process.version}\n`;
|
|
54
|
+
content += `Working Directory: ${process.cwd()}\n`;
|
|
55
|
+
content += `${'='.repeat(50)}\n\n`;
|
|
56
|
+
return content;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create log content footer
|
|
61
|
+
* @param {string} endTime - End timestamp
|
|
62
|
+
* @param {number} exitCode - Exit code
|
|
63
|
+
* @returns {string} Log footer content
|
|
64
|
+
*/
|
|
65
|
+
function createLogFooter(endTime, exitCode) {
|
|
66
|
+
let content = `\n${'='.repeat(50)}\n`;
|
|
67
|
+
content += `Finished: ${endTime}\n`;
|
|
68
|
+
content += `Exit Code: ${exitCode}\n`;
|
|
69
|
+
return content;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Write log file
|
|
74
|
+
* @param {string} logPath - Path to log file
|
|
75
|
+
* @param {string} content - Log content
|
|
76
|
+
* @returns {boolean} Success status
|
|
77
|
+
*/
|
|
78
|
+
function writeLogFile(logPath, content) {
|
|
79
|
+
try {
|
|
80
|
+
fs.writeFileSync(logPath, content, 'utf8');
|
|
81
|
+
return true;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error(`\nWarning: Could not save log file: ${err.message}`);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get log directory from environment or use system temp
|
|
90
|
+
* @returns {string} Log directory path
|
|
91
|
+
*/
|
|
92
|
+
function getLogDir() {
|
|
93
|
+
return process.env.START_LOG_DIR || os.tmpdir();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create log file path
|
|
98
|
+
* @param {string} environment - The isolation environment
|
|
99
|
+
* @returns {string} Full path to log file
|
|
100
|
+
*/
|
|
101
|
+
function createLogPath(environment) {
|
|
102
|
+
const logDir = getLogDir();
|
|
103
|
+
const logFilename = generateLogFilename(environment);
|
|
104
|
+
return path.join(logDir, logFilename);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Run command as an isolated user (without isolation environment)
|
|
109
|
+
* Uses sudo -u to switch users
|
|
110
|
+
* @param {string} cmd - Command to execute
|
|
111
|
+
* @param {string} username - User to run as
|
|
112
|
+
* @returns {Promise<{success: boolean, message: string, exitCode: number}>}
|
|
113
|
+
*/
|
|
114
|
+
function runAsIsolatedUser(cmd, username) {
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
const child = spawn('sudo', ['-n', '-u', username, 'sh', '-c', cmd], {
|
|
117
|
+
stdio: 'inherit',
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
child.on('exit', (code) => {
|
|
121
|
+
resolve({
|
|
122
|
+
success: code === 0,
|
|
123
|
+
message: `Command completed as user "${username}" with exit code ${code}`,
|
|
124
|
+
exitCode: code || 0,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
child.on('error', (err) => {
|
|
129
|
+
resolve({
|
|
130
|
+
success: false,
|
|
131
|
+
message: `Failed to run as user "${username}": ${err.message}`,
|
|
132
|
+
exitCode: 1,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
getTimestamp,
|
|
140
|
+
generateLogFilename,
|
|
141
|
+
createLogHeader,
|
|
142
|
+
createLogFooter,
|
|
143
|
+
writeLogFile,
|
|
144
|
+
getLogDir,
|
|
145
|
+
createLogPath,
|
|
146
|
+
runAsIsolatedUser,
|
|
147
|
+
};
|
package/src/lib/isolation.js
CHANGED
|
@@ -116,6 +116,101 @@ function getShell() {
|
|
|
116
116
|
return { shell, shellArg };
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Detect the best available shell in an isolation environment (docker/ssh)
|
|
121
|
+
* Tries shells in order: bash, zsh, sh
|
|
122
|
+
* @param {'docker'|'ssh'} environment - Isolation environment type
|
|
123
|
+
* @param {object} options - Options for the isolation environment
|
|
124
|
+
* @param {string} [shellPreference='auto'] - Shell preference: 'auto', 'bash', 'zsh', 'sh'
|
|
125
|
+
* @returns {string} Shell path to use
|
|
126
|
+
*/
|
|
127
|
+
function detectShellInEnvironment(
|
|
128
|
+
environment,
|
|
129
|
+
options,
|
|
130
|
+
shellPreference = 'auto'
|
|
131
|
+
) {
|
|
132
|
+
// If a specific shell is requested (not auto), use it directly
|
|
133
|
+
if (shellPreference && shellPreference !== 'auto') {
|
|
134
|
+
if (DEBUG) {
|
|
135
|
+
console.log(`[DEBUG] Using forced shell: ${shellPreference}`);
|
|
136
|
+
}
|
|
137
|
+
return shellPreference;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// In auto mode, try shells in order of preference
|
|
141
|
+
const shellsToTry = ['bash', 'zsh', 'sh'];
|
|
142
|
+
|
|
143
|
+
if (environment === 'docker') {
|
|
144
|
+
const image = options.image;
|
|
145
|
+
if (!image) {
|
|
146
|
+
return 'sh';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const shell of shellsToTry) {
|
|
150
|
+
try {
|
|
151
|
+
const result = spawnSync(
|
|
152
|
+
'docker',
|
|
153
|
+
['run', '--rm', image, 'sh', '-c', `command -v ${shell}`],
|
|
154
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
155
|
+
);
|
|
156
|
+
if (result.status === 0 && result.stdout.trim()) {
|
|
157
|
+
if (DEBUG) {
|
|
158
|
+
console.log(
|
|
159
|
+
`[DEBUG] Detected shell in docker image ${image}: ${result.stdout.trim()}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return result.stdout.trim();
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// Continue to next shell
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (DEBUG) {
|
|
170
|
+
console.log(
|
|
171
|
+
`[DEBUG] Could not detect shell in docker image ${image}, falling back to sh`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return 'sh';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (environment === 'ssh') {
|
|
178
|
+
const endpoint = options.endpoint;
|
|
179
|
+
if (!endpoint) {
|
|
180
|
+
return 'sh';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
// Run a single SSH command to check for available shells in order
|
|
185
|
+
const checkCmd = shellsToTry.map((s) => `command -v ${s}`).join(' || ');
|
|
186
|
+
const result = spawnSync('ssh', [endpoint, checkCmd], {
|
|
187
|
+
encoding: 'utf8',
|
|
188
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
189
|
+
});
|
|
190
|
+
if (result.status === 0 && result.stdout.trim()) {
|
|
191
|
+
const detected = result.stdout.trim();
|
|
192
|
+
if (DEBUG) {
|
|
193
|
+
console.log(
|
|
194
|
+
`[DEBUG] Detected shell on SSH host ${endpoint}: ${detected}`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return detected;
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// Fall through to default
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (DEBUG) {
|
|
204
|
+
console.log(
|
|
205
|
+
`[DEBUG] Could not detect shell on SSH host ${endpoint}, falling back to sh`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return 'sh';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return 'sh';
|
|
212
|
+
}
|
|
213
|
+
|
|
119
214
|
/**
|
|
120
215
|
* Check if the current process has a TTY attached
|
|
121
216
|
* @returns {boolean} True if TTY is available
|
|
@@ -553,15 +648,25 @@ function runInSsh(command, options = {}) {
|
|
|
553
648
|
const sessionName = options.session || generateSessionName('ssh');
|
|
554
649
|
const sshTarget = options.endpoint;
|
|
555
650
|
|
|
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
|
+
const shellToUse = detectShellInEnvironment('ssh', options, options.shell);
|
|
655
|
+
// Whether to wrap command with a shell (only when explicit shell is specified)
|
|
656
|
+
const useExplicitShell =
|
|
657
|
+
options.shell && options.shell !== 'auto' ? shellToUse : null;
|
|
658
|
+
|
|
556
659
|
try {
|
|
557
660
|
if (options.detached) {
|
|
558
661
|
// Detached mode: Run command in background on remote server using nohup
|
|
559
662
|
// The command will continue running even after SSH connection closes
|
|
560
|
-
const
|
|
663
|
+
const remoteShell = useExplicitShell || shellToUse;
|
|
664
|
+
const remoteCommand = `nohup ${remoteShell} -c ${JSON.stringify(command)} > /tmp/${sessionName}.log 2>&1 &`;
|
|
561
665
|
const sshArgs = [sshTarget, remoteCommand];
|
|
562
666
|
|
|
563
667
|
if (DEBUG) {
|
|
564
668
|
console.log(`[DEBUG] Running: ssh ${sshArgs.join(' ')}`);
|
|
669
|
+
console.log(`[DEBUG] shell: ${remoteShell}`);
|
|
565
670
|
}
|
|
566
671
|
|
|
567
672
|
const result = spawnSync('ssh', sshArgs, {
|
|
@@ -579,11 +684,15 @@ function runInSsh(command, options = {}) {
|
|
|
579
684
|
});
|
|
580
685
|
} else {
|
|
581
686
|
// Attached mode: Run command interactively over SSH
|
|
582
|
-
//
|
|
583
|
-
|
|
687
|
+
// When a specific shell is requested, wrap the command with that shell.
|
|
688
|
+
// In auto mode, pass the command directly and let the remote's default shell handle it.
|
|
689
|
+
const sshArgs = useExplicitShell
|
|
690
|
+
? [sshTarget, useExplicitShell, '-c', command]
|
|
691
|
+
: [sshTarget, command];
|
|
584
692
|
|
|
585
693
|
if (DEBUG) {
|
|
586
694
|
console.log(`[DEBUG] Running: ssh ${sshArgs.join(' ')}`);
|
|
695
|
+
console.log(`[DEBUG] shell: ${shellToUse}`);
|
|
587
696
|
}
|
|
588
697
|
|
|
589
698
|
return new Promise((resolve) => {
|
|
@@ -660,6 +769,9 @@ function runInDocker(command, options = {}) {
|
|
|
660
769
|
}
|
|
661
770
|
}
|
|
662
771
|
|
|
772
|
+
// Detect the shell to use in the container
|
|
773
|
+
const shellToUse = detectShellInEnvironment('docker', options, options.shell);
|
|
774
|
+
|
|
663
775
|
// Print the user command (this appears after any virtual commands like docker pull)
|
|
664
776
|
const { createCommandLine } = require('./output-blocks');
|
|
665
777
|
console.log(createCommandLine(command));
|
|
@@ -674,7 +786,7 @@ function runInDocker(command, options = {}) {
|
|
|
674
786
|
|
|
675
787
|
if (options.keepAlive) {
|
|
676
788
|
// With keep-alive: run command, then keep shell alive
|
|
677
|
-
effectiveCommand = `${command}; exec
|
|
789
|
+
effectiveCommand = `${command}; exec ${shellToUse}`;
|
|
678
790
|
}
|
|
679
791
|
// Without keep-alive: container exits naturally when command completes
|
|
680
792
|
|
|
@@ -691,10 +803,11 @@ function runInDocker(command, options = {}) {
|
|
|
691
803
|
dockerArgs.push('--user', options.user);
|
|
692
804
|
}
|
|
693
805
|
|
|
694
|
-
dockerArgs.push(options.image,
|
|
806
|
+
dockerArgs.push(options.image, shellToUse, '-c', effectiveCommand);
|
|
695
807
|
|
|
696
808
|
if (DEBUG) {
|
|
697
809
|
console.log(`[DEBUG] Running: docker ${dockerArgs.join(' ')}`);
|
|
810
|
+
console.log(`[DEBUG] shell: ${shellToUse}`);
|
|
698
811
|
console.log(`[DEBUG] keepAlive: ${options.keepAlive || false}`);
|
|
699
812
|
console.log(
|
|
700
813
|
`[DEBUG] autoRemoveDockerContainer: ${options.autoRemoveDockerContainer || false}`
|
|
@@ -735,7 +848,11 @@ function runInDocker(command, options = {}) {
|
|
|
735
848
|
dockerArgs.push('--user', options.user);
|
|
736
849
|
}
|
|
737
850
|
|
|
738
|
-
|
|
851
|
+
if (DEBUG) {
|
|
852
|
+
console.log(`[DEBUG] shell: ${shellToUse}`);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
dockerArgs.push(options.image, shellToUse, '-c', command);
|
|
739
856
|
|
|
740
857
|
if (DEBUG) {
|
|
741
858
|
console.log(`[DEBUG] Running: docker ${dockerArgs.join(' ')}`);
|
|
@@ -775,21 +892,48 @@ function runInDocker(command, options = {}) {
|
|
|
775
892
|
|
|
776
893
|
/**
|
|
777
894
|
* Run command in the specified isolation environment
|
|
895
|
+
* Supports stacked isolation where each level calls $ with remaining levels
|
|
778
896
|
* @param {string} backend - Isolation environment (screen, tmux, docker, ssh)
|
|
779
897
|
* @param {string} command - Command to execute
|
|
780
898
|
* @param {object} options - Options
|
|
781
899
|
* @returns {Promise<{success: boolean, message: string}>}
|
|
782
900
|
*/
|
|
783
901
|
function runIsolated(backend, command, options = {}) {
|
|
902
|
+
// If stacked isolation, build the command for next level
|
|
903
|
+
let effectiveCommand = command;
|
|
904
|
+
|
|
905
|
+
if (options.isolatedStack && options.isolatedStack.length > 1) {
|
|
906
|
+
// Lazy load to avoid circular dependency
|
|
907
|
+
const { buildNextLevelCommand } = require('./command-builder');
|
|
908
|
+
effectiveCommand = buildNextLevelCommand(options, command);
|
|
909
|
+
|
|
910
|
+
if (DEBUG) {
|
|
911
|
+
console.log(
|
|
912
|
+
`[DEBUG] Stacked isolation - level command: ${effectiveCommand}`
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Get current level option values
|
|
918
|
+
const currentOptions = {
|
|
919
|
+
...options,
|
|
920
|
+
// Use current level values from stacks
|
|
921
|
+
image: options.imageStack ? options.imageStack[0] : options.image,
|
|
922
|
+
endpoint: options.endpointStack
|
|
923
|
+
? options.endpointStack[0]
|
|
924
|
+
: options.endpoint,
|
|
925
|
+
session: options.sessionStack ? options.sessionStack[0] : options.session,
|
|
926
|
+
};
|
|
927
|
+
|
|
784
928
|
switch (backend) {
|
|
785
929
|
case 'screen':
|
|
786
|
-
return runInScreen(
|
|
930
|
+
return runInScreen(effectiveCommand, currentOptions);
|
|
787
931
|
case 'tmux':
|
|
788
|
-
return runInTmux(
|
|
932
|
+
return runInTmux(effectiveCommand, currentOptions);
|
|
789
933
|
case 'docker':
|
|
790
|
-
return runInDocker(
|
|
934
|
+
return runInDocker(effectiveCommand, currentOptions);
|
|
791
935
|
case 'ssh':
|
|
792
|
-
return runInSsh(
|
|
936
|
+
return runInSsh(effectiveCommand, currentOptions);
|
|
793
937
|
default:
|
|
794
938
|
return Promise.resolve({
|
|
795
939
|
success: false,
|
|
@@ -798,105 +942,6 @@ function runIsolated(backend, command, options = {}) {
|
|
|
798
942
|
}
|
|
799
943
|
}
|
|
800
944
|
|
|
801
|
-
/**
|
|
802
|
-
* Generate timestamp for logging
|
|
803
|
-
* @returns {string} ISO timestamp without 'T' and 'Z'
|
|
804
|
-
*/
|
|
805
|
-
function getTimestamp() {
|
|
806
|
-
return new Date().toISOString().replace('T', ' ').replace('Z', '');
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
/**
|
|
810
|
-
* Generate unique log filename
|
|
811
|
-
* @param {string} environment - The isolation environment name
|
|
812
|
-
* @returns {string} Log filename
|
|
813
|
-
*/
|
|
814
|
-
function generateLogFilename(environment) {
|
|
815
|
-
const timestamp = Date.now();
|
|
816
|
-
const random = Math.random().toString(36).substring(2, 8);
|
|
817
|
-
return `start-command-${environment}-${timestamp}-${random}.log`;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
/**
|
|
821
|
-
* Create log content header
|
|
822
|
-
* @param {object} params - Log parameters
|
|
823
|
-
* @param {string} params.command - The command being executed
|
|
824
|
-
* @param {string} params.environment - The isolation environment
|
|
825
|
-
* @param {string} params.mode - attached or detached
|
|
826
|
-
* @param {string} params.sessionName - Session/container name
|
|
827
|
-
* @param {string} [params.image] - Docker image (for docker environment)
|
|
828
|
-
* @param {string} [params.user] - User to run command as (optional)
|
|
829
|
-
* @param {string} params.startTime - Start timestamp
|
|
830
|
-
* @returns {string} Log header content
|
|
831
|
-
*/
|
|
832
|
-
function createLogHeader(params) {
|
|
833
|
-
let content = `=== Start Command Log ===\n`;
|
|
834
|
-
content += `Timestamp: ${params.startTime}\n`;
|
|
835
|
-
content += `Command: ${params.command}\n`;
|
|
836
|
-
content += `Environment: ${params.environment}\n`;
|
|
837
|
-
content += `Mode: ${params.mode}\n`;
|
|
838
|
-
content += `Session: ${params.sessionName}\n`;
|
|
839
|
-
if (params.image) {
|
|
840
|
-
content += `Image: ${params.image}\n`;
|
|
841
|
-
}
|
|
842
|
-
if (params.user) {
|
|
843
|
-
content += `User: ${params.user}\n`;
|
|
844
|
-
}
|
|
845
|
-
content += `Platform: ${process.platform}\n`;
|
|
846
|
-
content += `Node Version: ${process.version}\n`;
|
|
847
|
-
content += `Working Directory: ${process.cwd()}\n`;
|
|
848
|
-
content += `${'='.repeat(50)}\n\n`;
|
|
849
|
-
return content;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
/**
|
|
853
|
-
* Create log content footer
|
|
854
|
-
* @param {string} endTime - End timestamp
|
|
855
|
-
* @param {number} exitCode - Exit code
|
|
856
|
-
* @returns {string} Log footer content
|
|
857
|
-
*/
|
|
858
|
-
function createLogFooter(endTime, exitCode) {
|
|
859
|
-
let content = `\n${'='.repeat(50)}\n`;
|
|
860
|
-
content += `Finished: ${endTime}\n`;
|
|
861
|
-
content += `Exit Code: ${exitCode}\n`;
|
|
862
|
-
return content;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
/**
|
|
866
|
-
* Write log file
|
|
867
|
-
* @param {string} logPath - Path to log file
|
|
868
|
-
* @param {string} content - Log content
|
|
869
|
-
* @returns {boolean} Success status
|
|
870
|
-
*/
|
|
871
|
-
function writeLogFile(logPath, content) {
|
|
872
|
-
try {
|
|
873
|
-
fs.writeFileSync(logPath, content, 'utf8');
|
|
874
|
-
return true;
|
|
875
|
-
} catch (err) {
|
|
876
|
-
console.error(`\nWarning: Could not save log file: ${err.message}`);
|
|
877
|
-
return false;
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
/**
|
|
882
|
-
* Get log directory from environment or use system temp
|
|
883
|
-
* @returns {string} Log directory path
|
|
884
|
-
*/
|
|
885
|
-
function getLogDir() {
|
|
886
|
-
return process.env.START_LOG_DIR || os.tmpdir();
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
/**
|
|
890
|
-
* Create log file path
|
|
891
|
-
* @param {string} environment - The isolation environment
|
|
892
|
-
* @returns {string} Full path to log file
|
|
893
|
-
*/
|
|
894
|
-
function createLogPath(environment) {
|
|
895
|
-
const logDir = getLogDir();
|
|
896
|
-
const logFilename = generateLogFilename(environment);
|
|
897
|
-
return path.join(logDir, logFilename);
|
|
898
|
-
}
|
|
899
|
-
|
|
900
945
|
/**
|
|
901
946
|
* Reset screen version cache (useful for testing)
|
|
902
947
|
*/
|
|
@@ -905,36 +950,17 @@ function resetScreenVersionCache() {
|
|
|
905
950
|
screenVersionChecked = false;
|
|
906
951
|
}
|
|
907
952
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
});
|
|
920
|
-
|
|
921
|
-
child.on('exit', (code) => {
|
|
922
|
-
resolve({
|
|
923
|
-
success: code === 0,
|
|
924
|
-
message: `Command completed as user "${username}" with exit code ${code}`,
|
|
925
|
-
exitCode: code || 0,
|
|
926
|
-
});
|
|
927
|
-
});
|
|
928
|
-
|
|
929
|
-
child.on('error', (err) => {
|
|
930
|
-
resolve({
|
|
931
|
-
success: false,
|
|
932
|
-
message: `Failed to run as user "${username}": ${err.message}`,
|
|
933
|
-
exitCode: 1,
|
|
934
|
-
});
|
|
935
|
-
});
|
|
936
|
-
});
|
|
937
|
-
}
|
|
953
|
+
// Log utilities and runAsIsolatedUser extracted to isolation-log-utils.js
|
|
954
|
+
const {
|
|
955
|
+
getTimestamp,
|
|
956
|
+
generateLogFilename,
|
|
957
|
+
createLogHeader,
|
|
958
|
+
createLogFooter,
|
|
959
|
+
writeLogFile,
|
|
960
|
+
getLogDir,
|
|
961
|
+
createLogPath,
|
|
962
|
+
runAsIsolatedUser,
|
|
963
|
+
} = require('./isolation-log-utils');
|
|
938
964
|
|
|
939
965
|
// Re-export docker utilities from docker-utils for backwards compatibility
|
|
940
966
|
const {
|
|
@@ -945,6 +971,7 @@ const {
|
|
|
945
971
|
module.exports = {
|
|
946
972
|
isCommandAvailable,
|
|
947
973
|
hasTTY,
|
|
974
|
+
detectShellInEnvironment,
|
|
948
975
|
runInScreen,
|
|
949
976
|
runInTmux,
|
|
950
977
|
runInDocker,
|