@orchagent/cli 0.3.91 → 0.3.93
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/diff-format.js +300 -0
- package/dist/commands/diff.js +2 -129
- package/dist/commands/health.js +90 -7
- package/dist/commands/index.js +4 -0
- package/dist/commands/init-wizard.js +129 -64
- package/dist/commands/init.js +71 -1
- package/dist/commands/publish.js +74 -66
- package/dist/commands/run.js +107 -29
- package/dist/commands/scaffold.js +213 -0
- package/dist/commands/schedule.js +40 -3
- package/dist/commands/templates/cron-job.js +259 -0
- package/dist/commands/update.js +46 -9
- package/dist/commands/validate.js +264 -0
- package/dist/lib/scaffold-orchestration.js +237 -0
- package/dist/lib/validate.js +478 -0
- package/package.json +1 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cron-job template for `orch init --template cron-job`.
|
|
4
|
+
*
|
|
5
|
+
* Provides Python and JavaScript scaffolding for scheduled tasks —
|
|
6
|
+
* daily reports, data syncs, cleanups, notifications, etc.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.CRON_JOB_SCHEMA = exports.CRON_JOB_MAIN_JS = exports.CRON_JOB_MAIN_PY = void 0;
|
|
10
|
+
exports.cronJobReadme = cronJobReadme;
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Python template
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
exports.CRON_JOB_MAIN_PY = `"""
|
|
15
|
+
orchagent scheduled job.
|
|
16
|
+
|
|
17
|
+
Runs on a cron schedule to perform periodic tasks.
|
|
18
|
+
|
|
19
|
+
Schedule examples:
|
|
20
|
+
orch schedule create org/my-job --cron "0 9 * * 1" # Every Monday 9 AM UTC
|
|
21
|
+
orch schedule create org/my-job --cron "0 0 * * *" # Daily at midnight
|
|
22
|
+
orch schedule create org/my-job --cron "0 */6 * * *" # Every 6 hours
|
|
23
|
+
|
|
24
|
+
Local test:
|
|
25
|
+
echo '{}' | python main.py
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import sys
|
|
31
|
+
from datetime import datetime, timezone
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main():
|
|
35
|
+
raw = sys.stdin.read()
|
|
36
|
+
try:
|
|
37
|
+
data = json.loads(raw) if raw.strip() else {}
|
|
38
|
+
except json.JSONDecodeError:
|
|
39
|
+
print(json.dumps({"error": "Invalid JSON input"}))
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
|
|
42
|
+
dry_run = data.get("options", {}).get("dry_run", False)
|
|
43
|
+
|
|
44
|
+
# --- Your scheduled job logic here ---
|
|
45
|
+
# Common patterns:
|
|
46
|
+
# - Fetch data from an API and store results
|
|
47
|
+
# - Generate a daily/weekly report
|
|
48
|
+
# - Clean up old records or files
|
|
49
|
+
# - Send digest notifications (email, Slack, Discord)
|
|
50
|
+
#
|
|
51
|
+
# To use workspace secrets (API keys, webhook URLs):
|
|
52
|
+
# 1. Add to "required_secrets" in orchagent.json
|
|
53
|
+
# 2. Set in workspace: orch secrets set MY_SECRET <value>
|
|
54
|
+
# 3. Access via: os.environ["MY_SECRET"]
|
|
55
|
+
#
|
|
56
|
+
# Example: send to a webhook
|
|
57
|
+
# import urllib.request
|
|
58
|
+
# webhook_url = os.environ["WEBHOOK_URL"]
|
|
59
|
+
# req = urllib.request.Request(webhook_url, data=json.dumps(payload).encode(),
|
|
60
|
+
# headers={"Content-Type": "application/json"})
|
|
61
|
+
# urllib.request.urlopen(req)
|
|
62
|
+
|
|
63
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
64
|
+
|
|
65
|
+
report = {
|
|
66
|
+
"generated_at": now,
|
|
67
|
+
"status": "completed" if not dry_run else "dry_run",
|
|
68
|
+
"summary": "Scheduled job ran successfully",
|
|
69
|
+
"items_processed": 0,
|
|
70
|
+
}
|
|
71
|
+
# --- End your logic ---
|
|
72
|
+
|
|
73
|
+
print(json.dumps({
|
|
74
|
+
"result": report,
|
|
75
|
+
"success": True,
|
|
76
|
+
}))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
main()
|
|
81
|
+
`;
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// JavaScript template
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
exports.CRON_JOB_MAIN_JS = `/**
|
|
86
|
+
* orchagent scheduled job.
|
|
87
|
+
*
|
|
88
|
+
* Runs on a cron schedule to perform periodic tasks.
|
|
89
|
+
*
|
|
90
|
+
* Schedule examples:
|
|
91
|
+
* orch schedule create org/my-job --cron "0 9 * * 1" # Every Monday 9 AM UTC
|
|
92
|
+
* orch schedule create org/my-job --cron "0 0 * * *" # Daily at midnight
|
|
93
|
+
* orch schedule create org/my-job --cron "0 */6 * * *" # Every 6 hours
|
|
94
|
+
*
|
|
95
|
+
* Local test:
|
|
96
|
+
* echo '{}' | node main.js
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
const fs = require('fs');
|
|
100
|
+
|
|
101
|
+
function main() {
|
|
102
|
+
const raw = fs.readFileSync('/dev/stdin', 'utf-8');
|
|
103
|
+
let data;
|
|
104
|
+
try {
|
|
105
|
+
data = raw.trim() ? JSON.parse(raw) : {};
|
|
106
|
+
} catch {
|
|
107
|
+
console.log(JSON.stringify({ error: 'Invalid JSON input' }));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const dryRun = (data.options || {}).dry_run || false;
|
|
112
|
+
|
|
113
|
+
// --- Your scheduled job logic here ---
|
|
114
|
+
// Common patterns:
|
|
115
|
+
// - Fetch data from an API and store results
|
|
116
|
+
// - Generate a daily/weekly report
|
|
117
|
+
// - Clean up old records or files
|
|
118
|
+
// - Send digest notifications (email, Slack, Discord)
|
|
119
|
+
//
|
|
120
|
+
// To use workspace secrets (API keys, webhook URLs):
|
|
121
|
+
// 1. Add to "required_secrets" in orchagent.json
|
|
122
|
+
// 2. Set in workspace: orch secrets set MY_SECRET value
|
|
123
|
+
// 3. Access via: process.env.MY_SECRET
|
|
124
|
+
//
|
|
125
|
+
// Example: send to a webhook
|
|
126
|
+
// const https = require('https');
|
|
127
|
+
// const url = new URL(process.env.WEBHOOK_URL);
|
|
128
|
+
// const req = https.request(url, { method: 'POST', headers: { 'Content-Type': 'application/json' } });
|
|
129
|
+
// req.write(JSON.stringify(payload));
|
|
130
|
+
// req.end();
|
|
131
|
+
|
|
132
|
+
const now = new Date().toISOString();
|
|
133
|
+
|
|
134
|
+
const report = {
|
|
135
|
+
generated_at: now,
|
|
136
|
+
status: dryRun ? 'dry_run' : 'completed',
|
|
137
|
+
summary: 'Scheduled job ran successfully',
|
|
138
|
+
items_processed: 0,
|
|
139
|
+
};
|
|
140
|
+
// --- End your logic ---
|
|
141
|
+
|
|
142
|
+
console.log(JSON.stringify({
|
|
143
|
+
result: report,
|
|
144
|
+
success: true,
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
main();
|
|
149
|
+
`;
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Schema
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
exports.CRON_JOB_SCHEMA = `{
|
|
154
|
+
"input": {
|
|
155
|
+
"type": "object",
|
|
156
|
+
"properties": {
|
|
157
|
+
"options": {
|
|
158
|
+
"type": "object",
|
|
159
|
+
"description": "Optional configuration for this run",
|
|
160
|
+
"properties": {
|
|
161
|
+
"dry_run": {
|
|
162
|
+
"type": "boolean",
|
|
163
|
+
"description": "If true, simulate without making changes"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"output": {
|
|
170
|
+
"type": "object",
|
|
171
|
+
"properties": {
|
|
172
|
+
"result": {
|
|
173
|
+
"type": "object",
|
|
174
|
+
"description": "Job results and summary"
|
|
175
|
+
},
|
|
176
|
+
"success": {
|
|
177
|
+
"type": "boolean",
|
|
178
|
+
"description": "Whether the job completed successfully"
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
"required": ["result", "success"]
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
`;
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// README
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
function cronJobReadme(agentName) {
|
|
189
|
+
return `# ${agentName}
|
|
190
|
+
|
|
191
|
+
A scheduled job that runs on a cron schedule.
|
|
192
|
+
|
|
193
|
+
## Setup
|
|
194
|
+
|
|
195
|
+
### 1. Edit the job logic
|
|
196
|
+
|
|
197
|
+
Edit \`main.py\` (or \`main.js\`) with your scheduled task logic — data processing, report generation, API syncs, notifications, etc.
|
|
198
|
+
|
|
199
|
+
### 2. Publish
|
|
200
|
+
|
|
201
|
+
\`\`\`sh
|
|
202
|
+
orch publish
|
|
203
|
+
\`\`\`
|
|
204
|
+
|
|
205
|
+
### 3. Schedule
|
|
206
|
+
|
|
207
|
+
\`\`\`sh
|
|
208
|
+
# Every Monday at 9 AM UTC
|
|
209
|
+
orch schedule create <org>/${agentName} --cron "0 9 * * 1"
|
|
210
|
+
|
|
211
|
+
# Every day at midnight
|
|
212
|
+
orch schedule create <org>/${agentName} --cron "0 0 * * *"
|
|
213
|
+
|
|
214
|
+
# Every 6 hours
|
|
215
|
+
orch schedule create <org>/${agentName} --cron "0 */6 * * *"
|
|
216
|
+
\`\`\`
|
|
217
|
+
|
|
218
|
+
### 4. Monitor
|
|
219
|
+
|
|
220
|
+
\`\`\`sh
|
|
221
|
+
orch schedule list # View all schedules
|
|
222
|
+
orch logs <org>/${agentName} # View recent runs
|
|
223
|
+
orch metrics <org>/${agentName} # View execution stats
|
|
224
|
+
\`\`\`
|
|
225
|
+
|
|
226
|
+
## Common Cron Patterns
|
|
227
|
+
|
|
228
|
+
| Pattern | Schedule |
|
|
229
|
+
|---------|----------|
|
|
230
|
+
| \`0 9 * * 1\` | Every Monday at 9 AM |
|
|
231
|
+
| \`0 0 * * *\` | Daily at midnight |
|
|
232
|
+
| \`0 */6 * * *\` | Every 6 hours |
|
|
233
|
+
| \`0 9 * * 1-5\` | Weekdays at 9 AM |
|
|
234
|
+
| \`0 0 1 * *\` | First of each month |
|
|
235
|
+
|
|
236
|
+
## Input
|
|
237
|
+
|
|
238
|
+
The job receives optional input when triggered:
|
|
239
|
+
|
|
240
|
+
| Field | Type | Description |
|
|
241
|
+
|-------|------|-------------|
|
|
242
|
+
| \`options.dry_run\` | boolean | If true, simulate without making changes |
|
|
243
|
+
|
|
244
|
+
You can also trigger manually with custom input:
|
|
245
|
+
|
|
246
|
+
\`\`\`sh
|
|
247
|
+
orch run <org>/${agentName} --data '{"options": {"dry_run": true}}'
|
|
248
|
+
\`\`\`
|
|
249
|
+
|
|
250
|
+
## Environment Variables
|
|
251
|
+
|
|
252
|
+
To use API keys or webhook URLs, add them to \`required_secrets\` in orchagent.json, then set them in your workspace:
|
|
253
|
+
|
|
254
|
+
\`\`\`sh
|
|
255
|
+
orch secrets set MY_API_KEY <value>
|
|
256
|
+
orch secrets set WEBHOOK_URL <value>
|
|
257
|
+
\`\`\`
|
|
258
|
+
`;
|
|
259
|
+
}
|
package/dist/commands/update.js
CHANGED
|
@@ -9,6 +9,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const chalk_1 = __importDefault(require("chalk"));
|
|
10
10
|
const config_1 = require("../lib/config");
|
|
11
11
|
const api_1 = require("../lib/api");
|
|
12
|
+
const errors_1 = require("../lib/errors");
|
|
12
13
|
const analytics_1 = require("../lib/analytics");
|
|
13
14
|
const adapters_1 = require("../adapters");
|
|
14
15
|
const installed_1 = require("../lib/installed");
|
|
@@ -16,20 +17,47 @@ const agents_md_utils_1 = require("../lib/agents-md-utils");
|
|
|
16
17
|
async function fetchLatestAgent(config, agentRef) {
|
|
17
18
|
const [org, name] = agentRef.split('/');
|
|
18
19
|
if (!org || !name)
|
|
19
|
-
return
|
|
20
|
+
return { status: 'bad_ref' };
|
|
21
|
+
// Try public endpoint first
|
|
20
22
|
try {
|
|
21
|
-
// Try to get latest version
|
|
22
23
|
const agent = await (0, api_1.publicRequest)(config, `/public/agents/${org}/${name}/latest`);
|
|
23
24
|
return {
|
|
25
|
+
status: 'found',
|
|
24
26
|
agent: { ...agent, org_slug: org },
|
|
25
|
-
latestVersion: agent.version
|
|
27
|
+
latestVersion: agent.version,
|
|
28
|
+
private: false,
|
|
26
29
|
};
|
|
27
30
|
}
|
|
28
31
|
catch (err) {
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
// Network errors should propagate — don't confuse with 404
|
|
33
|
+
if (err instanceof errors_1.NetworkError)
|
|
34
|
+
throw err;
|
|
35
|
+
if (!(err instanceof api_1.ApiError) || err.status !== 404)
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
// Public endpoint returned 404 — try authenticated endpoint for private agents
|
|
39
|
+
if (!config.apiKey) {
|
|
40
|
+
return { status: 'not_found_no_auth' };
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const workspaceId = await (0, api_1.resolveWorkspaceIdForOrg)(config, org);
|
|
44
|
+
const userOrg = await (0, api_1.getOrg)(config, workspaceId);
|
|
45
|
+
if (userOrg.slug !== org) {
|
|
46
|
+
return { status: 'not_found' };
|
|
31
47
|
}
|
|
32
|
-
|
|
48
|
+
const myAgent = await (0, api_1.getMyAgent)(config, name, 'latest', workspaceId);
|
|
49
|
+
if (!myAgent) {
|
|
50
|
+
return { status: 'not_found' };
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
status: 'found',
|
|
54
|
+
agent: { ...myAgent, org_slug: org },
|
|
55
|
+
latestVersion: myAgent.version,
|
|
56
|
+
private: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return { status: 'not_found' };
|
|
33
61
|
}
|
|
34
62
|
}
|
|
35
63
|
function registerUpdateCommand(program) {
|
|
@@ -77,11 +105,20 @@ function registerUpdateCommand(program) {
|
|
|
77
105
|
fileStatuses.push({ item, status: await (0, installed_1.checkModified)(item) });
|
|
78
106
|
}
|
|
79
107
|
// Fetch latest version once per agent
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
82
|
-
process.stdout.write(` ${chalk_1.default.yellow('?')} ${agentName} -
|
|
108
|
+
const result = await fetchLatestAgent(resolved, agentName);
|
|
109
|
+
if (result.status === 'not_found_no_auth') {
|
|
110
|
+
process.stdout.write(` ${chalk_1.default.yellow('?')} ${agentName} - not found publicly. Log in with ${chalk_1.default.cyan('orch login')} to check private agents.\n`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (result.status === 'not_found') {
|
|
114
|
+
process.stdout.write(` ${chalk_1.default.yellow('?')} ${agentName} - agent not found\n`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (result.status === 'bad_ref') {
|
|
118
|
+
process.stdout.write(` ${chalk_1.default.yellow('?')} ${agentName} - invalid agent reference\n`);
|
|
83
119
|
continue;
|
|
84
120
|
}
|
|
121
|
+
const latest = result;
|
|
85
122
|
// Use the version from the first entry (all entries for the same
|
|
86
123
|
// agent should share the same version after install/update)
|
|
87
124
|
const installedVersion = entries[0].version;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `orch validate` — Validate agent or skill configuration without publishing.
|
|
4
|
+
*
|
|
5
|
+
* Runs all pre-publish checks locally: config files, schemas, prompt,
|
|
6
|
+
* dependencies, code scanning. Optionally runs server-side validation
|
|
7
|
+
* with --server flag.
|
|
8
|
+
*
|
|
9
|
+
* Exit codes: 0 = valid, 1 = errors found
|
|
10
|
+
*/
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.registerValidateCommand = registerValidateCommand;
|
|
16
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
17
|
+
const config_1 = require("../lib/config");
|
|
18
|
+
const api_1 = require("../lib/api");
|
|
19
|
+
const validate_1 = require("../lib/validate");
|
|
20
|
+
const publish_1 = require("./publish");
|
|
21
|
+
const errors_1 = require("../lib/errors");
|
|
22
|
+
const analytics_1 = require("../lib/analytics");
|
|
23
|
+
function issueIcon(level) {
|
|
24
|
+
switch (level) {
|
|
25
|
+
case 'error': return chalk_1.default.red('✗');
|
|
26
|
+
case 'warning': return chalk_1.default.yellow('⚠');
|
|
27
|
+
case 'info': return chalk_1.default.blue('ℹ');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function formatIssue(issue) {
|
|
31
|
+
const icon = issueIcon(issue.level);
|
|
32
|
+
const file = issue.file ? chalk_1.default.dim(` (${issue.file})`) : '';
|
|
33
|
+
return ` ${icon} ${issue.message}${file}`;
|
|
34
|
+
}
|
|
35
|
+
function printResult(result, serverIssues) {
|
|
36
|
+
const allIssues = [...result.issues, ...(serverIssues || [])];
|
|
37
|
+
const errors = allIssues.filter(i => i.level === 'error');
|
|
38
|
+
const warnings = allIssues.filter(i => i.level === 'warning');
|
|
39
|
+
const infos = allIssues.filter(i => i.level === 'info');
|
|
40
|
+
const m = result.metadata;
|
|
41
|
+
// Header
|
|
42
|
+
const label = m.isSkill ? 'skill' : 'agent';
|
|
43
|
+
const name = m.agentName || '(unknown)';
|
|
44
|
+
process.stderr.write(`\nValidating ${label}: ${chalk_1.default.bold(name)}\n\n`);
|
|
45
|
+
// Summary line: type, engine, mode
|
|
46
|
+
if (!m.isSkill && m.agentType && m.executionEngine) {
|
|
47
|
+
process.stderr.write(chalk_1.default.dim(` Type: ${m.agentType} (${m.executionEngine}), Run mode: ${m.runMode || 'on_demand'}\n\n`));
|
|
48
|
+
}
|
|
49
|
+
// All issues sorted: errors first, then warnings, then info
|
|
50
|
+
for (const issue of errors)
|
|
51
|
+
process.stderr.write(formatIssue(issue) + '\n');
|
|
52
|
+
for (const issue of warnings)
|
|
53
|
+
process.stderr.write(formatIssue(issue) + '\n');
|
|
54
|
+
for (const issue of infos)
|
|
55
|
+
process.stderr.write(formatIssue(issue) + '\n');
|
|
56
|
+
// Metadata summary (only if no critical errors)
|
|
57
|
+
if (errors.length === 0 && !m.isSkill) {
|
|
58
|
+
if (m.hasPrompt)
|
|
59
|
+
process.stderr.write(` ${chalk_1.default.green('✓')} prompt.md found\n`);
|
|
60
|
+
if (m.hasSchema)
|
|
61
|
+
process.stderr.write(` ${chalk_1.default.green('✓')} schema.json found\n`);
|
|
62
|
+
if (m.executionEngine === 'managed_loop') {
|
|
63
|
+
process.stderr.write(` ${chalk_1.default.green('✓')} Custom tools: ${m.customToolCount}, Max turns: ${m.maxTurns || 25}\n`);
|
|
64
|
+
}
|
|
65
|
+
if (m.bundleEntrypoint) {
|
|
66
|
+
process.stderr.write(` ${chalk_1.default.green('✓')} Entrypoint: ${m.bundleEntrypoint}\n`);
|
|
67
|
+
}
|
|
68
|
+
if (m.bundleSizeBytes !== undefined && m.bundleFileCount !== undefined) {
|
|
69
|
+
process.stderr.write(` ${chalk_1.default.green('✓')} Bundle: ${m.bundleFileCount} files, ${(m.bundleSizeBytes / 1024).toFixed(1)} KB\n`);
|
|
70
|
+
}
|
|
71
|
+
if (m.sdkCompatible) {
|
|
72
|
+
process.stderr.write(` ${chalk_1.default.green('✓')} SDK detected (Local Ready)\n`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Final verdict
|
|
76
|
+
process.stderr.write('\n');
|
|
77
|
+
const parts = [];
|
|
78
|
+
if (warnings.length > 0)
|
|
79
|
+
parts.push(`${warnings.length} warning${warnings.length > 1 ? 's' : ''}`);
|
|
80
|
+
if (infos.length > 0)
|
|
81
|
+
parts.push(`${infos.length} note${infos.length > 1 ? 's' : ''}`);
|
|
82
|
+
const suffix = parts.length > 0 ? ` (${parts.join(', ')})` : '';
|
|
83
|
+
if (errors.length > 0) {
|
|
84
|
+
process.stderr.write(chalk_1.default.red.bold(`✗ Validation failed: ${errors.length} error${errors.length > 1 ? 's' : ''}${suffix}\n`));
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
process.stderr.write(chalk_1.default.green.bold(`✓ Validation passed${suffix}\n`));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function toJsonOutput(result, serverIssues) {
|
|
91
|
+
const allIssues = [...result.issues, ...(serverIssues || [])];
|
|
92
|
+
return {
|
|
93
|
+
valid: result.valid && allIssues.filter(i => i.level === 'error').length === 0,
|
|
94
|
+
errors: allIssues.filter(i => i.level === 'error').map(i => ({ message: i.message, file: i.file })),
|
|
95
|
+
warnings: allIssues.filter(i => i.level === 'warning').map(i => ({ message: i.message, file: i.file })),
|
|
96
|
+
info: allIssues.filter(i => i.level === 'info').map(i => ({ message: i.message, file: i.file })),
|
|
97
|
+
metadata: {
|
|
98
|
+
name: result.metadata.agentName,
|
|
99
|
+
type: result.metadata.agentType,
|
|
100
|
+
execution_engine: result.metadata.executionEngine,
|
|
101
|
+
run_mode: result.metadata.runMode,
|
|
102
|
+
is_skill: result.metadata.isSkill,
|
|
103
|
+
has_prompt: result.metadata.hasPrompt,
|
|
104
|
+
has_schema: result.metadata.hasSchema,
|
|
105
|
+
sdk_compatible: result.metadata.sdkCompatible,
|
|
106
|
+
entrypoint: result.metadata.bundleEntrypoint,
|
|
107
|
+
custom_tools: result.metadata.customToolCount,
|
|
108
|
+
max_turns: result.metadata.maxTurns,
|
|
109
|
+
bundle_size_bytes: result.metadata.bundleSizeBytes,
|
|
110
|
+
bundle_file_count: result.metadata.bundleFileCount,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function registerValidateCommand(program) {
|
|
115
|
+
program
|
|
116
|
+
.command('validate')
|
|
117
|
+
.alias('lint')
|
|
118
|
+
.description('Validate agent or skill configuration without publishing')
|
|
119
|
+
.option('--profile <name>', 'Use API key from named profile')
|
|
120
|
+
.option('--json', 'Output as JSON (for CI/CD)')
|
|
121
|
+
.option('--server', 'Also run server-side validation (requires auth)')
|
|
122
|
+
.option('--url <url>', 'Agent URL (for code-based agents without local code)')
|
|
123
|
+
.option('--docker', 'Validate with Dockerfile inclusion')
|
|
124
|
+
.action(async (options) => {
|
|
125
|
+
const cwd = process.cwd();
|
|
126
|
+
// Run local validation
|
|
127
|
+
const result = await (0, validate_1.validateAgentProject)(cwd, {
|
|
128
|
+
url: options.url,
|
|
129
|
+
docker: options.docker,
|
|
130
|
+
});
|
|
131
|
+
// Online checks (dependencies + server-side validation)
|
|
132
|
+
const serverIssues = [];
|
|
133
|
+
if (options.server || result.metadata.manifest?.manifest?.dependencies?.length) {
|
|
134
|
+
try {
|
|
135
|
+
const config = await (0, config_1.getResolvedConfig)({}, options.profile);
|
|
136
|
+
if (!config.apiKey) {
|
|
137
|
+
if (options.server) {
|
|
138
|
+
serverIssues.push({
|
|
139
|
+
level: 'warning',
|
|
140
|
+
message: 'Server validation skipped: not logged in. Run `orch login` first.',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// Resolve workspace
|
|
146
|
+
const configFile = await (0, config_1.loadConfig)();
|
|
147
|
+
let workspaceId;
|
|
148
|
+
if (configFile.workspace && !options.profile) {
|
|
149
|
+
try {
|
|
150
|
+
const { workspaces } = await (0, api_1.request)(config, 'GET', '/workspaces');
|
|
151
|
+
const ws = workspaces.find(w => w.slug === configFile.workspace);
|
|
152
|
+
if (ws)
|
|
153
|
+
workspaceId = ws.id;
|
|
154
|
+
}
|
|
155
|
+
catch { /* skip workspace resolution */ }
|
|
156
|
+
}
|
|
157
|
+
// Dependency checks
|
|
158
|
+
const deps = result.metadata.manifest?.manifest?.dependencies;
|
|
159
|
+
if (deps?.length) {
|
|
160
|
+
try {
|
|
161
|
+
const org = await (0, api_1.getOrg)(config, workspaceId);
|
|
162
|
+
const depResults = await (0, publish_1.checkDependencies)(config, deps, org.slug, workspaceId);
|
|
163
|
+
const notFound = depResults.filter(r => r.status === 'not_found');
|
|
164
|
+
const notCallable = depResults.filter(r => r.status === 'found_not_callable');
|
|
165
|
+
if (notFound.length > 0) {
|
|
166
|
+
for (const dep of notFound) {
|
|
167
|
+
serverIssues.push({
|
|
168
|
+
level: 'warning',
|
|
169
|
+
message: `Unpublished dependency: ${dep.ref}`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (notCallable.length > 0) {
|
|
174
|
+
for (const dep of notCallable) {
|
|
175
|
+
serverIssues.push({
|
|
176
|
+
level: 'warning',
|
|
177
|
+
message: `Dependency has callable: false: ${dep.ref}`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
serverIssues.push({
|
|
184
|
+
level: 'warning',
|
|
185
|
+
message: 'Could not check dependencies (network error)',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Server-side validation
|
|
190
|
+
if (options.server && result.valid && result.metadata.agentName) {
|
|
191
|
+
try {
|
|
192
|
+
const m = result.metadata;
|
|
193
|
+
const manifest = m.manifest;
|
|
194
|
+
const validation = await (0, api_1.validateAgentPublish)(config, {
|
|
195
|
+
name: m.agentName,
|
|
196
|
+
type: m.agentType || 'agent',
|
|
197
|
+
run_mode: m.runMode,
|
|
198
|
+
callable: m.callable,
|
|
199
|
+
description: manifest?.description,
|
|
200
|
+
is_public: false,
|
|
201
|
+
supported_providers: m.supportedProviders,
|
|
202
|
+
default_models: manifest?.default_models,
|
|
203
|
+
timeout_seconds: manifest?.timeout_seconds,
|
|
204
|
+
manifest: manifest?.manifest,
|
|
205
|
+
required_secrets: m.requiredSecrets,
|
|
206
|
+
default_skills: manifest?.default_skills,
|
|
207
|
+
skills_locked: manifest?.skills_locked,
|
|
208
|
+
environment: manifest?.environment,
|
|
209
|
+
}, workspaceId);
|
|
210
|
+
if (validation.warnings?.length) {
|
|
211
|
+
for (const w of validation.warnings) {
|
|
212
|
+
serverIssues.push({ level: 'warning', message: `Server: ${w}` });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!validation.valid) {
|
|
216
|
+
for (const e of validation.errors) {
|
|
217
|
+
serverIssues.push({ level: 'error', message: `Server: ${e}` });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
serverIssues.push({ level: 'info', message: 'Server-side validation passed' });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
serverIssues.push({
|
|
226
|
+
level: 'warning',
|
|
227
|
+
message: 'Could not reach server for validation (offline?)',
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
if (options.server) {
|
|
235
|
+
serverIssues.push({
|
|
236
|
+
level: 'warning',
|
|
237
|
+
message: 'Server validation skipped: authentication error',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const allValid = result.valid && serverIssues.filter(i => i.level === 'error').length === 0;
|
|
243
|
+
await (0, analytics_1.track)('cli_validate', {
|
|
244
|
+
valid: allValid,
|
|
245
|
+
errors: [...result.issues, ...serverIssues].filter(i => i.level === 'error').length,
|
|
246
|
+
warnings: [...result.issues, ...serverIssues].filter(i => i.level === 'warning').length,
|
|
247
|
+
type: result.metadata.agentType,
|
|
248
|
+
engine: result.metadata.executionEngine,
|
|
249
|
+
server: options.server || false,
|
|
250
|
+
json: options.json || false,
|
|
251
|
+
});
|
|
252
|
+
if (options.json) {
|
|
253
|
+
process.stdout.write(JSON.stringify(toJsonOutput(result, serverIssues), null, 2) + '\n');
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
printResult(result, serverIssues);
|
|
257
|
+
}
|
|
258
|
+
if (!allValid) {
|
|
259
|
+
const err = new errors_1.CliError('Validation failed', errors_1.ExitCodes.INVALID_INPUT);
|
|
260
|
+
err.displayed = true;
|
|
261
|
+
throw err;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|