-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathshell.ts
More file actions
173 lines (158 loc) · 5.61 KB
/
Copy pathshell.ts
File metadata and controls
173 lines (158 loc) · 5.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import { execSync, execFileSync, exec, execFile, spawn } from 'node:child_process';
/**
* Escape a value for safe interpolation inside a single-quoted shell string.
* Works by ending the current single-quote, inserting an escaped single-quote,
* and re-opening the single-quote: "it's" → 'it'\''s'
*/
export function sq(value: string): string {
return "'" + value.replace(/'/g, "'\\''") + "'";
}
// ---- Test interception ----
type CommandInterceptor = (
args: string[],
opts?: { cwd?: string; input?: string },
) => { intercepted: true; result: string } | { intercepted: true; error: string } | { intercepted: false };
let _interceptor: CommandInterceptor | null = null;
/** @internal Install a command interceptor for testing. Returns a cleanup function. */
export function _setInterceptor(fn: CommandInterceptor | null): void {
_interceptor = fn;
}
function checkIntercept(args: string[], opts?: { cwd?: string; input?: string }) {
if (!_interceptor) return null;
return _interceptor(args, opts);
}
// ---- String-based commands (for static/trusted command strings only) ----
export function run(cmd: string, opts?: { cwd?: string; input?: string }): string {
const result = checkIntercept(cmd.split(/\s+/), opts);
if (result?.intercepted) {
if ('error' in result) throw new Error(result.error);
return result.result;
}
return execSync(cmd, {
cwd: opts?.cwd,
input: opts?.input,
encoding: 'utf-8',
stdio: [opts?.input ? 'pipe' : 'pipe', 'pipe', 'pipe'],
}).trim();
}
export function runAsync(cmd: string, opts?: { cwd?: string; input?: string }): Promise<string> {
const result = checkIntercept(cmd.split(/\s+/), opts);
if (result?.intercepted) {
if ('error' in result) return Promise.reject(new Error(result.error));
return Promise.resolve(result.result);
}
return new Promise((resolve, reject) => {
const child = exec(cmd, { cwd: opts?.cwd, encoding: 'utf-8' }, (err, stdout, stderr) => {
if (err) {
reject(new Error(`Command failed: ${cmd}\n${stdout}\n${stderr}`.trim()));
} else {
resolve(stdout.trim());
}
});
if (opts?.input) {
child.stdin?.write(opts.input);
child.stdin?.end();
}
});
}
/**
* Run a shell command, streaming its stdout/stderr to the parent process live
* (so output appears in CI logs as it happens) while also capturing it so the
* thrown error on failure includes the child's real output.
*
* Use this for user-defined commands (build/publish) where we don't need to
* parse the output but DO want it visible — unlike the capturing `runAsync`,
* which buffers everything and is meant for internal helpers whose stdout we
* parse. A failing custom command (e.g. `vsce publish`) writes its real error
* to stdout; this surfaces both streams instead of swallowing them.
*/
export function runStreaming(cmd: string, opts?: { cwd?: string; input?: string }): Promise<void> {
const result = checkIntercept(cmd.split(/\s+/), opts);
if (result?.intercepted) {
if ('error' in result) return Promise.reject(new Error(result.error));
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const child = spawn(cmd, {
cwd: opts?.cwd,
shell: true,
stdio: [opts?.input ? 'pipe' : 'inherit', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout?.on('data', (chunk) => {
stdout += chunk;
process.stdout.write(chunk);
});
child.stderr?.on('data', (chunk) => {
stderr += chunk;
process.stderr.write(chunk);
});
if (opts?.input) {
child.stdin?.write(opts.input);
child.stdin?.end();
}
child.on('error', (err) => reject(err));
child.on('close', (code) => {
if (code === 0) {
resolve();
} else {
const detail = `${stdout}\n${stderr}`.trim();
reject(new Error(`Command failed (exit code ${code}): ${cmd}${detail ? `\n${detail}` : ''}`));
}
});
});
}
export function tryRun(cmd: string, opts?: { cwd?: string }): string | null {
try {
return run(cmd, opts);
} catch {
return null;
}
}
// ---- Array-based commands (shell-injection safe) ----
/** Run a command with an argument array — bypasses the shell entirely */
export function runArgs(args: string[], opts?: { cwd?: string; input?: string }): string {
const result = checkIntercept(args, opts);
if (result?.intercepted) {
if ('error' in result) throw new Error(result.error);
return result.result;
}
const [cmd, ...rest] = args;
return execFileSync(cmd!, rest, {
cwd: opts?.cwd,
input: opts?.input,
encoding: 'utf-8',
stdio: [opts?.input ? 'pipe' : 'pipe', 'pipe', 'pipe'],
}).trim();
}
/** Async version of runArgs */
export function runArgsAsync(args: string[], opts?: { cwd?: string; input?: string }): Promise<string> {
const result = checkIntercept(args, opts);
if (result?.intercepted) {
if ('error' in result) return Promise.reject(new Error(result.error));
return Promise.resolve(result.result);
}
const [cmd, ...rest] = args;
return new Promise((resolve, reject) => {
const child = execFile(cmd!, rest, { cwd: opts?.cwd, encoding: 'utf-8' }, (err, stdout, stderr) => {
if (err) {
reject(new Error(`Command failed: ${args.join(' ')}\n${stdout}\n${stderr}`.trim()));
} else {
resolve(stdout.trim());
}
});
if (opts?.input) {
child.stdin?.write(opts.input);
child.stdin?.end();
}
});
}
/** tryRun equivalent for argument arrays */
export function tryRunArgs(args: string[], opts?: { cwd?: string }): string | null {
try {
return runArgs(args, opts);
} catch {
return null;
}
}