@orchagent/cli 0.3.22 → 0.3.24
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/dist/commands/call.js +19 -5
- package/dist/commands/index.js +2 -0
- package/dist/commands/run.js +78 -81
- package/dist/commands/test.js +528 -0
- package/dist/index.js +20 -2
- package/dist/lib/spinner.js +115 -0
- package/package.json +4 -1
package/dist/commands/call.js
CHANGED
|
@@ -10,6 +10,7 @@ const config_1 = require("../lib/config");
|
|
|
10
10
|
const api_1 = require("../lib/api");
|
|
11
11
|
const errors_1 = require("../lib/errors");
|
|
12
12
|
const output_1 = require("../lib/output");
|
|
13
|
+
const spinner_1 = require("../lib/spinner");
|
|
13
14
|
const llm_1 = require("../lib/llm");
|
|
14
15
|
const analytics_1 = require("../lib/analytics");
|
|
15
16
|
const DEFAULT_VERSION = 'latest';
|
|
@@ -311,11 +312,21 @@ argument or --file option instead.
|
|
|
311
312
|
sourceLabel = multipart.sourceLabel;
|
|
312
313
|
}
|
|
313
314
|
const url = `${resolved.apiUrl.replace(/\/$/, '')}/${org}/${parsed.agent}/${parsed.version}/${endpoint}`;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
315
|
+
// Make the API call with a spinner
|
|
316
|
+
const spinner = (0, spinner_1.createSpinner)(`Calling ${org}/${parsed.agent}@${parsed.version}...`);
|
|
317
|
+
spinner.start();
|
|
318
|
+
let response;
|
|
319
|
+
try {
|
|
320
|
+
response = await (0, api_1.safeFetchWithRetryForCalls)(url, {
|
|
321
|
+
method: 'POST',
|
|
322
|
+
headers,
|
|
323
|
+
body,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
spinner.fail(`Call failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
328
|
+
throw err;
|
|
329
|
+
}
|
|
319
330
|
if (!response.ok) {
|
|
320
331
|
const text = await response.text();
|
|
321
332
|
let payload;
|
|
@@ -330,6 +341,7 @@ argument or --file option instead.
|
|
|
330
341
|
? payload.error?.code
|
|
331
342
|
: undefined;
|
|
332
343
|
if (errorCode === 'LLM_KEY_REQUIRED') {
|
|
344
|
+
spinner.fail('LLM key required');
|
|
333
345
|
throw new errors_1.CliError('This public agent requires you to provide an LLM key.\n' +
|
|
334
346
|
'Use --key <key> --provider <provider> or set OPENAI_API_KEY/ANTHROPIC_API_KEY env var.');
|
|
335
347
|
}
|
|
@@ -339,8 +351,10 @@ argument or --file option instead.
|
|
|
339
351
|
payload.message ||
|
|
340
352
|
response.statusText
|
|
341
353
|
: response.statusText;
|
|
354
|
+
spinner.fail(`Call failed: ${message}`);
|
|
342
355
|
throw new errors_1.CliError(message);
|
|
343
356
|
}
|
|
357
|
+
spinner.succeed(`Called ${org}/${parsed.agent}@${parsed.version}`);
|
|
344
358
|
// Track successful call
|
|
345
359
|
const inputType = filePaths.length > 0
|
|
346
360
|
? 'file'
|
package/dist/commands/index.js
CHANGED
|
@@ -27,6 +27,7 @@ const formats_1 = require("./formats");
|
|
|
27
27
|
const update_1 = require("./update");
|
|
28
28
|
const env_1 = require("./env");
|
|
29
29
|
const list_1 = require("./list");
|
|
30
|
+
const test_1 = require("./test");
|
|
30
31
|
function registerCommands(program) {
|
|
31
32
|
(0, login_1.registerLoginCommand)(program);
|
|
32
33
|
(0, whoami_1.registerWhoamiCommand)(program);
|
|
@@ -54,4 +55,5 @@ function registerCommands(program) {
|
|
|
54
55
|
(0, update_1.registerUpdateCommand)(program);
|
|
55
56
|
(0, env_1.registerEnvCommand)(program);
|
|
56
57
|
(0, list_1.registerListCommand)(program);
|
|
58
|
+
(0, test_1.registerTestCommand)(program);
|
|
57
59
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -45,6 +45,7 @@ const config_1 = require("../lib/config");
|
|
|
45
45
|
const api_1 = require("../lib/api");
|
|
46
46
|
const errors_1 = require("../lib/errors");
|
|
47
47
|
const output_1 = require("../lib/output");
|
|
48
|
+
const spinner_1 = require("../lib/spinner");
|
|
48
49
|
const llm_1 = require("../lib/llm");
|
|
49
50
|
const DEFAULT_VERSION = 'latest';
|
|
50
51
|
const AGENTS_DIR = path_1.default.join(os_1.default.homedir(), '.orchagent', 'agents');
|
|
@@ -210,17 +211,18 @@ async function downloadDependenciesRecursively(config, depStatuses, visited = ne
|
|
|
210
211
|
continue;
|
|
211
212
|
visited.add(depRef);
|
|
212
213
|
const [org, agent] = status.dep.id.split('/');
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
214
|
+
await (0, spinner_1.withSpinner)(`Downloading dependency: ${depRef}...`, async () => {
|
|
215
|
+
// Save the dependency metadata locally
|
|
216
|
+
await saveAgentLocally(org, agent, status.agentData);
|
|
217
|
+
// For bundle-based agents, also extract the bundle
|
|
218
|
+
if (status.agentData.has_bundle) {
|
|
219
|
+
await saveBundleLocally(config, org, agent, status.dep.version, status.agentData.id);
|
|
220
|
+
}
|
|
221
|
+
// Install if it's a pip/source code agent
|
|
222
|
+
if (status.agentData.type === 'code' && (status.agentData.source_url || status.agentData.pip_package)) {
|
|
223
|
+
await installCodeAgent(status.agentData);
|
|
224
|
+
}
|
|
225
|
+
}, { successText: `Downloaded ${depRef}` });
|
|
224
226
|
// Download default skills
|
|
225
227
|
const defaultSkills = status.agentData.default_skills || [];
|
|
226
228
|
for (const skillRef of defaultSkills) {
|
|
@@ -327,20 +329,18 @@ async function executePromptLocally(agentData, inputData, skillPrompts = [], con
|
|
|
327
329
|
}));
|
|
328
330
|
// Show which provider is being used (primary)
|
|
329
331
|
const primary = providersWithModels[0];
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
process.stderr.write(`Running with ${primary.provider} (${primary.model})...\n`);
|
|
336
|
-
}
|
|
332
|
+
const spinnerText = providersWithModels.length > 1
|
|
333
|
+
? `Running with ${primary.provider} (${primary.model}), ${providersWithModels.length - 1} fallback(s) available...`
|
|
334
|
+
: `Running with ${primary.provider} (${primary.model})...`;
|
|
337
335
|
// Use fallback if multiple providers, otherwise single call
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
336
|
+
return await (0, spinner_1.withSpinner)(spinnerText, async () => {
|
|
337
|
+
if (providersWithModels.length > 1) {
|
|
338
|
+
return await (0, llm_1.callLlmWithFallback)(providersWithModels, prompt, agentData.output_schema);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
return await (0, llm_1.callLlm)(primary.provider, primary.apiKey, primary.model, prompt, agentData.output_schema);
|
|
342
|
+
}
|
|
343
|
+
}, { successText: `Completed with ${primary.provider}` });
|
|
344
344
|
}
|
|
345
345
|
// Provider override: use single provider (existing behavior)
|
|
346
346
|
const detected = await (0, llm_1.detectLlmKey)(providersToCheck, config);
|
|
@@ -352,11 +352,10 @@ async function executePromptLocally(agentData, inputData, skillPrompts = [], con
|
|
|
352
352
|
const { provider, key, model: serverModel } = detected;
|
|
353
353
|
// Priority: CLI override > server config model > agent default model > hardcoded default
|
|
354
354
|
const model = modelOverride || serverModel || agentData.default_models?.[provider] || (0, llm_1.getDefaultModel)(provider);
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
return response;
|
|
355
|
+
// Call the LLM with spinner
|
|
356
|
+
return await (0, spinner_1.withSpinner)(`Running with ${provider} (${model})...`, async () => {
|
|
357
|
+
return await (0, llm_1.callLlm)(provider, key, model, prompt, agentData.output_schema);
|
|
358
|
+
}, { successText: `Completed with ${provider}` });
|
|
360
359
|
}
|
|
361
360
|
function parseSkillRef(value) {
|
|
362
361
|
const [ref, versionPart] = value.split('@');
|
|
@@ -436,21 +435,22 @@ async function installCodeAgent(agentData) {
|
|
|
436
435
|
if (agentData.pip_package) {
|
|
437
436
|
const installed = await checkPackageInstalled(agentData.pip_package);
|
|
438
437
|
if (installed) {
|
|
439
|
-
|
|
438
|
+
const spinner = (0, spinner_1.createSpinner)(`Package ${agentData.pip_package}`);
|
|
439
|
+
spinner.succeed(`Package ${agentData.pip_package} already installed`);
|
|
440
440
|
return;
|
|
441
441
|
}
|
|
442
442
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
443
|
+
await (0, spinner_1.withSpinner)(`Installing ${installSource}...`, async () => {
|
|
444
|
+
const { code } = await runCommand('python3', ['-m', 'pip', 'install', '--quiet', '--disable-pip-version-check', installSource]);
|
|
445
|
+
if (code !== 0) {
|
|
446
|
+
throw new errors_1.CliError(`Failed to install agent (exit code ${code}).\n\n` +
|
|
447
|
+
'Troubleshooting:\n' +
|
|
448
|
+
' - Check Python is installed: python3 --version\n' +
|
|
449
|
+
' - Check pip is available: pip --version\n' +
|
|
450
|
+
' - Check network connectivity\n' +
|
|
451
|
+
' - Try installing manually: pip install <package>');
|
|
452
|
+
}
|
|
453
|
+
}, { successText: 'Installation complete' });
|
|
454
454
|
}
|
|
455
455
|
async function executeCodeAgent(agentData, args) {
|
|
456
456
|
if (!agentData.run_command) {
|
|
@@ -502,24 +502,27 @@ async function executeBundleAgent(config, org, agentName, version, agentData, ar
|
|
|
502
502
|
const bundleZip = path_1.default.join(tempDir, 'bundle.zip');
|
|
503
503
|
const extractDir = path_1.default.join(tempDir, 'agent');
|
|
504
504
|
try {
|
|
505
|
-
// Download the bundle
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
505
|
+
// Download the bundle with spinner
|
|
506
|
+
const bundleBuffer = await (0, spinner_1.withSpinner)(`Downloading ${org}/${agentName}@${version} bundle...`, async () => {
|
|
507
|
+
const buffer = await downloadBundleWithFallback(config, org, agentName, version, agentData.id);
|
|
508
|
+
await promises_1.default.writeFile(bundleZip, buffer);
|
|
509
|
+
return buffer;
|
|
510
|
+
}, { successText: (buf) => `Downloaded bundle (${buf.length} bytes)` });
|
|
511
|
+
// Extract the bundle with spinner
|
|
511
512
|
await promises_1.default.mkdir(extractDir, { recursive: true });
|
|
512
|
-
|
|
513
|
-
|
|
513
|
+
await (0, spinner_1.withSpinner)('Extracting bundle...', async () => {
|
|
514
|
+
await unzipBundle(bundleZip, extractDir);
|
|
515
|
+
}, { successText: 'Bundle extracted' });
|
|
514
516
|
// Check if requirements.txt exists and install dependencies
|
|
515
517
|
const requirementsPath = path_1.default.join(extractDir, 'requirements.txt');
|
|
516
518
|
try {
|
|
517
519
|
await promises_1.default.access(requirementsPath);
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
520
|
+
await (0, spinner_1.withSpinner)('Installing dependencies...', async () => {
|
|
521
|
+
const { code } = await runCommand('python3', ['-m', 'pip', 'install', '-q', '--disable-pip-version-check', '-r', requirementsPath]);
|
|
522
|
+
if (code !== 0) {
|
|
523
|
+
throw new errors_1.CliError('Failed to install dependencies from requirements.txt');
|
|
524
|
+
}
|
|
525
|
+
}, { successText: 'Dependencies installed' });
|
|
523
526
|
}
|
|
524
527
|
catch (err) {
|
|
525
528
|
if (err.code !== 'ENOENT') {
|
|
@@ -739,8 +742,7 @@ async function saveBundleLocally(config, org, agent, version, agentId) {
|
|
|
739
742
|
// Metadata doesn't exist, need to download
|
|
740
743
|
}
|
|
741
744
|
// Download and extract bundle
|
|
742
|
-
|
|
743
|
-
const bundleBuffer = await downloadBundleWithFallback(config, org, agent, version, agentId);
|
|
745
|
+
const bundleBuffer = await (0, spinner_1.withSpinner)(`Downloading bundle for ${org}/${agent}@${version}...`, async () => downloadBundleWithFallback(config, org, agent, version, agentId), { successText: `Downloaded bundle for ${org}/${agent}@${version}` });
|
|
744
746
|
const tempZip = path_1.default.join(os_1.default.tmpdir(), `bundle-${Date.now()}.zip`);
|
|
745
747
|
await promises_1.default.writeFile(tempZip, bundleBuffer);
|
|
746
748
|
// Clean and recreate bundle directory
|
|
@@ -818,23 +820,23 @@ Note: Use 'run' for local execution, 'call' for server-side execution.
|
|
|
818
820
|
if (!org) {
|
|
819
821
|
throw new errors_1.CliError('Missing org. Use org/agent format.');
|
|
820
822
|
}
|
|
821
|
-
// Download agent definition
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
}
|
|
837
|
-
}
|
|
823
|
+
// Download agent definition with spinner
|
|
824
|
+
const agentData = await (0, spinner_1.withSpinner)(`Downloading ${org}/${parsed.agent}@${parsed.version}...`, async () => {
|
|
825
|
+
try {
|
|
826
|
+
return await downloadAgent(resolved, org, parsed.agent, parsed.version);
|
|
827
|
+
}
|
|
828
|
+
catch (err) {
|
|
829
|
+
// Fall back to getting public agent info if download endpoint not available
|
|
830
|
+
const agentMeta = await (0, api_1.getPublicAgent)(resolved, org, parsed.agent, parsed.version);
|
|
831
|
+
return {
|
|
832
|
+
type: agentMeta.type || 'code',
|
|
833
|
+
name: agentMeta.name,
|
|
834
|
+
version: agentMeta.version,
|
|
835
|
+
description: agentMeta.description || undefined,
|
|
836
|
+
supported_providers: agentMeta.supported_providers || ['any'],
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
}, { successText: `Downloaded ${org}/${parsed.agent}@${parsed.version}` });
|
|
838
840
|
// Skills cannot be run directly - they're instructions to inject into agents
|
|
839
841
|
if (agentData.type === 'skill') {
|
|
840
842
|
throw new errors_1.CliError('Skills cannot be run directly.\n\n' +
|
|
@@ -845,8 +847,7 @@ Note: Use 'run' for local execution, 'call' for server-side execution.
|
|
|
845
847
|
}
|
|
846
848
|
// Check for dependencies (orchestrator agents)
|
|
847
849
|
if (agentData.dependencies && agentData.dependencies.length > 0) {
|
|
848
|
-
|
|
849
|
-
const depStatuses = await checkDependencies(resolved, agentData.dependencies);
|
|
850
|
+
const depStatuses = await (0, spinner_1.withSpinner)('Checking dependencies...', async () => checkDependencies(resolved, agentData.dependencies), { successText: `Found ${agentData.dependencies.length} dependencies` });
|
|
850
851
|
let choice;
|
|
851
852
|
if (options.withDeps) {
|
|
852
853
|
// Auto-download deps without prompting
|
|
@@ -865,9 +866,7 @@ Note: Use 'run' for local execution, 'call' for server-side execution.
|
|
|
865
866
|
process.exit(0);
|
|
866
867
|
}
|
|
867
868
|
// choice === 'local' - download dependencies
|
|
868
|
-
process.stderr.write(`\nDownloading dependencies...\n`);
|
|
869
869
|
await downloadDependenciesRecursively(resolved, depStatuses);
|
|
870
|
-
process.stderr.write(`\nAll dependencies downloaded.\n`);
|
|
871
870
|
}
|
|
872
871
|
// Check if user is overriding locked skills
|
|
873
872
|
const agentSkillsLocked = agentData.skills_locked;
|
|
@@ -954,12 +953,10 @@ Note: Use 'run' for local execution, 'call' for server-side execution.
|
|
|
954
953
|
}
|
|
955
954
|
}
|
|
956
955
|
if (skillRefs.length > 0) {
|
|
957
|
-
|
|
958
|
-
skillPrompts = await loadSkillPrompts(resolved, skillRefs, org);
|
|
956
|
+
skillPrompts = await (0, spinner_1.withSpinner)(`Loading ${skillRefs.length} skill(s)...`, async () => loadSkillPrompts(resolved, skillRefs, org), { successText: `Loaded ${skillRefs.length} skill(s)` });
|
|
959
957
|
}
|
|
960
958
|
}
|
|
961
|
-
// Execute locally
|
|
962
|
-
process.stderr.write(`Executing locally...\n\n`);
|
|
959
|
+
// Execute locally (the spinner is inside executePromptLocally)
|
|
963
960
|
const result = await executePromptLocally(agentData, inputData, skillPrompts, resolved, options.provider, options.model);
|
|
964
961
|
if (options.json) {
|
|
965
962
|
(0, output_1.printJson)(result);
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerTestCommand = registerTestCommand;
|
|
7
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
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"));
|
|
14
|
+
const errors_1 = require("../lib/errors");
|
|
15
|
+
const config_1 = require("../lib/config");
|
|
16
|
+
const llm_1 = require("../lib/llm");
|
|
17
|
+
/**
|
|
18
|
+
* Parse SKILL.md frontmatter
|
|
19
|
+
*/
|
|
20
|
+
async function parseSkillMd(filePath) {
|
|
21
|
+
try {
|
|
22
|
+
const content = await promises_1.default.readFile(filePath, 'utf-8');
|
|
23
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
24
|
+
if (!match)
|
|
25
|
+
return null;
|
|
26
|
+
const frontmatter = yaml_1.default.parse(match[1]);
|
|
27
|
+
const body = match[2].trim();
|
|
28
|
+
if (!frontmatter.name || !frontmatter.description)
|
|
29
|
+
return null;
|
|
30
|
+
return { frontmatter, body };
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Run a command and return the result
|
|
38
|
+
*/
|
|
39
|
+
function runCommand(command, args, cwd, verbose) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const proc = (0, child_process_1.spawn)(command, args, {
|
|
42
|
+
cwd,
|
|
43
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
44
|
+
shell: true,
|
|
45
|
+
});
|
|
46
|
+
let stdout = '';
|
|
47
|
+
let stderr = '';
|
|
48
|
+
proc.stdout?.on('data', (data) => {
|
|
49
|
+
const text = data.toString();
|
|
50
|
+
stdout += text;
|
|
51
|
+
if (verbose) {
|
|
52
|
+
process.stdout.write(text);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
proc.stderr?.on('data', (data) => {
|
|
56
|
+
const text = data.toString();
|
|
57
|
+
stderr += text;
|
|
58
|
+
// Always show stderr for test output
|
|
59
|
+
process.stderr.write(text);
|
|
60
|
+
});
|
|
61
|
+
proc.on('close', (code) => {
|
|
62
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
63
|
+
});
|
|
64
|
+
proc.on('error', (err) => {
|
|
65
|
+
resolve({ code: 1, stdout, stderr: err.message });
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Check if a command exists
|
|
71
|
+
*/
|
|
72
|
+
async function commandExists(command) {
|
|
73
|
+
try {
|
|
74
|
+
const proc = (0, child_process_1.spawn)('which', [command], { shell: true });
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
proc.on('close', (code) => resolve(code === 0));
|
|
77
|
+
proc.on('error', () => resolve(false));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Check if a file exists
|
|
86
|
+
*/
|
|
87
|
+
async function fileExists(filePath) {
|
|
88
|
+
try {
|
|
89
|
+
await promises_1.default.access(filePath);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Detect the agent type from the directory structure
|
|
98
|
+
*/
|
|
99
|
+
async function detectAgentType(agentDir) {
|
|
100
|
+
// Check for SKILL.md first
|
|
101
|
+
if (await fileExists(path_1.default.join(agentDir, 'SKILL.md'))) {
|
|
102
|
+
return 'skill';
|
|
103
|
+
}
|
|
104
|
+
// Check for prompt.md (prompt agent)
|
|
105
|
+
if (await fileExists(path_1.default.join(agentDir, 'prompt.md'))) {
|
|
106
|
+
return 'prompt';
|
|
107
|
+
}
|
|
108
|
+
// Check for orchagent.json
|
|
109
|
+
const manifestPath = path_1.default.join(agentDir, 'orchagent.json');
|
|
110
|
+
if (await fileExists(manifestPath)) {
|
|
111
|
+
try {
|
|
112
|
+
const raw = await promises_1.default.readFile(manifestPath, 'utf-8');
|
|
113
|
+
const manifest = JSON.parse(raw);
|
|
114
|
+
if (manifest.type === 'prompt')
|
|
115
|
+
return 'prompt';
|
|
116
|
+
if (manifest.type === 'skill')
|
|
117
|
+
return 'skill';
|
|
118
|
+
if (manifest.type === 'code') {
|
|
119
|
+
// Detect language
|
|
120
|
+
if (await fileExists(path_1.default.join(agentDir, 'requirements.txt')))
|
|
121
|
+
return 'code-python';
|
|
122
|
+
if (await fileExists(path_1.default.join(agentDir, 'pyproject.toml')))
|
|
123
|
+
return 'code-python';
|
|
124
|
+
if (await fileExists(path_1.default.join(agentDir, 'package.json')))
|
|
125
|
+
return 'code-js';
|
|
126
|
+
// Default to Python for code agents
|
|
127
|
+
return 'code-python';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Invalid manifest, continue detection
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Fallback: detect by file presence
|
|
135
|
+
if (await fileExists(path_1.default.join(agentDir, 'requirements.txt')))
|
|
136
|
+
return 'code-python';
|
|
137
|
+
if (await fileExists(path_1.default.join(agentDir, 'pyproject.toml')))
|
|
138
|
+
return 'code-python';
|
|
139
|
+
if (await fileExists(path_1.default.join(agentDir, 'package.json')))
|
|
140
|
+
return 'code-js';
|
|
141
|
+
return 'unknown';
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Recursively walk a directory and return all files
|
|
145
|
+
*/
|
|
146
|
+
async function walkDir(dir, files = []) {
|
|
147
|
+
try {
|
|
148
|
+
const entries = await promises_1.default.readdir(dir, { withFileTypes: true });
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
151
|
+
// Skip common non-source directories
|
|
152
|
+
if (entry.isDirectory()) {
|
|
153
|
+
if (['node_modules', '__pycache__', '.git', 'dist', 'build', '.venv', 'venv'].includes(entry.name)) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
await walkDir(fullPath, files);
|
|
157
|
+
}
|
|
158
|
+
else if (entry.isFile()) {
|
|
159
|
+
files.push(fullPath);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Directory doesn't exist or not readable
|
|
165
|
+
}
|
|
166
|
+
return files;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Discover test files in the agent directory
|
|
170
|
+
*/
|
|
171
|
+
async function discoverTests(agentDir) {
|
|
172
|
+
const result = {
|
|
173
|
+
python: [],
|
|
174
|
+
javascript: [],
|
|
175
|
+
fixtures: [],
|
|
176
|
+
};
|
|
177
|
+
// Get all files recursively
|
|
178
|
+
const allFiles = await walkDir(agentDir);
|
|
179
|
+
for (const file of allFiles) {
|
|
180
|
+
const basename = path_1.default.basename(file);
|
|
181
|
+
const relPath = path_1.default.relative(agentDir, file);
|
|
182
|
+
// Python test patterns: test_*.py, *_test.py
|
|
183
|
+
if (basename.endsWith('.py')) {
|
|
184
|
+
if (basename.startsWith('test_') || basename.endsWith('_test.py')) {
|
|
185
|
+
result.python.push(file);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// JS/TS test patterns: *.test.ts, *.test.js, *.spec.ts, *.spec.js
|
|
189
|
+
if (basename.endsWith('.test.ts') || basename.endsWith('.test.js') ||
|
|
190
|
+
basename.endsWith('.spec.ts') || basename.endsWith('.spec.js')) {
|
|
191
|
+
result.javascript.push(file);
|
|
192
|
+
}
|
|
193
|
+
// Fixture patterns: tests/fixture*.json or fixture*.json in tests/ subdirs
|
|
194
|
+
if (basename.endsWith('.json') && basename.startsWith('fixture')) {
|
|
195
|
+
if (relPath.includes('tests' + path_1.default.sep) || relPath.startsWith('tests' + path_1.default.sep)) {
|
|
196
|
+
result.fixtures.push(file);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Run Python tests using pytest
|
|
204
|
+
*/
|
|
205
|
+
async function runPythonTests(agentDir, verbose) {
|
|
206
|
+
process.stderr.write(chalk_1.default.blue('\nRunning Python tests...\n\n'));
|
|
207
|
+
// Check if pytest is available
|
|
208
|
+
const hasPytest = await commandExists('pytest');
|
|
209
|
+
let command;
|
|
210
|
+
let args;
|
|
211
|
+
if (hasPytest) {
|
|
212
|
+
command = 'pytest';
|
|
213
|
+
args = verbose ? ['-v'] : [];
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
command = 'python3';
|
|
217
|
+
args = ['-m', 'pytest'];
|
|
218
|
+
if (verbose)
|
|
219
|
+
args.push('-v');
|
|
220
|
+
}
|
|
221
|
+
const { code } = await runCommand(command, args, agentDir, verbose);
|
|
222
|
+
return code;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Run JavaScript/TypeScript tests
|
|
226
|
+
*/
|
|
227
|
+
async function runJsTests(agentDir, verbose) {
|
|
228
|
+
process.stderr.write(chalk_1.default.blue('\nRunning JavaScript/TypeScript tests...\n\n'));
|
|
229
|
+
// Check for vitest first
|
|
230
|
+
const hasVitest = await fileExists(path_1.default.join(agentDir, 'node_modules', '.bin', 'vitest'));
|
|
231
|
+
if (hasVitest) {
|
|
232
|
+
const args = ['run'];
|
|
233
|
+
if (verbose)
|
|
234
|
+
args.push('--reporter=verbose');
|
|
235
|
+
const { code } = await runCommand('npx', ['vitest', ...args], agentDir, verbose);
|
|
236
|
+
return code;
|
|
237
|
+
}
|
|
238
|
+
// Fall back to npm test
|
|
239
|
+
const packageJsonPath = path_1.default.join(agentDir, 'package.json');
|
|
240
|
+
if (await fileExists(packageJsonPath)) {
|
|
241
|
+
try {
|
|
242
|
+
const raw = await promises_1.default.readFile(packageJsonPath, 'utf-8');
|
|
243
|
+
const pkg = JSON.parse(raw);
|
|
244
|
+
if (pkg.scripts?.test) {
|
|
245
|
+
const { code } = await runCommand('npm', ['test'], agentDir, verbose);
|
|
246
|
+
return code;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// Invalid package.json
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
process.stderr.write(chalk_1.default.yellow('No JavaScript test runner found. Install vitest or add a test script to package.json.\n'));
|
|
254
|
+
return 1;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Run fixture-based tests for prompt agents
|
|
258
|
+
*/
|
|
259
|
+
async function runFixtureTests(agentDir, fixtures, verbose, config) {
|
|
260
|
+
process.stderr.write(chalk_1.default.blue('\nRunning fixture tests...\n\n'));
|
|
261
|
+
// Read prompt
|
|
262
|
+
let prompt;
|
|
263
|
+
const promptPath = path_1.default.join(agentDir, 'prompt.md');
|
|
264
|
+
const skillPath = path_1.default.join(agentDir, 'SKILL.md');
|
|
265
|
+
// Check if this is a skill
|
|
266
|
+
const skillData = await parseSkillMd(skillPath);
|
|
267
|
+
if (skillData) {
|
|
268
|
+
prompt = skillData.body;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
try {
|
|
272
|
+
prompt = await promises_1.default.readFile(promptPath, 'utf-8');
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
throw new errors_1.CliError('No prompt.md or SKILL.md found for fixture tests');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Read output schema if available
|
|
279
|
+
let outputSchema;
|
|
280
|
+
const schemaPath = path_1.default.join(agentDir, 'schema.json');
|
|
281
|
+
try {
|
|
282
|
+
const raw = await promises_1.default.readFile(schemaPath, 'utf-8');
|
|
283
|
+
const schemas = JSON.parse(raw);
|
|
284
|
+
outputSchema = schemas.output;
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// Schema is optional
|
|
288
|
+
}
|
|
289
|
+
// Detect LLM key
|
|
290
|
+
const detected = await (0, llm_1.detectLlmKey)(['any'], config);
|
|
291
|
+
if (!detected) {
|
|
292
|
+
throw new errors_1.CliError('No LLM key found for fixture tests.\n' +
|
|
293
|
+
'Set an environment variable (e.g., OPENAI_API_KEY) or run `orchagent keys add <provider>`');
|
|
294
|
+
}
|
|
295
|
+
const { provider, key } = detected;
|
|
296
|
+
const model = (0, llm_1.getDefaultModel)(provider);
|
|
297
|
+
let passed = 0;
|
|
298
|
+
let failed = 0;
|
|
299
|
+
for (const fixturePath of fixtures) {
|
|
300
|
+
const fixtureName = path_1.default.basename(fixturePath);
|
|
301
|
+
process.stderr.write(` ${fixtureName}: `);
|
|
302
|
+
try {
|
|
303
|
+
const raw = await promises_1.default.readFile(fixturePath, 'utf-8');
|
|
304
|
+
const fixture = JSON.parse(raw);
|
|
305
|
+
// Build and call LLM
|
|
306
|
+
const fullPrompt = (0, llm_1.buildPrompt)(prompt, fixture.input);
|
|
307
|
+
const result = await (0, llm_1.callLlm)(provider, key, model, fullPrompt, outputSchema);
|
|
308
|
+
// Validate result
|
|
309
|
+
let testPassed = true;
|
|
310
|
+
const failures = [];
|
|
311
|
+
if (fixture.expected_output) {
|
|
312
|
+
if (!(0, fast_deep_equal_1.default)(result, fixture.expected_output)) {
|
|
313
|
+
testPassed = false;
|
|
314
|
+
failures.push(`Expected: ${JSON.stringify(fixture.expected_output, null, 2)}\nGot: ${JSON.stringify(result, null, 2)}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (fixture.expected_contains) {
|
|
318
|
+
// Check if output contains expected strings
|
|
319
|
+
const resultStr = JSON.stringify(result);
|
|
320
|
+
for (const expected of fixture.expected_contains) {
|
|
321
|
+
if (!resultStr.includes(expected)) {
|
|
322
|
+
testPassed = false;
|
|
323
|
+
failures.push(`Expected output to contain: "${expected}"`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (testPassed) {
|
|
328
|
+
process.stderr.write(chalk_1.default.green('PASS\n'));
|
|
329
|
+
passed++;
|
|
330
|
+
if (verbose) {
|
|
331
|
+
process.stderr.write(chalk_1.default.gray(` Input: ${JSON.stringify(fixture.input)}\n`));
|
|
332
|
+
process.stderr.write(chalk_1.default.gray(` Output: ${JSON.stringify(result)}\n`));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
process.stderr.write(chalk_1.default.red('FAIL\n'));
|
|
337
|
+
failed++;
|
|
338
|
+
for (const failure of failures) {
|
|
339
|
+
process.stderr.write(chalk_1.default.red(` ${failure}\n`));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
process.stderr.write(chalk_1.default.red('ERROR\n'));
|
|
345
|
+
failed++;
|
|
346
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
347
|
+
process.stderr.write(chalk_1.default.red(` ${message}\n`));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
process.stderr.write('\n');
|
|
351
|
+
process.stderr.write(`Fixtures: ${passed} passed, ${failed} failed\n`);
|
|
352
|
+
return failed > 0 ? 1 : 0;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Watch mode: re-run tests on file changes
|
|
356
|
+
*/
|
|
357
|
+
async function watchTests(agentDir, agentType, verbose, config) {
|
|
358
|
+
process.stderr.write(chalk_1.default.cyan('\nWatching for file changes... (press Ctrl+C to exit)\n\n'));
|
|
359
|
+
const runTests = async () => {
|
|
360
|
+
process.stderr.write(chalk_1.default.dim(`\n[${new Date().toLocaleTimeString()}] Running tests...\n`));
|
|
361
|
+
// Re-discover tests each time to pick up new files
|
|
362
|
+
const testFiles = await discoverTests(agentDir);
|
|
363
|
+
await executeTests(agentDir, agentType, testFiles, verbose, config);
|
|
364
|
+
};
|
|
365
|
+
// Initial run
|
|
366
|
+
await runTests();
|
|
367
|
+
// Set up chokidar watcher
|
|
368
|
+
let debounceTimer = null;
|
|
369
|
+
const onChange = (filePath) => {
|
|
370
|
+
if (debounceTimer)
|
|
371
|
+
clearTimeout(debounceTimer);
|
|
372
|
+
if (verbose) {
|
|
373
|
+
process.stderr.write(chalk_1.default.dim(` Changed: ${path_1.default.relative(agentDir, filePath)}\n`));
|
|
374
|
+
}
|
|
375
|
+
debounceTimer = setTimeout(runTests, 300);
|
|
376
|
+
};
|
|
377
|
+
const watcher = chokidar_1.default.watch(agentDir, {
|
|
378
|
+
ignored: /(node_modules|__pycache__|\.git|dist|build|\.venv|venv)/,
|
|
379
|
+
persistent: true,
|
|
380
|
+
ignoreInitial: true,
|
|
381
|
+
});
|
|
382
|
+
watcher
|
|
383
|
+
.on('change', onChange)
|
|
384
|
+
.on('add', onChange)
|
|
385
|
+
.on('unlink', onChange)
|
|
386
|
+
.on('error', (error) => {
|
|
387
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
388
|
+
process.stderr.write(chalk_1.default.red(`Watcher error: ${message}\n`));
|
|
389
|
+
});
|
|
390
|
+
// Keep process alive
|
|
391
|
+
await new Promise(() => { });
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Execute tests based on agent type and discovered test files
|
|
395
|
+
*/
|
|
396
|
+
async function executeTests(agentDir, agentType, testFiles, verbose, config) {
|
|
397
|
+
let exitCode = 0;
|
|
398
|
+
// Run tests based on what's available
|
|
399
|
+
const hasTests = testFiles.python.length > 0 ||
|
|
400
|
+
testFiles.javascript.length > 0 ||
|
|
401
|
+
testFiles.fixtures.length > 0;
|
|
402
|
+
if (!hasTests) {
|
|
403
|
+
// For prompt agents/skills, suggest creating fixtures
|
|
404
|
+
if (agentType === 'prompt' || agentType === 'skill') {
|
|
405
|
+
process.stderr.write(chalk_1.default.yellow('No test files found.\n\n'));
|
|
406
|
+
process.stderr.write('For prompt agents, create fixture files in tests/:\n');
|
|
407
|
+
process.stderr.write(chalk_1.default.gray(' tests/fixture-1.json:\n'));
|
|
408
|
+
process.stderr.write(chalk_1.default.gray(' {\n'));
|
|
409
|
+
process.stderr.write(chalk_1.default.gray(' "input": {"text": "Hello world"},\n'));
|
|
410
|
+
process.stderr.write(chalk_1.default.gray(' "expected_contains": ["response"]\n'));
|
|
411
|
+
process.stderr.write(chalk_1.default.gray(' }\n\n'));
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
process.stderr.write(chalk_1.default.yellow('No test files found.\n\n'));
|
|
415
|
+
process.stderr.write('Supported test file patterns:\n');
|
|
416
|
+
process.stderr.write(chalk_1.default.gray(' Python: test_*.py, *_test.py, tests/test_*.py\n'));
|
|
417
|
+
process.stderr.write(chalk_1.default.gray(' JS/TS: *.test.ts, *.spec.ts, tests/*.test.ts\n'));
|
|
418
|
+
process.stderr.write(chalk_1.default.gray(' Fixtures: tests/fixture-*.json\n\n'));
|
|
419
|
+
}
|
|
420
|
+
return 1;
|
|
421
|
+
}
|
|
422
|
+
// Run Python tests if found
|
|
423
|
+
if (testFiles.python.length > 0) {
|
|
424
|
+
if (verbose) {
|
|
425
|
+
process.stderr.write(chalk_1.default.gray(`Found ${testFiles.python.length} Python test file(s)\n`));
|
|
426
|
+
}
|
|
427
|
+
const code = await runPythonTests(agentDir, verbose);
|
|
428
|
+
if (code !== 0)
|
|
429
|
+
exitCode = 1;
|
|
430
|
+
}
|
|
431
|
+
// Run JS/TS tests if found
|
|
432
|
+
if (testFiles.javascript.length > 0) {
|
|
433
|
+
if (verbose) {
|
|
434
|
+
process.stderr.write(chalk_1.default.gray(`Found ${testFiles.javascript.length} JavaScript/TypeScript test file(s)\n`));
|
|
435
|
+
}
|
|
436
|
+
const code = await runJsTests(agentDir, verbose);
|
|
437
|
+
if (code !== 0)
|
|
438
|
+
exitCode = 1;
|
|
439
|
+
}
|
|
440
|
+
// Run fixture tests if found (for prompt agents)
|
|
441
|
+
if (testFiles.fixtures.length > 0) {
|
|
442
|
+
if (verbose) {
|
|
443
|
+
process.stderr.write(chalk_1.default.gray(`Found ${testFiles.fixtures.length} fixture file(s)\n`));
|
|
444
|
+
}
|
|
445
|
+
const code = await runFixtureTests(agentDir, testFiles.fixtures, verbose, config);
|
|
446
|
+
if (code !== 0)
|
|
447
|
+
exitCode = 1;
|
|
448
|
+
}
|
|
449
|
+
return exitCode;
|
|
450
|
+
}
|
|
451
|
+
function registerTestCommand(program) {
|
|
452
|
+
program
|
|
453
|
+
.command('test [path]')
|
|
454
|
+
.description('Run agent test suite locally')
|
|
455
|
+
.option('-v, --verbose', 'Show detailed test output')
|
|
456
|
+
.option('-w, --watch', 'Watch for file changes and re-run tests')
|
|
457
|
+
.addHelpText('after', `
|
|
458
|
+
Examples:
|
|
459
|
+
orch test Run tests in current directory
|
|
460
|
+
orch test ./my-agent Run tests in specified directory
|
|
461
|
+
orch test --verbose Show detailed test output
|
|
462
|
+
orch test --watch Watch mode - re-run on file changes
|
|
463
|
+
|
|
464
|
+
Test Discovery:
|
|
465
|
+
Python: test_*.py, *_test.py, tests/test_*.py, tests/*_test.py
|
|
466
|
+
JS/TS: *.test.ts, *.test.js, *.spec.ts, *.spec.js, tests/*.test.*
|
|
467
|
+
Fixtures: tests/fixture-*.json (for prompt agents)
|
|
468
|
+
|
|
469
|
+
Fixture Format (tests/fixture-1.json):
|
|
470
|
+
{
|
|
471
|
+
"input": {"key": "value"},
|
|
472
|
+
"expected_output": {"result": "expected"},
|
|
473
|
+
"expected_contains": ["substring"],
|
|
474
|
+
"description": "Test description"
|
|
475
|
+
}
|
|
476
|
+
`)
|
|
477
|
+
.action(async (agentPath, options) => {
|
|
478
|
+
const agentDir = agentPath
|
|
479
|
+
? path_1.default.resolve(process.cwd(), agentPath)
|
|
480
|
+
: process.cwd();
|
|
481
|
+
// Verify directory exists
|
|
482
|
+
try {
|
|
483
|
+
const stat = await promises_1.default.stat(agentDir);
|
|
484
|
+
if (!stat.isDirectory()) {
|
|
485
|
+
throw new errors_1.CliError(`Not a directory: ${agentDir}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
if (err.code === 'ENOENT') {
|
|
490
|
+
throw new errors_1.CliError(`Directory not found: ${agentDir}`);
|
|
491
|
+
}
|
|
492
|
+
throw err;
|
|
493
|
+
}
|
|
494
|
+
// Detect agent type
|
|
495
|
+
const agentType = await detectAgentType(agentDir);
|
|
496
|
+
if (options.verbose) {
|
|
497
|
+
process.stderr.write(chalk_1.default.gray(`Detected agent type: ${agentType}\n`));
|
|
498
|
+
}
|
|
499
|
+
// Discover test files
|
|
500
|
+
const testFiles = await discoverTests(agentDir);
|
|
501
|
+
if (options.verbose) {
|
|
502
|
+
const totalTests = testFiles.python.length + testFiles.javascript.length + testFiles.fixtures.length;
|
|
503
|
+
process.stderr.write(chalk_1.default.gray(`Discovered ${totalTests} test file(s)\n`));
|
|
504
|
+
}
|
|
505
|
+
// Get config for LLM access (needed for fixture tests)
|
|
506
|
+
let config;
|
|
507
|
+
try {
|
|
508
|
+
config = await (0, config_1.getResolvedConfig)();
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
// Config not available, fixture tests will use env vars only
|
|
512
|
+
}
|
|
513
|
+
// Watch mode
|
|
514
|
+
if (options.watch) {
|
|
515
|
+
await watchTests(agentDir, agentType, !!options.verbose, config);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
// Run tests
|
|
519
|
+
const exitCode = await executeTests(agentDir, agentType, testFiles, !!options.verbose, config);
|
|
520
|
+
if (exitCode === 0) {
|
|
521
|
+
process.stderr.write(chalk_1.default.green('\nAll tests passed.\n'));
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
process.stderr.write(chalk_1.default.red('\nSome tests failed.\n'));
|
|
525
|
+
}
|
|
526
|
+
process.exit(exitCode);
|
|
527
|
+
});
|
|
528
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -45,6 +45,8 @@ const commander_1 = require("commander");
|
|
|
45
45
|
const commands_1 = require("./commands");
|
|
46
46
|
const errors_1 = require("./lib/errors");
|
|
47
47
|
const analytics_1 = require("./lib/analytics");
|
|
48
|
+
const config_1 = require("./lib/config");
|
|
49
|
+
const spinner_1 = require("./lib/spinner");
|
|
48
50
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
49
51
|
(0, analytics_1.initPostHog)();
|
|
50
52
|
const program = new commander_1.Command();
|
|
@@ -52,6 +54,7 @@ program
|
|
|
52
54
|
.name('orchagent')
|
|
53
55
|
.description('orchagent CLI')
|
|
54
56
|
.version(package_json_1.default.version)
|
|
57
|
+
.option('--no-progress', 'Disable progress spinners (useful for CI/scripts)')
|
|
55
58
|
.addHelpText('after', `
|
|
56
59
|
Quick Reference:
|
|
57
60
|
run Download and run an agent locally (your machine)
|
|
@@ -64,7 +67,22 @@ Documentation: https://docs.orchagent.io
|
|
|
64
67
|
orchagent docs agents Building agents guide
|
|
65
68
|
`);
|
|
66
69
|
(0, commands_1.registerCommands)(program);
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
// Initialize progress setting before parsing
|
|
71
|
+
async function main() {
|
|
72
|
+
// Check config for no_progress setting
|
|
73
|
+
const config = await (0, config_1.loadConfig)();
|
|
74
|
+
if (config.no_progress) {
|
|
75
|
+
(0, spinner_1.setProgressEnabled)(false);
|
|
76
|
+
}
|
|
77
|
+
// Parse args - hook will set noProgress if --no-progress flag is passed
|
|
78
|
+
program.hook('preAction', () => {
|
|
79
|
+
const opts = program.opts();
|
|
80
|
+
if (opts.progress === false) {
|
|
81
|
+
(0, spinner_1.setProgressEnabled)(false);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
await program.parseAsync(process.argv);
|
|
85
|
+
}
|
|
86
|
+
main()
|
|
69
87
|
.catch(errors_1.exitWithError)
|
|
70
88
|
.finally(() => (0, analytics_1.shutdownPostHog)());
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.setProgressEnabled = setProgressEnabled;
|
|
7
|
+
exports.isProgressEnabled = isProgressEnabled;
|
|
8
|
+
exports.createSpinner = createSpinner;
|
|
9
|
+
exports.withSpinner = withSpinner;
|
|
10
|
+
exports.createProgressSpinner = createProgressSpinner;
|
|
11
|
+
const ora_1 = __importDefault(require("ora"));
|
|
12
|
+
// Global flag to control spinner visibility (set via --no-progress)
|
|
13
|
+
let progressEnabled = true;
|
|
14
|
+
/**
|
|
15
|
+
* Set whether progress spinners are enabled.
|
|
16
|
+
* Disable for CI/scripts with --no-progress flag.
|
|
17
|
+
*/
|
|
18
|
+
function setProgressEnabled(enabled) {
|
|
19
|
+
progressEnabled = enabled;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check if progress spinners are enabled.
|
|
23
|
+
*/
|
|
24
|
+
function isProgressEnabled() {
|
|
25
|
+
return progressEnabled;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create a spinner with the given text.
|
|
29
|
+
* Returns a spinner that auto-disables in non-TTY environments.
|
|
30
|
+
* If progress is disabled (--no-progress), returns a no-op spinner.
|
|
31
|
+
*/
|
|
32
|
+
function createSpinner(text) {
|
|
33
|
+
if (!progressEnabled) {
|
|
34
|
+
// Return a no-op spinner that writes to stderr instead
|
|
35
|
+
const noopSpinner = {
|
|
36
|
+
text,
|
|
37
|
+
isSpinning: false,
|
|
38
|
+
start() {
|
|
39
|
+
process.stderr.write(`${text}\n`);
|
|
40
|
+
return noopSpinner;
|
|
41
|
+
},
|
|
42
|
+
stop() {
|
|
43
|
+
return noopSpinner;
|
|
44
|
+
},
|
|
45
|
+
succeed(msg) {
|
|
46
|
+
if (msg)
|
|
47
|
+
process.stderr.write(`${msg}\n`);
|
|
48
|
+
return noopSpinner;
|
|
49
|
+
},
|
|
50
|
+
fail(msg) {
|
|
51
|
+
if (msg)
|
|
52
|
+
process.stderr.write(`${msg}\n`);
|
|
53
|
+
return noopSpinner;
|
|
54
|
+
},
|
|
55
|
+
warn(msg) {
|
|
56
|
+
if (msg)
|
|
57
|
+
process.stderr.write(`${msg}\n`);
|
|
58
|
+
return noopSpinner;
|
|
59
|
+
},
|
|
60
|
+
info(msg) {
|
|
61
|
+
if (msg)
|
|
62
|
+
process.stderr.write(`${msg}\n`);
|
|
63
|
+
return noopSpinner;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
return noopSpinner;
|
|
67
|
+
}
|
|
68
|
+
// ora automatically handles non-TTY by falling back to text output
|
|
69
|
+
return (0, ora_1.default)({
|
|
70
|
+
text,
|
|
71
|
+
stream: process.stderr, // Use stderr so it doesn't interfere with JSON output
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Execute an async function with a spinner.
|
|
76
|
+
* Automatically handles success/failure states.
|
|
77
|
+
*
|
|
78
|
+
* @param text - Text to show while operation is running
|
|
79
|
+
* @param fn - Async function to execute
|
|
80
|
+
* @param options - Optional success/fail message overrides
|
|
81
|
+
* @returns Result of the async function
|
|
82
|
+
*/
|
|
83
|
+
async function withSpinner(text, fn, options) {
|
|
84
|
+
const spinner = createSpinner(text);
|
|
85
|
+
spinner.start();
|
|
86
|
+
try {
|
|
87
|
+
const result = await fn();
|
|
88
|
+
const successMsg = typeof options?.successText === 'function'
|
|
89
|
+
? options.successText(result)
|
|
90
|
+
: options?.successText;
|
|
91
|
+
spinner.succeed(successMsg);
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
const failMsg = typeof options?.failText === 'function'
|
|
96
|
+
? options.failText(err)
|
|
97
|
+
: options?.failText || (err instanceof Error ? err.message : 'Failed');
|
|
98
|
+
spinner.fail(failMsg);
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Create a spinner that can be updated with progress info.
|
|
104
|
+
* Useful for operations like downloads where you want to show progress.
|
|
105
|
+
*/
|
|
106
|
+
function createProgressSpinner(initialText) {
|
|
107
|
+
const spinner = createSpinner(initialText);
|
|
108
|
+
const updateProgress = (current, total, unit = 'bytes') => {
|
|
109
|
+
if (progressEnabled && spinner.isSpinning) {
|
|
110
|
+
const percent = total > 0 ? Math.round((current / total) * 100) : 0;
|
|
111
|
+
spinner.text = `${initialText} (${percent}% - ${current}/${total} ${unit})`;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
return { spinner, updateProgress };
|
|
115
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orchagent/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.24",
|
|
4
4
|
"description": "Command-line interface for the orchagent AI agent marketplace",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "orchagent <hello@orchagent.io>",
|
|
@@ -46,9 +46,12 @@
|
|
|
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",
|
|
54
|
+
"ora": "^9.1.0",
|
|
52
55
|
"posthog-node": "^4.0.0",
|
|
53
56
|
"yaml": "^2.8.2"
|
|
54
57
|
},
|