cli4ai 1.2.0 β†’ 1.2.1

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.
Files changed (113) hide show
  1. package/README.md +39 -0
  2. package/dist/bin.d.ts +6 -0
  3. package/dist/bin.js +105 -0
  4. package/dist/cli.d.ts +5 -0
  5. package/dist/cli.js +335 -0
  6. package/dist/commands/add.d.ts +11 -0
  7. package/dist/commands/add.js +459 -0
  8. package/dist/commands/browse.d.ts +4 -0
  9. package/dist/commands/browse.js +379 -0
  10. package/dist/commands/config.d.ts +10 -0
  11. package/dist/commands/config.js +121 -0
  12. package/dist/commands/info.d.ts +9 -0
  13. package/dist/commands/info.js +122 -0
  14. package/dist/commands/init.d.ts +10 -0
  15. package/dist/commands/init.js +458 -0
  16. package/dist/commands/list.d.ts +10 -0
  17. package/dist/commands/list.js +76 -0
  18. package/dist/commands/mcp-config.d.ts +10 -0
  19. package/dist/commands/mcp-config.js +49 -0
  20. package/dist/commands/remotes.d.ts +22 -0
  21. package/dist/commands/remotes.js +196 -0
  22. package/dist/commands/remove.d.ts +8 -0
  23. package/dist/commands/remove.js +61 -0
  24. package/dist/commands/routines.d.ts +29 -0
  25. package/dist/commands/routines.js +363 -0
  26. package/dist/commands/run.d.ts +12 -0
  27. package/dist/commands/run.js +104 -0
  28. package/dist/commands/scheduler.d.ts +27 -0
  29. package/dist/commands/scheduler.js +350 -0
  30. package/dist/commands/search.d.ts +9 -0
  31. package/dist/commands/search.js +159 -0
  32. package/dist/commands/secrets.d.ts +28 -0
  33. package/dist/commands/secrets.js +236 -0
  34. package/dist/commands/serve.d.ts +13 -0
  35. package/dist/commands/serve.js +49 -0
  36. package/dist/commands/start.d.ts +8 -0
  37. package/dist/commands/start.js +27 -0
  38. package/dist/commands/update.d.ts +17 -0
  39. package/dist/commands/update.js +210 -0
  40. package/dist/core/config.d.ts +91 -0
  41. package/dist/core/config.js +738 -0
  42. package/dist/core/execute.d.ts +51 -0
  43. package/dist/core/execute.js +475 -0
  44. package/dist/core/link.d.ts +39 -0
  45. package/dist/core/link.js +214 -0
  46. package/dist/core/lockfile.d.ts +63 -0
  47. package/dist/core/lockfile.js +140 -0
  48. package/dist/core/manifest.d.ts +96 -0
  49. package/dist/core/manifest.js +224 -0
  50. package/dist/core/registry.d.ts +74 -0
  51. package/dist/core/registry.js +116 -0
  52. package/dist/core/remote-client.d.ts +98 -0
  53. package/dist/core/remote-client.js +252 -0
  54. package/dist/core/remotes.d.ts +88 -0
  55. package/dist/core/remotes.js +206 -0
  56. package/dist/core/routine-engine.d.ts +124 -0
  57. package/dist/core/routine-engine.js +699 -0
  58. package/dist/core/routines.d.ts +36 -0
  59. package/dist/core/routines.js +132 -0
  60. package/dist/core/scheduler-daemon.d.ts +10 -0
  61. package/dist/core/scheduler-daemon.js +77 -0
  62. package/dist/core/scheduler.d.ts +131 -0
  63. package/dist/core/scheduler.js +492 -0
  64. package/dist/core/secrets.d.ts +48 -0
  65. package/dist/core/secrets.js +384 -0
  66. package/dist/lib/cli.d.ts +84 -0
  67. package/dist/lib/cli.js +216 -0
  68. package/dist/mcp/adapter.d.ts +35 -0
  69. package/dist/mcp/adapter.js +94 -0
  70. package/dist/mcp/config-gen.d.ts +31 -0
  71. package/dist/mcp/config-gen.js +75 -0
  72. package/dist/mcp/server.d.ts +41 -0
  73. package/dist/mcp/server.js +296 -0
  74. package/dist/server/service.d.ts +85 -0
  75. package/dist/server/service.js +304 -0
  76. package/package.json +6 -3
  77. package/src/bin.ts +0 -118
  78. package/src/cli.ts +0 -412
  79. package/src/commands/add.ts +0 -562
  80. package/src/commands/browse.ts +0 -449
  81. package/src/commands/config.ts +0 -154
  82. package/src/commands/info.ts +0 -133
  83. package/src/commands/init.ts +0 -514
  84. package/src/commands/list.ts +0 -95
  85. package/src/commands/mcp-config.ts +0 -69
  86. package/src/commands/remotes.ts +0 -253
  87. package/src/commands/remove.ts +0 -78
  88. package/src/commands/routines.ts +0 -427
  89. package/src/commands/run.ts +0 -127
  90. package/src/commands/scheduler.ts +0 -438
  91. package/src/commands/search.ts +0 -185
  92. package/src/commands/secrets.ts +0 -292
  93. package/src/commands/serve.ts +0 -66
  94. package/src/commands/start.ts +0 -40
  95. package/src/commands/update.ts +0 -252
  96. package/src/core/config.ts +0 -845
  97. package/src/core/execute.ts +0 -569
  98. package/src/core/link.ts +0 -246
  99. package/src/core/lockfile.ts +0 -187
  100. package/src/core/manifest.ts +0 -327
  101. package/src/core/registry.ts +0 -165
  102. package/src/core/remote-client.ts +0 -419
  103. package/src/core/remotes.ts +0 -268
  104. package/src/core/routine-engine.ts +0 -895
  105. package/src/core/routines.ts +0 -171
  106. package/src/core/scheduler-daemon.ts +0 -94
  107. package/src/core/scheduler.ts +0 -606
  108. package/src/core/secrets.ts +0 -430
  109. package/src/lib/cli.ts +0 -261
  110. package/src/mcp/adapter.ts +0 -131
  111. package/src/mcp/config-gen.ts +0 -106
  112. package/src/mcp/server.ts +0 -365
  113. package/src/server/service.ts +0 -434
@@ -1,569 +0,0 @@
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, homedir } 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
-
19
- /**
20
- * Expand ~ to home directory in paths
21
- */
22
- function expandTilde(path: string): string {
23
- if (path.startsWith('~/')) {
24
- return resolve(homedir(), path.slice(2));
25
- }
26
- if (path === '~') {
27
- return homedir();
28
- }
29
- return path;
30
- }
31
-
32
- export type ExecuteCaptureMode = 'inherit' | 'pipe';
33
-
34
- /**
35
- * Permission scope levels for tool execution
36
- * - read: Only allow read operations (no mutations)
37
- * - write: Allow write operations but no destructive actions
38
- * - full: Full access (default)
39
- */
40
- export type ScopeLevel = 'read' | 'write' | 'full';
41
-
42
- export interface ExecuteToolOptions {
43
- packageName: string;
44
- command?: string;
45
- args: string[];
46
- cwd: string;
47
- env?: Record<string, string>;
48
- stdin?: string;
49
- capture: ExecuteCaptureMode;
50
- timeoutMs?: number;
51
- teeStderr?: boolean;
52
- /** Permission scope for the tool */
53
- scope?: ScopeLevel;
54
- /** Run in sandboxed environment with restricted file system access */
55
- sandbox?: boolean;
56
- }
57
-
58
- export interface ExecuteToolResult {
59
- exitCode: number;
60
- durationMs: number;
61
- stdout?: string;
62
- stderr?: string;
63
- packagePath: string;
64
- entryPath: string;
65
- runtime: 'node';
66
- }
67
-
68
- export class ExecuteToolError extends Error {
69
- constructor(
70
- public code: string,
71
- message: string,
72
- public details?: Record<string, unknown>
73
- ) {
74
- super(message);
75
- this.name = 'ExecuteToolError';
76
- }
77
- }
78
-
79
- // Known system tools and how to install them
80
- const INSTALL_COMMANDS: Record<string, { check: string; install: Record<string, string>; description: string }> = {
81
- 'yt-dlp': {
82
- check: 'yt-dlp --version',
83
- install: {
84
- darwin: 'brew install yt-dlp',
85
- linux: 'pipx install yt-dlp || sudo apt install -y pipx && pipx install yt-dlp',
86
- win32: 'pip install yt-dlp'
87
- },
88
- description: 'YouTube video/audio downloader'
89
- },
90
- 'gh': {
91
- check: 'gh --version',
92
- install: {
93
- darwin: 'brew install gh',
94
- linux: 'sudo apt install gh -y',
95
- win32: 'winget install GitHub.cli'
96
- },
97
- description: 'GitHub CLI'
98
- },
99
- 'ffmpeg': {
100
- check: 'ffmpeg -version',
101
- install: {
102
- darwin: 'brew install ffmpeg',
103
- linux: 'sudo apt install ffmpeg -y',
104
- win32: 'winget install FFmpeg.FFmpeg'
105
- },
106
- description: 'Media processing tool'
107
- },
108
- 'node': {
109
- check: 'node --version',
110
- install: {
111
- darwin: 'brew install node',
112
- linux: 'curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt-get install -y nodejs',
113
- win32: 'winget install OpenJS.NodeJS.LTS'
114
- },
115
- description: 'JavaScript runtime'
116
- }
117
- };
118
-
119
- /**
120
- * Check if a command exists on the system
121
- */
122
- function commandExists(cmd: string): boolean {
123
- // Use spawnSync with argument array to prevent command injection
124
- const os = platform();
125
- if (os === 'win32') {
126
- const result = spawnSync('where', [cmd], { stdio: 'pipe' });
127
- return result.status === 0;
128
- }
129
- const result = spawnSync('which', [cmd], { stdio: 'pipe' });
130
- return result.status === 0;
131
- }
132
-
133
- /**
134
- * Prompt user for confirmation
135
- */
136
- async function confirm(message: string): Promise<boolean> {
137
- const rl = createInterface({
138
- input: process.stdin,
139
- output: process.stderr
140
- });
141
-
142
- return new Promise((resolve) => {
143
- rl.question(`${message} [y/N] `, (answer) => {
144
- rl.close();
145
- resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
146
- });
147
- });
148
- }
149
-
150
- /**
151
- * Try to install a system dependency
152
- */
153
- async function installDependency(name: string): Promise<boolean> {
154
- const info = INSTALL_COMMANDS[name];
155
- if (!info) {
156
- log(`⚠️ Unknown dependency: ${name}`);
157
- log(` Please install it manually`);
158
- return false;
159
- }
160
-
161
- const os = platform();
162
- const installCmd = info.install[os];
163
-
164
- if (!installCmd) {
165
- log(`⚠️ No install command for ${name} on ${os}`);
166
- return false;
167
- }
168
-
169
- log(`\nπŸ“¦ ${name} - ${info.description}`);
170
- log(` Install command: ${installCmd}\n`);
171
-
172
- // SECURITY: Warn about curl|bash pattern
173
- if (installCmd.includes('curl') && (installCmd.includes('| bash') || installCmd.includes('|bash'))) {
174
- log(`⚠️ SECURITY WARNING: This command downloads and executes a script from the internet.`);
175
- log(` Only proceed if you trust the source (${name}).\n`);
176
- }
177
-
178
- const shouldInstall = await confirm(`Install ${name}?`);
179
- if (!shouldInstall) return false;
180
-
181
- log(`\nInstalling ${name}...`);
182
-
183
- try {
184
- // Use spawnSync with shell:true for complex install commands (from trusted INSTALL_COMMANDS list)
185
- // This is safe because installCmd comes from hardcoded list, not user input
186
- const result = spawnSync(installCmd, {
187
- stdio: 'inherit',
188
- shell: true
189
- });
190
- if (result.status === 0) {
191
- log(`βœ“ ${name} installed successfully\n`);
192
- return true;
193
- }
194
- log(`βœ— Failed to install ${name}`);
195
- log(` Try running manually: ${installCmd}\n`);
196
- return false;
197
- } catch {
198
- log(`βœ— Failed to install ${name}`);
199
- log(` Try running manually: ${installCmd}\n`);
200
- return false;
201
- }
202
- }
203
-
204
- /**
205
- * Check and install missing peer dependencies
206
- */
207
- async function checkPeerDependencies(pkgPath: string): Promise<void> {
208
- // Try to load cli4ai.json for peer dependencies
209
- const cli4aiPath = resolve(pkgPath, 'cli4ai.json');
210
- let peerDeps: Record<string, string> = {};
211
-
212
- if (existsSync(cli4aiPath)) {
213
- try {
214
- const cli4ai = JSON.parse(readFileSync(cli4aiPath, 'utf-8'));
215
- peerDeps = cli4ai.peerDependencies || {};
216
- } catch {}
217
- }
218
-
219
- const missing: string[] = [];
220
-
221
- for (const dep of Object.keys(peerDeps)) {
222
- // Skip npm packages (they start with @ or contain /)
223
- if (dep.startsWith('@') || dep.includes('/')) continue;
224
-
225
- if (!commandExists(dep)) {
226
- missing.push(dep);
227
- }
228
- }
229
-
230
- if (missing.length === 0) return;
231
-
232
- log(`\n⚠️ Missing system dependencies: ${missing.join(', ')}\n`);
233
-
234
- for (const dep of missing) {
235
- const installed = await installDependency(dep);
236
- if (!installed) {
237
- throw new ExecuteToolError('MISSING_DEPENDENCY', `Cannot run without ${dep}`, {
238
- dependency: dep,
239
- packagePath: pkgPath
240
- });
241
- }
242
- }
243
- }
244
-
245
- /**
246
- * Prompt for a secret value
247
- */
248
- async function promptSecret(key: string, description?: string): Promise<string> {
249
- const rl = createInterface({
250
- input: process.stdin,
251
- output: process.stderr
252
- });
253
-
254
- if (description) {
255
- log(` ${description}`);
256
- }
257
-
258
- return new Promise((resolve) => {
259
- if (process.stdin.isTTY) {
260
- // Hide input
261
- process.stderr.write(` Enter ${key}: `);
262
- let value = '';
263
-
264
- process.stdin.setRawMode(true);
265
- process.stdin.resume();
266
- process.stdin.setEncoding('utf8');
267
-
268
- const onData = (char: string) => {
269
- if (char === '\n' || char === '\r') {
270
- process.stdin.setRawMode(false);
271
- process.stdin.removeListener('data', onData);
272
- process.stderr.write('\n');
273
- rl.close();
274
- resolve(value);
275
- } else if (char === '\u0003') {
276
- process.exit(1);
277
- } else if (char === '\u007F') {
278
- if (value.length > 0) value = value.slice(0, -1);
279
- } else {
280
- value += char;
281
- }
282
- };
283
-
284
- process.stdin.on('data', onData);
285
- } else {
286
- rl.question(` Enter ${key}: `, (answer) => {
287
- rl.close();
288
- resolve(answer);
289
- });
290
- }
291
- });
292
- }
293
-
294
- /**
295
- * Check required secrets and prompt for missing ones
296
- */
297
- async function checkAndPromptSecrets(pkgPath: string, pkgName: string): Promise<Record<string, string>> {
298
- const cli4aiPath = resolve(pkgPath, 'cli4ai.json');
299
- const secretsEnv: Record<string, string> = {};
300
-
301
- if (!existsSync(cli4aiPath)) return secretsEnv;
302
-
303
- let envDefs: Record<string, { required?: boolean; description?: string }> = {};
304
-
305
- try {
306
- const cli4ai = JSON.parse(readFileSync(cli4aiPath, 'utf-8'));
307
- envDefs = cli4ai.env || {};
308
- } catch {
309
- return secretsEnv;
310
- }
311
-
312
- const missingRequired: Array<{ key: string; description?: string }> = [];
313
-
314
- for (const [key, def] of Object.entries(envDefs)) {
315
- // SECURITY: Use package-scoped secret lookup (tries scoped first, then global)
316
- const value = getSecret(key, pkgName);
317
-
318
- if (value) {
319
- secretsEnv[key] = value;
320
- } else if (def.required) {
321
- missingRequired.push({ key, description: def.description });
322
- }
323
- }
324
-
325
- if (missingRequired.length === 0) return secretsEnv;
326
-
327
- log(`\n⚠️ Missing required secrets for ${pkgName}:\n`);
328
-
329
- // Import setSecret dynamically to avoid circular dependency issues
330
- const { setSecret } = await import('./secrets.js');
331
-
332
- for (const { key, description } of missingRequired) {
333
- const value = await promptSecret(key, description);
334
-
335
- if (!value) {
336
- throw new ExecuteToolError('ENV_MISSING', `${key} is required to run ${pkgName}`, {
337
- package: pkgName,
338
- secret: key,
339
- hint: `Set it with: cli4ai secrets set ${key} --scope ${pkgName}`
340
- });
341
- }
342
-
343
- // Expand ~ to home directory for paths
344
- const expandedValue = expandTilde(value);
345
- // SECURITY: Store secret scoped to package
346
- setSecret(key, expandedValue, pkgName);
347
- secretsEnv[key] = expandedValue;
348
- log(` βœ“ ${key} saved to vault (scoped to ${pkgName})\n`);
349
- }
350
-
351
- log('');
352
- return secretsEnv;
353
- }
354
-
355
- function buildRuntimeCommand(entryPath: string, cmdArgs: string[]): { execCmd: string; execArgs: string[]; runtime: 'node' } {
356
- // Use tsx for TypeScript files, node for JavaScript
357
- if (entryPath.endsWith('.ts') || entryPath.endsWith('.tsx')) {
358
- return { execCmd: 'npx', execArgs: ['tsx', entryPath, ...cmdArgs], runtime: 'node' };
359
- }
360
- return { execCmd: 'node', execArgs: [entryPath, ...cmdArgs], runtime: 'node' };
361
- }
362
-
363
- /**
364
- * Build security environment variables for scope and sandbox restrictions
365
- */
366
- function buildSecurityEnv(
367
- scope: ScopeLevel,
368
- sandbox: boolean,
369
- cwd: string
370
- ): Record<string, string> {
371
- const env: Record<string, string> = {};
372
-
373
- // Set scope environment variable for tools to respect
374
- env.CLI4AI_SCOPE = scope;
375
-
376
- // Sandbox restrictions
377
- if (sandbox) {
378
- env.CLI4AI_SANDBOX = '1';
379
-
380
- // Restrict file system access to temp directories and package directory
381
- // Tools should check these env vars and restrict their operations
382
- const tmpDir = process.env.TMPDIR || process.env.TMP || process.env.TEMP || '/tmp';
383
- env.CLI4AI_SANDBOX_ALLOWED_PATHS = [
384
- tmpDir,
385
- cwd, // Allow access to current working directory
386
- ].join(':');
387
-
388
- // Restrict network access in sandbox mode
389
- // Tools should check this and limit network operations
390
- env.CLI4AI_SANDBOX_NETWORK = 'restricted';
391
- }
392
-
393
- return env;
394
- }
395
-
396
- async function ensureRuntimeAvailable(): Promise<void> {
397
- if (!commandExists('node')) {
398
- log('⚠️ Node.js is required to run this tool\n');
399
- const installed = await installDependency('node');
400
- if (!installed) {
401
- throw new ExecuteToolError('MISSING_DEPENDENCY', 'Node.js is required', {
402
- hint: 'Install Node.js: https://nodejs.org/en/download/'
403
- });
404
- }
405
- }
406
- }
407
-
408
- function collectStream(stream: NodeJS.ReadableStream): Promise<string> {
409
- return new Promise((resolve, reject) => {
410
- const chunks: Buffer[] = [];
411
- stream.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
412
- stream.on('error', reject);
413
- stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
414
- });
415
- }
416
-
417
- /**
418
- * Execute a tool command.
419
- *
420
- * - When capture === 'inherit', the child inherits stdio (interactive).
421
- * - When capture === 'pipe', stdout/stderr can be captured and returned.
422
- */
423
- export async function executeTool(options: ExecuteToolOptions): Promise<ExecuteToolResult> {
424
- const startTime = Date.now();
425
- const invocationDir = options.cwd;
426
-
427
- const pkg = findPackage(options.packageName, invocationDir);
428
- if (!pkg) {
429
- throw new ExecuteToolError('NOT_FOUND', `Package not found: ${options.packageName}`, {
430
- hint: 'Run "cli4ai list" to see installed packages, or "cli4ai add <package>" to install'
431
- });
432
- }
433
-
434
- const manifest = loadManifest(pkg.path);
435
-
436
- await ensureRuntimeAvailable();
437
-
438
- await checkPeerDependencies(pkg.path);
439
-
440
- const secretsEnv = await checkAndPromptSecrets(pkg.path, options.packageName);
441
-
442
- const entryPath = resolve(pkg.path, manifest.entry);
443
- if (!existsSync(entryPath)) {
444
- throw new ExecuteToolError('NOT_FOUND', `Entry point not found: ${entryPath}`, {
445
- manifest: resolve(pkg.path, 'cli4ai.json')
446
- });
447
- }
448
-
449
- const cmdArgs: string[] = [];
450
- if (options.command) cmdArgs.push(options.command);
451
- cmdArgs.push(...options.args);
452
-
453
- const { execCmd, execArgs, runtime } = buildRuntimeCommand(entryPath, cmdArgs);
454
-
455
- const teeStderr = options.teeStderr ?? true;
456
-
457
- // Build security environment for scope and sandbox
458
- const scope = options.scope ?? 'full';
459
- const sandbox = options.sandbox ?? false;
460
- const securityEnv = buildSecurityEnv(scope, sandbox, invocationDir);
461
-
462
- // Log security restrictions if active
463
- if (scope !== 'full' || sandbox) {
464
- const restrictions: string[] = [];
465
- if (scope !== 'full') restrictions.push(`scope=${scope}`);
466
- if (sandbox) restrictions.push('sandbox=enabled');
467
- log(`πŸ”’ Security: ${restrictions.join(', ')}`);
468
- }
469
-
470
- if (options.capture === 'inherit') {
471
- const proc = spawn(execCmd, execArgs, {
472
- stdio: 'inherit',
473
- cwd: invocationDir,
474
- env: {
475
- ...process.env,
476
- INIT_CWD: process.env.INIT_CWD ?? invocationDir,
477
- CLI4AI_CWD: invocationDir,
478
- C4AI_PACKAGE_DIR: pkg.path,
479
- C4AI_PACKAGE_NAME: pkg.name,
480
- C4AI_ENTRY: entryPath,
481
- ...secretsEnv,
482
- ...securityEnv,
483
- ...(options.env ?? {})
484
- }
485
- });
486
-
487
- const exitCode = await new Promise<number>((resolve, reject) => {
488
- proc.on('close', (code) => resolve(code ?? 0));
489
- proc.on('error', (err) => {
490
- reject(new ExecuteToolError('API_ERROR', `Failed to execute: ${err.message}`));
491
- });
492
- });
493
-
494
- return {
495
- exitCode,
496
- durationMs: Date.now() - startTime,
497
- packagePath: pkg.path,
498
- entryPath,
499
- runtime
500
- };
501
- }
502
-
503
- // capture === 'pipe'
504
- const proc = spawn(execCmd, execArgs, {
505
- stdio: ['pipe', 'pipe', 'pipe'],
506
- cwd: invocationDir,
507
- env: {
508
- ...process.env,
509
- INIT_CWD: process.env.INIT_CWD ?? invocationDir,
510
- CLI4AI_CWD: invocationDir,
511
- C4AI_PACKAGE_DIR: pkg.path,
512
- C4AI_PACKAGE_NAME: pkg.name,
513
- C4AI_ENTRY: entryPath,
514
- ...secretsEnv,
515
- ...securityEnv,
516
- ...(options.env ?? {})
517
- }
518
- });
519
-
520
- if (options.stdin !== undefined) {
521
- proc.stdin.write(options.stdin);
522
- proc.stdin.end();
523
- } else {
524
- // Don’t block on stdin if nothing is provided
525
- proc.stdin.end();
526
- }
527
-
528
- if (teeStderr && proc.stderr) {
529
- proc.stderr.on('data', (chunk) => {
530
- process.stderr.write(chunk);
531
- });
532
- }
533
-
534
- const stdoutPromise = proc.stdout ? collectStream(proc.stdout) : Promise.resolve('');
535
- const stderrPromise = proc.stderr ? collectStream(proc.stderr) : Promise.resolve('');
536
-
537
- let timeout: NodeJS.Timeout | undefined;
538
- if (options.timeoutMs && options.timeoutMs > 0) {
539
- timeout = setTimeout(() => {
540
- try {
541
- proc.kill('SIGTERM');
542
- } catch {}
543
- setTimeout(() => {
544
- try {
545
- proc.kill('SIGKILL');
546
- } catch {}
547
- }, 250);
548
- }, options.timeoutMs);
549
- }
550
-
551
- const exitCode = await new Promise<number>((resolve, reject) => {
552
- proc.on('close', (code) => resolve(code ?? 0));
553
- proc.on('error', (err) => reject(new ExecuteToolError('API_ERROR', `Failed to execute: ${err.message}`)));
554
- }).finally(() => {
555
- if (timeout) clearTimeout(timeout);
556
- });
557
-
558
- const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
559
-
560
- return {
561
- exitCode,
562
- durationMs: Date.now() - startTime,
563
- stdout,
564
- stderr,
565
- packagePath: pkg.path,
566
- entryPath,
567
- runtime
568
- };
569
- }