@orchagent/cli 0.3.53 → 0.3.55
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/index.js +4 -0
- package/dist/commands/init.js +73 -52
- package/dist/commands/publish.js +133 -74
- package/dist/commands/run.js +87 -31
- package/dist/commands/schedule.js +129 -2
- package/dist/commands/service.js +401 -0
- package/dist/commands/transfer.js +135 -0
- package/dist/lib/api.js +18 -1
- package/package.json +1 -1
|
@@ -0,0 +1,401 @@
|
|
|
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.registerServiceCommand = registerServiceCommand;
|
|
7
|
+
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const config_1 = require("../lib/config");
|
|
10
|
+
const api_1 = require("../lib/api");
|
|
11
|
+
const errors_1 = require("../lib/errors");
|
|
12
|
+
const output_1 = require("../lib/output");
|
|
13
|
+
const spinner_1 = require("../lib/spinner");
|
|
14
|
+
// ============================================
|
|
15
|
+
// HELPERS
|
|
16
|
+
// ============================================
|
|
17
|
+
async function resolveWorkspaceId(config, slug) {
|
|
18
|
+
const configFile = await (0, config_1.loadConfig)();
|
|
19
|
+
const targetSlug = slug ?? configFile.workspace;
|
|
20
|
+
if (!targetSlug) {
|
|
21
|
+
throw new errors_1.CliError('No workspace specified. Use --workspace <slug> or run `orch workspace use <slug>` first.');
|
|
22
|
+
}
|
|
23
|
+
const response = await (0, api_1.request)(config, 'GET', '/workspaces');
|
|
24
|
+
const workspace = response.workspaces.find((w) => w.slug === targetSlug);
|
|
25
|
+
if (!workspace) {
|
|
26
|
+
throw new errors_1.CliError(`Workspace '${targetSlug}' not found.`);
|
|
27
|
+
}
|
|
28
|
+
return workspace.id;
|
|
29
|
+
}
|
|
30
|
+
function formatDate(iso) {
|
|
31
|
+
if (!iso)
|
|
32
|
+
return '-';
|
|
33
|
+
return new Date(iso).toLocaleString();
|
|
34
|
+
}
|
|
35
|
+
function stateColor(state) {
|
|
36
|
+
switch (state) {
|
|
37
|
+
case 'running':
|
|
38
|
+
return chalk_1.default.green(state);
|
|
39
|
+
case 'provisioning':
|
|
40
|
+
return chalk_1.default.yellow(state);
|
|
41
|
+
case 'unhealthy':
|
|
42
|
+
case 'failed':
|
|
43
|
+
return chalk_1.default.red(state);
|
|
44
|
+
case 'deleting':
|
|
45
|
+
case 'deleted':
|
|
46
|
+
return chalk_1.default.gray(state);
|
|
47
|
+
default:
|
|
48
|
+
return state;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function healthColor(health) {
|
|
52
|
+
switch (health) {
|
|
53
|
+
case 'healthy':
|
|
54
|
+
return chalk_1.default.green(health);
|
|
55
|
+
case 'degraded':
|
|
56
|
+
return chalk_1.default.yellow(health);
|
|
57
|
+
case 'unhealthy':
|
|
58
|
+
return chalk_1.default.red(health);
|
|
59
|
+
default:
|
|
60
|
+
return chalk_1.default.gray(health);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function severityColor(severity, message) {
|
|
64
|
+
switch (severity.toUpperCase()) {
|
|
65
|
+
case 'ERROR':
|
|
66
|
+
case 'CRITICAL':
|
|
67
|
+
return chalk_1.default.red(message);
|
|
68
|
+
case 'WARNING':
|
|
69
|
+
return chalk_1.default.yellow(message);
|
|
70
|
+
case 'INFO':
|
|
71
|
+
return chalk_1.default.white(message);
|
|
72
|
+
default:
|
|
73
|
+
return chalk_1.default.gray(message);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ============================================
|
|
77
|
+
// COMMAND REGISTRATION
|
|
78
|
+
// ============================================
|
|
79
|
+
function registerServiceCommand(program) {
|
|
80
|
+
const service = program
|
|
81
|
+
.command('service')
|
|
82
|
+
.description('Manage always-on automation services');
|
|
83
|
+
// orch service deploy <org/agent[@version]>
|
|
84
|
+
service
|
|
85
|
+
.command('deploy <agent>')
|
|
86
|
+
.description('Deploy an always-on automation service')
|
|
87
|
+
.option('--workspace <slug>', 'Workspace slug (default: current workspace)')
|
|
88
|
+
.option('--name <service-name>', 'Service name (default: agent name)')
|
|
89
|
+
.option('--min-instances <n>', 'Minimum instances (default: 1)', '1')
|
|
90
|
+
.option('--max-instances <n>', 'Maximum instances (default: 1)', '1')
|
|
91
|
+
.option('--env <KEY=VALUE>', 'Environment variable (repeatable)', collectKeyValue, {})
|
|
92
|
+
.option('--secret <NAME>', 'Workspace secret name (repeatable)', collectArray, [])
|
|
93
|
+
.option('--command <cmd>', 'Override entrypoint command')
|
|
94
|
+
.option('--arg <value>', 'Command argument (repeatable)', collectArray, [])
|
|
95
|
+
.option('--json', 'Output as JSON')
|
|
96
|
+
.action(async (agentArg, options) => {
|
|
97
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
98
|
+
if (!config.apiKey) {
|
|
99
|
+
throw new errors_1.CliError('Missing API key. Run `orch login` first.');
|
|
100
|
+
}
|
|
101
|
+
const workspaceId = await resolveWorkspaceId(config, options.workspace);
|
|
102
|
+
// Parse agent ref: org/agent[@version]
|
|
103
|
+
const parts = agentArg.split('/');
|
|
104
|
+
if (parts.length !== 2) {
|
|
105
|
+
throw new errors_1.CliError('Agent must be in format: org/agent[@version]');
|
|
106
|
+
}
|
|
107
|
+
const [, agentPart] = parts;
|
|
108
|
+
const atIndex = agentPart.indexOf('@');
|
|
109
|
+
const agentName = atIndex >= 0 ? agentPart.slice(0, atIndex) : agentPart;
|
|
110
|
+
const agentVersion = atIndex >= 0 ? agentPart.slice(atIndex + 1) : 'latest';
|
|
111
|
+
const serviceName = options.name || agentName;
|
|
112
|
+
const minInstances = parseInt(options.minInstances, 10);
|
|
113
|
+
const maxInstances = parseInt(options.maxInstances, 10);
|
|
114
|
+
if (isNaN(minInstances) || isNaN(maxInstances)) {
|
|
115
|
+
throw new errors_1.CliError('--min-instances and --max-instances must be numbers');
|
|
116
|
+
}
|
|
117
|
+
// First, resolve the agent to get agent_id
|
|
118
|
+
const spinner = (0, spinner_1.createSpinner)('Resolving agent...');
|
|
119
|
+
spinner.start();
|
|
120
|
+
let agentId;
|
|
121
|
+
try {
|
|
122
|
+
// List agents for the workspace via the correct gateway endpoint
|
|
123
|
+
const agentsList = await (0, api_1.request)(config, 'GET', `/agents?workspace_id=${workspaceId}`);
|
|
124
|
+
let match;
|
|
125
|
+
if (agentVersion === 'latest') {
|
|
126
|
+
// Filter all matching agents by name, sort by created_at desc to get newest
|
|
127
|
+
const candidates = agentsList
|
|
128
|
+
.filter(a => a.name === agentName)
|
|
129
|
+
.sort((a, b) => (b.created_at ?? '').localeCompare(a.created_at ?? ''));
|
|
130
|
+
match = candidates[0];
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
match = agentsList.find(a => a.name === agentName && a.version === agentVersion);
|
|
134
|
+
}
|
|
135
|
+
if (!match) {
|
|
136
|
+
spinner.fail('Agent not found');
|
|
137
|
+
throw new errors_1.CliError(`Agent '${agentName}' (version ${agentVersion}) not found in workspace`);
|
|
138
|
+
}
|
|
139
|
+
agentId = match.id;
|
|
140
|
+
spinner.succeed('Agent resolved');
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
if (e instanceof errors_1.CliError)
|
|
144
|
+
throw e;
|
|
145
|
+
spinner.fail('Failed to resolve agent');
|
|
146
|
+
throw e;
|
|
147
|
+
}
|
|
148
|
+
const deploySpinner = (0, spinner_1.createSpinner)('Deploying service...');
|
|
149
|
+
deploySpinner.start();
|
|
150
|
+
try {
|
|
151
|
+
const result = await (0, api_1.request)(config, 'POST', `/workspaces/${workspaceId}/services`, {
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
agent_id: agentId,
|
|
154
|
+
agent_name: agentName,
|
|
155
|
+
agent_version: agentVersion,
|
|
156
|
+
service_name: serviceName,
|
|
157
|
+
min_instances: minInstances,
|
|
158
|
+
max_instances: maxInstances,
|
|
159
|
+
command: options.command || null,
|
|
160
|
+
args: options.arg.length > 0 ? options.arg : null,
|
|
161
|
+
env: Object.keys(options.env).length > 0 ? options.env : null,
|
|
162
|
+
secret_names: options.secret.length > 0 ? options.secret : null,
|
|
163
|
+
}),
|
|
164
|
+
headers: { 'Content-Type': 'application/json' },
|
|
165
|
+
});
|
|
166
|
+
deploySpinner.succeed('Service deployed');
|
|
167
|
+
if (options.json) {
|
|
168
|
+
(0, output_1.printJson)(result.service);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const svc = result.service;
|
|
172
|
+
process.stdout.write(`\n${chalk_1.default.green('\u2713')} Service deployed successfully\n\n`);
|
|
173
|
+
process.stdout.write(` ${chalk_1.default.bold('ID:')} ${svc.id}\n`);
|
|
174
|
+
process.stdout.write(` ${chalk_1.default.bold('Name:')} ${svc.service_name}\n`);
|
|
175
|
+
process.stdout.write(` ${chalk_1.default.bold('Agent:')} ${svc.agent_name}@${svc.agent_version}\n`);
|
|
176
|
+
process.stdout.write(` ${chalk_1.default.bold('State:')} ${stateColor(svc.current_state)}\n`);
|
|
177
|
+
process.stdout.write(` ${chalk_1.default.bold('URL:')} ${svc.cloud_run_url || '-'}\n`);
|
|
178
|
+
process.stdout.write(`\n`);
|
|
179
|
+
process.stdout.write(chalk_1.default.gray(`View logs: orch service logs ${svc.id}\n`));
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
deploySpinner.fail('Deploy failed');
|
|
183
|
+
throw e;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
// orch service list
|
|
187
|
+
service
|
|
188
|
+
.command('list')
|
|
189
|
+
.description('List automation services in your workspace')
|
|
190
|
+
.option('--workspace <slug>', 'Workspace slug (default: current workspace)')
|
|
191
|
+
.option('--status <state>', 'Filter by state')
|
|
192
|
+
.option('--json', 'Output as JSON')
|
|
193
|
+
.action(async (options) => {
|
|
194
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
195
|
+
if (!config.apiKey) {
|
|
196
|
+
throw new errors_1.CliError('Missing API key. Run `orch login` first.');
|
|
197
|
+
}
|
|
198
|
+
const workspaceId = await resolveWorkspaceId(config, options.workspace);
|
|
199
|
+
const params = new URLSearchParams();
|
|
200
|
+
if (options.status)
|
|
201
|
+
params.set('status', options.status);
|
|
202
|
+
params.set('limit', '100');
|
|
203
|
+
const qs = params.toString() ? `?${params.toString()}` : '';
|
|
204
|
+
const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/services${qs}`);
|
|
205
|
+
if (options.json) {
|
|
206
|
+
(0, output_1.printJson)(result);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (!result.services.length) {
|
|
210
|
+
process.stdout.write('No services found.\n');
|
|
211
|
+
process.stdout.write(chalk_1.default.gray('Deploy one with: orch service deploy <org/agent>\n'));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const table = new cli_table3_1.default({
|
|
215
|
+
head: [
|
|
216
|
+
chalk_1.default.bold('Name'),
|
|
217
|
+
chalk_1.default.bold('Agent'),
|
|
218
|
+
chalk_1.default.bold('State'),
|
|
219
|
+
chalk_1.default.bold('Health'),
|
|
220
|
+
chalk_1.default.bold('Restarts'),
|
|
221
|
+
chalk_1.default.bold('Deployed'),
|
|
222
|
+
],
|
|
223
|
+
});
|
|
224
|
+
for (const svc of result.services) {
|
|
225
|
+
const stateLabel = svc.auto_paused_at
|
|
226
|
+
? chalk_1.default.bgRed.white(' CRASH-LOOP ')
|
|
227
|
+
: stateColor(svc.current_state);
|
|
228
|
+
const restartsLabel = svc.consecutive_restart_failures > 0
|
|
229
|
+
? `${svc.restart_count} (${chalk_1.default.red(String(svc.consecutive_restart_failures))} fails)`
|
|
230
|
+
: String(svc.restart_count);
|
|
231
|
+
table.push([
|
|
232
|
+
svc.service_name,
|
|
233
|
+
`${svc.agent_name}@${svc.agent_version}`,
|
|
234
|
+
stateLabel,
|
|
235
|
+
healthColor(svc.health_status),
|
|
236
|
+
restartsLabel,
|
|
237
|
+
formatDate(svc.last_deployed_at),
|
|
238
|
+
]);
|
|
239
|
+
}
|
|
240
|
+
process.stdout.write(`${table.toString()}\n`);
|
|
241
|
+
process.stdout.write(chalk_1.default.gray(`\n${result.total} service${result.total !== 1 ? 's' : ''}\n`));
|
|
242
|
+
});
|
|
243
|
+
// orch service logs <service-id>
|
|
244
|
+
service
|
|
245
|
+
.command('logs <service-id>')
|
|
246
|
+
.description('Fetch recent logs for a service')
|
|
247
|
+
.option('--workspace <slug>', 'Workspace slug (default: current workspace)')
|
|
248
|
+
.option('--limit <n>', 'Number of log lines (default: 100)', '100')
|
|
249
|
+
.option('--since <timestamp>', 'Only show logs after this ISO timestamp')
|
|
250
|
+
.option('--json', 'Output as JSON')
|
|
251
|
+
.action(async (serviceId, options) => {
|
|
252
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
253
|
+
if (!config.apiKey) {
|
|
254
|
+
throw new errors_1.CliError('Missing API key. Run `orch login` first.');
|
|
255
|
+
}
|
|
256
|
+
const workspaceId = await resolveWorkspaceId(config, options.workspace);
|
|
257
|
+
const params = new URLSearchParams();
|
|
258
|
+
params.set('limit', options.limit);
|
|
259
|
+
if (options.since)
|
|
260
|
+
params.set('since', options.since);
|
|
261
|
+
const qs = `?${params.toString()}`;
|
|
262
|
+
const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/services/${serviceId}/logs${qs}`);
|
|
263
|
+
if (options.json) {
|
|
264
|
+
(0, output_1.printJson)(result);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (!result.logs.length) {
|
|
268
|
+
process.stdout.write('No logs found.\n');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
for (const entry of result.logs) {
|
|
272
|
+
const ts = entry.timestamp ? new Date(entry.timestamp).toISOString().replace('T', ' ').replace('Z', '') : '???';
|
|
273
|
+
const sev = entry.severity.padEnd(7);
|
|
274
|
+
process.stdout.write(`${chalk_1.default.gray(ts)} ${severityColor(entry.severity, sev)} ${entry.message}\n`);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
// orch service restart <service-id>
|
|
278
|
+
service
|
|
279
|
+
.command('restart <service-id>')
|
|
280
|
+
.description('Restart an automation service')
|
|
281
|
+
.option('--workspace <slug>', 'Workspace slug (default: current workspace)')
|
|
282
|
+
.option('--json', 'Output as JSON')
|
|
283
|
+
.action(async (serviceId, options) => {
|
|
284
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
285
|
+
if (!config.apiKey) {
|
|
286
|
+
throw new errors_1.CliError('Missing API key. Run `orch login` first.');
|
|
287
|
+
}
|
|
288
|
+
const workspaceId = await resolveWorkspaceId(config, options.workspace);
|
|
289
|
+
const spinner = (0, spinner_1.createSpinner)('Restarting service...');
|
|
290
|
+
spinner.start();
|
|
291
|
+
try {
|
|
292
|
+
const result = await (0, api_1.request)(config, 'POST', `/workspaces/${workspaceId}/services/${serviceId}/restart`);
|
|
293
|
+
spinner.succeed('Service restarted');
|
|
294
|
+
if (options.json) {
|
|
295
|
+
(0, output_1.printJson)(result.service);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
process.stdout.write(`${chalk_1.default.green('\u2713')} Service '${result.service.service_name}' restarted (restarts: ${result.service.restart_count})\n`);
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
spinner.fail('Restart failed');
|
|
302
|
+
throw e;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
// orch service info <service-id>
|
|
306
|
+
service
|
|
307
|
+
.command('info <service-id>')
|
|
308
|
+
.description('Show detailed service information with events')
|
|
309
|
+
.option('--workspace <slug>', 'Workspace slug (default: current workspace)')
|
|
310
|
+
.option('--json', 'Output as JSON')
|
|
311
|
+
.action(async (serviceId, options) => {
|
|
312
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
313
|
+
if (!config.apiKey) {
|
|
314
|
+
throw new errors_1.CliError('Missing API key. Run `orch login` first.');
|
|
315
|
+
}
|
|
316
|
+
const workspaceId = await resolveWorkspaceId(config, options.workspace);
|
|
317
|
+
const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/services/${serviceId}`);
|
|
318
|
+
if (options.json) {
|
|
319
|
+
(0, output_1.printJson)(result.service);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const svc = result.service;
|
|
323
|
+
process.stdout.write(`\n${chalk_1.default.bold('Service Details')}\n\n`);
|
|
324
|
+
process.stdout.write(` ID: ${svc.id}\n`);
|
|
325
|
+
process.stdout.write(` Name: ${svc.service_name}\n`);
|
|
326
|
+
process.stdout.write(` Agent: ${svc.agent_name}@${svc.agent_version}\n`);
|
|
327
|
+
process.stdout.write(` State: ${stateColor(svc.current_state)}\n`);
|
|
328
|
+
process.stdout.write(` Health: ${healthColor(svc.health_status)}\n`);
|
|
329
|
+
if (svc.auto_paused_at) {
|
|
330
|
+
process.stdout.write(` ${chalk_1.default.bgRed.white(' CRASH-LOOP ')} auto-paused at ${formatDate(svc.auto_paused_at)}\n`);
|
|
331
|
+
}
|
|
332
|
+
process.stdout.write(` Restarts: ${svc.restart_count}\n`);
|
|
333
|
+
if (svc.consecutive_restart_failures > 0) {
|
|
334
|
+
process.stdout.write(` Fail Streak: ${chalk_1.default.red(String(svc.consecutive_restart_failures))} / ${svc.max_restart_failures}\n`);
|
|
335
|
+
}
|
|
336
|
+
process.stdout.write(` Instances: ${svc.min_instances}-${svc.max_instances}\n`);
|
|
337
|
+
process.stdout.write(` Cloud Run: ${svc.cloud_run_service || '-'}\n`);
|
|
338
|
+
process.stdout.write(` URL: ${svc.cloud_run_url || '-'}\n`);
|
|
339
|
+
process.stdout.write(` Deployed: ${formatDate(svc.last_deployed_at)}\n`);
|
|
340
|
+
process.stdout.write(` Last Restart: ${formatDate(svc.last_restart_at)}\n`);
|
|
341
|
+
if (svc.last_error) {
|
|
342
|
+
process.stdout.write(` Last Error: ${chalk_1.default.red(svc.last_error)}\n`);
|
|
343
|
+
}
|
|
344
|
+
if (svc.alert_webhook_url) {
|
|
345
|
+
process.stdout.write(` Alert URL: ${svc.alert_webhook_url.slice(0, 50)}...\n`);
|
|
346
|
+
}
|
|
347
|
+
// Events timeline
|
|
348
|
+
const events = svc.events || [];
|
|
349
|
+
if (events.length > 0) {
|
|
350
|
+
process.stdout.write(`\n${chalk_1.default.bold('Recent Events')}\n`);
|
|
351
|
+
for (const e of events.slice(0, 10)) {
|
|
352
|
+
const color = e.event_type.includes('fail') || e.event_type.includes('paused') ? chalk_1.default.red : chalk_1.default.gray;
|
|
353
|
+
process.stdout.write(` ${chalk_1.default.gray(formatDate(e.created_at))} ${color(e.event_type)} ${e.message || ''}\n`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
process.stdout.write('\n');
|
|
357
|
+
});
|
|
358
|
+
// orch service delete <service-id>
|
|
359
|
+
service
|
|
360
|
+
.command('delete <service-id>')
|
|
361
|
+
.description('Delete an automation service')
|
|
362
|
+
.option('--workspace <slug>', 'Workspace slug (default: current workspace)')
|
|
363
|
+
.option('--json', 'Output as JSON')
|
|
364
|
+
.action(async (serviceId, options) => {
|
|
365
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
366
|
+
if (!config.apiKey) {
|
|
367
|
+
throw new errors_1.CliError('Missing API key. Run `orch login` first.');
|
|
368
|
+
}
|
|
369
|
+
const workspaceId = await resolveWorkspaceId(config, options.workspace);
|
|
370
|
+
const spinner = (0, spinner_1.createSpinner)('Deleting service...');
|
|
371
|
+
spinner.start();
|
|
372
|
+
try {
|
|
373
|
+
const result = await (0, api_1.request)(config, 'DELETE', `/workspaces/${workspaceId}/services/${serviceId}`);
|
|
374
|
+
spinner.succeed('Service deleted');
|
|
375
|
+
if (options.json) {
|
|
376
|
+
(0, output_1.printJson)(result);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
process.stdout.write(`${chalk_1.default.green('\u2713')} Service '${result.service.service_name}' deleted\n`);
|
|
380
|
+
}
|
|
381
|
+
catch (e) {
|
|
382
|
+
spinner.fail('Delete failed');
|
|
383
|
+
throw e;
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
// ============================================
|
|
388
|
+
// OPTION COLLECTORS
|
|
389
|
+
// ============================================
|
|
390
|
+
function collectKeyValue(value, previous) {
|
|
391
|
+
const idx = value.indexOf('=');
|
|
392
|
+
if (idx < 0) {
|
|
393
|
+
throw new errors_1.CliError(`Invalid env format: '${value}'. Use KEY=VALUE.`);
|
|
394
|
+
}
|
|
395
|
+
previous[value.slice(0, idx)] = value.slice(idx + 1);
|
|
396
|
+
return previous;
|
|
397
|
+
}
|
|
398
|
+
function collectArray(value, previous) {
|
|
399
|
+
previous.push(value);
|
|
400
|
+
return previous;
|
|
401
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
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.registerTransferCommand = registerTransferCommand;
|
|
7
|
+
const promises_1 = __importDefault(require("readline/promises"));
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const config_1 = require("../lib/config");
|
|
10
|
+
const api_1 = require("../lib/api");
|
|
11
|
+
const errors_1 = require("../lib/errors");
|
|
12
|
+
const analytics_1 = require("../lib/analytics");
|
|
13
|
+
const output_1 = require("../lib/output");
|
|
14
|
+
async function promptText(message) {
|
|
15
|
+
const rl = promises_1.default.createInterface({
|
|
16
|
+
input: process.stdin,
|
|
17
|
+
output: process.stdout,
|
|
18
|
+
});
|
|
19
|
+
const answer = await rl.question(message);
|
|
20
|
+
rl.close();
|
|
21
|
+
return answer.trim();
|
|
22
|
+
}
|
|
23
|
+
function registerTransferCommand(program) {
|
|
24
|
+
program
|
|
25
|
+
.command('transfer <agent-name>')
|
|
26
|
+
.description('Transfer an agent to another workspace')
|
|
27
|
+
.requiredOption('--to <workspace-slug>', 'Target workspace slug')
|
|
28
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
29
|
+
.option('--dry-run', 'Show what would be transferred without making changes')
|
|
30
|
+
.option('--json', 'Output result as JSON')
|
|
31
|
+
.addHelpText('after', `
|
|
32
|
+
Examples:
|
|
33
|
+
orch transfer my-agent --to team-workspace # Transfer agent to another workspace
|
|
34
|
+
orch transfer my-agent --to team-workspace --dry-run # Preview transfer
|
|
35
|
+
orch transfer my-agent --to team-workspace --yes # Skip confirmation
|
|
36
|
+
`)
|
|
37
|
+
.action(async (agentName, options) => {
|
|
38
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
39
|
+
if (!config.apiKey) {
|
|
40
|
+
throw new errors_1.CliError('Not logged in. Run `orchagent login` first.');
|
|
41
|
+
}
|
|
42
|
+
process.stdout.write('Finding agent and workspaces...\n');
|
|
43
|
+
// Fetch workspaces and agents in parallel
|
|
44
|
+
const [workspacesResponse, agents] = await Promise.all([
|
|
45
|
+
(0, api_1.request)(config, 'GET', '/workspaces'),
|
|
46
|
+
(0, api_1.listMyAgents)(config),
|
|
47
|
+
]);
|
|
48
|
+
// Find the target workspace by slug
|
|
49
|
+
const targetWorkspace = workspacesResponse.workspaces.find((w) => w.slug === options.to);
|
|
50
|
+
if (!targetWorkspace) {
|
|
51
|
+
throw new errors_1.CliError(`Workspace '${options.to}' not found. Run \`orchagent workspace list\` to see available workspaces.`);
|
|
52
|
+
}
|
|
53
|
+
// Find the agent by name
|
|
54
|
+
const matching = agents.filter((a) => a.name === agentName);
|
|
55
|
+
if (matching.length === 0) {
|
|
56
|
+
throw new errors_1.CliError(`Agent '${agentName}' not found in current workspace.`);
|
|
57
|
+
}
|
|
58
|
+
// Use the most recent version to get the agent ID
|
|
59
|
+
const agent = matching.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
|
|
60
|
+
// Check transfer eligibility
|
|
61
|
+
const check = await (0, api_1.checkAgentTransfer)(config, agent.id, targetWorkspace.id);
|
|
62
|
+
// Show transfer summary
|
|
63
|
+
process.stdout.write(`\n${chalk_1.default.bold('Agent:')} ${agent.name}\n`);
|
|
64
|
+
process.stdout.write(`${chalk_1.default.bold('Target workspace:')} ${targetWorkspace.name} (${targetWorkspace.slug})\n`);
|
|
65
|
+
const { details } = check;
|
|
66
|
+
process.stdout.write(`${chalk_1.default.bold('Versions:')} ${details.version_count}\n`);
|
|
67
|
+
if (details.grants_count > 0) {
|
|
68
|
+
process.stdout.write(`${chalk_1.default.bold('Grants to revoke:')} ${details.grants_count}\n`);
|
|
69
|
+
}
|
|
70
|
+
if (details.keys_count > 0) {
|
|
71
|
+
process.stdout.write(`${chalk_1.default.bold('Keys to delete:')} ${details.keys_count}\n`);
|
|
72
|
+
}
|
|
73
|
+
if (details.schedules_count > 0) {
|
|
74
|
+
process.stdout.write(`${chalk_1.default.bold('Schedules to disable:')} ${details.schedules_count}\n`);
|
|
75
|
+
}
|
|
76
|
+
process.stdout.write('\n');
|
|
77
|
+
// Show warnings
|
|
78
|
+
if (check.warnings.length > 0) {
|
|
79
|
+
for (const warning of check.warnings) {
|
|
80
|
+
process.stdout.write(chalk_1.default.yellow(`Warning: ${warning}\n`));
|
|
81
|
+
}
|
|
82
|
+
process.stdout.write('\n');
|
|
83
|
+
}
|
|
84
|
+
// Show blockers and exit if any
|
|
85
|
+
if (check.blockers.length > 0) {
|
|
86
|
+
for (const blocker of check.blockers) {
|
|
87
|
+
process.stdout.write(chalk_1.default.red(`Blocker: ${blocker}\n`));
|
|
88
|
+
}
|
|
89
|
+
process.stdout.write(chalk_1.default.red('\nTransfer cannot proceed due to blockers above.\n'));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
// Handle dry-run
|
|
93
|
+
if (options.dryRun) {
|
|
94
|
+
process.stdout.write('DRY RUN - No changes will be made\n\n');
|
|
95
|
+
process.stdout.write(`Would transfer: ${agent.name} (${details.version_count} version(s))\n`);
|
|
96
|
+
process.stdout.write(`Target: ${targetWorkspace.name} (${targetWorkspace.slug})\n`);
|
|
97
|
+
if (details.grants_count > 0 || details.keys_count > 0 || details.schedules_count > 0) {
|
|
98
|
+
process.stdout.write(chalk_1.default.yellow(`Cleanup: ${details.grants_count} grant(s) revoked, ${details.keys_count} key(s) deleted, ${details.schedules_count} schedule(s) disabled\n`));
|
|
99
|
+
}
|
|
100
|
+
process.stdout.write('\nNo changes made (dry run)\n');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Prompt for confirmation
|
|
104
|
+
if (!options.yes) {
|
|
105
|
+
process.stdout.write(chalk_1.default.yellow('This will transfer the agent and all its versions to the target workspace.\n'));
|
|
106
|
+
process.stdout.write(chalk_1.default.yellow('Existing grants, keys, and schedules in the source workspace will be cleaned up.\n\n'));
|
|
107
|
+
const confirmName = await promptText(`Type "${agent.name}" to confirm transfer: `);
|
|
108
|
+
if (confirmName !== agent.name) {
|
|
109
|
+
process.stdout.write(chalk_1.default.red('\nTransfer cancelled. Name did not match.\n'));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Perform transfer
|
|
114
|
+
process.stdout.write('Transferring agent...\n');
|
|
115
|
+
const result = await (0, api_1.transferAgent)(config, agent.id, {
|
|
116
|
+
target_workspace_id: targetWorkspace.id,
|
|
117
|
+
confirmation_name: agent.name,
|
|
118
|
+
});
|
|
119
|
+
await (0, analytics_1.track)('cli_transfer', {
|
|
120
|
+
agent_name: result.agent_name,
|
|
121
|
+
versions_transferred: result.versions_transferred,
|
|
122
|
+
target_workspace: result.target_workspace.slug,
|
|
123
|
+
});
|
|
124
|
+
if (options.json) {
|
|
125
|
+
(0, output_1.printJson)(result);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
process.stdout.write(`\n${chalk_1.default.green('+')} Transferred ${result.agent_name} (${result.versions_transferred} version(s))\n`);
|
|
129
|
+
process.stdout.write(` From: ${result.source_workspace.name} (${result.source_workspace.slug})\n`);
|
|
130
|
+
process.stdout.write(` To: ${result.target_workspace.name} (${result.target_workspace.slug})\n`);
|
|
131
|
+
if (result.cleanup.grants_revoked > 0 || result.cleanup.keys_deleted > 0 || result.cleanup.schedules_disabled > 0) {
|
|
132
|
+
process.stdout.write(chalk_1.default.gray(`\nCleanup: ${result.cleanup.grants_revoked} grant(s) revoked, ${result.cleanup.keys_deleted} key(s) deleted, ${result.cleanup.schedules_disabled} schedule(s) disabled\n`));
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
package/dist/lib/api.js
CHANGED
|
@@ -54,6 +54,8 @@ exports.getAgentWithFallback = getAgentWithFallback;
|
|
|
54
54
|
exports.downloadCodeBundleAuthenticated = downloadCodeBundleAuthenticated;
|
|
55
55
|
exports.checkAgentDelete = checkAgentDelete;
|
|
56
56
|
exports.deleteAgent = deleteAgent;
|
|
57
|
+
exports.checkAgentTransfer = checkAgentTransfer;
|
|
58
|
+
exports.transferAgent = transferAgent;
|
|
57
59
|
exports.previewAgentVersion = previewAgentVersion;
|
|
58
60
|
exports.reportInstall = reportInstall;
|
|
59
61
|
exports.fetchUserProfile = fetchUserProfile;
|
|
@@ -248,7 +250,7 @@ async function fetchLlmKeys(config) {
|
|
|
248
250
|
return result.keys;
|
|
249
251
|
}
|
|
250
252
|
/**
|
|
251
|
-
* Download a
|
|
253
|
+
* Download a code-runtime bundle for local execution.
|
|
252
254
|
*/
|
|
253
255
|
async function downloadCodeBundle(config, org, agent, version) {
|
|
254
256
|
const response = await safeFetch(`${config.apiUrl.replace(/\/$/, '')}/public/agents/${org}/${agent}/${version}/bundle`);
|
|
@@ -366,6 +368,21 @@ async function deleteAgent(config, agentId, confirmationName) {
|
|
|
366
368
|
const params = confirmationName ? `?confirmation_name=${encodeURIComponent(confirmationName)}` : '';
|
|
367
369
|
return request(config, 'DELETE', `/agents/${agentId}${params}`);
|
|
368
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Check if an agent can be transferred to another workspace.
|
|
373
|
+
*/
|
|
374
|
+
async function checkAgentTransfer(config, agentId, targetWorkspaceId) {
|
|
375
|
+
return request(config, 'GET', `/agents/${agentId}/transfer-check?target_workspace_id=${encodeURIComponent(targetWorkspaceId)}`);
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Transfer an agent to another workspace.
|
|
379
|
+
*/
|
|
380
|
+
async function transferAgent(config, agentId, data) {
|
|
381
|
+
return request(config, 'POST', `/agents/${agentId}/transfer`, {
|
|
382
|
+
body: JSON.stringify(data),
|
|
383
|
+
headers: { 'Content-Type': 'application/json' },
|
|
384
|
+
});
|
|
385
|
+
}
|
|
369
386
|
/**
|
|
370
387
|
* Preview the next version number for an agent.
|
|
371
388
|
*/
|
package/package.json
CHANGED