command-stream 0.9.0 → 0.9.2
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/js/src/$.ansi.mjs +147 -0
- package/js/src/$.mjs +49 -6382
- package/js/src/$.process-runner-base.mjs +563 -0
- package/js/src/$.process-runner-execution.mjs +1497 -0
- package/js/src/$.process-runner-orchestration.mjs +250 -0
- package/js/src/$.process-runner-pipeline.mjs +1162 -0
- package/js/src/$.process-runner-stream-kill.mjs +312 -0
- package/js/src/$.process-runner-virtual.mjs +297 -0
- package/js/src/$.quote.mjs +161 -0
- package/js/src/$.result.mjs +23 -0
- package/js/src/$.shell-settings.mjs +84 -0
- package/js/src/$.shell.mjs +157 -0
- package/js/src/$.state.mjs +401 -0
- package/js/src/$.stream-emitter.mjs +111 -0
- package/js/src/$.stream-utils.mjs +390 -0
- package/js/src/$.trace.mjs +36 -0
- package/js/src/$.utils.mjs +2 -23
- package/js/src/$.virtual-commands.mjs +113 -0
- package/js/src/commands/$.which.mjs +3 -1
- package/js/src/commands/index.mjs +24 -0
- package/js/src/shell-parser.mjs +125 -83
- package/js/tests/resource-cleanup-internals.test.mjs +22 -24
- package/js/tests/sigint-cleanup.test.mjs +3 -0
- package/package.json +1 -1
- package/rust/src/ansi.rs +194 -0
- package/rust/src/events.rs +305 -0
- package/rust/src/lib.rs +71 -60
- package/rust/src/macros.rs +165 -0
- package/rust/src/pipeline.rs +411 -0
- package/rust/src/quote.rs +161 -0
- package/rust/src/state.rs +333 -0
- package/rust/src/stream.rs +369 -0
- package/rust/src/trace.rs +152 -0
- package/rust/src/utils.rs +53 -158
- package/rust/tests/events.rs +207 -0
- package/rust/tests/macros.rs +77 -0
- package/rust/tests/pipeline.rs +93 -0
- package/rust/tests/state.rs +207 -0
- package/rust/tests/stream.rs +102 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Shell quoting and command building utilities
|
|
2
|
+
// Handles safe interpolation of values into shell commands
|
|
3
|
+
|
|
4
|
+
import { trace } from './$.trace.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Quote a value for safe shell interpolation
|
|
8
|
+
* @param {*} value - Value to quote
|
|
9
|
+
* @returns {string} Safely quoted string
|
|
10
|
+
*/
|
|
11
|
+
export function quote(value) {
|
|
12
|
+
if (value === null || value === undefined) {
|
|
13
|
+
return "''";
|
|
14
|
+
}
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
return value.map(quote).join(' ');
|
|
17
|
+
}
|
|
18
|
+
if (typeof value !== 'string') {
|
|
19
|
+
value = String(value);
|
|
20
|
+
}
|
|
21
|
+
if (value === '') {
|
|
22
|
+
return "''";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// If the value is already properly quoted and doesn't need further escaping,
|
|
26
|
+
// check if we can use it as-is or with simpler quoting
|
|
27
|
+
if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
|
|
28
|
+
// If it's already single-quoted and doesn't contain unescaped single quotes in the middle,
|
|
29
|
+
// we can potentially use it as-is
|
|
30
|
+
const inner = value.slice(1, -1);
|
|
31
|
+
if (!inner.includes("'")) {
|
|
32
|
+
// The inner content has no single quotes, so the original quoting is fine
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length > 2) {
|
|
38
|
+
// If it's already double-quoted, wrap it in single quotes to preserve it
|
|
39
|
+
return `'${value}'`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check if the string needs quoting at all
|
|
43
|
+
// Safe characters: alphanumeric, dash, underscore, dot, slash, colon, equals, comma, plus
|
|
44
|
+
// This regex matches strings that DON'T need quoting
|
|
45
|
+
const safePattern = /^[a-zA-Z0-9_\-./=,+@:]+$/;
|
|
46
|
+
|
|
47
|
+
if (safePattern.test(value)) {
|
|
48
|
+
// The string is safe and doesn't need quoting
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Default behavior: wrap in single quotes and escape any internal single quotes
|
|
53
|
+
// This handles spaces, special shell characters, etc.
|
|
54
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build a shell command from template strings and values
|
|
59
|
+
* @param {string[]} strings - Template literal strings
|
|
60
|
+
* @param {*[]} values - Interpolated values
|
|
61
|
+
* @returns {string} Complete shell command
|
|
62
|
+
*/
|
|
63
|
+
export function buildShellCommand(strings, values) {
|
|
64
|
+
trace(
|
|
65
|
+
'Utils',
|
|
66
|
+
() =>
|
|
67
|
+
`buildShellCommand ENTER | ${JSON.stringify(
|
|
68
|
+
{
|
|
69
|
+
stringsLength: strings.length,
|
|
70
|
+
valuesLength: values.length,
|
|
71
|
+
},
|
|
72
|
+
null,
|
|
73
|
+
2
|
|
74
|
+
)}`
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Special case: if we have a single value with empty surrounding strings,
|
|
78
|
+
// and the value looks like a complete shell command, treat it as raw
|
|
79
|
+
if (
|
|
80
|
+
values.length === 1 &&
|
|
81
|
+
strings.length === 2 &&
|
|
82
|
+
strings[0] === '' &&
|
|
83
|
+
strings[1] === '' &&
|
|
84
|
+
typeof values[0] === 'string'
|
|
85
|
+
) {
|
|
86
|
+
const commandStr = values[0];
|
|
87
|
+
// Check if this looks like a complete shell command (contains spaces and shell-safe characters)
|
|
88
|
+
const commandPattern = /^[a-zA-Z0-9_\-./=,+@:\s"'`$(){}<>|&;*?[\]~\\]+$/;
|
|
89
|
+
if (commandPattern.test(commandStr) && commandStr.trim().length > 0) {
|
|
90
|
+
trace(
|
|
91
|
+
'Utils',
|
|
92
|
+
() =>
|
|
93
|
+
`BRANCH: buildShellCommand => COMPLETE_COMMAND | ${JSON.stringify({ command: commandStr }, null, 2)}`
|
|
94
|
+
);
|
|
95
|
+
return commandStr;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let out = '';
|
|
100
|
+
for (let i = 0; i < strings.length; i++) {
|
|
101
|
+
out += strings[i];
|
|
102
|
+
if (i < values.length) {
|
|
103
|
+
const v = values[i];
|
|
104
|
+
if (
|
|
105
|
+
v &&
|
|
106
|
+
typeof v === 'object' &&
|
|
107
|
+
Object.prototype.hasOwnProperty.call(v, 'raw')
|
|
108
|
+
) {
|
|
109
|
+
trace(
|
|
110
|
+
'Utils',
|
|
111
|
+
() =>
|
|
112
|
+
`BRANCH: buildShellCommand => RAW_VALUE | ${JSON.stringify({ value: String(v.raw) }, null, 2)}`
|
|
113
|
+
);
|
|
114
|
+
out += String(v.raw);
|
|
115
|
+
} else {
|
|
116
|
+
const quoted = quote(v);
|
|
117
|
+
trace(
|
|
118
|
+
'Utils',
|
|
119
|
+
() =>
|
|
120
|
+
`BRANCH: buildShellCommand => QUOTED_VALUE | ${JSON.stringify({ original: v, quoted }, null, 2)}`
|
|
121
|
+
);
|
|
122
|
+
out += quoted;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
trace(
|
|
128
|
+
'Utils',
|
|
129
|
+
() =>
|
|
130
|
+
`buildShellCommand EXIT | ${JSON.stringify({ command: out }, null, 2)}`
|
|
131
|
+
);
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Mark a value as raw (not to be quoted)
|
|
137
|
+
* @param {*} value - Value to mark as raw
|
|
138
|
+
* @returns {{ raw: string }} Raw value wrapper
|
|
139
|
+
*/
|
|
140
|
+
export function raw(value) {
|
|
141
|
+
trace('API', () => `raw() called with value: ${String(value).slice(0, 50)}`);
|
|
142
|
+
return { raw: String(value) };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Pump a readable stream, calling onChunk for each chunk
|
|
147
|
+
* @param {Readable} readable - Readable stream
|
|
148
|
+
* @param {function} onChunk - Callback for each chunk
|
|
149
|
+
*/
|
|
150
|
+
export async function pumpReadable(readable, onChunk) {
|
|
151
|
+
if (!readable) {
|
|
152
|
+
trace('Utils', () => 'pumpReadable: No readable stream provided');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
trace('Utils', () => 'pumpReadable: Starting to pump readable stream');
|
|
156
|
+
for await (const chunk of readable) {
|
|
157
|
+
const { asBuffer } = await import('./$.stream-utils.mjs');
|
|
158
|
+
await onChunk(asBuffer(chunk));
|
|
159
|
+
}
|
|
160
|
+
trace('Utils', () => 'pumpReadable: Finished pumping readable stream');
|
|
161
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Result creation utilities for command-stream
|
|
2
|
+
// Creates standardized result objects
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create a standardized result object
|
|
6
|
+
* @param {object} params - Result parameters
|
|
7
|
+
* @param {number} params.code - Exit code
|
|
8
|
+
* @param {string} params.stdout - Standard output
|
|
9
|
+
* @param {string} params.stderr - Standard error
|
|
10
|
+
* @param {string} params.stdin - Standard input that was sent
|
|
11
|
+
* @returns {object} Result object with text() method
|
|
12
|
+
*/
|
|
13
|
+
export function createResult({ code, stdout = '', stderr = '', stdin = '' }) {
|
|
14
|
+
return {
|
|
15
|
+
code,
|
|
16
|
+
stdout,
|
|
17
|
+
stderr,
|
|
18
|
+
stdin,
|
|
19
|
+
text() {
|
|
20
|
+
return Promise.resolve(stdout);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Shell settings management (set, unset, shell object)
|
|
2
|
+
// Provides bash-like shell option management
|
|
3
|
+
|
|
4
|
+
import { trace } from './$.trace.mjs';
|
|
5
|
+
import { getShellSettings, setShellSettings } from './$.state.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Set a shell option
|
|
9
|
+
* @param {string} option - Option to set
|
|
10
|
+
* @returns {object} Current shell settings
|
|
11
|
+
*/
|
|
12
|
+
export function set(option) {
|
|
13
|
+
trace('API', () => `set() called with option: ${option}`);
|
|
14
|
+
const mapping = {
|
|
15
|
+
e: 'errexit', // set -e: exit on error
|
|
16
|
+
errexit: 'errexit',
|
|
17
|
+
v: 'verbose', // set -v: verbose
|
|
18
|
+
verbose: 'verbose',
|
|
19
|
+
x: 'xtrace', // set -x: trace execution
|
|
20
|
+
xtrace: 'xtrace',
|
|
21
|
+
u: 'nounset', // set -u: error on unset vars
|
|
22
|
+
nounset: 'nounset',
|
|
23
|
+
'o pipefail': 'pipefail', // set -o pipefail
|
|
24
|
+
pipefail: 'pipefail',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const globalShellSettings = getShellSettings();
|
|
28
|
+
|
|
29
|
+
if (mapping[option]) {
|
|
30
|
+
setShellSettings({ [mapping[option]]: true });
|
|
31
|
+
if (globalShellSettings.verbose) {
|
|
32
|
+
console.log(`+ set -${option}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return getShellSettings();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Unset a shell option
|
|
40
|
+
* @param {string} option - Option to unset
|
|
41
|
+
* @returns {object} Current shell settings
|
|
42
|
+
*/
|
|
43
|
+
export function unset(option) {
|
|
44
|
+
trace('API', () => `unset() called with option: ${option}`);
|
|
45
|
+
const mapping = {
|
|
46
|
+
e: 'errexit',
|
|
47
|
+
errexit: 'errexit',
|
|
48
|
+
v: 'verbose',
|
|
49
|
+
verbose: 'verbose',
|
|
50
|
+
x: 'xtrace',
|
|
51
|
+
xtrace: 'xtrace',
|
|
52
|
+
u: 'nounset',
|
|
53
|
+
nounset: 'nounset',
|
|
54
|
+
'o pipefail': 'pipefail',
|
|
55
|
+
pipefail: 'pipefail',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const globalShellSettings = getShellSettings();
|
|
59
|
+
|
|
60
|
+
if (mapping[option]) {
|
|
61
|
+
setShellSettings({ [mapping[option]]: false });
|
|
62
|
+
if (globalShellSettings.verbose) {
|
|
63
|
+
console.log(`+ set +${option}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return getShellSettings();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Convenience object for common shell patterns
|
|
71
|
+
*/
|
|
72
|
+
export const shell = {
|
|
73
|
+
set,
|
|
74
|
+
unset,
|
|
75
|
+
settings: () => ({ ...getShellSettings() }),
|
|
76
|
+
|
|
77
|
+
// Bash-like shortcuts
|
|
78
|
+
errexit: (enable = true) => (enable ? set('e') : unset('e')),
|
|
79
|
+
verbose: (enable = true) => (enable ? set('v') : unset('v')),
|
|
80
|
+
xtrace: (enable = true) => (enable ? set('x') : unset('x')),
|
|
81
|
+
pipefail: (enable = true) =>
|
|
82
|
+
enable ? set('o pipefail') : unset('o pipefail'),
|
|
83
|
+
nounset: (enable = true) => (enable ? set('u') : unset('u')),
|
|
84
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Shell detection utilities for command-stream
|
|
2
|
+
// Handles cross-platform shell detection and caching
|
|
3
|
+
|
|
4
|
+
import cp from 'child_process';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { trace } from './$.trace.mjs';
|
|
7
|
+
|
|
8
|
+
// Shell detection cache
|
|
9
|
+
let cachedShell = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Find an available shell by checking multiple options in order
|
|
13
|
+
* Returns the shell command and arguments to use
|
|
14
|
+
* @returns {{ cmd: string, args: string[] }} Shell command and arguments
|
|
15
|
+
*/
|
|
16
|
+
export function findAvailableShell() {
|
|
17
|
+
if (cachedShell) {
|
|
18
|
+
trace('ShellDetection', () => `Using cached shell: ${cachedShell.cmd}`);
|
|
19
|
+
return cachedShell;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const isWindows = process.platform === 'win32';
|
|
23
|
+
|
|
24
|
+
// Windows-specific shells
|
|
25
|
+
const windowsShells = [
|
|
26
|
+
// Git Bash is the most Unix-compatible option on Windows
|
|
27
|
+
// Check common installation paths
|
|
28
|
+
{
|
|
29
|
+
cmd: 'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
30
|
+
args: ['-c'],
|
|
31
|
+
checkPath: true,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
cmd: 'C:\\Program Files\\Git\\usr\\bin\\bash.exe',
|
|
35
|
+
args: ['-c'],
|
|
36
|
+
checkPath: true,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
cmd: 'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
|
40
|
+
args: ['-c'],
|
|
41
|
+
checkPath: true,
|
|
42
|
+
},
|
|
43
|
+
// Git Bash via PATH (if added to PATH by user)
|
|
44
|
+
{ cmd: 'bash.exe', args: ['-c'], checkPath: false },
|
|
45
|
+
// WSL bash as fallback
|
|
46
|
+
{ cmd: 'wsl.exe', args: ['bash', '-c'], checkPath: false },
|
|
47
|
+
// PowerShell as last resort (different syntax for commands)
|
|
48
|
+
{ cmd: 'powershell.exe', args: ['-Command'], checkPath: false },
|
|
49
|
+
{ cmd: 'pwsh.exe', args: ['-Command'], checkPath: false },
|
|
50
|
+
// cmd.exe as final fallback
|
|
51
|
+
{ cmd: 'cmd.exe', args: ['/c'], checkPath: false },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Unix-specific shells
|
|
55
|
+
const unixShells = [
|
|
56
|
+
// Try absolute paths first (most reliable)
|
|
57
|
+
{ cmd: '/bin/sh', args: ['-l', '-c'], checkPath: true },
|
|
58
|
+
{ cmd: '/usr/bin/sh', args: ['-l', '-c'], checkPath: true },
|
|
59
|
+
{ cmd: '/bin/bash', args: ['-l', '-c'], checkPath: true },
|
|
60
|
+
{ cmd: '/usr/bin/bash', args: ['-l', '-c'], checkPath: true },
|
|
61
|
+
{ cmd: '/bin/zsh', args: ['-l', '-c'], checkPath: true },
|
|
62
|
+
{ cmd: '/usr/bin/zsh', args: ['-l', '-c'], checkPath: true },
|
|
63
|
+
// macOS specific paths
|
|
64
|
+
{ cmd: '/usr/local/bin/bash', args: ['-l', '-c'], checkPath: true },
|
|
65
|
+
{ cmd: '/usr/local/bin/zsh', args: ['-l', '-c'], checkPath: true },
|
|
66
|
+
// Linux brew paths
|
|
67
|
+
{
|
|
68
|
+
cmd: '/home/linuxbrew/.linuxbrew/bin/bash',
|
|
69
|
+
args: ['-l', '-c'],
|
|
70
|
+
checkPath: true,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
cmd: '/home/linuxbrew/.linuxbrew/bin/zsh',
|
|
74
|
+
args: ['-l', '-c'],
|
|
75
|
+
checkPath: true,
|
|
76
|
+
},
|
|
77
|
+
// Try shells in PATH as fallback (which might not work in all environments)
|
|
78
|
+
// Using separate -l and -c flags for better compatibility
|
|
79
|
+
{ cmd: 'sh', args: ['-l', '-c'], checkPath: false },
|
|
80
|
+
{ cmd: 'bash', args: ['-l', '-c'], checkPath: false },
|
|
81
|
+
{ cmd: 'zsh', args: ['-l', '-c'], checkPath: false },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// Select shells based on platform
|
|
85
|
+
const shellsToTry = isWindows ? windowsShells : unixShells;
|
|
86
|
+
|
|
87
|
+
for (const shell of shellsToTry) {
|
|
88
|
+
try {
|
|
89
|
+
if (shell.checkPath) {
|
|
90
|
+
// Check if the absolute path exists
|
|
91
|
+
if (fs.existsSync(shell.cmd)) {
|
|
92
|
+
trace(
|
|
93
|
+
'ShellDetection',
|
|
94
|
+
() => `Found shell at absolute path: ${shell.cmd}`
|
|
95
|
+
);
|
|
96
|
+
cachedShell = { cmd: shell.cmd, args: shell.args };
|
|
97
|
+
return cachedShell;
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
// On Windows, use 'where' instead of 'which'
|
|
101
|
+
const whichCmd = isWindows ? 'where' : 'which';
|
|
102
|
+
const result = cp.spawnSync(whichCmd, [shell.cmd], {
|
|
103
|
+
encoding: 'utf-8',
|
|
104
|
+
// On Windows, we need shell: true for 'where' to work
|
|
105
|
+
shell: isWindows,
|
|
106
|
+
});
|
|
107
|
+
if (result.status === 0 && result.stdout) {
|
|
108
|
+
const shellPath = result.stdout.trim().split('\n')[0]; // Take first result
|
|
109
|
+
trace(
|
|
110
|
+
'ShellDetection',
|
|
111
|
+
() => `Found shell in PATH: ${shell.cmd} => ${shellPath}`
|
|
112
|
+
);
|
|
113
|
+
cachedShell = { cmd: shell.cmd, args: shell.args };
|
|
114
|
+
return cachedShell;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (e) {
|
|
118
|
+
// Continue to next shell option
|
|
119
|
+
trace(
|
|
120
|
+
'ShellDetection',
|
|
121
|
+
() => `Failed to check shell ${shell.cmd}: ${e.message}`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Final fallback based on platform
|
|
127
|
+
if (isWindows) {
|
|
128
|
+
trace(
|
|
129
|
+
'ShellDetection',
|
|
130
|
+
() => 'WARNING: No shell found, using cmd.exe as fallback on Windows'
|
|
131
|
+
);
|
|
132
|
+
cachedShell = { cmd: 'cmd.exe', args: ['/c'] };
|
|
133
|
+
} else {
|
|
134
|
+
trace(
|
|
135
|
+
'ShellDetection',
|
|
136
|
+
() => 'WARNING: No shell found, using /bin/sh as fallback'
|
|
137
|
+
);
|
|
138
|
+
cachedShell = { cmd: '/bin/sh', args: ['-l', '-c'] };
|
|
139
|
+
}
|
|
140
|
+
return cachedShell;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Clear the shell cache (useful for testing)
|
|
145
|
+
*/
|
|
146
|
+
export function clearShellCache() {
|
|
147
|
+
cachedShell = null;
|
|
148
|
+
trace('ShellDetection', () => 'Shell cache cleared');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get the currently cached shell (if any)
|
|
153
|
+
* @returns {{ cmd: string, args: string[] } | null}
|
|
154
|
+
*/
|
|
155
|
+
export function getCachedShell() {
|
|
156
|
+
return cachedShell;
|
|
157
|
+
}
|