cli4ai 0.8.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/README.md +275 -0
- package/package.json +49 -0
- package/src/bin.ts +120 -0
- package/src/cli.ts +256 -0
- package/src/commands/add.ts +530 -0
- package/src/commands/browse.ts +449 -0
- package/src/commands/config.ts +126 -0
- package/src/commands/info.ts +102 -0
- package/src/commands/init.test.ts +163 -0
- package/src/commands/init.ts +560 -0
- package/src/commands/list.ts +89 -0
- package/src/commands/mcp-config.ts +59 -0
- package/src/commands/remove.ts +72 -0
- package/src/commands/routines.ts +393 -0
- package/src/commands/run.ts +45 -0
- package/src/commands/search.ts +148 -0
- package/src/commands/secrets.ts +273 -0
- package/src/commands/start.ts +40 -0
- package/src/commands/update.ts +218 -0
- package/src/core/config.test.ts +188 -0
- package/src/core/config.ts +649 -0
- package/src/core/execute.ts +507 -0
- package/src/core/link.test.ts +238 -0
- package/src/core/link.ts +190 -0
- package/src/core/lockfile.test.ts +337 -0
- package/src/core/lockfile.ts +308 -0
- package/src/core/manifest.test.ts +327 -0
- package/src/core/manifest.ts +319 -0
- package/src/core/routine-engine.test.ts +139 -0
- package/src/core/routine-engine.ts +725 -0
- package/src/core/routines.ts +111 -0
- package/src/core/secrets.test.ts +79 -0
- package/src/core/secrets.ts +430 -0
- package/src/lib/cli.ts +234 -0
- package/src/mcp/adapter.test.ts +132 -0
- package/src/mcp/adapter.ts +123 -0
- package/src/mcp/config-gen.test.ts +214 -0
- package/src/mcp/config-gen.ts +106 -0
- package/src/mcp/server.ts +363 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool execution helper used by `cli4ai run` and routines.
|
|
3
|
+
*
|
|
4
|
+
* This refactors the core execution logic so callers can either:
|
|
5
|
+
* - inherit stdio (interactive `cli4ai run`)
|
|
6
|
+
* - capture output (routines / orchestration)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn, spawnSync } from 'child_process';
|
|
10
|
+
import { resolve } from 'path';
|
|
11
|
+
import { existsSync, readFileSync } from 'fs';
|
|
12
|
+
import { createInterface } from 'readline';
|
|
13
|
+
import { platform } from 'os';
|
|
14
|
+
import { log } from '../lib/cli.js';
|
|
15
|
+
import { findPackage } from './config.js';
|
|
16
|
+
import { loadManifest, type Manifest } from './manifest.js';
|
|
17
|
+
import { getSecret } from './secrets.js';
|
|
18
|
+
import { checkPackageIntegrity } from './lockfile.js';
|
|
19
|
+
|
|
20
|
+
export type ExecuteCaptureMode = 'inherit' | 'pipe';
|
|
21
|
+
|
|
22
|
+
export interface ExecuteToolOptions {
|
|
23
|
+
packageName: string;
|
|
24
|
+
command?: string;
|
|
25
|
+
args: string[];
|
|
26
|
+
cwd: string;
|
|
27
|
+
env?: Record<string, string>;
|
|
28
|
+
stdin?: string;
|
|
29
|
+
capture: ExecuteCaptureMode;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
teeStderr?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ExecuteToolResult {
|
|
35
|
+
exitCode: number;
|
|
36
|
+
durationMs: number;
|
|
37
|
+
stdout?: string;
|
|
38
|
+
stderr?: string;
|
|
39
|
+
packagePath: string;
|
|
40
|
+
entryPath: string;
|
|
41
|
+
runtime: 'bun' | 'node';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class ExecuteToolError extends Error {
|
|
45
|
+
constructor(
|
|
46
|
+
public code: string,
|
|
47
|
+
message: string,
|
|
48
|
+
public details?: Record<string, unknown>
|
|
49
|
+
) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = 'ExecuteToolError';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Known system tools and how to install them
|
|
56
|
+
const INSTALL_COMMANDS: Record<string, { check: string; install: Record<string, string>; description: string }> = {
|
|
57
|
+
'yt-dlp': {
|
|
58
|
+
check: 'yt-dlp --version',
|
|
59
|
+
install: {
|
|
60
|
+
darwin: 'brew install yt-dlp',
|
|
61
|
+
linux: 'pipx install yt-dlp || sudo apt install -y pipx && pipx install yt-dlp',
|
|
62
|
+
win32: 'pip install yt-dlp'
|
|
63
|
+
},
|
|
64
|
+
description: 'YouTube video/audio downloader'
|
|
65
|
+
},
|
|
66
|
+
'gh': {
|
|
67
|
+
check: 'gh --version',
|
|
68
|
+
install: {
|
|
69
|
+
darwin: 'brew install gh',
|
|
70
|
+
linux: 'sudo apt install gh -y',
|
|
71
|
+
win32: 'winget install GitHub.cli'
|
|
72
|
+
},
|
|
73
|
+
description: 'GitHub CLI'
|
|
74
|
+
},
|
|
75
|
+
'ffmpeg': {
|
|
76
|
+
check: 'ffmpeg -version',
|
|
77
|
+
install: {
|
|
78
|
+
darwin: 'brew install ffmpeg',
|
|
79
|
+
linux: 'sudo apt install ffmpeg -y',
|
|
80
|
+
win32: 'winget install FFmpeg.FFmpeg'
|
|
81
|
+
},
|
|
82
|
+
description: 'Media processing tool'
|
|
83
|
+
},
|
|
84
|
+
'bun': {
|
|
85
|
+
check: 'bun --version',
|
|
86
|
+
install: {
|
|
87
|
+
darwin: 'curl -fsSL https://bun.sh/install | bash',
|
|
88
|
+
linux: 'curl -fsSL https://bun.sh/install | bash',
|
|
89
|
+
win32: 'powershell -c "irm bun.sh/install.ps1 | iex"'
|
|
90
|
+
},
|
|
91
|
+
description: 'JavaScript runtime'
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if a command exists on the system
|
|
97
|
+
*/
|
|
98
|
+
function commandExists(cmd: string): boolean {
|
|
99
|
+
// Use spawnSync with argument array to prevent command injection
|
|
100
|
+
const os = platform();
|
|
101
|
+
if (os === 'win32') {
|
|
102
|
+
const result = spawnSync('where', [cmd], { stdio: 'pipe' });
|
|
103
|
+
return result.status === 0;
|
|
104
|
+
}
|
|
105
|
+
const result = spawnSync('which', [cmd], { stdio: 'pipe' });
|
|
106
|
+
return result.status === 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Prompt user for confirmation
|
|
111
|
+
*/
|
|
112
|
+
async function confirm(message: string): Promise<boolean> {
|
|
113
|
+
const rl = createInterface({
|
|
114
|
+
input: process.stdin,
|
|
115
|
+
output: process.stderr
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
120
|
+
rl.close();
|
|
121
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Try to install a system dependency
|
|
128
|
+
*/
|
|
129
|
+
async function installDependency(name: string): Promise<boolean> {
|
|
130
|
+
const info = INSTALL_COMMANDS[name];
|
|
131
|
+
if (!info) {
|
|
132
|
+
log(`⚠️ Unknown dependency: ${name}`);
|
|
133
|
+
log(` Please install it manually`);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const os = platform();
|
|
138
|
+
const installCmd = info.install[os];
|
|
139
|
+
|
|
140
|
+
if (!installCmd) {
|
|
141
|
+
log(`⚠️ No install command for ${name} on ${os}`);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
log(`\n📦 ${name} - ${info.description}`);
|
|
146
|
+
log(` Install command: ${installCmd}\n`);
|
|
147
|
+
|
|
148
|
+
const shouldInstall = await confirm(`Install ${name}?`);
|
|
149
|
+
if (!shouldInstall) return false;
|
|
150
|
+
|
|
151
|
+
log(`\nInstalling ${name}...`);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
// Use spawnSync with shell:true for complex install commands (from trusted INSTALL_COMMANDS list)
|
|
155
|
+
// This is safe because installCmd comes from hardcoded list, not user input
|
|
156
|
+
const result = spawnSync(installCmd, {
|
|
157
|
+
stdio: 'inherit',
|
|
158
|
+
shell: true
|
|
159
|
+
});
|
|
160
|
+
if (result.status === 0) {
|
|
161
|
+
log(`✓ ${name} installed successfully\n`);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
log(`✗ Failed to install ${name}`);
|
|
165
|
+
log(` Try running manually: ${installCmd}\n`);
|
|
166
|
+
return false;
|
|
167
|
+
} catch {
|
|
168
|
+
log(`✗ Failed to install ${name}`);
|
|
169
|
+
log(` Try running manually: ${installCmd}\n`);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check and install missing peer dependencies
|
|
176
|
+
*/
|
|
177
|
+
async function checkPeerDependencies(pkgPath: string): Promise<void> {
|
|
178
|
+
// Try to load cli4ai.json for peer dependencies
|
|
179
|
+
const cli4aiPath = resolve(pkgPath, 'cli4ai.json');
|
|
180
|
+
let peerDeps: Record<string, string> = {};
|
|
181
|
+
|
|
182
|
+
if (existsSync(cli4aiPath)) {
|
|
183
|
+
try {
|
|
184
|
+
const cli4ai = JSON.parse(readFileSync(cli4aiPath, 'utf-8'));
|
|
185
|
+
peerDeps = cli4ai.peerDependencies || {};
|
|
186
|
+
} catch {}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const missing: string[] = [];
|
|
190
|
+
|
|
191
|
+
for (const dep of Object.keys(peerDeps)) {
|
|
192
|
+
// Skip npm packages (they start with @ or contain /)
|
|
193
|
+
if (dep.startsWith('@') || dep.includes('/')) continue;
|
|
194
|
+
|
|
195
|
+
if (!commandExists(dep)) {
|
|
196
|
+
missing.push(dep);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (missing.length === 0) return;
|
|
201
|
+
|
|
202
|
+
log(`\n⚠️ Missing system dependencies: ${missing.join(', ')}\n`);
|
|
203
|
+
|
|
204
|
+
for (const dep of missing) {
|
|
205
|
+
const installed = await installDependency(dep);
|
|
206
|
+
if (!installed) {
|
|
207
|
+
throw new ExecuteToolError('MISSING_DEPENDENCY', `Cannot run without ${dep}`, {
|
|
208
|
+
dependency: dep,
|
|
209
|
+
packagePath: pkgPath
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Prompt for a secret value
|
|
217
|
+
*/
|
|
218
|
+
async function promptSecret(key: string, description?: string): Promise<string> {
|
|
219
|
+
const rl = createInterface({
|
|
220
|
+
input: process.stdin,
|
|
221
|
+
output: process.stderr
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (description) {
|
|
225
|
+
log(` ${description}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return new Promise((resolve) => {
|
|
229
|
+
if (process.stdin.isTTY) {
|
|
230
|
+
// Hide input
|
|
231
|
+
process.stderr.write(` Enter ${key}: `);
|
|
232
|
+
let value = '';
|
|
233
|
+
|
|
234
|
+
process.stdin.setRawMode(true);
|
|
235
|
+
process.stdin.resume();
|
|
236
|
+
process.stdin.setEncoding('utf8');
|
|
237
|
+
|
|
238
|
+
const onData = (char: string) => {
|
|
239
|
+
if (char === '\n' || char === '\r') {
|
|
240
|
+
process.stdin.setRawMode(false);
|
|
241
|
+
process.stdin.removeListener('data', onData);
|
|
242
|
+
process.stderr.write('\n');
|
|
243
|
+
rl.close();
|
|
244
|
+
resolve(value);
|
|
245
|
+
} else if (char === '\u0003') {
|
|
246
|
+
process.exit(1);
|
|
247
|
+
} else if (char === '\u007F') {
|
|
248
|
+
if (value.length > 0) value = value.slice(0, -1);
|
|
249
|
+
} else {
|
|
250
|
+
value += char;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
process.stdin.on('data', onData);
|
|
255
|
+
} else {
|
|
256
|
+
rl.question(` Enter ${key}: `, (answer) => {
|
|
257
|
+
rl.close();
|
|
258
|
+
resolve(answer);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Check required secrets and prompt for missing ones
|
|
266
|
+
*/
|
|
267
|
+
async function checkAndPromptSecrets(pkgPath: string, pkgName: string): Promise<Record<string, string>> {
|
|
268
|
+
const cli4aiPath = resolve(pkgPath, 'cli4ai.json');
|
|
269
|
+
const secretsEnv: Record<string, string> = {};
|
|
270
|
+
|
|
271
|
+
if (!existsSync(cli4aiPath)) return secretsEnv;
|
|
272
|
+
|
|
273
|
+
let envDefs: Record<string, { required?: boolean; description?: string }> = {};
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const cli4ai = JSON.parse(readFileSync(cli4aiPath, 'utf-8'));
|
|
277
|
+
envDefs = cli4ai.env || {};
|
|
278
|
+
} catch {
|
|
279
|
+
return secretsEnv;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const missingRequired: Array<{ key: string; description?: string }> = [];
|
|
283
|
+
|
|
284
|
+
for (const [key, def] of Object.entries(envDefs)) {
|
|
285
|
+
const value = getSecret(key);
|
|
286
|
+
|
|
287
|
+
if (value) {
|
|
288
|
+
secretsEnv[key] = value;
|
|
289
|
+
} else if (def.required) {
|
|
290
|
+
missingRequired.push({ key, description: def.description });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (missingRequired.length === 0) return secretsEnv;
|
|
295
|
+
|
|
296
|
+
log(`\n⚠️ Missing required secrets for ${pkgName}:\n`);
|
|
297
|
+
|
|
298
|
+
// Import setSecret dynamically to avoid circular dependency issues
|
|
299
|
+
const { setSecret } = await import('./secrets.js');
|
|
300
|
+
|
|
301
|
+
for (const { key, description } of missingRequired) {
|
|
302
|
+
const value = await promptSecret(key, description);
|
|
303
|
+
|
|
304
|
+
if (!value) {
|
|
305
|
+
throw new ExecuteToolError('ENV_MISSING', `${key} is required to run ${pkgName}`, {
|
|
306
|
+
package: pkgName,
|
|
307
|
+
secret: key,
|
|
308
|
+
hint: `Set it with: cli4ai secrets set ${key}`
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
setSecret(key, value);
|
|
313
|
+
secretsEnv[key] = value;
|
|
314
|
+
log(` ✓ ${key} saved to vault\n`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
log('');
|
|
318
|
+
return secretsEnv;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function buildRuntimeCommand(entryPath: string, manifest: Manifest, cmdArgs: string[]): { execCmd: string; execArgs: string[]; runtime: 'bun' | 'node' } {
|
|
322
|
+
const runtime = manifest.runtime || 'bun';
|
|
323
|
+
switch (runtime) {
|
|
324
|
+
case 'node':
|
|
325
|
+
return { execCmd: 'node', execArgs: [entryPath, ...cmdArgs], runtime: 'node' };
|
|
326
|
+
case 'bun':
|
|
327
|
+
default:
|
|
328
|
+
return { execCmd: 'bun', execArgs: ['run', entryPath, ...cmdArgs], runtime: 'bun' };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function collectStream(stream: NodeJS.ReadableStream): Promise<string> {
|
|
333
|
+
return new Promise((resolve, reject) => {
|
|
334
|
+
const chunks: Buffer[] = [];
|
|
335
|
+
stream.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
336
|
+
stream.on('error', reject);
|
|
337
|
+
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Execute a tool command.
|
|
343
|
+
*
|
|
344
|
+
* - When capture === 'inherit', the child inherits stdio (interactive).
|
|
345
|
+
* - When capture === 'pipe', stdout/stderr can be captured and returned.
|
|
346
|
+
*/
|
|
347
|
+
export async function executeTool(options: ExecuteToolOptions): Promise<ExecuteToolResult> {
|
|
348
|
+
const startTime = Date.now();
|
|
349
|
+
const invocationDir = options.cwd;
|
|
350
|
+
|
|
351
|
+
const pkg = findPackage(options.packageName, invocationDir);
|
|
352
|
+
if (!pkg) {
|
|
353
|
+
throw new ExecuteToolError('NOT_FOUND', `Package not found: ${options.packageName}`, {
|
|
354
|
+
hint: 'Run "cli4ai list" to see installed packages, or "cli4ai add <package>" to install'
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const manifest = loadManifest(pkg.path);
|
|
359
|
+
|
|
360
|
+
const integrityCheck = checkPackageIntegrity(invocationDir, options.packageName, pkg.path);
|
|
361
|
+
if (!integrityCheck.valid) {
|
|
362
|
+
log(`\n⚠️ SECURITY WARNING: Package integrity check failed for ${options.packageName}`);
|
|
363
|
+
if (integrityCheck.error) log(` ${integrityCheck.error}`);
|
|
364
|
+
if (integrityCheck.expected && integrityCheck.actual) {
|
|
365
|
+
log(` Expected: ${integrityCheck.expected.slice(0, 30)}...`);
|
|
366
|
+
log(` Actual: ${integrityCheck.actual.slice(0, 30)}...`);
|
|
367
|
+
}
|
|
368
|
+
log(`\n The package may have been tampered with or modified since installation.`);
|
|
369
|
+
log(` To fix: reinstall with "cli4ai remove ${options.packageName} && cli4ai add ${options.packageName}"\n`);
|
|
370
|
+
|
|
371
|
+
const shouldContinue = await confirm('Continue anyway? (NOT RECOMMENDED)');
|
|
372
|
+
if (!shouldContinue) {
|
|
373
|
+
throw new ExecuteToolError('INTEGRITY_ERROR', 'Package integrity verification failed', {
|
|
374
|
+
package: options.packageName,
|
|
375
|
+
hint: `Reinstall the package with: cli4ai remove ${options.packageName} && cli4ai add ${options.packageName}`
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
log('');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!commandExists('bun')) {
|
|
382
|
+
log('⚠️ bun is required to run cli4ai tools\n');
|
|
383
|
+
const installed = await installDependency('bun');
|
|
384
|
+
if (!installed) {
|
|
385
|
+
throw new ExecuteToolError('MISSING_DEPENDENCY', 'bun is required', {
|
|
386
|
+
hint: 'Install bun: curl -fsSL https://bun.sh/install | bash'
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
await checkPeerDependencies(pkg.path);
|
|
392
|
+
|
|
393
|
+
const secretsEnv = await checkAndPromptSecrets(pkg.path, options.packageName);
|
|
394
|
+
|
|
395
|
+
const entryPath = resolve(pkg.path, manifest.entry);
|
|
396
|
+
if (!existsSync(entryPath)) {
|
|
397
|
+
throw new ExecuteToolError('NOT_FOUND', `Entry point not found: ${entryPath}`, {
|
|
398
|
+
manifest: resolve(pkg.path, 'cli4ai.json')
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const cmdArgs: string[] = [];
|
|
403
|
+
if (options.command) cmdArgs.push(options.command);
|
|
404
|
+
cmdArgs.push(...options.args);
|
|
405
|
+
|
|
406
|
+
const { execCmd, execArgs, runtime } = buildRuntimeCommand(entryPath, manifest, cmdArgs);
|
|
407
|
+
|
|
408
|
+
const teeStderr = options.teeStderr ?? true;
|
|
409
|
+
|
|
410
|
+
if (options.capture === 'inherit') {
|
|
411
|
+
const proc = spawn(execCmd, execArgs, {
|
|
412
|
+
stdio: 'inherit',
|
|
413
|
+
cwd: invocationDir,
|
|
414
|
+
env: {
|
|
415
|
+
...process.env,
|
|
416
|
+
INIT_CWD: process.env.INIT_CWD ?? invocationDir,
|
|
417
|
+
CLI4AI_CWD: invocationDir,
|
|
418
|
+
C4AI_PACKAGE_DIR: pkg.path,
|
|
419
|
+
C4AI_PACKAGE_NAME: pkg.name,
|
|
420
|
+
C4AI_ENTRY: entryPath,
|
|
421
|
+
...secretsEnv,
|
|
422
|
+
...(options.env ?? {})
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const exitCode = await new Promise<number>((resolve, reject) => {
|
|
427
|
+
proc.on('close', (code) => resolve(code ?? 0));
|
|
428
|
+
proc.on('error', (err) => {
|
|
429
|
+
reject(new ExecuteToolError('API_ERROR', `Failed to execute: ${err.message}`));
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
exitCode,
|
|
435
|
+
durationMs: Date.now() - startTime,
|
|
436
|
+
packagePath: pkg.path,
|
|
437
|
+
entryPath,
|
|
438
|
+
runtime
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// capture === 'pipe'
|
|
443
|
+
const proc = spawn(execCmd, execArgs, {
|
|
444
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
445
|
+
cwd: invocationDir,
|
|
446
|
+
env: {
|
|
447
|
+
...process.env,
|
|
448
|
+
INIT_CWD: process.env.INIT_CWD ?? invocationDir,
|
|
449
|
+
CLI4AI_CWD: invocationDir,
|
|
450
|
+
C4AI_PACKAGE_DIR: pkg.path,
|
|
451
|
+
C4AI_PACKAGE_NAME: pkg.name,
|
|
452
|
+
C4AI_ENTRY: entryPath,
|
|
453
|
+
...secretsEnv,
|
|
454
|
+
...(options.env ?? {})
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
if (options.stdin !== undefined) {
|
|
459
|
+
proc.stdin.write(options.stdin);
|
|
460
|
+
proc.stdin.end();
|
|
461
|
+
} else {
|
|
462
|
+
// Don’t block on stdin if nothing is provided
|
|
463
|
+
proc.stdin.end();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (teeStderr && proc.stderr) {
|
|
467
|
+
proc.stderr.on('data', (chunk) => {
|
|
468
|
+
process.stderr.write(chunk);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const stdoutPromise = proc.stdout ? collectStream(proc.stdout) : Promise.resolve('');
|
|
473
|
+
const stderrPromise = proc.stderr ? collectStream(proc.stderr) : Promise.resolve('');
|
|
474
|
+
|
|
475
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
476
|
+
if (options.timeoutMs && options.timeoutMs > 0) {
|
|
477
|
+
timeout = setTimeout(() => {
|
|
478
|
+
try {
|
|
479
|
+
proc.kill('SIGTERM');
|
|
480
|
+
} catch {}
|
|
481
|
+
setTimeout(() => {
|
|
482
|
+
try {
|
|
483
|
+
proc.kill('SIGKILL');
|
|
484
|
+
} catch {}
|
|
485
|
+
}, 250);
|
|
486
|
+
}, options.timeoutMs);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const exitCode = await new Promise<number>((resolve, reject) => {
|
|
490
|
+
proc.on('close', (code) => resolve(code ?? 0));
|
|
491
|
+
proc.on('error', (err) => reject(new ExecuteToolError('API_ERROR', `Failed to execute: ${err.message}`)));
|
|
492
|
+
}).finally(() => {
|
|
493
|
+
if (timeout) clearTimeout(timeout);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
exitCode,
|
|
500
|
+
durationMs: Date.now() - startTime,
|
|
501
|
+
stdout,
|
|
502
|
+
stderr,
|
|
503
|
+
packagePath: pkg.path,
|
|
504
|
+
entryPath,
|
|
505
|
+
runtime
|
|
506
|
+
};
|
|
507
|
+
}
|