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