@bctrl/cli 0.1.2 → 0.1.4

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.
Files changed (42) hide show
  1. package/README.md +95 -97
  2. package/dist/api/auth.d.ts +5 -6
  3. package/dist/api/auth.js +4 -5
  4. package/dist/api/client.d.ts +1 -0
  5. package/dist/api/client.js +3 -0
  6. package/dist/api/errors.js +2 -2
  7. package/dist/commands/ai/index.js +59 -102
  8. package/dist/commands/{browser → api-key}/index.d.ts +1 -1
  9. package/dist/commands/api-key/index.js +47 -0
  10. package/dist/commands/auth/login.js +2 -1
  11. package/dist/commands/auth/status.js +5 -0
  12. package/dist/commands/browser-extension/index.d.ts +3 -0
  13. package/dist/commands/browser-extension/index.js +86 -0
  14. package/dist/commands/file/index.js +9 -3
  15. package/dist/commands/{invocation → help}/index.d.ts +1 -1
  16. package/dist/commands/help/index.js +17 -0
  17. package/dist/commands/proxy/index.d.ts +3 -0
  18. package/dist/commands/proxy/index.js +66 -0
  19. package/dist/commands/run/index.js +205 -155
  20. package/dist/commands/runtime/index.js +265 -116
  21. package/dist/commands/shared/help.js +1 -1
  22. package/dist/commands/shared/rest.d.ts +1 -0
  23. package/dist/commands/shared/rest.js +14 -6
  24. package/dist/commands/space/index.js +17 -32
  25. package/dist/commands/space/list.d.ts +1 -11
  26. package/dist/commands/space/list.js +14 -23
  27. package/dist/commands/subaccount/index.js +29 -80
  28. package/dist/commands/tool/index.js +15 -7
  29. package/dist/commands/tool-call/index.js +6 -1
  30. package/dist/commands/toolset/index.js +6 -3
  31. package/dist/commands/usage/index.d.ts +3 -0
  32. package/dist/commands/usage/index.js +13 -0
  33. package/dist/commands/vault/index.js +104 -25
  34. package/dist/config/auth-store.d.ts +10 -12
  35. package/dist/config/auth-store.js +4 -5
  36. package/dist/factory.js +1 -1
  37. package/dist/generated/help.d.ts +125 -110
  38. package/dist/generated/help.js +175 -141
  39. package/dist/root.js +10 -4
  40. package/package.json +47 -47
  41. package/dist/commands/browser/index.js +0 -53
  42. package/dist/commands/invocation/index.js +0 -36
@@ -1,12 +1,10 @@
1
- import { Command } from 'commander';
2
- import { CliError } from '../../runtime/errors.js';
3
- import { addCliHelp } from '../shared/help.js';
4
- import { readBlob, readJsonFile, writeBinary } from '../shared/io.js';
1
+ import { Command, Option } from 'commander';
2
+ import { readBlob, readJsonFile } from '../shared/io.js';
5
3
  import { parsePositiveInteger } from '../shared/options.js';
6
4
  import { addOutputFlags, outputData } from '../shared/output.js';
7
- import { addPaginationFlags, createJsonBodyCommand, createListCommand, createViewCommand, pathTemplate, requestAndPrint, } from '../shared/rest.js';
5
+ import { addPaginationFlags, createDeleteCommand, createJsonBodyCommand, createListCommand, createViewCommand, requestAndPrint, } from '../shared/rest.js';
8
6
  export function createRuntimeCommand(factory) {
9
- const command = new Command('runtime').description('Create, start, stop, and manage BCTRL runtimes');
7
+ const command = new Command('runtime').description('Manage runtimes');
10
8
  command.addCommand(createListCommand(factory, {
11
9
  description: 'List runtimes',
12
10
  path: '/runtimes',
@@ -21,188 +19,339 @@ export function createRuntimeCommand(factory) {
21
19
  }),
22
20
  }));
23
21
  command.addCommand(createViewCommand(factory, {
24
- description: 'View a runtime',
25
- path: '/runtimes/{id}',
22
+ name: 'get',
23
+ description: 'Get a runtime',
24
+ path: '/runtimes/{runtimeId}',
25
+ argName: 'runtimeId',
26
26
  }));
27
- command.addCommand(addCliHelp(createJsonBodyCommand(factory, {
27
+ command.addCommand(createRuntimeCreateCommand(factory));
28
+ command.addCommand(createRuntimePatchCommand(factory));
29
+ command.addCommand(createDeleteCommand(factory, {
30
+ description: 'Delete a runtime and its browser state',
31
+ path: '/runtimes/{runtimeId}',
32
+ argNames: ['runtimeId'],
33
+ configure: (cmd) => cmd.option('--force', 'Stop the runtime first if it is still active'),
34
+ query: (_args, options) => ({ force: options.force === true ? true : undefined }),
35
+ }));
36
+ command.addCommand(createRuntimeStartCommand(factory));
37
+ command.addCommand(addOutputFlags(new Command('stop').description('Stop a runtime').argument('<runtimeId>')).action(async (runtimeId, options) => {
38
+ await requestAndPrint(factory, 'post', `/runtimes/${encodeURIComponent(runtimeId)}/stop`, {
39
+ output: options,
40
+ });
41
+ }));
42
+ command.addCommand(createRuntimeFileCommand(factory));
43
+ command.addCommand(createRuntimeInvocationCommand(factory));
44
+ command.addCommand(createRuntimeTargetCommand(factory));
45
+ return command;
46
+ }
47
+ function createRuntimeTargetCommand(factory) {
48
+ const command = new Command('target').description('Manage runtime targets (tabs)');
49
+ command.addCommand(addOutputFlags(new Command('list').description('List targets').argument('<runtimeId>')).action(async (runtimeId, options) => {
50
+ await requestAndPrint(factory, 'get', `/runtimes/${encodeURIComponent(runtimeId)}/targets`, {
51
+ output: options,
52
+ });
53
+ }));
54
+ command.addCommand(addOutputFlags(new Command('create')
55
+ .description('Open a new target')
56
+ .argument('<runtimeId>')
57
+ .option('--uri <uri>', 'Navigate the new target to this URI')
58
+ .option('--activate', 'Focus the new target')).action(async (runtimeId, options) => {
59
+ await requestAndPrint(factory, 'post', `/runtimes/${encodeURIComponent(runtimeId)}/targets`, {
60
+ body: {
61
+ ...(options.uri ? { uri: options.uri } : {}),
62
+ ...(options.activate ? { activate: true } : {}),
63
+ },
64
+ output: options,
65
+ });
66
+ }));
67
+ command.addCommand(addOutputFlags(new Command('get').description('Get a target').argument('<runtimeId>').argument('<targetId>')).action(async (runtimeId, targetId, options) => {
68
+ await requestAndPrint(factory, 'get', `/runtimes/${encodeURIComponent(runtimeId)}/targets/${encodeURIComponent(targetId)}`, { output: options });
69
+ }));
70
+ command.addCommand(addOutputFlags(new Command('activate')
71
+ .description('Focus a target')
72
+ .argument('<runtimeId>')
73
+ .argument('<targetId>')).action(async (runtimeId, targetId, options) => {
74
+ await requestAndPrint(factory, 'post', `/runtimes/${encodeURIComponent(runtimeId)}/targets/${encodeURIComponent(targetId)}/activate`, { output: options });
75
+ }));
76
+ command.addCommand(addOutputFlags(new Command('delete')
77
+ .description('Close a target')
78
+ .argument('<runtimeId>')
79
+ .argument('<targetId>')).action(async (runtimeId, targetId, options) => {
80
+ await requestAndPrint(factory, 'delete', `/runtimes/${encodeURIComponent(runtimeId)}/targets/${encodeURIComponent(targetId)}`, { output: options });
81
+ }));
82
+ return command;
83
+ }
84
+ function createRuntimeCreateCommand(factory) {
85
+ return createJsonBodyCommand(factory, {
28
86
  name: 'create',
29
87
  description: 'Create a runtime',
30
88
  method: 'post',
31
89
  path: '/runtimes',
32
90
  configure: (cmd) => cmd
33
- .option('--space <id>', 'Space id')
91
+ .option('--space <id>', 'Space id; omitted uses caller default space')
34
92
  .option('--name <name>', 'Runtime name')
93
+ .option('--profile', 'Persist browser state across runtime starts')
94
+ .option('--proxy <id-or-url>', 'Saved proxy id or inline custom proxy URL')
95
+ .addOption(new Option('--device <device>', 'Fingerprint device filter').choices([
96
+ 'desktop',
97
+ 'mobile',
98
+ ]))
99
+ .addOption(new Option('--os <os>', 'Fingerprint OS filter').choices([
100
+ 'windows',
101
+ 'macos',
102
+ 'android',
103
+ 'ios',
104
+ ]))
105
+ .addOption(new Option('--browser <browser>', 'Fingerprint browser filter').choices([
106
+ 'chrome',
107
+ 'edge',
108
+ 'safari',
109
+ ]))
110
+ .option('--browser-version <version>', 'Fingerprint browser version filter')
111
+ .option('--locale <locale>', 'Fingerprint locale/language filter; repeat to set an ordered language stack', collectLocale, [])
35
112
  .option('--config-file <path>', 'Runtime config JSON file')
36
113
  .option('--metadata-file <path>', 'Runtime metadata JSON file')
37
114
  .option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
38
115
  body: async (_args, options) => {
39
116
  if (typeof options.input === 'string')
40
117
  return readJsonFile(options.input);
41
- if (typeof options.space !== 'string' || options.space.length === 0) {
42
- throw new CliError('runtime create requires --space unless --input is used');
43
- }
118
+ const config = typeof options.configFile === 'string'
119
+ ? await readJsonFile(options.configFile, '--config-file')
120
+ : undefined;
44
121
  return {
45
122
  type: 'browser',
46
123
  spaceId: options.space,
47
124
  name: options.name,
48
- config: typeof options.configFile === 'string' ? await readJsonFile(options.configFile, '--config-file') : undefined,
125
+ config: buildRuntimeCreateConfig(config, options),
49
126
  metadata: typeof options.metadataFile === 'string'
50
127
  ? await readJsonFile(options.metadataFile, '--metadata-file')
51
128
  : undefined,
52
129
  };
53
130
  },
54
- }), 'runtime.create'));
55
- command.addCommand(addCliHelp(addOutputFlags(new Command('start')
131
+ });
132
+ }
133
+ function collectLocale(value, previous) {
134
+ return [...previous, value];
135
+ }
136
+ function isRecord(value) {
137
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
138
+ }
139
+ function buildRuntimeCreateConfig(config, options) {
140
+ const fingerprint = {
141
+ ...(options.device ? { device: options.device } : {}),
142
+ ...(options.os ? { os: options.os } : {}),
143
+ ...(options.browser ? { browser: options.browser } : {}),
144
+ ...(options.browserVersion ? { browserVersion: options.browserVersion } : {}),
145
+ ...(options.locale && options.locale.length === 1 ? { locale: options.locale[0] } : {}),
146
+ ...(options.locale && options.locale.length > 1 ? { locale: options.locale } : {}),
147
+ };
148
+ const overrides = {
149
+ ...(options.profile === true ? { profile: true } : {}),
150
+ ...(options.proxy ? { proxy: options.proxy } : {}),
151
+ ...(Object.keys(fingerprint).length > 0 ? { fingerprint } : {}),
152
+ };
153
+ if (Object.keys(overrides).length === 0) {
154
+ return config;
155
+ }
156
+ if (config !== undefined && !isRecord(config)) {
157
+ throw new Error('--config-file must contain a JSON object when combined with runtime flags');
158
+ }
159
+ if (!isRecord(config)) {
160
+ return overrides;
161
+ }
162
+ return {
163
+ ...config,
164
+ ...overrides,
165
+ ...(isRecord(config.fingerprint) || 'fingerprint' in overrides
166
+ ? {
167
+ fingerprint: {
168
+ ...(isRecord(config.fingerprint) ? config.fingerprint : {}),
169
+ ...(isRecord(overrides.fingerprint) ? overrides.fingerprint : {}),
170
+ },
171
+ }
172
+ : {}),
173
+ };
174
+ }
175
+ function createRuntimePatchCommand(factory) {
176
+ return createJsonBodyCommand(factory, {
177
+ name: 'patch',
178
+ description: 'Update a runtime',
179
+ method: 'patch',
180
+ path: '/runtimes/{runtimeId}',
181
+ argNames: ['runtimeId'],
182
+ configure: (cmd) => cmd
183
+ .option('--name <name>', 'Runtime name')
184
+ .option('--idle-timeout-minutes <minutes>', 'Idle timeout in minutes', parsePositiveInteger)
185
+ .option('--config-file <path>', 'Runtime config JSON file')
186
+ .option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
187
+ body: async (_args, options) => {
188
+ if (typeof options.input === 'string')
189
+ return readJsonFile(options.input);
190
+ return {
191
+ name: options.name,
192
+ idleTimeoutMinutes: options.idleTimeoutMinutes,
193
+ config: typeof options.configFile === 'string'
194
+ ? await readJsonFile(options.configFile, '--config-file')
195
+ : undefined,
196
+ };
197
+ },
198
+ });
199
+ }
200
+ function createRuntimeStartCommand(factory) {
201
+ return addOutputFlags(new Command('start')
56
202
  .description('Start a runtime')
57
- .argument('<id>'))
58
- .action(async (id, options) => {
59
- await requestAndPrint(factory, 'post', `/runtimes/${encodeURIComponent(id)}/start`, {
60
- output: options,
61
- });
62
- }), 'runtime.start'));
63
- command.addCommand(addOutputFlags(new Command('stop')
64
- .description('Stop a runtime')
65
- .argument('<id>'))
66
- .action(async (id, options) => {
67
- await requestAndPrint(factory, 'post', `/runtimes/${encodeURIComponent(id)}/stop`, {
68
- output: options,
69
- });
70
- }));
71
- command.addCommand(createRuntimeConnectionCommand(factory));
72
- command.addCommand(createRuntimeFileCommand(factory));
73
- command.addCommand(addOutputFlags(new Command('runs')
74
- .description('List runs for a runtime')
75
- .argument('<id>')
76
- .option('-L, --limit <number>', 'Maximum number of results to return', parsePositiveInteger)
77
- .option('--cursor <cursor>', 'Pagination cursor'))
78
- .action(async (id, options) => {
79
- await requestAndPrint(factory, 'get', `/runtimes/${encodeURIComponent(id)}/runs`, {
80
- query: { limit: options.limit, cursor: options.cursor },
203
+ .argument('<runtimeId>')
204
+ .option('--idempotency-key <key>', 'Idempotency key for retry-safe start')).action(async (runtimeId, options) => {
205
+ await requestAndPrint(factory, 'post', `/runtimes/${encodeURIComponent(runtimeId)}/start`, {
206
+ idempotencyKey: options.idempotencyKey,
81
207
  output: options,
82
208
  });
209
+ });
210
+ }
211
+ function createRuntimeInvocationCommand(factory) {
212
+ const command = new Command('invocation').description('Create, wait for, and cancel invocations');
213
+ command.addCommand(createRuntimeInvocationCreateCommand(factory));
214
+ command.addCommand(createRuntimeInvocationWaitCommand(factory));
215
+ command.addCommand(addOutputFlags(new Command('cancel')
216
+ .description('Cancel an invocation')
217
+ .argument('<runtimeId>')
218
+ .argument('<invocationId>')).action(async (runtimeId, invocationId, options) => {
219
+ await requestAndPrint(factory, 'post', `/runtimes/${encodeURIComponent(runtimeId)}/invocations/${encodeURIComponent(invocationId)}/cancel`, { output: options });
83
220
  }));
84
221
  return command;
85
222
  }
86
- function createRuntimeConnectionCommand(factory) {
87
- const command = new Command('connection').description('Manage runtime connections');
88
- command.addCommand(addOutputFlags(new Command('list')
89
- .description('List runtime connections')
90
- .argument('<runtime>')
91
- .option('--status <status>', 'Filter by connection status')
92
- .option('-L, --limit <number>', 'Maximum number of results to return', parsePositiveInteger))
93
- .action(async (runtime, options) => {
94
- await requestAndPrint(factory, 'get', `/runtimes/${encodeURIComponent(runtime)}/connections`, {
95
- query: { status: options.status, limit: options.limit },
223
+ function createRuntimeInvocationCreateCommand(factory) {
224
+ return addOutputFlags(new Command('create')
225
+ .description('Create a runtime invocation')
226
+ .argument('<runtimeId>')
227
+ .option('--action <action>', 'Invocation action')
228
+ .option('--instruction <text>', 'Instruction for action/agent invocations')
229
+ .option('--target <target>', "Target to act on: 'active' (default), 'new', or a target id")
230
+ .option('--model <model>', 'Model id')
231
+ .option('--tool <id...>', 'Inline tool id')
232
+ .option('--toolset <id>', 'Toolset id')
233
+ .option('--timeout-ms <ms>', 'Invocation timeout in milliseconds', parsePositiveInteger)
234
+ .option('--idempotency-key <key>', 'Idempotency key for retry-safe create')
235
+ .option('--input <path>', 'Read full JSON request body from file, or - for stdin')).action(async (runtimeId, options) => {
236
+ const body = typeof options.input === 'string'
237
+ ? await readJsonFile(options.input)
238
+ : {
239
+ action: options.action,
240
+ instruction: options.instruction,
241
+ target: options.target === undefined
242
+ ? undefined
243
+ : options.target === 'active' || options.target === 'new'
244
+ ? options.target
245
+ : { id: options.target },
246
+ model: options.model,
247
+ toolIds: options.tool,
248
+ toolsetId: options.toolset,
249
+ timeoutMs: options.timeoutMs,
250
+ };
251
+ await requestAndPrint(factory, 'post', `/runtimes/${encodeURIComponent(runtimeId)}/invocations`, {
252
+ body,
253
+ idempotencyKey: options.idempotencyKey,
96
254
  output: options,
97
255
  });
98
- }));
99
- command.addCommand(addCliHelp(createJsonBodyCommand(factory, {
100
- name: 'create',
101
- description: 'Create a runtime connection',
256
+ });
257
+ }
258
+ function createRuntimeInvocationWaitCommand(factory) {
259
+ return createJsonBodyCommand(factory, {
260
+ name: 'wait',
261
+ description: 'Wait for an invocation',
102
262
  method: 'post',
103
- path: '/runtimes/{runtime}/connections',
104
- argNames: ['runtime'],
263
+ path: '/runtimes/{runtimeId}/invocations/{invocationId}/wait',
264
+ argNames: ['runtimeId', 'invocationId'],
105
265
  configure: (cmd) => cmd
106
- .option('--protocol <protocol>', 'Connection protocol', 'cdp')
266
+ .option('--timeout-ms <ms>', 'Wait timeout in milliseconds', parsePositiveInteger)
107
267
  .option('--input <path>', 'Read JSON request body from file, or - for stdin'),
108
268
  body: async (_args, options) => {
109
269
  if (typeof options.input === 'string')
110
270
  return readJsonFile(options.input);
111
- return {
112
- protocol: options.protocol,
113
- };
271
+ return { timeoutMs: options.timeoutMs };
114
272
  },
115
- }), 'runtime.connection.create'));
116
- command.addCommand(addOutputFlags(new Command('delete')
117
- .description('Revoke a runtime connection')
118
- .argument('<runtime>')
119
- .argument('<connection>'))
120
- .action(async (runtime, connection, options) => {
121
- await requestAndPrint(factory, 'delete', pathTemplate('/runtimes/{runtime}/connections/{connection}', { runtime, connection }), { output: options });
122
- }));
123
- return command;
273
+ });
124
274
  }
125
275
  function createRuntimeFileCommand(factory) {
126
276
  const command = new Command('file').description('Move files into and out of runtimes');
127
- command.addCommand(addOutputFlags(new Command('push')
128
- .description('Upload and stage a local file into a runtime')
129
- .argument('<runtime>')
130
- .argument('<local-path>')
131
- .option('--storage-path <path>', 'Advanced: durable BCTRL storage path')
277
+ command.addCommand(addOutputFlags(addPaginationFlags(new Command('list').description('List runtime files').argument('<runtimeId>'))
278
+ .option('--type <type>', 'Filter by file type')).action(async (runtimeId, options) => {
279
+ await requestAndPrint(factory, 'get', `/runtimes/${encodeURIComponent(runtimeId)}/files`, {
280
+ query: { type: options.type, limit: options.limit, cursor: options.cursor },
281
+ output: options,
282
+ });
283
+ }));
284
+ command.addCommand(createRuntimeFileUploadCommand(factory));
285
+ command.addCommand(createRuntimeFileStageCommand(factory));
286
+ command.addCommand(createRuntimeFileCollectCommand(factory));
287
+ return command;
288
+ }
289
+ function createRuntimeFileUploadCommand(factory) {
290
+ return addOutputFlags(new Command('upload')
291
+ .description('Upload a local file into a runtime workspace')
292
+ .argument('<runtimeId>')
293
+ .argument('<localPath>')
294
+ .option('--destination-path <path>', 'Durable BCTRL storage destination')
295
+ .option('--runtime-path <path>', 'Runtime workspace destination')
132
296
  .option('--name <name>', 'Display name')
133
- .option('--metadata-file <path>', 'Metadata JSON file'))
134
- .action(async (runtime, localPath, options) => {
297
+ .option('--metadata-file <path>', 'Metadata JSON file')).action(async (runtimeId, localPath, options) => {
135
298
  const client = await factory.apiClient();
136
299
  const file = await readBlob(localPath);
137
300
  const metadata = typeof options.metadataFile === 'string'
138
301
  ? JSON.stringify(await readJsonFile(options.metadataFile, '--metadata-file'))
139
302
  : undefined;
140
- const result = await client.uploadFile(`/runtimes/${encodeURIComponent(runtime)}/files/upload`, {
303
+ const result = await client.uploadFile(`/runtimes/${encodeURIComponent(runtimeId)}/files/upload`, {
141
304
  file: file.blob,
142
305
  fileName: options.name ?? file.fileName,
143
306
  fields: {
144
- ...(options.storagePath ? { storagePath: options.storagePath } : {}),
307
+ ...(options.destinationPath ? { destinationPath: options.destinationPath } : {}),
308
+ ...(options.runtimePath ? { runtimePath: options.runtimePath } : {}),
145
309
  ...(options.name ? { name: options.name } : {}),
146
310
  ...(metadata ? { metadata } : {}),
147
311
  },
148
312
  });
149
313
  await outputData(factory.io, result, options);
150
- }));
151
- command.addCommand(addOutputFlags(new Command('pull')
152
- .description('Collect a runtime file and download it locally')
153
- .argument('<runtime>')
154
- .argument('<runtime-path>')
155
- .requiredOption('--to <path>', 'Output path, or - for stdout')
156
- .option('--name <name>', 'Durable file name')
157
- .option('--storage-path <path>', 'Advanced: durable BCTRL storage path'))
158
- .action(async (runtime, runtimePath, options) => {
159
- const client = await factory.apiClient();
160
- const collected = await client.post(`/runtimes/${encodeURIComponent(runtime)}/files/collect`, {
161
- body: {
162
- path: runtimePath,
163
- name: options.name,
164
- storagePath: options.storagePath,
165
- },
166
- });
167
- if (!collected.id) {
168
- throw new CliError('Expected runtime file collect response to include id');
169
- }
170
- const data = await client.download(`/files/${encodeURIComponent(collected.id)}/content`);
171
- await writeBinary(options.to, data);
172
- }));
173
- command.addCommand(createJsonBodyCommand(factory, {
314
+ });
315
+ }
316
+ function createRuntimeFileStageCommand(factory) {
317
+ return createJsonBodyCommand(factory, {
174
318
  name: 'stage',
175
319
  description: 'Stage a durable file into a runtime',
176
320
  method: 'post',
177
- path: '/runtimes/{runtime}/files/stage',
178
- argNames: ['runtime'],
321
+ path: '/runtimes/{runtimeId}/files/stage',
322
+ argNames: ['runtimeId'],
179
323
  configure: (cmd) => cmd
180
324
  .option('--file <id>', 'Durable file id')
181
- .option('--storage-path <path>', 'Advanced: durable BCTRL storage path')
182
- .option('--name <name>', 'Runtime-local name')
325
+ .option('--runtime-path <path>', 'Runtime workspace destination')
326
+ .option('--name <name>', 'Runtime-local display name')
183
327
  .option('--input <path>', 'Read JSON request body from file, or - for stdin'),
184
328
  body: async (_args, options) => {
185
329
  if (typeof options.input === 'string')
186
330
  return readJsonFile(options.input);
187
- return { fileId: options.file, storagePath: options.storagePath, name: options.name };
331
+ return { fileId: options.file, runtimePath: options.runtimePath, name: options.name };
188
332
  },
189
- }));
190
- command.addCommand(createJsonBodyCommand(factory, {
333
+ });
334
+ }
335
+ function createRuntimeFileCollectCommand(factory) {
336
+ return createJsonBodyCommand(factory, {
191
337
  name: 'collect',
192
338
  description: 'Collect a runtime-local file into durable storage',
193
339
  method: 'post',
194
- path: '/runtimes/{runtime}/files/collect',
195
- argNames: ['runtime'],
340
+ path: '/runtimes/{runtimeId}/files/collect',
341
+ argNames: ['runtimeId'],
196
342
  configure: (cmd) => cmd
197
- .requiredOption('--path <path>', 'Runtime-local path')
343
+ .requiredOption('--runtime-path <path>', 'Runtime workspace source')
198
344
  .option('--name <name>', 'Durable file name')
199
- .option('--storage-path <path>', 'Advanced: durable BCTRL storage path')
345
+ .option('--destination-path <path>', 'Durable BCTRL storage destination')
200
346
  .option('--input <path>', 'Read JSON request body from file, or - for stdin'),
201
347
  body: async (_args, options) => {
202
348
  if (typeof options.input === 'string')
203
349
  return readJsonFile(options.input);
204
- return { path: options.path, name: options.name, storagePath: options.storagePath };
350
+ return {
351
+ runtimePath: options.runtimePath,
352
+ name: options.name,
353
+ destinationPath: options.destinationPath,
354
+ };
205
355
  },
206
- }));
207
- return command;
356
+ });
208
357
  }
@@ -10,7 +10,7 @@ function toCommandHelpSpec(help) {
10
10
  const raw = help;
11
11
  return {
12
12
  purpose: raw.summary,
13
- flags: raw.flags?.map((flag) => ({ ...flag })),
13
+ flags: raw.cli?.flags?.map((flag) => ({ ...flag })),
14
14
  input: raw.input?.fields.map((field) => ({ ...field })),
15
15
  output: raw.output?.fields.map((field) => ({ ...field })),
16
16
  examples: raw.examples?.map(formatHelpItem).filter((value) => Boolean(value)),
@@ -14,6 +14,7 @@ export declare function addJsonBodyFlag(command: Command): Command;
14
14
  export declare function requestAndPrint(deps: ApiDeps, method: ClientMethod, path: string, options?: {
15
15
  query?: Record<string, string | number | boolean | undefined>;
16
16
  body?: unknown;
17
+ idempotencyKey?: string;
17
18
  output?: OutputFlags;
18
19
  }): Promise<void>;
19
20
  export declare function bodyFromInputOption(path: string | undefined): Promise<unknown>;
@@ -16,16 +16,24 @@ export function addJsonBodyFlag(command) {
16
16
  }
17
17
  export async function requestAndPrint(deps, method, path, options) {
18
18
  const client = await deps.apiClient();
19
+ const requestOptions = options
20
+ ? {
21
+ ...(options.query !== undefined ? { query: options.query } : {}),
22
+ ...(options.body !== undefined ? { body: options.body } : {}),
23
+ ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
24
+ }
25
+ : undefined;
26
+ const clientOptions = requestOptions && Object.keys(requestOptions).length > 0 ? requestOptions : undefined;
19
27
  const result = method === 'get'
20
- ? await client.get(path, options)
28
+ ? await client.get(path, clientOptions)
21
29
  : method === 'post'
22
- ? await client.post(path, options)
30
+ ? await client.post(path, clientOptions)
23
31
  : method === 'patch'
24
- ? await client.patch(path, options)
32
+ ? await client.patch(path, clientOptions)
25
33
  : method === 'put'
26
- ? await client.put(path, options)
27
- : await client.delete(path, options);
28
- await outputData(deps.io, result, options?.output);
34
+ ? await client.put(path, clientOptions)
35
+ : await client.delete(path, clientOptions);
36
+ await outputData(deps.io, result, options?.output ? outputFlags(options.output) : undefined);
29
37
  }
30
38
  export async function bodyFromInputOption(path) {
31
39
  return path ? readJsonFile(path) : {};
@@ -1,14 +1,15 @@
1
1
  import { Command } from 'commander';
2
- import { createJsonBodyCommand, createViewCommand } from '../shared/rest.js';
2
+ import { createDeleteCommand, createJsonBodyCommand, createViewCommand } from '../shared/rest.js';
3
3
  import { readJsonFile } from '../shared/io.js';
4
- import { addOutputFlags, outputData } from '../shared/output.js';
5
4
  import { createSpaceListCommand } from './list.js';
6
5
  export function createSpaceCommand(factory) {
7
6
  const command = new Command('space').description('Manage BCTRL spaces');
8
7
  command.addCommand(createSpaceListCommand(factory));
9
8
  command.addCommand(createViewCommand(factory, {
9
+ name: 'get',
10
10
  description: 'View a space',
11
- path: '/spaces/{id}',
11
+ path: '/spaces/{spaceId}',
12
+ argName: 'spaceId',
12
13
  }));
13
14
  command.addCommand(createJsonBodyCommand(factory, {
14
15
  name: 'create',
@@ -17,7 +18,6 @@ export function createSpaceCommand(factory) {
17
18
  path: '/spaces',
18
19
  configure: (cmd) => cmd
19
20
  .option('--name <name>', 'Space name')
20
- .option('--region <region>', 'Space region')
21
21
  .option('--subaccount-id <id>', 'Subaccount id')
22
22
  .option('--input <path>', 'Read JSON request body from file, or - for stdin'),
23
23
  body: async (_args, options) => {
@@ -25,17 +25,16 @@ export function createSpaceCommand(factory) {
25
25
  return readJsonFile(options.input);
26
26
  return {
27
27
  name: options.name,
28
- region: options.region,
29
28
  subaccountId: options.subaccountId,
30
29
  };
31
30
  },
32
31
  }));
33
32
  command.addCommand(createJsonBodyCommand(factory, {
34
- name: 'edit',
33
+ name: 'patch',
35
34
  description: 'Edit a space',
36
35
  method: 'patch',
37
- path: '/spaces/{id}',
38
- argNames: ['id'],
36
+ path: '/spaces/{spaceId}',
37
+ argNames: ['spaceId'],
39
38
  configure: (cmd) => cmd
40
39
  .option('--name <name>', 'Space name')
41
40
  .option('--input <path>', 'Read JSON request body from file, or - for stdin'),
@@ -45,8 +44,12 @@ export function createSpaceCommand(factory) {
45
44
  return { name: options.name };
46
45
  },
47
46
  }));
47
+ command.addCommand(createDeleteCommand(factory, {
48
+ description: 'Delete a space',
49
+ path: '/spaces/{spaceId}',
50
+ argNames: ['spaceId'],
51
+ }));
48
52
  command.addCommand(createSpaceEnvironmentCommand(factory));
49
- command.addCommand(createSpaceAgentContextCommand(factory));
50
53
  return command;
51
54
  }
52
55
  function createSpaceEnvironmentCommand(factory) {
@@ -54,33 +57,15 @@ function createSpaceEnvironmentCommand(factory) {
54
57
  command.addCommand(createViewCommand(factory, {
55
58
  name: 'get',
56
59
  description: 'Get a space environment',
57
- path: '/spaces/{id}/environment',
60
+ path: '/spaces/{spaceId}/environment',
61
+ argName: 'spaceId',
58
62
  }));
59
63
  command.addCommand(createJsonBodyCommand(factory, {
60
- name: 'set',
64
+ name: 'patch',
61
65
  description: 'Update a space environment',
62
66
  method: 'patch',
63
- path: '/spaces/{id}/environment',
64
- argNames: ['id'],
67
+ path: '/spaces/{spaceId}/environment',
68
+ argNames: ['spaceId'],
65
69
  }));
66
70
  return command;
67
71
  }
68
- function createSpaceAgentContextCommand(factory) {
69
- return addOutputFlags(new Command('agent-context')
70
- .description('Get machine-readable context for coding agents')
71
- .argument('<id>')
72
- .option('--runtime <id>', 'Runtime id')
73
- .option('--topic <topic>', 'Context topic')
74
- .option('--detail <level>', 'Detail level'))
75
- .action(async (id, options) => {
76
- const client = await factory.apiClient();
77
- const result = await client.get(`/spaces/${encodeURIComponent(id)}/agent-context`, {
78
- query: {
79
- runtimeId: options.runtime,
80
- topic: options.topic,
81
- detail: options.detail,
82
- },
83
- });
84
- await outputData(factory.io, result, options);
85
- });
86
- }
@@ -1,13 +1,3 @@
1
1
  import { Command } from 'commander';
2
- import type { BctrlApiClient } from '../../api/client.js';
3
2
  import type { Factory } from '../../factory.js';
4
- import type { IOStreams } from '../../io/streams.js';
5
- import { type OutputFlags } from '../shared/output.js';
6
- export type SpaceListOptions = {
7
- io: IOStreams;
8
- apiClient: () => Promise<BctrlApiClient>;
9
- limit?: number;
10
- output?: OutputFlags;
11
- };
12
- export declare function createSpaceListCommand(factory: Factory, run?: (options: SpaceListOptions) => Promise<void>): Command;
13
- export declare function spaceListRun(options: SpaceListOptions): Promise<void>;
3
+ export declare function createSpaceListCommand(factory: Factory): Command;