@orchagent/cli 0.3.23 → 0.3.25

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.
@@ -9,9 +9,32 @@ const path_1 = __importDefault(require("path"));
9
9
  const child_process_1 = require("child_process");
10
10
  const chalk_1 = __importDefault(require("chalk"));
11
11
  const yaml_1 = __importDefault(require("yaml"));
12
+ const fast_deep_equal_1 = __importDefault(require("fast-deep-equal"));
13
+ const chokidar_1 = __importDefault(require("chokidar"));
12
14
  const errors_1 = require("../lib/errors");
13
15
  const config_1 = require("../lib/config");
14
16
  const llm_1 = require("../lib/llm");
17
+ /**
18
+ * Validate a fixture and return helpful errors
19
+ */
20
+ function validateFixture(data, fixturePath) {
21
+ const fileName = path_1.default.basename(fixturePath);
22
+ if (typeof data !== 'object' || data === null) {
23
+ throw new errors_1.CliError(`Invalid fixture ${fileName}: must be a JSON object`);
24
+ }
25
+ const obj = data;
26
+ if (!obj.input || typeof obj.input !== 'object') {
27
+ throw new errors_1.CliError(`Invalid fixture ${fileName}: missing required "input" field.\n` +
28
+ ` Expected format: { "input": {...}, "expected_output": {...} }`);
29
+ }
30
+ if (!obj.expected_output && !obj.expected_contains) {
31
+ throw new errors_1.CliError(`Invalid fixture ${fileName}: must have "expected_output" or "expected_contains".\n` +
32
+ ` Add one of:\n` +
33
+ ` "expected_output": {"key": "exact value to match"}\n` +
34
+ ` "expected_contains": ["substring to find"]`);
35
+ }
36
+ return data;
37
+ }
15
38
  /**
16
39
  * Parse SKILL.md frontmatter
17
40
  */
@@ -68,8 +91,10 @@ function runCommand(command, args, cwd, verbose) {
68
91
  * Check if a command exists
69
92
  */
70
93
  async function commandExists(command) {
94
+ const isWindows = process.platform === 'win32';
95
+ const checker = isWindows ? 'where' : 'which';
71
96
  try {
72
- const proc = (0, child_process_1.spawn)('which', [command], { shell: true });
97
+ const proc = (0, child_process_1.spawn)(checker, [command], { shell: true, stdio: 'ignore' });
73
98
  return new Promise((resolve) => {
74
99
  proc.on('close', (code) => resolve(code === 0));
75
100
  proc.on('error', () => resolve(false));
@@ -202,22 +227,28 @@ async function discoverTests(agentDir) {
202
227
  */
203
228
  async function runPythonTests(agentDir, verbose) {
204
229
  process.stderr.write(chalk_1.default.blue('\nRunning Python tests...\n\n'));
205
- // Check if pytest is available
230
+ // Check if pytest is available directly
206
231
  const hasPytest = await commandExists('pytest');
207
- let command;
208
- let args;
209
232
  if (hasPytest) {
210
- command = 'pytest';
211
- args = verbose ? ['-v'] : [];
233
+ const args = verbose ? ['-v'] : [];
234
+ const { code } = await runCommand('pytest', args, agentDir, verbose);
235
+ return code;
212
236
  }
213
- else {
214
- command = 'python3';
215
- args = ['-m', 'pytest'];
216
- if (verbose)
217
- args.push('-v');
237
+ // Try Python commands in order of preference
238
+ const pythonCommands = process.platform === 'win32'
239
+ ? ['python', 'py', 'python3']
240
+ : ['python3', 'python'];
241
+ for (const pythonCmd of pythonCommands) {
242
+ if (await commandExists(pythonCmd)) {
243
+ const args = ['-m', 'pytest'];
244
+ if (verbose)
245
+ args.push('-v');
246
+ const { code } = await runCommand(pythonCmd, args, agentDir, verbose);
247
+ return code;
248
+ }
218
249
  }
219
- const { code } = await runCommand(command, args, agentDir, verbose);
220
- return code;
250
+ process.stderr.write(chalk_1.default.red('No Python interpreter found. Install Python and pytest.\n'));
251
+ return 1;
221
252
  }
222
253
  /**
223
254
  * Run JavaScript/TypeScript tests
@@ -290,8 +321,8 @@ async function runFixtureTests(agentDir, fixtures, verbose, config) {
290
321
  throw new errors_1.CliError('No LLM key found for fixture tests.\n' +
291
322
  'Set an environment variable (e.g., OPENAI_API_KEY) or run `orchagent keys add <provider>`');
292
323
  }
293
- const { provider, key } = detected;
294
- const model = (0, llm_1.getDefaultModel)(provider);
324
+ const { provider, key, model: serverModel } = detected;
325
+ const model = serverModel ?? (0, llm_1.getDefaultModel)(provider);
295
326
  let passed = 0;
296
327
  let failed = 0;
297
328
  for (const fixturePath of fixtures) {
@@ -299,7 +330,14 @@ async function runFixtureTests(agentDir, fixtures, verbose, config) {
299
330
  process.stderr.write(` ${fixtureName}: `);
300
331
  try {
301
332
  const raw = await promises_1.default.readFile(fixturePath, 'utf-8');
302
- const fixture = JSON.parse(raw);
333
+ let parsed;
334
+ try {
335
+ parsed = JSON.parse(raw);
336
+ }
337
+ catch (e) {
338
+ throw new errors_1.CliError(`Invalid JSON in ${path_1.default.basename(fixturePath)}: ${e.message}`);
339
+ }
340
+ const fixture = validateFixture(parsed, fixturePath);
303
341
  // Build and call LLM
304
342
  const fullPrompt = (0, llm_1.buildPrompt)(prompt, fixture.input);
305
343
  const result = await (0, llm_1.callLlm)(provider, key, model, fullPrompt, outputSchema);
@@ -307,12 +345,9 @@ async function runFixtureTests(agentDir, fixtures, verbose, config) {
307
345
  let testPassed = true;
308
346
  const failures = [];
309
347
  if (fixture.expected_output) {
310
- // Exact match comparison
311
- const resultStr = JSON.stringify(result, Object.keys(result).sort());
312
- const expectedStr = JSON.stringify(fixture.expected_output, Object.keys(fixture.expected_output).sort());
313
- if (resultStr !== expectedStr) {
348
+ if (!(0, fast_deep_equal_1.default)(result, fixture.expected_output)) {
314
349
  testPassed = false;
315
- failures.push(`Expected: ${expectedStr}\nGot: ${resultStr}`);
350
+ failures.push(`Expected: ${JSON.stringify(fixture.expected_output, null, 2)}\nGot: ${JSON.stringify(result, null, 2)}`);
316
351
  }
317
352
  }
318
353
  if (fixture.expected_contains) {
@@ -355,46 +390,39 @@ async function runFixtureTests(agentDir, fixtures, verbose, config) {
355
390
  /**
356
391
  * Watch mode: re-run tests on file changes
357
392
  */
358
- async function watchTests(agentDir, agentType, testFiles, verbose, config) {
393
+ async function watchTests(agentDir, agentType, verbose, config) {
359
394
  process.stderr.write(chalk_1.default.cyan('\nWatching for file changes... (press Ctrl+C to exit)\n\n'));
360
- // Collect all files to watch
361
- const watchPaths = [agentDir];
362
395
  const runTests = async () => {
363
396
  process.stderr.write(chalk_1.default.dim(`\n[${new Date().toLocaleTimeString()}] Running tests...\n`));
397
+ // Re-discover tests each time to pick up new files
398
+ const testFiles = await discoverTests(agentDir);
364
399
  await executeTests(agentDir, agentType, testFiles, verbose, config);
365
400
  };
366
401
  // Initial run
367
402
  await runTests();
368
- // Set up file watcher using fs.watch (basic implementation)
369
- const watchers = [];
403
+ // Set up chokidar watcher
370
404
  let debounceTimer = null;
371
- const onChange = () => {
405
+ const onChange = (filePath) => {
372
406
  if (debounceTimer)
373
407
  clearTimeout(debounceTimer);
374
- debounceTimer = setTimeout(runTests, 500);
375
- };
376
- for (const watchPath of watchPaths) {
377
- try {
378
- // Use recursive watch on the directory
379
- const ac = new AbortController();
380
- (async () => {
381
- try {
382
- const watcher = promises_1.default.watch(watchPath, { recursive: true, signal: ac.signal });
383
- for await (const _event of watcher) {
384
- onChange();
385
- }
386
- }
387
- catch (err) {
388
- if (err.name !== 'AbortError') {
389
- // Watcher error, ignore
390
- }
391
- }
392
- })();
393
- }
394
- catch {
395
- // Watch not supported, fall back to polling
408
+ if (verbose) {
409
+ process.stderr.write(chalk_1.default.dim(` Changed: ${path_1.default.relative(agentDir, filePath)}\n`));
396
410
  }
397
- }
411
+ debounceTimer = setTimeout(runTests, 300);
412
+ };
413
+ const watcher = chokidar_1.default.watch(agentDir, {
414
+ ignored: /(node_modules|__pycache__|\.git|dist|build|\.venv|venv)/,
415
+ persistent: true,
416
+ ignoreInitial: true,
417
+ });
418
+ watcher
419
+ .on('change', onChange)
420
+ .on('add', onChange)
421
+ .on('unlink', onChange)
422
+ .on('error', (error) => {
423
+ const message = error instanceof Error ? error.message : String(error);
424
+ process.stderr.write(chalk_1.default.red(`Watcher error: ${message}\n`));
425
+ });
398
426
  // Keep process alive
399
427
  await new Promise(() => { });
400
428
  }
@@ -520,7 +548,7 @@ Fixture Format (tests/fixture-1.json):
520
548
  }
521
549
  // Watch mode
522
550
  if (options.watch) {
523
- await watchTests(agentDir, agentType, testFiles, !!options.verbose, config);
551
+ await watchTests(agentDir, agentType, !!options.verbose, config);
524
552
  return;
525
553
  }
526
554
  // Run tests
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchagent/cli",
3
- "version": "0.3.23",
3
+ "version": "0.3.25",
4
4
  "description": "Command-line interface for the orchagent AI agent marketplace",
5
5
  "license": "MIT",
6
6
  "author": "orchagent <hello@orchagent.io>",
@@ -46,8 +46,10 @@
46
46
  "@sentry/node": "^9.3.0",
47
47
  "archiver": "^7.0.0",
48
48
  "chalk": "^4.1.2",
49
+ "chokidar": "^4.0.0",
49
50
  "cli-table3": "^0.6.3",
50
51
  "commander": "^11.1.0",
52
+ "fast-deep-equal": "^3.1.3",
51
53
  "open": "^8.4.2",
52
54
  "ora": "^9.1.0",
53
55
  "posthog-node": "^4.0.0",