cli4ai 0.8.3 → 0.9.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.
@@ -2,7 +2,7 @@
2
2
  * Tests for config.ts
3
3
  */
4
4
 
5
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
5
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
6
6
  import { mkdtempSync, rmSync, writeFileSync, mkdirSync, existsSync, readFileSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import { tmpdir } from 'os';
@@ -31,7 +31,7 @@ describe('config', () => {
31
31
  test('has expected defaults', () => {
32
32
  expect(DEFAULT_CONFIG.registry).toBe('https://registry.cli4ai.com');
33
33
  expect(DEFAULT_CONFIG.localRegistries).toEqual([]);
34
- expect(DEFAULT_CONFIG.defaultRuntime).toBe('bun');
34
+ expect(DEFAULT_CONFIG.defaultRuntime).toBe('node');
35
35
  expect(DEFAULT_CONFIG.mcp.transport).toBe('stdio');
36
36
  expect(DEFAULT_CONFIG.telemetry).toBe(false);
37
37
  });
@@ -96,7 +96,7 @@ export interface Config {
96
96
  localRegistries: string[];
97
97
 
98
98
  // Runtime defaults
99
- defaultRuntime: 'bun' | 'node';
99
+ defaultRuntime: 'node';
100
100
 
101
101
  // MCP defaults
102
102
  mcp: {
@@ -123,7 +123,7 @@ export interface InstalledPackage {
123
123
  export const DEFAULT_CONFIG: Config = {
124
124
  registry: 'https://registry.cli4ai.com',
125
125
  localRegistries: [],
126
- defaultRuntime: 'bun',
126
+ defaultRuntime: 'node',
127
127
  mcp: {
128
128
  transport: 'stdio',
129
129
  port: 3100
@@ -743,7 +743,18 @@ export function getNpmGlobalPackages(): InstalledPackage[] {
743
743
  * Try to find a package in a global directory
744
744
  */
745
745
  function findPackageInGlobalDir(globalDir: string, name: string): InstalledPackage | null {
746
+ // SECURITY: Validate name to prevent path traversal
747
+ if (name.includes('..') || name.includes('/') || name.includes('\\') || name.startsWith('.')) {
748
+ return null;
749
+ }
750
+
746
751
  const scopedPath = resolve(globalDir, '@cli4ai', name);
752
+
753
+ // SECURITY: Verify resolved path is under globalDir
754
+ if (!scopedPath.startsWith(resolve(globalDir))) {
755
+ return null;
756
+ }
757
+
747
758
  if (!existsSync(scopedPath)) return null;
748
759
 
749
760
  const manifestPath = resolve(scopedPath, 'cli4ai.json');
@@ -51,7 +51,7 @@ export interface ExecuteToolResult {
51
51
  stderr?: string;
52
52
  packagePath: string;
53
53
  entryPath: string;
54
- runtime: 'bun' | 'node';
54
+ runtime: 'node';
55
55
  }
56
56
 
57
57
  export class ExecuteToolError extends Error {
@@ -94,12 +94,12 @@ const INSTALL_COMMANDS: Record<string, { check: string; install: Record<string,
94
94
  },
95
95
  description: 'Media processing tool'
96
96
  },
97
- 'bun': {
98
- check: 'bun --version',
97
+ 'node': {
98
+ check: 'node --version',
99
99
  install: {
100
- darwin: 'curl -fsSL https://bun.sh/install | bash',
101
- linux: 'curl -fsSL https://bun.sh/install | bash',
102
- win32: 'powershell -c "irm bun.sh/install.ps1 | iex"'
100
+ darwin: 'brew install node',
101
+ linux: 'curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt-get install -y nodejs',
102
+ win32: 'winget install OpenJS.NodeJS.LTS'
103
103
  },
104
104
  description: 'JavaScript runtime'
105
105
  }
@@ -158,6 +158,12 @@ async function installDependency(name: string): Promise<boolean> {
158
158
  log(`\nšŸ“¦ ${name} - ${info.description}`);
159
159
  log(` Install command: ${installCmd}\n`);
160
160
 
161
+ // SECURITY: Warn about curl|bash pattern
162
+ if (installCmd.includes('curl') && (installCmd.includes('| bash') || installCmd.includes('|bash'))) {
163
+ log(`āš ļø SECURITY WARNING: This command downloads and executes a script from the internet.`);
164
+ log(` Only proceed if you trust the source (${name}).\n`);
165
+ }
166
+
161
167
  const shouldInstall = await confirm(`Install ${name}?`);
162
168
  if (!shouldInstall) return false;
163
169
 
@@ -295,7 +301,8 @@ async function checkAndPromptSecrets(pkgPath: string, pkgName: string): Promise<
295
301
  const missingRequired: Array<{ key: string; description?: string }> = [];
296
302
 
297
303
  for (const [key, def] of Object.entries(envDefs)) {
298
- const value = getSecret(key);
304
+ // SECURITY: Use package-scoped secret lookup (tries scoped first, then global)
305
+ const value = getSecret(key, pkgName);
299
306
 
300
307
  if (value) {
301
308
  secretsEnv[key] = value;
@@ -318,62 +325,39 @@ async function checkAndPromptSecrets(pkgPath: string, pkgName: string): Promise<
318
325
  throw new ExecuteToolError('ENV_MISSING', `${key} is required to run ${pkgName}`, {
319
326
  package: pkgName,
320
327
  secret: key,
321
- hint: `Set it with: cli4ai secrets set ${key}`
328
+ hint: `Set it with: cli4ai secrets set ${key} --scope ${pkgName}`
322
329
  });
323
330
  }
324
331
 
325
332
  // Expand ~ to home directory for paths
326
333
  const expandedValue = expandTilde(value);
327
- setSecret(key, expandedValue);
334
+ // SECURITY: Store secret scoped to package
335
+ setSecret(key, expandedValue, pkgName);
328
336
  secretsEnv[key] = expandedValue;
329
- log(` āœ“ ${key} saved to vault\n`);
337
+ log(` āœ“ ${key} saved to vault (scoped to ${pkgName})\n`);
330
338
  }
331
339
 
332
340
  log('');
333
341
  return secretsEnv;
334
342
  }
335
343
 
336
- function buildRuntimeCommand(entryPath: string, runtime: 'bun' | 'node', cmdArgs: string[]): { execCmd: string; execArgs: string[]; runtime: 'bun' | 'node' } {
337
- switch (runtime) {
338
- case 'node':
339
- return { execCmd: 'node', execArgs: [entryPath, ...cmdArgs], runtime: 'node' };
340
- case 'bun':
341
- default:
342
- return { execCmd: 'bun', execArgs: ['run', entryPath, ...cmdArgs], runtime: 'bun' };
344
+ function buildRuntimeCommand(entryPath: string, cmdArgs: string[]): { execCmd: string; execArgs: string[]; runtime: 'node' } {
345
+ // Use tsx for TypeScript files, node for JavaScript
346
+ if (entryPath.endsWith('.ts') || entryPath.endsWith('.tsx')) {
347
+ return { execCmd: 'npx', execArgs: ['tsx', entryPath, ...cmdArgs], runtime: 'node' };
343
348
  }
349
+ return { execCmd: 'node', execArgs: [entryPath, ...cmdArgs], runtime: 'node' };
344
350
  }
345
351
 
346
- function resolveRuntime(manifest: Manifest): 'bun' | 'node' {
347
- if (manifest.runtime === 'node') return 'node';
348
- if (manifest.runtime === 'bun') return 'bun';
349
-
350
- // Unspecified runtime: prefer bun if installed, otherwise fall back to node if available.
351
- if (commandExists('bun')) return 'bun';
352
- if (commandExists('node')) return 'node';
353
-
354
- // Default to bun (will prompt install below).
355
- return 'bun';
356
- }
357
-
358
- async function ensureRuntimeAvailable(runtime: 'bun' | 'node'): Promise<void> {
359
- if (runtime === 'bun') {
360
- if (!commandExists('bun')) {
361
- log('āš ļø bun is required to run this tool\n');
362
- const installed = await installDependency('bun');
363
- if (!installed) {
364
- throw new ExecuteToolError('MISSING_DEPENDENCY', 'bun is required', {
365
- hint: 'Install bun: curl -fsSL https://bun.sh/install | bash'
366
- });
367
- }
368
- }
369
- return;
370
- }
371
-
372
- // runtime === 'node'
352
+ async function ensureRuntimeAvailable(): Promise<void> {
373
353
  if (!commandExists('node')) {
374
- throw new ExecuteToolError('MISSING_DEPENDENCY', 'node is required', {
375
- hint: 'Install Node.js: https://nodejs.org/en/download/'
376
- });
354
+ log('āš ļø Node.js is required to run this tool\n');
355
+ const installed = await installDependency('node');
356
+ if (!installed) {
357
+ throw new ExecuteToolError('MISSING_DEPENDENCY', 'Node.js is required', {
358
+ hint: 'Install Node.js: https://nodejs.org/en/download/'
359
+ });
360
+ }
377
361
  }
378
362
  }
379
363
 
@@ -426,8 +410,7 @@ export async function executeTool(options: ExecuteToolOptions): Promise<ExecuteT
426
410
  log('');
427
411
  }
428
412
 
429
- const runtime = resolveRuntime(manifest);
430
- await ensureRuntimeAvailable(runtime);
413
+ await ensureRuntimeAvailable();
431
414
 
432
415
  await checkPeerDependencies(pkg.path);
433
416
 
@@ -444,7 +427,7 @@ export async function executeTool(options: ExecuteToolOptions): Promise<ExecuteT
444
427
  if (options.command) cmdArgs.push(options.command);
445
428
  cmdArgs.push(...options.args);
446
429
 
447
- const { execCmd, execArgs } = buildRuntimeCommand(entryPath, runtime, cmdArgs);
430
+ const { execCmd, execArgs, runtime } = buildRuntimeCommand(entryPath, cmdArgs);
448
431
 
449
432
  const teeStderr = options.teeStderr ?? true;
450
433
 
@@ -2,7 +2,7 @@
2
2
  * Tests for link.ts
3
3
  */
4
4
 
5
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
5
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
6
6
  import { mkdtempSync, rmSync, readFileSync, writeFileSync, existsSync, statSync, mkdirSync } from 'fs';
7
7
  import { join, resolve } from 'path';
8
8
  import { tmpdir, homedir } from 'os';
@@ -36,7 +36,7 @@ describe('link', () => {
36
36
  name,
37
37
  version,
38
38
  entry: 'run.ts',
39
- runtime: 'bun'
39
+ runtime: 'node'
40
40
  });
41
41
 
42
42
  describe('C4AI_BIN constant', () => {
@@ -101,7 +101,7 @@ describe('link', () => {
101
101
  expect(existsSync(binPath)).toBe(true);
102
102
  const content = readFileSync(binPath, 'utf-8');
103
103
  expect(content).toContain('#!/bin/sh');
104
- expect(content).toContain('bun run');
104
+ expect(content).toContain('npx tsx');
105
105
  expect(content).toContain('run.ts');
106
106
  });
107
107
 
@@ -123,7 +123,7 @@ describe('link', () => {
123
123
  expect(content).not.toContain('node run');
124
124
  });
125
125
 
126
- test('defaults to bun runtime', () => {
126
+ test('defaults to node runtime with tsx for TypeScript', () => {
127
127
  const manifest: Manifest = {
128
128
  name: 'no-runtime',
129
129
  version: '1.0.0',
@@ -135,7 +135,7 @@ describe('link', () => {
135
135
  const binPath = linkPackageDirect(manifest, packagePath);
136
136
 
137
137
  const content = readFileSync(binPath, 'utf-8');
138
- expect(content).toContain('bun run');
138
+ expect(content).toContain('npx tsx');
139
139
  });
140
140
 
141
141
  test('includes full path to entry', () => {
package/src/core/link.ts CHANGED
@@ -115,25 +115,18 @@ export function linkPackageDirect(manifest: Manifest, packagePath: string): stri
115
115
 
116
116
  const binPath = join(C4AI_BIN, manifest.name);
117
117
  const entryPath = resolve(packagePath, manifest.entry);
118
- const runtime = manifest.runtime || 'bun';
119
118
 
120
119
  // SECURITY: Shell-escape the entry path to prevent injection
121
120
  const safeEntryPath = shellEscape(entryPath);
122
121
  const safeName = shellEscape(manifest.name);
123
122
  const safeVersion = shellEscape(manifest.version);
124
123
 
125
- // Build runtime command based on runtime type
126
- // - bun: bun run <file>
127
- // - node: node <file>
124
+ // Build runtime command - use tsx for TypeScript, node for JavaScript
128
125
  let execCommand: string;
129
- switch (runtime) {
130
- case 'node':
131
- execCommand = `node ${safeEntryPath}`;
132
- break;
133
- case 'bun':
134
- default:
135
- execCommand = `bun run ${safeEntryPath}`;
136
- break;
126
+ if (entryPath.endsWith('.ts') || entryPath.endsWith('.tsx')) {
127
+ execCommand = `npx tsx ${safeEntryPath}`;
128
+ } else {
129
+ execCommand = `node ${safeEntryPath}`;
137
130
  }
138
131
 
139
132
  // Create wrapper script that runs the tool directly
@@ -152,18 +145,17 @@ exec ${execCommand} "$@"
152
145
  // Windows compatibility: generate .cmd and .ps1 launchers
153
146
  if (process.platform === 'win32') {
154
147
  const quotedEntry = `"${entryPath.replaceAll('"', '""')}"`;
155
- const runtimeCmd = runtime === 'node' ? 'node' : 'bun';
156
148
 
157
- const cmdContent =
158
- runtime === 'node'
159
- ? `@echo off\r\n${runtimeCmd} ${quotedEntry} %*\r\nexit /b %errorlevel%\r\n`
160
- : `@echo off\r\n${runtimeCmd} run ${quotedEntry} %*\r\nexit /b %errorlevel%\r\n`;
149
+ let cmdContent: string;
150
+ let ps1Content: string;
151
+ if (entryPath.endsWith('.ts') || entryPath.endsWith('.tsx')) {
152
+ cmdContent = `@echo off\r\nnpx tsx ${quotedEntry} %*\r\nexit /b %errorlevel%\r\n`;
153
+ ps1Content = `& npx tsx ${quotedEntry} @args\nexit $LASTEXITCODE\n`;
154
+ } else {
155
+ cmdContent = `@echo off\r\nnode ${quotedEntry} %*\r\nexit /b %errorlevel%\r\n`;
156
+ ps1Content = `& node ${quotedEntry} @args\nexit $LASTEXITCODE\n`;
157
+ }
161
158
  writeFileSync(binPath + '.cmd', cmdContent);
162
-
163
- const ps1Content =
164
- runtime === 'node'
165
- ? `& "${runtimeCmd}" ${quotedEntry} @args\nexit $LASTEXITCODE\n`
166
- : `& "${runtimeCmd}" run ${quotedEntry} @args\nexit $LASTEXITCODE\n`;
167
159
  writeFileSync(binPath + '.ps1', ps1Content);
168
160
  }
169
161
 
@@ -174,6 +166,11 @@ exec ${execCommand} "$@"
174
166
  * Remove executable link for a package
175
167
  */
176
168
  export function unlinkPackage(packageName: string): boolean {
169
+ // SECURITY: Validate package name to prevent path traversal
170
+ if (!isShellSafe(packageName) || packageName.includes('..')) {
171
+ throw new Error(`Invalid package name: ${packageName}`);
172
+ }
173
+
177
174
  const basePath = join(C4AI_BIN, packageName);
178
175
  const candidates = process.platform === 'win32'
179
176
  ? [basePath, basePath + '.cmd', basePath + '.ps1']
@@ -2,7 +2,7 @@
2
2
  * Tests for lockfile.ts
3
3
  */
4
4
 
5
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
5
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
6
6
  import { mkdtempSync, rmSync, readFileSync, writeFileSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import { tmpdir } from 'os';
@@ -2,7 +2,7 @@
2
2
  * Tests for manifest.ts
3
3
  */
4
4
 
5
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
5
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
6
6
  import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import { tmpdir } from 'os';
@@ -304,7 +304,7 @@ describe('manifest', () => {
304
304
  expect(manifest.name).toBe('my-tool');
305
305
  expect(manifest.version).toBe('1.0.0');
306
306
  expect(manifest.entry).toBe('run.ts');
307
- expect(manifest.runtime).toBe('bun');
307
+ expect(manifest.runtime).toBe('node');
308
308
  });
309
309
 
310
310
  test('normalizes name', () => {
@@ -73,8 +73,8 @@ export interface Manifest {
73
73
  homepage?: string;
74
74
  keywords?: string[];
75
75
 
76
- // Runtime (bun or node only - deno not supported)
77
- runtime?: 'bun' | 'node';
76
+ // Runtime (node is default, bun kept for backwards compatibility)
77
+ runtime?: 'node' | 'bun';
78
78
 
79
79
  // Commands (for MCP generation)
80
80
  commands?: Record<string, CommandDef>;
@@ -134,6 +134,14 @@ export function validateManifest(manifest: unknown, source?: string): Manifest {
134
134
  });
135
135
  }
136
136
 
137
+ // SECURITY: Validate entry is a relative path that stays within package directory
138
+ if (m.entry.startsWith('/') || m.entry.startsWith('\\') || m.entry.includes('..')) {
139
+ throw new ManifestValidationError(
140
+ 'Invalid "entry" - must be a relative path without ".." (security: path traversal)',
141
+ { source, got: m.entry }
142
+ );
143
+ }
144
+
137
145
  // Optional: runtime (deno not supported)
138
146
  if (m.runtime !== undefined && !['bun', 'node'].includes(m.runtime as string)) {
139
147
  throw new ManifestValidationError('Invalid "runtime" (must be bun or node)', {
@@ -230,7 +238,7 @@ export function loadFromPackageJson(dir: string): Manifest | null {
230
238
  description: pkg.description,
231
239
  author: pkg.author,
232
240
  license: pkg.license,
233
- runtime: 'bun',
241
+ runtime: 'node',
234
242
  keywords: pkg.keywords
235
243
  };
236
244
  } catch {
@@ -312,7 +320,7 @@ export function createManifest(name: string, options: Partial<Manifest> = {}): M
312
320
  name: name.toLowerCase().replace(/[^a-z0-9-]/g, '-'),
313
321
  version: '1.0.0',
314
322
  entry: 'run.ts',
315
- runtime: 'bun',
323
+ runtime: 'node',
316
324
  description: options.description || `${name} tool`,
317
325
  ...options
318
326
  };
@@ -2,7 +2,7 @@
2
2
  * Tests for routine-engine.ts
3
3
  */
4
4
 
5
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
5
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
6
6
  import { mkdtempSync, rmSync, writeFileSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import { tmpdir } from 'os';
@@ -2,7 +2,7 @@
2
2
  * Tests for scheduler core functionality.
3
3
  */
4
4
 
5
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
5
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
6
6
  import { rmSync, existsSync, readdirSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import {
@@ -82,8 +82,8 @@ describe('getNextRunTime', () => {
82
82
  const nextRun = getNextRunTime(schedule, now);
83
83
 
84
84
  expect(nextRun).not.toBeNull();
85
- expect(nextRun!.getMinutes()).toBe(0);
86
- expect(nextRun!.getHours()).toBe(11); // Next hour
85
+ expect(nextRun!.getUTCMinutes()).toBe(0);
86
+ expect(nextRun!.getUTCHours()).toBe(11); // Next hour in UTC
87
87
  });
88
88
 
89
89
  test('returns earliest when both cron and interval specified', () => {
@@ -141,8 +141,13 @@ export function getDaemonPid(): number | null {
141
141
  }
142
142
 
143
143
  try {
144
- const pid = parseInt(readFileSync(SCHEDULER_PID_FILE, 'utf-8').trim(), 10);
145
- if (isNaN(pid)) return null;
144
+ const content = readFileSync(SCHEDULER_PID_FILE, 'utf-8').trim();
145
+ // SECURITY: Validate PID format and bounds
146
+ if (!/^\d+$/.test(content)) return null;
147
+ const pid = parseInt(content, 10);
148
+ // PIDs must be positive integers within reasonable bounds
149
+ // Max PID varies by OS but is typically 32768-4194304
150
+ if (!Number.isInteger(pid) || pid < 1 || pid > 4194304) return null;
146
151
  return pid;
147
152
  } catch {
148
153
  return null;
@@ -2,7 +2,7 @@
2
2
  * Tests for secrets.ts
3
3
  */
4
4
 
5
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
5
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
6
6
  import { mkdtempSync, rmSync, writeFileSync, existsSync, readFileSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import { tmpdir, hostname, userInfo } from 'os';
package/src/lib/cli.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  import { Command } from 'commander';
7
7
 
8
8
  export const BRAND = 'cli4ai - cli4ai.com';
9
- export const VERSION = '0.8.3';
9
+ export const VERSION = '0.9.1';
10
10
 
11
11
  // ═══════════════════════════════════════════════════════════════════════════
12
12
  // TYPES
@@ -2,7 +2,7 @@
2
2
  * Tests for mcp/adapter.ts
3
3
  */
4
4
 
5
- import { describe, test, expect } from 'bun:test';
5
+ import { describe, test, expect } from 'vitest';
6
6
  import {
7
7
  manifestToMcpTools,
8
8
  commandToMcpTool,
@@ -2,7 +2,7 @@
2
2
  * Tests for mcp/config-gen.ts
3
3
  */
4
4
 
5
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
5
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
6
6
  import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import { tmpdir } from 'os';
package/src/mcp/server.ts CHANGED
@@ -251,10 +251,9 @@ export class McpServer {
251
251
 
252
252
  // Execute the CLI tool
253
253
  const entryPath = resolve(this.packagePath, this.manifest.entry);
254
- const runtime = this.manifest.runtime || 'bun';
255
254
 
256
255
  try {
257
- const result = await this.executeCommand(runtime, entryPath, cmdArgs);
256
+ const result = await this.executeCommand(entryPath, cmdArgs);
258
257
  auditLog(this.manifest.name, name, args, 'success');
259
258
  this.sendResult(id, {
260
259
  content: [{ type: 'text', text: result }]
@@ -269,30 +268,25 @@ export class McpServer {
269
268
  }
270
269
  }
271
270
 
272
- private executeCommand(runtime: string, entryPath: string, args: string[]): Promise<string> {
271
+ private executeCommand(entryPath: string, args: string[]): Promise<string> {
273
272
  return new Promise((resolve, reject) => {
274
- // Build runtime-specific command and arguments
275
- // - bun: bun run <file> [args]
276
- // - node: node <file> [args]
273
+ // Use tsx for TypeScript files, node for JavaScript
277
274
  let cmd: string;
278
275
  let cmdArgs: string[];
279
- switch (runtime) {
280
- case 'node':
281
- cmd = 'node';
282
- cmdArgs = [entryPath, ...args];
283
- break;
284
- case 'bun':
285
- default:
286
- cmd = 'bun';
287
- cmdArgs = ['run', entryPath, ...args];
288
- break;
276
+ if (entryPath.endsWith('.ts') || entryPath.endsWith('.tsx')) {
277
+ cmd = 'npx';
278
+ cmdArgs = ['tsx', entryPath, ...args];
279
+ } else {
280
+ cmd = 'node';
281
+ cmdArgs = [entryPath, ...args];
289
282
  }
290
283
 
291
284
  // Inject secrets from manifest env definitions
285
+ // SECURITY: Use package-scoped secret lookup (tries scoped first, then global)
292
286
  const secretsEnv: Record<string, string> = {};
293
287
  if (this.manifest.env) {
294
288
  for (const key of Object.keys(this.manifest.env)) {
295
- const value = getSecret(key);
289
+ const value = getSecret(key, this.manifest.name);
296
290
  if (value) {
297
291
  secretsEnv[key] = value;
298
292
  }