@orchagent/cli 0.3.6 → 0.3.8

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.
@@ -229,7 +229,7 @@ Note: Use 'call' for server-side execution (requires login), 'run' for local exe
229
229
  sourceLabel = multipart.sourceLabel;
230
230
  }
231
231
  const url = `${resolved.apiUrl.replace(/\/$/, '')}/${org}/${parsed.agent}/${parsed.version}/${endpoint}`;
232
- const response = await (0, api_1.safeFetch)(url, {
232
+ const response = await (0, api_1.safeFetchWithRetryForCalls)(url, {
233
233
  method: 'POST',
234
234
  headers,
235
235
  body,
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.registerEnvCommand = registerEnvCommand;
40
+ const cli_table3_1 = __importDefault(require("cli-table3"));
41
+ const chalk_1 = __importDefault(require("chalk"));
42
+ const fs = __importStar(require("fs/promises"));
43
+ const config_1 = require("../lib/config");
44
+ const api_1 = require("../lib/api");
45
+ const errors_1 = require("../lib/errors");
46
+ const analytics_1 = require("../lib/analytics");
47
+ async function resolveWorkspaceId(config, slug) {
48
+ const configFile = await (0, config_1.loadConfig)();
49
+ const targetSlug = slug ?? configFile.workspace;
50
+ if (!targetSlug) {
51
+ // Use user's personal org
52
+ const org = await (0, api_1.getOrg)(config);
53
+ return org.id;
54
+ }
55
+ const response = await (0, api_1.request)(config, 'GET', '/workspaces');
56
+ const workspace = response.workspaces.find((w) => w.slug === targetSlug);
57
+ if (!workspace) {
58
+ throw new errors_1.CliError(`Workspace '${targetSlug}' not found`);
59
+ }
60
+ return workspace.id;
61
+ }
62
+ function statusColor(status) {
63
+ switch (status) {
64
+ case 'ready':
65
+ return chalk_1.default.green(status);
66
+ case 'building':
67
+ return chalk_1.default.yellow(status);
68
+ case 'failed':
69
+ return chalk_1.default.red(status);
70
+ case 'pending':
71
+ return chalk_1.default.gray(status);
72
+ default:
73
+ return chalk_1.default.gray(status ?? 'unknown');
74
+ }
75
+ }
76
+ async function listEnvs(config, options) {
77
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
78
+ const result = await (0, api_1.listEnvironments)(config, workspaceId);
79
+ if (result.environments.length === 0) {
80
+ console.log(chalk_1.default.gray('No environments found.'));
81
+ console.log(chalk_1.default.gray('Use `orch env create` to create one, or include a Dockerfile in your agent bundle.'));
82
+ return;
83
+ }
84
+ const table = new cli_table3_1.default({
85
+ head: [
86
+ chalk_1.default.cyan('Name'),
87
+ chalk_1.default.cyan('Status'),
88
+ chalk_1.default.cyan('Agents'),
89
+ chalk_1.default.cyan('Type'),
90
+ chalk_1.default.cyan('ID'),
91
+ ],
92
+ style: { head: [], border: [] },
93
+ });
94
+ for (const env of result.environments) {
95
+ const isDefault = env.environment.id === result.default_environment_id;
96
+ const name = isDefault
97
+ ? `${env.environment.name} ${chalk_1.default.yellow('(default)')}`
98
+ : env.environment.name;
99
+ table.push([
100
+ name,
101
+ statusColor(env.build?.status),
102
+ env.agent_count.toString(),
103
+ env.environment.is_predefined ? chalk_1.default.blue('predefined') : chalk_1.default.gray('custom'),
104
+ env.environment.id.slice(0, 8),
105
+ ]);
106
+ }
107
+ console.log();
108
+ console.log(chalk_1.default.bold('Environments:'));
109
+ console.log(table.toString());
110
+ console.log();
111
+ if (result.default_environment_id) {
112
+ const defaultEnv = result.environments.find((e) => e.environment.id === result.default_environment_id);
113
+ if (defaultEnv) {
114
+ console.log(chalk_1.default.gray(`Workspace default: ${chalk_1.default.white(defaultEnv.environment.name)}`));
115
+ console.log(chalk_1.default.gray('All new agents will use this environment unless they include their own Dockerfile.'));
116
+ }
117
+ }
118
+ }
119
+ async function getEnvStatus(config, environmentId) {
120
+ const result = await (0, api_1.getEnvironment)(config, environmentId);
121
+ console.log();
122
+ console.log(chalk_1.default.bold(`Environment: ${result.environment.name}`));
123
+ console.log();
124
+ console.log(` ID: ${result.environment.id}`);
125
+ console.log(` Status: ${statusColor(result.build?.status)}`);
126
+ console.log(` Agents: ${result.agent_count}`);
127
+ console.log(` Type: ${result.environment.is_predefined ? 'predefined' : 'custom'}`);
128
+ console.log(` Created: ${new Date(result.environment.created_at).toLocaleString()}`);
129
+ if (result.build?.status === 'failed') {
130
+ console.log();
131
+ console.log(chalk_1.default.red('Build Error:'));
132
+ console.log(chalk_1.default.red(` ${result.build.error_message || 'Unknown error'}`));
133
+ }
134
+ if (result.build?.build_logs) {
135
+ console.log();
136
+ console.log(chalk_1.default.gray('Build Logs:'));
137
+ console.log(chalk_1.default.gray(result.build.build_logs));
138
+ }
139
+ console.log();
140
+ console.log(chalk_1.default.gray('Dockerfile:'));
141
+ console.log(chalk_1.default.gray('---'));
142
+ console.log(result.environment.dockerfile_content);
143
+ console.log(chalk_1.default.gray('---'));
144
+ }
145
+ async function createEnv(config, options) {
146
+ let dockerfileContent;
147
+ try {
148
+ dockerfileContent = await fs.readFile(options.file, 'utf-8');
149
+ }
150
+ catch (err) {
151
+ throw new errors_1.CliError(`Failed to read Dockerfile: ${options.file}`);
152
+ }
153
+ console.log(chalk_1.default.gray(`Creating environment '${options.name}'...`));
154
+ const result = await (0, api_1.createEnvironment)(config, options.name, dockerfileContent);
155
+ if (result.reused) {
156
+ console.log(chalk_1.default.cyan('Existing environment with same Dockerfile found, reusing.'));
157
+ console.log(`Environment ID: ${result.environment.id}`);
158
+ }
159
+ else {
160
+ console.log(chalk_1.default.green('Environment created, build started.'));
161
+ console.log(`Environment ID: ${result.environment.id}`);
162
+ console.log();
163
+ console.log(chalk_1.default.gray(`Check build status: orch env status ${result.environment.id}`));
164
+ }
165
+ await (0, analytics_1.track)('env_create', {
166
+ environment_id: result.environment.id,
167
+ reused: result.reused,
168
+ });
169
+ }
170
+ async function deleteEnv(config, environmentId) {
171
+ console.log(chalk_1.default.gray(`Deleting environment ${environmentId}...`));
172
+ await (0, api_1.deleteEnvironment)(config, environmentId);
173
+ console.log(chalk_1.default.green('Environment deleted.'));
174
+ await (0, analytics_1.track)('env_delete', { environment_id: environmentId });
175
+ }
176
+ async function setDefault(config, environmentId, options) {
177
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
178
+ console.log(chalk_1.default.gray(`Setting default environment for workspace...`));
179
+ await (0, api_1.setWorkspaceDefaultEnvironment)(config, workspaceId, environmentId);
180
+ console.log(chalk_1.default.green('Default environment set for workspace.'));
181
+ console.log(chalk_1.default.gray('All new agents will use this environment unless they include their own Dockerfile.'));
182
+ await (0, analytics_1.track)('env_set_default', {
183
+ environment_id: environmentId,
184
+ workspace_id: workspaceId,
185
+ });
186
+ }
187
+ async function clearDefault(config, options) {
188
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
189
+ console.log(chalk_1.default.gray(`Clearing default environment for workspace...`));
190
+ await (0, api_1.setWorkspaceDefaultEnvironment)(config, workspaceId, null);
191
+ console.log(chalk_1.default.green('Default environment cleared. Agents will use base image.'));
192
+ await (0, analytics_1.track)('env_clear_default', { workspace_id: workspaceId });
193
+ }
194
+ function registerEnvCommand(program) {
195
+ const env = program
196
+ .command('env')
197
+ .description('Manage custom Docker environments for code agents');
198
+ env
199
+ .command('list')
200
+ .description('List environments in workspace')
201
+ .option('-w, --workspace <slug>', 'Workspace slug')
202
+ .action(async (options) => {
203
+ const config = await (0, config_1.getResolvedConfig)();
204
+ await listEnvs(config, options);
205
+ });
206
+ env
207
+ .command('status <environment-id>')
208
+ .description('Check environment build status')
209
+ .action(async (environmentId) => {
210
+ const config = await (0, config_1.getResolvedConfig)();
211
+ await getEnvStatus(config, environmentId);
212
+ });
213
+ env
214
+ .command('create')
215
+ .description('Create environment from Dockerfile')
216
+ .requiredOption('-f, --file <path>', 'Path to Dockerfile')
217
+ .requiredOption('-n, --name <name>', 'Environment name')
218
+ .action(async (options) => {
219
+ const config = await (0, config_1.getResolvedConfig)();
220
+ await createEnv(config, options);
221
+ });
222
+ env
223
+ .command('delete <environment-id>')
224
+ .description('Delete an environment')
225
+ .action(async (environmentId) => {
226
+ const config = await (0, config_1.getResolvedConfig)();
227
+ await deleteEnv(config, environmentId);
228
+ });
229
+ env
230
+ .command('set-default <environment-id>')
231
+ .description('Set workspace default environment (all agents use this)')
232
+ .option('-w, --workspace <slug>', 'Workspace slug (defaults to current)')
233
+ .action(async (environmentId, options) => {
234
+ const config = await (0, config_1.getResolvedConfig)();
235
+ await setDefault(config, environmentId, options);
236
+ });
237
+ env
238
+ .command('clear-default')
239
+ .description('Clear workspace default environment (agents use base image)')
240
+ .option('-w, --workspace <slug>', 'Workspace slug (defaults to current)')
241
+ .action(async (options) => {
242
+ const config = await (0, config_1.getResolvedConfig)();
243
+ await clearDefault(config, options);
244
+ });
245
+ }
@@ -25,6 +25,7 @@ const config_1 = require("./config");
25
25
  const install_1 = require("./install");
26
26
  const formats_1 = require("./formats");
27
27
  const update_1 = require("./update");
28
+ const env_1 = require("./env");
28
29
  function registerCommands(program) {
29
30
  (0, login_1.registerLoginCommand)(program);
30
31
  (0, whoami_1.registerWhoamiCommand)(program);
@@ -50,4 +51,5 @@ function registerCommands(program) {
50
51
  (0, install_1.registerInstallCommand)(program);
51
52
  (0, formats_1.registerFormatsCommand)(program);
52
53
  (0, update_1.registerUpdateCommand)(program);
54
+ (0, env_1.registerEnvCommand)(program);
53
55
  }
@@ -70,20 +70,50 @@ function registerInstallCommand(program) {
70
70
  .option('--format <formats>', 'Comma-separated format IDs (e.g., claude-code,cursor)')
71
71
  .option('--scope <scope>', 'Install scope: user (home dir) or project (current dir)', 'user')
72
72
  .option('--dry-run', 'Show what would be installed without making changes')
73
+ .option('--json', 'Output result as JSON (for automation/tooling)')
73
74
  .action(async (agentArg, options) => {
75
+ const jsonMode = options.json === true;
76
+ const log = (msg) => { if (!jsonMode)
77
+ process.stdout.write(msg); };
78
+ const logErr = (msg) => { if (!jsonMode)
79
+ process.stderr.write(msg); };
80
+ // Result tracking for JSON output
81
+ const result = {
82
+ success: false,
83
+ agent: '',
84
+ version: '',
85
+ scope: '',
86
+ formats: [],
87
+ files: [],
88
+ warnings: [],
89
+ errors: [],
90
+ };
74
91
  const resolved = await (0, config_1.getResolvedConfig)();
75
92
  const parsed = parseAgentRef(agentArg);
76
93
  const org = parsed.org ?? resolved.defaultOrg;
77
94
  if (!org) {
95
+ if (jsonMode) {
96
+ result.errors.push('Missing org. Use org/agent format or set default org.');
97
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
98
+ process.exit(1);
99
+ }
78
100
  throw new errors_1.CliError('Missing org. Use org/agent format or set default org.');
79
101
  }
102
+ result.agent = `${org}/${parsed.name}`;
103
+ result.version = parsed.version;
80
104
  // Determine target formats
81
105
  let targetFormats = [];
82
106
  if (options.format) {
83
107
  targetFormats = options.format.split(',').map(f => f.trim());
84
108
  const invalid = targetFormats.filter(f => !adapters_1.adapterRegistry.has(f));
85
109
  if (invalid.length > 0) {
86
- throw new errors_1.CliError(`Unknown format(s): ${invalid.join(', ')}. Available: ${adapters_1.adapterRegistry.getIds().join(', ')}`);
110
+ const errMsg = `Unknown format(s): ${invalid.join(', ')}. Available: ${adapters_1.adapterRegistry.getIds().join(', ')}`;
111
+ if (jsonMode) {
112
+ result.errors.push(errMsg);
113
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
114
+ process.exit(1);
115
+ }
116
+ throw new errors_1.CliError(errMsg);
87
117
  }
88
118
  }
89
119
  else {
@@ -96,41 +126,65 @@ function registerInstallCommand(program) {
96
126
  targetFormats = ['claude-code'];
97
127
  }
98
128
  }
129
+ result.formats = targetFormats;
99
130
  // Validate scope
100
131
  let scope = options.scope;
101
132
  if (scope !== 'user' && scope !== 'project') {
102
- throw new errors_1.CliError('Scope must be "user" or "project"');
133
+ const errMsg = 'Scope must be "user" or "project"';
134
+ if (jsonMode) {
135
+ result.errors.push(errMsg);
136
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
137
+ process.exit(1);
138
+ }
139
+ throw new errors_1.CliError(errMsg);
103
140
  }
141
+ result.scope = scope;
104
142
  // Download agent
105
- process.stdout.write(`Fetching ${org}/${parsed.name}@${parsed.version}...\n`);
106
- const agent = await downloadAgentWithFallback(resolved, org, parsed.name, parsed.version);
143
+ log(`Fetching ${org}/${parsed.name}@${parsed.version}...\n`);
144
+ let agent;
145
+ try {
146
+ agent = await downloadAgentWithFallback(resolved, org, parsed.name, parsed.version);
147
+ }
148
+ catch (err) {
149
+ if (jsonMode) {
150
+ result.errors.push(err instanceof Error ? err.message : String(err));
151
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
152
+ process.exit(1);
153
+ }
154
+ throw err;
155
+ }
107
156
  // Install for each format
108
157
  let filesWritten = 0;
109
158
  for (const formatId of targetFormats) {
110
159
  const adapter = adapters_1.adapterRegistry.get(formatId);
111
160
  if (!adapter) {
112
- process.stderr.write(`Warning: Unknown format '${formatId}', skipping\n`);
161
+ const warn = `Unknown format '${formatId}', skipping`;
162
+ result.warnings.push(warn);
163
+ logErr(`Warning: ${warn}\n`);
113
164
  continue;
114
165
  }
115
166
  // Check if can convert
116
167
  const checkResult = adapter.canConvert(agent);
117
168
  if (!checkResult.canConvert) {
118
- process.stderr.write(`Cannot convert to ${adapter.name}:\n`);
169
+ logErr(`Cannot convert to ${adapter.name}:\n`);
119
170
  for (const err of checkResult.errors) {
120
- process.stderr.write(` - ${err}\n`);
171
+ result.errors.push(`${adapter.name}: ${err}`);
172
+ logErr(` - ${err}\n`);
121
173
  }
122
174
  continue;
123
175
  }
124
176
  // Show warnings
125
177
  for (const warn of checkResult.warnings) {
126
- process.stdout.write(`Warning (${formatId}): ${warn}\n`);
178
+ result.warnings.push(`${formatId}: ${warn}`);
179
+ log(`Warning (${formatId}): ${warn}\n`);
127
180
  }
128
181
  // Determine scope for this adapter (use local variable to not affect other formats)
129
182
  let effectiveScope = scope;
130
183
  const supportedScopes = adapter.installPaths.map(p => p.scope);
131
184
  if (!supportedScopes.includes(effectiveScope)) {
132
- process.stderr.write(`Warning: ${adapter.name} doesn't support '${scope}' scope. ` +
133
- `Supported: ${supportedScopes.join(', ')}. Using '${supportedScopes[0]}' instead.\n`);
185
+ const warn = `${adapter.name} doesn't support '${scope}' scope. Using '${supportedScopes[0]}' instead.`;
186
+ result.warnings.push(warn);
187
+ logErr(`Warning: ${warn}\n`);
134
188
  effectiveScope = supportedScopes[0];
135
189
  }
136
190
  // Convert
@@ -142,8 +196,9 @@ function registerInstallCommand(program) {
142
196
  const fullDir = path_1.default.join(baseDir, file.installPath);
143
197
  const fullPath = path_1.default.join(fullDir, file.filename);
144
198
  if (options.dryRun) {
145
- process.stdout.write(`Would install: ${fullPath}\n`);
146
- process.stdout.write(`Content preview:\n${file.content.slice(0, 500)}...\n\n`);
199
+ log(`Would install: ${fullPath}\n`);
200
+ log(`Content preview:\n${file.content.slice(0, 500)}...\n\n`);
201
+ result.files.push({ path: fullPath, format: formatId });
147
202
  continue;
148
203
  }
149
204
  // Create directory and write file
@@ -174,7 +229,8 @@ function registerInstallCommand(program) {
174
229
  contentHash: (0, installed_1.computeHash)(file.content),
175
230
  };
176
231
  await (0, installed_1.trackInstall)(installedAgent);
177
- process.stdout.write(`Installed: ${fullPath}\n`);
232
+ result.files.push({ path: fullPath, format: formatId });
233
+ log(`Installed: ${fullPath}\n`);
178
234
  }
179
235
  }
180
236
  if (!options.dryRun) {
@@ -184,18 +240,33 @@ function registerInstallCommand(program) {
184
240
  formats: targetFormats,
185
241
  scope,
186
242
  });
187
- process.stdout.write(`\nAgent installed successfully!\n`);
243
+ result.success = true;
244
+ log(`\nAgent installed successfully!\n`);
188
245
  if (scope === 'user') {
189
- process.stdout.write(`Available in all your projects.\n`);
246
+ log(`Available in all your projects.\n`);
190
247
  }
191
248
  else {
192
- process.stdout.write(`Available in this project only.\n`);
249
+ log(`Available in this project only.\n`);
193
250
  }
194
251
  }
195
252
  else {
196
- process.stderr.write(`\nNo files were installed. Check warnings above.\n`);
253
+ result.errors.push('No files were installed. Check warnings.');
254
+ logErr(`\nNo files were installed. Check warnings above.\n`);
255
+ }
256
+ }
257
+ else {
258
+ // Dry run is considered success if we got file list
259
+ result.success = result.files.length > 0;
260
+ }
261
+ // Output JSON result
262
+ if (jsonMode) {
263
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
264
+ if (!result.success) {
197
265
  process.exit(1);
198
266
  }
199
267
  }
268
+ else if (!result.success && !options.dryRun) {
269
+ process.exit(1);
270
+ }
200
271
  });
201
272
  }
@@ -8,6 +8,7 @@ const promises_1 = __importDefault(require("fs/promises"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const os_1 = __importDefault(require("os"));
10
10
  const yaml_1 = __importDefault(require("yaml"));
11
+ const chalk_1 = __importDefault(require("chalk"));
11
12
  const config_1 = require("../lib/config");
12
13
  const api_1 = require("../lib/api");
13
14
  const errors_1 = require("../lib/errors");
@@ -463,6 +464,20 @@ function registerPublishCommand(program) {
463
464
  process.stdout.write(` Uploading bundle...\n`);
464
465
  const uploadResult = await (0, api_1.uploadCodeBundle)(config, agentId, bundlePath, manifest.entrypoint);
465
466
  process.stdout.write(` Uploaded: ${uploadResult.code_hash.substring(0, 12)}...\n`);
467
+ // Show environment info if applicable
468
+ if (uploadResult.environment_id) {
469
+ if (uploadResult.environment_source === 'dockerfile_new') {
470
+ process.stdout.write(` ${chalk_1.default.cyan('Custom environment detected (Dockerfile)')}\n`);
471
+ process.stdout.write(` ${chalk_1.default.yellow('Environment building...')} Agent will be ready when build completes.\n`);
472
+ process.stdout.write(` ${chalk_1.default.gray(`Check status: orch env status ${uploadResult.environment_id}`)}\n`);
473
+ }
474
+ else if (uploadResult.environment_source === 'dockerfile_reused') {
475
+ process.stdout.write(` ${chalk_1.default.green('Custom environment (reusing existing build)')}\n`);
476
+ }
477
+ else if (uploadResult.environment_source === 'workspace_default') {
478
+ process.stdout.write(` ${chalk_1.default.cyan('Using workspace default environment')}\n`);
479
+ }
480
+ }
466
481
  }
467
482
  finally {
468
483
  // Clean up temp files
@@ -164,7 +164,24 @@ Instructions and guidance for AI agents...
164
164
  .option('--scope <scope>', 'Install scope: user or project', 'project')
165
165
  .option('--dry-run', 'Show what would be installed without making changes')
166
166
  .option('--format <formats>', 'Comma-separated format IDs (e.g., claude-code,cursor)')
167
+ .option('--json', 'Output result as JSON (for automation/tooling)')
167
168
  .action(async (skillRef, options) => {
169
+ const jsonMode = options.json === true;
170
+ const log = (msg) => { if (!jsonMode)
171
+ process.stdout.write(msg); };
172
+ const logErr = (msg) => { if (!jsonMode)
173
+ process.stderr.write(msg); };
174
+ // Result tracking for JSON output
175
+ const result = {
176
+ success: false,
177
+ skill: '',
178
+ version: '',
179
+ scope: '',
180
+ tools: [],
181
+ files: [],
182
+ warnings: [],
183
+ errors: [],
184
+ };
168
185
  const resolved = await (0, config_1.getResolvedConfig)();
169
186
  // Determine target formats
170
187
  let targetFormats = [];
@@ -173,7 +190,13 @@ Instructions and guidance for AI agents...
173
190
  // Validate format IDs
174
191
  const invalid = targetFormats.filter(f => !config_1.VALID_FORMAT_IDS.includes(f));
175
192
  if (invalid.length > 0) {
176
- throw new errors_1.CliError(`Invalid format ID(s): ${invalid.join(', ')}. Valid: ${config_1.VALID_FORMAT_IDS.join(', ')}`);
193
+ const errMsg = `Invalid format ID(s): ${invalid.join(', ')}. Valid: ${config_1.VALID_FORMAT_IDS.join(', ')}`;
194
+ if (jsonMode) {
195
+ result.errors.push(errMsg);
196
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
197
+ process.exit(1);
198
+ }
199
+ throw new errors_1.CliError(errMsg);
177
200
  }
178
201
  }
179
202
  else {
@@ -183,7 +206,9 @@ Instructions and guidance for AI agents...
183
206
  targetFormats = defaults.filter(f => config_1.VALID_FORMAT_IDS.includes(f));
184
207
  const skipped = defaults.filter(f => !config_1.VALID_FORMAT_IDS.includes(f));
185
208
  if (skipped.length > 0) {
186
- process.stderr.write(`Note: Skipping ${skipped.join(', ')} (no skill directory)\n`);
209
+ const warn = `Skipping ${skipped.join(', ')} (no skill directory)`;
210
+ result.warnings.push(warn);
211
+ logErr(`Note: ${warn}\n`);
187
212
  }
188
213
  }
189
214
  }
@@ -194,17 +219,43 @@ Instructions and guidance for AI agents...
194
219
  const parsed = parseSkillRef(skillRef);
195
220
  const org = parsed.org ?? resolved.defaultOrg;
196
221
  if (!org) {
197
- throw new errors_1.CliError('Missing org. Use org/skill or set default org.');
222
+ const errMsg = 'Missing org. Use org/skill or set default org.';
223
+ if (jsonMode) {
224
+ result.errors.push(errMsg);
225
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
226
+ process.exit(1);
227
+ }
228
+ throw new errors_1.CliError(errMsg);
198
229
  }
230
+ result.skill = `${org}/${parsed.skill}`;
231
+ result.version = parsed.version;
199
232
  // Download skill (tries public first, falls back to authenticated for private)
200
- const skillData = await downloadSkillWithFallback(resolved, org, parsed.skill, parsed.version);
233
+ let skillData;
234
+ try {
235
+ skillData = await downloadSkillWithFallback(resolved, org, parsed.skill, parsed.version);
236
+ }
237
+ catch (err) {
238
+ if (jsonMode) {
239
+ result.errors.push(err instanceof Error ? err.message : String(err));
240
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
241
+ process.exit(1);
242
+ }
243
+ throw err;
244
+ }
201
245
  if (!skillData.prompt) {
246
+ const errMsg = 'Skill has no content. The skill exists but has an empty prompt.';
247
+ if (jsonMode) {
248
+ result.errors.push(errMsg);
249
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
250
+ process.exit(1);
251
+ }
202
252
  throw new errors_1.CliError('Skill has no content.\n\n' +
203
253
  'The skill exists but has an empty prompt. This may be a publishing issue.\n' +
204
254
  'Try re-publishing the skill or contact the skill author.');
205
255
  }
206
256
  // Determine scope (--global is legacy alias for --scope user)
207
257
  const scope = options.global ? 'user' : (options.scope || 'project');
258
+ result.scope = scope;
208
259
  // Build skill content with header
209
260
  const skillContent = `# ${skillData.name}
210
261
 
@@ -216,16 +267,21 @@ ${skillData.prompt}
216
267
  `;
217
268
  // Dry run - show what would be installed
218
269
  if (options.dryRun) {
219
- process.stdout.write(`Would install ${org}/${parsed.skill}@${parsed.version}\n\n`);
220
- process.stdout.write(`Target directories (scope: ${scope}):\n`);
270
+ log(`Would install ${org}/${parsed.skill}@${parsed.version}\n\n`);
271
+ log(`Target directories (scope: ${scope}):\n`);
221
272
  for (const tool of toolDirs) {
222
273
  const baseDir = scope === 'user' ? os_1.default.homedir() : process.cwd();
223
274
  const toolPath = scope === 'user' ? tool.userPath : tool.projectPath;
224
275
  const skillDir = path_1.default.join(baseDir, toolPath);
225
276
  const skillFile = path_1.default.join(skillDir, `${parsed.skill}.md`);
226
- process.stdout.write(` - ${tool.name}: ${skillFile}\n`);
277
+ result.files.push({ path: skillFile, tool: tool.name });
278
+ log(` - ${tool.name}: ${skillFile}\n`);
279
+ }
280
+ log(`\nNo changes made (dry run)\n`);
281
+ result.success = true;
282
+ if (jsonMode) {
283
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
227
284
  }
228
- process.stdout.write(`\nNo changes made (dry run)\n`);
229
285
  return;
230
286
  }
231
287
  // Install to target AI tool directories
@@ -239,15 +295,26 @@ ${skillData.prompt}
239
295
  await promises_1.default.mkdir(skillDir, { recursive: true });
240
296
  await promises_1.default.writeFile(skillFile, skillContent);
241
297
  installed.push(tool.name);
298
+ result.files.push({ path: skillFile, tool: tool.name });
242
299
  }
243
300
  catch (err) {
244
301
  // Skip if we can't write (e.g., permission issues)
245
- process.stderr.write(`Warning: Could not install to ${toolPath}: ${err.message}\n`);
302
+ const warn = `Could not install to ${toolPath}: ${err.message}`;
303
+ result.warnings.push(warn);
304
+ logErr(`Warning: ${warn}\n`);
246
305
  }
247
306
  }
248
307
  if (installed.length === 0) {
249
- throw new errors_1.CliError('Failed to install skill to any directory');
308
+ const errMsg = 'Failed to install skill to any directory';
309
+ if (jsonMode) {
310
+ result.errors.push(errMsg);
311
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
312
+ process.exit(1);
313
+ }
314
+ throw new errors_1.CliError(errMsg);
250
315
  }
316
+ result.tools = installed;
317
+ result.success = true;
251
318
  await (0, analytics_1.track)('cli_skill_install', {
252
319
  skill: `${org}/${parsed.skill}`,
253
320
  scope,
@@ -257,11 +324,16 @@ ${skillData.prompt}
257
324
  if (resolved.apiKey) {
258
325
  (0, api_1.reportInstall)(resolved, org, parsed.skill, parsed.version, package_json_1.default.version).catch(() => { });
259
326
  }
260
- process.stdout.write(`Installed ${org}/${parsed.skill}@${parsed.version}\n`);
261
- process.stdout.write(`\nAvailable for:\n`);
262
- for (const tool of installed) {
263
- process.stdout.write(` - ${tool}\n`);
327
+ if (jsonMode) {
328
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
329
+ }
330
+ else {
331
+ log(`Installed ${org}/${parsed.skill}@${parsed.version}\n`);
332
+ log(`\nAvailable for:\n`);
333
+ for (const tool of installed) {
334
+ log(` - ${tool}\n`);
335
+ }
336
+ log(`\nLocation: ${scope === 'user' ? '~/' : './'}\n`);
264
337
  }
265
- process.stdout.write(`\nLocation: ${scope === 'user' ? '~/' : './'}\n`);
266
338
  });
267
339
  }
package/dist/lib/api.js CHANGED
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ApiError = void 0;
37
37
  exports.safeFetch = safeFetch;
38
+ exports.safeFetchWithRetryForCalls = safeFetchWithRetryForCalls;
38
39
  exports.request = request;
39
40
  exports.publicRequest = publicRequest;
40
41
  exports.getOrg = getOrg;
@@ -58,15 +59,23 @@ exports.deleteAgent = deleteAgent;
58
59
  exports.previewAgentVersion = previewAgentVersion;
59
60
  exports.reportInstall = reportInstall;
60
61
  exports.fetchUserProfile = fetchUserProfile;
62
+ exports.listEnvironments = listEnvironments;
63
+ exports.getEnvironment = getEnvironment;
64
+ exports.createEnvironment = createEnvironment;
65
+ exports.deleteEnvironment = deleteEnvironment;
66
+ exports.setWorkspaceDefaultEnvironment = setWorkspaceDefaultEnvironment;
61
67
  const errors_1 = require("./errors");
62
68
  const DEFAULT_TIMEOUT_MS = 15000;
69
+ const CALL_TIMEOUT_MS = 120000; // 2 minutes for agent calls (can take time)
63
70
  const MAX_RETRIES = 3;
64
71
  const BASE_DELAY_MS = 1000;
65
72
  async function safeFetch(url, options) {
73
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
74
+ const { timeoutMs: _, ...fetchOptions } = options ?? {};
66
75
  try {
67
76
  return await fetch(url, {
68
- ...options,
69
- signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
77
+ ...fetchOptions,
78
+ signal: AbortSignal.timeout(timeoutMs),
70
79
  });
71
80
  }
72
81
  catch (err) {
@@ -77,6 +86,44 @@ async function safeFetch(url, options) {
77
86
  throw new errors_1.NetworkError(url, err instanceof Error ? err : undefined);
78
87
  }
79
88
  }
89
+ /**
90
+ * safeFetch with retry logic for connection failures.
91
+ * Use for important operations that should retry on transient errors.
92
+ */
93
+ async function safeFetchWithRetryForCalls(url, options) {
94
+ let lastError;
95
+ const timeoutMs = options?.timeoutMs ?? CALL_TIMEOUT_MS;
96
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
97
+ try {
98
+ const response = await safeFetch(url, { ...options, timeoutMs });
99
+ // Don't retry client errors (except 429)
100
+ if (response.status >= 400 && response.status < 500 && response.status !== 429) {
101
+ return response;
102
+ }
103
+ // Retry on 5xx or 429
104
+ if (response.status >= 500 || response.status === 429) {
105
+ if (attempt < MAX_RETRIES) {
106
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
107
+ const jitter = Math.random() * 500;
108
+ process.stderr.write(`Request failed (${response.status}), retrying in ${Math.round((delay + jitter) / 1000)}s...\n`);
109
+ await new Promise(r => setTimeout(r, delay + jitter));
110
+ continue;
111
+ }
112
+ }
113
+ return response;
114
+ }
115
+ catch (error) {
116
+ lastError = error;
117
+ if (attempt < MAX_RETRIES) {
118
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
119
+ const jitter = Math.random() * 500;
120
+ process.stderr.write(`Connection error, retrying in ${Math.round((delay + jitter) / 1000)}s...\n`);
121
+ await new Promise(r => setTimeout(r, delay + jitter));
122
+ }
123
+ }
124
+ }
125
+ throw lastError ?? new errors_1.NetworkError(url);
126
+ }
80
127
  async function safeFetchWithRetry(url, options) {
81
128
  let lastError;
82
129
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
@@ -357,3 +404,40 @@ async function fetchUserProfile(config) {
357
404
  const result = await request(config, 'GET', '/users/me');
358
405
  return result.user;
359
406
  }
407
+ /**
408
+ * List environments in a workspace (plus predefined).
409
+ */
410
+ async function listEnvironments(config, workspaceId) {
411
+ const params = workspaceId ? `?workspace_id=${encodeURIComponent(workspaceId)}` : '';
412
+ return request(config, 'GET', `/environments${params}`);
413
+ }
414
+ /**
415
+ * Get environment details including build status.
416
+ */
417
+ async function getEnvironment(config, environmentId) {
418
+ return request(config, 'GET', `/environments/${environmentId}`);
419
+ }
420
+ /**
421
+ * Create a new environment from Dockerfile.
422
+ */
423
+ async function createEnvironment(config, name, dockerfileContent) {
424
+ return request(config, 'POST', '/environments', {
425
+ body: JSON.stringify({ name, dockerfile_content: dockerfileContent }),
426
+ headers: { 'Content-Type': 'application/json' },
427
+ });
428
+ }
429
+ /**
430
+ * Delete an environment (must have no agents using it).
431
+ */
432
+ async function deleteEnvironment(config, environmentId) {
433
+ return request(config, 'DELETE', `/environments/${environmentId}`);
434
+ }
435
+ /**
436
+ * Set workspace default environment.
437
+ */
438
+ async function setWorkspaceDefaultEnvironment(config, workspaceId, environmentId) {
439
+ return request(config, 'POST', `/environments/workspaces/${workspaceId}/default-environment`, {
440
+ body: JSON.stringify({ environment_id: environmentId }),
441
+ headers: { 'Content-Type': 'application/json' },
442
+ });
443
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchagent/cli",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Command-line interface for the orchagent AI agent marketplace",
5
5
  "license": "MIT",
6
6
  "author": "orchagent <hello@orchagent.io>",