@bctrl/cli 0.1.0

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 (76) hide show
  1. package/README.md +44 -0
  2. package/dist/api/auth.d.ts +15 -0
  3. package/dist/api/auth.js +32 -0
  4. package/dist/api/client.d.ts +22 -0
  5. package/dist/api/client.js +130 -0
  6. package/dist/api/errors.d.ts +2 -0
  7. package/dist/api/errors.js +95 -0
  8. package/dist/bin/bctrl.d.ts +2 -0
  9. package/dist/bin/bctrl.js +6 -0
  10. package/dist/commands/ai/index.d.ts +3 -0
  11. package/dist/commands/ai/index.js +130 -0
  12. package/dist/commands/auth/index.d.ts +3 -0
  13. package/dist/commands/auth/index.js +13 -0
  14. package/dist/commands/auth/login.d.ts +11 -0
  15. package/dist/commands/auth/login.js +52 -0
  16. package/dist/commands/auth/logout.d.ts +10 -0
  17. package/dist/commands/auth/logout.js +23 -0
  18. package/dist/commands/auth/status.d.ts +12 -0
  19. package/dist/commands/auth/status.js +31 -0
  20. package/dist/commands/auth/token.d.ts +11 -0
  21. package/dist/commands/auth/token.js +24 -0
  22. package/dist/commands/browser/index.d.ts +3 -0
  23. package/dist/commands/browser/index.js +53 -0
  24. package/dist/commands/file/index.d.ts +3 -0
  25. package/dist/commands/file/index.js +88 -0
  26. package/dist/commands/invocation/index.d.ts +3 -0
  27. package/dist/commands/invocation/index.js +36 -0
  28. package/dist/commands/run/index.d.ts +3 -0
  29. package/dist/commands/run/index.js +247 -0
  30. package/dist/commands/runtime/index.d.ts +3 -0
  31. package/dist/commands/runtime/index.js +197 -0
  32. package/dist/commands/shared/hidden-input.d.ts +2 -0
  33. package/dist/commands/shared/hidden-input.js +60 -0
  34. package/dist/commands/shared/io.d.ts +9 -0
  35. package/dist/commands/shared/io.js +58 -0
  36. package/dist/commands/shared/options.d.ts +2 -0
  37. package/dist/commands/shared/options.js +15 -0
  38. package/dist/commands/shared/output.d.ts +10 -0
  39. package/dist/commands/shared/output.js +105 -0
  40. package/dist/commands/shared/rest.d.ts +54 -0
  41. package/dist/commands/shared/rest.js +121 -0
  42. package/dist/commands/space/index.d.ts +3 -0
  43. package/dist/commands/space/index.js +86 -0
  44. package/dist/commands/space/list.d.ts +13 -0
  45. package/dist/commands/space/list.js +25 -0
  46. package/dist/commands/subaccount/index.d.ts +3 -0
  47. package/dist/commands/subaccount/index.js +162 -0
  48. package/dist/commands/tool/index.d.ts +3 -0
  49. package/dist/commands/tool/index.js +40 -0
  50. package/dist/commands/tool-call/index.d.ts +3 -0
  51. package/dist/commands/tool-call/index.js +25 -0
  52. package/dist/commands/toolset/index.d.ts +3 -0
  53. package/dist/commands/toolset/index.js +33 -0
  54. package/dist/commands/vault/index.d.ts +3 -0
  55. package/dist/commands/vault/index.js +96 -0
  56. package/dist/commands/version/version.d.ts +9 -0
  57. package/dist/commands/version/version.js +14 -0
  58. package/dist/config/auth-store.d.ts +37 -0
  59. package/dist/config/auth-store.js +71 -0
  60. package/dist/config/config.d.ts +12 -0
  61. package/dist/config/config.js +35 -0
  62. package/dist/factory.d.ts +15 -0
  63. package/dist/factory.js +15 -0
  64. package/dist/index.d.ts +3 -0
  65. package/dist/index.js +3 -0
  66. package/dist/io/streams.d.ts +22 -0
  67. package/dist/io/streams.js +42 -0
  68. package/dist/root.d.ts +3 -0
  69. package/dist/root.js +40 -0
  70. package/dist/runtime/errors.d.ts +19 -0
  71. package/dist/runtime/errors.js +23 -0
  72. package/dist/runtime/exit-codes.d.ts +7 -0
  73. package/dist/runtime/exit-codes.js +6 -0
  74. package/dist/runtime/main.d.ts +1 -0
  75. package/dist/runtime/main.js +27 -0
  76. package/package.json +47 -0
@@ -0,0 +1,11 @@
1
+ import { Command } from 'commander';
2
+ import type { BctrlConfig } from '../../config/config.js';
3
+ import type { Factory } from '../../factory.js';
4
+ import type { IOStreams } from '../../io/streams.js';
5
+ export type AuthTokenOptions = {
6
+ io: IOStreams;
7
+ config: () => Promise<BctrlConfig>;
8
+ reveal?: boolean;
9
+ };
10
+ export declare function createAuthTokenCommand(factory: Factory, run?: (options: AuthTokenOptions) => Promise<void>): Command;
11
+ export declare function authTokenRun(options: AuthTokenOptions): Promise<void>;
@@ -0,0 +1,24 @@
1
+ import { Command } from 'commander';
2
+ import { AuthError, CliError } from '../../runtime/errors.js';
3
+ export function createAuthTokenCommand(factory, run = authTokenRun) {
4
+ return new Command('token')
5
+ .description('Print the active BCTRL API key')
6
+ .option('--reveal', 'Allow printing the API key to a terminal')
7
+ .action(async (options) => {
8
+ await run({
9
+ io: factory.io,
10
+ config: factory.config,
11
+ reveal: options.reveal,
12
+ });
13
+ });
14
+ }
15
+ export async function authTokenRun(options) {
16
+ const config = await options.config();
17
+ if (!config.activeToken) {
18
+ throw new AuthError();
19
+ }
20
+ if (options.io.isStdoutTTY() && options.reveal !== true) {
21
+ throw new CliError('Refusing to print API key to a terminal without --reveal');
22
+ }
23
+ options.io.writeOut(`${config.activeToken.token}\n`);
24
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { Factory } from '../../factory.js';
3
+ export declare function createBrowserCommand(factory: Factory): Command;
@@ -0,0 +1,53 @@
1
+ import { Command } from 'commander';
2
+ import { readBlob, readJsonFile } from '../shared/io.js';
3
+ import { addOutputFlags, outputData } from '../shared/output.js';
4
+ import { createDeleteCommand, createJsonBodyCommand, createListCommand, createViewCommand, } from '../shared/rest.js';
5
+ export function createBrowserCommand(factory) {
6
+ const command = new Command('browser').description('Manage browser resources');
7
+ command.addCommand(createBrowserExtensionCommand(factory));
8
+ return command;
9
+ }
10
+ function createBrowserExtensionCommand(factory) {
11
+ const command = new Command('extension').description('Manage browser extensions');
12
+ command.addCommand(createListCommand(factory, {
13
+ description: 'List browser extensions',
14
+ path: '/browser-extensions',
15
+ configure: (cmd) => cmd,
16
+ }));
17
+ command.addCommand(createViewCommand(factory, {
18
+ description: 'View a browser extension',
19
+ path: '/browser-extensions/{id}',
20
+ }));
21
+ command.addCommand(addOutputFlags(new Command('upload')
22
+ .description('Upload a browser extension package')
23
+ .argument('<path>'))
24
+ .action(async (path, options) => {
25
+ const client = await factory.apiClient();
26
+ const file = await readBlob(path);
27
+ const result = await client.uploadFile('/browser-extensions/upload', {
28
+ file: file.blob,
29
+ fileName: file.fileName,
30
+ });
31
+ await outputData(factory.io, result, options);
32
+ }));
33
+ command.addCommand(createJsonBodyCommand(factory, {
34
+ name: 'import',
35
+ description: 'Import a browser extension from a URL',
36
+ method: 'post',
37
+ path: '/browser-extensions/import',
38
+ configure: (cmd) => cmd
39
+ .option('--url <url>', 'Extension URL')
40
+ .option('--from-file <path>', 'Read full JSON request body from file, or - for stdin'),
41
+ body: async (_args, options) => {
42
+ if (typeof options.fromFile === 'string') {
43
+ return readJsonFile(options.fromFile);
44
+ }
45
+ return { source: { type: 'url', url: options.url } };
46
+ },
47
+ }));
48
+ command.addCommand(createDeleteCommand(factory, {
49
+ description: 'Delete a browser extension',
50
+ path: '/browser-extensions/{id}',
51
+ }));
52
+ return command;
53
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { Factory } from '../../factory.js';
3
+ export declare function createFileCommand(factory: Factory): Command;
@@ -0,0 +1,88 @@
1
+ import { Command } from 'commander';
2
+ import { readBlob, readJsonFile, writeBinary } from '../shared/io.js';
3
+ import { parsePositiveInteger } from '../shared/options.js';
4
+ import { addOutputFlags, outputData } from '../shared/output.js';
5
+ import { createDeleteCommand, createJsonBodyCommand, createListCommand, createViewCommand, } from '../shared/rest.js';
6
+ export function createFileCommand(factory) {
7
+ const command = new Command('file').description('Upload, download, and manage BCTRL files');
8
+ command.addCommand(createListCommand(factory, {
9
+ description: 'List files',
10
+ path: '/files',
11
+ configure: (cmd) => cmd
12
+ .option('--space <id>', 'Filter by space id')
13
+ .option('--source <source>', 'Filter by file source')
14
+ .option('--prefix <path>', 'Filter by path prefix')
15
+ .option('--query <text>', 'Search query')
16
+ .option('-L, --limit <number>', 'Maximum number of results to return', parsePositiveInteger)
17
+ .option('--cursor <cursor>', 'Pagination cursor'),
18
+ query: (options) => ({
19
+ spaceId: typeof options.space === 'string' ? options.space : undefined,
20
+ source: typeof options.source === 'string' ? options.source : undefined,
21
+ prefix: typeof options.prefix === 'string' ? options.prefix : undefined,
22
+ query: typeof options.query === 'string' ? options.query : undefined,
23
+ limit: typeof options.limit === 'number' ? options.limit : undefined,
24
+ cursor: typeof options.cursor === 'string' ? options.cursor : undefined,
25
+ }),
26
+ }));
27
+ command.addCommand(createViewCommand(factory, {
28
+ description: 'View file metadata',
29
+ path: '/files/{fileId}',
30
+ argName: 'fileId',
31
+ }));
32
+ command.addCommand(addOutputFlags(new Command('upload')
33
+ .description('Upload a file')
34
+ .argument('<path>')
35
+ .requiredOption('--space <id>', 'Space id')
36
+ .option('--path <storagePath>', 'Storage path')
37
+ .option('--name <name>', 'Display name'))
38
+ .action(async (path, options) => {
39
+ const client = await factory.apiClient();
40
+ const file = await readBlob(path);
41
+ const result = await client.uploadFile('/files', {
42
+ file: file.blob,
43
+ fileName: options.name ?? file.fileName,
44
+ query: { spaceId: options.space },
45
+ fields: {
46
+ ...(options.path ? { path: options.path } : {}),
47
+ ...(options.name ? { name: options.name } : {}),
48
+ },
49
+ });
50
+ await outputData(factory.io, result, options);
51
+ }));
52
+ command.addCommand(new Command('download')
53
+ .description('Download file content')
54
+ .argument('<id>')
55
+ .requiredOption('--to <path>', 'Output path, or - for stdout')
56
+ .action(async (id, options) => {
57
+ const client = await factory.apiClient();
58
+ const data = await client.download(`/files/${encodeURIComponent(id)}/content`);
59
+ await writeBinary(options.to, data);
60
+ }));
61
+ command.addCommand(createJsonBodyCommand(factory, {
62
+ name: 'edit',
63
+ description: 'Edit file metadata',
64
+ method: 'patch',
65
+ path: '/files/{fileId}',
66
+ argNames: ['fileId'],
67
+ configure: (cmd) => cmd
68
+ .option('--name <name>', 'Display name')
69
+ .option('--metadata-file <path>', 'Metadata JSON file')
70
+ .option('--from-file <path>', 'Read full JSON request body from file, or - for stdin'),
71
+ body: async (_args, options) => {
72
+ if (typeof options.fromFile === 'string')
73
+ return readJsonFile(options.fromFile);
74
+ return {
75
+ name: options.name,
76
+ metadata: typeof options.metadataFile === 'string'
77
+ ? await readJsonFile(options.metadataFile, '--metadata-file')
78
+ : undefined,
79
+ };
80
+ },
81
+ }));
82
+ command.addCommand(createDeleteCommand(factory, {
83
+ description: 'Delete a file',
84
+ path: '/files/{fileId}',
85
+ argNames: ['fileId'],
86
+ }));
87
+ return command;
88
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { Factory } from '../../factory.js';
3
+ export declare function createInvocationCommand(factory: Factory): Command;
@@ -0,0 +1,36 @@
1
+ import { Command } from 'commander';
2
+ import { readJsonFile } from '../shared/io.js';
3
+ import { addOutputFlags } from '../shared/output.js';
4
+ import { createJsonBodyCommand, createViewCommand, requestAndPrint } from '../shared/rest.js';
5
+ export function createInvocationCommand(factory) {
6
+ const command = new Command('invocation')
7
+ .description('Inspect and control BCTRL invocations');
8
+ command.addCommand(createViewCommand(factory, {
9
+ description: 'View an invocation',
10
+ path: '/invocations/{id}',
11
+ }));
12
+ command.addCommand(createJsonBodyCommand(factory, {
13
+ name: 'wait',
14
+ description: 'Wait for an invocation',
15
+ method: 'post',
16
+ path: '/invocations/{id}/wait',
17
+ argNames: ['id'],
18
+ configure: (cmd) => cmd
19
+ .option('--timeout-ms <ms>', 'Timeout in milliseconds')
20
+ .option('--from-file <path>', 'Read JSON request body from file, or - for stdin'),
21
+ body: async (_args, options) => {
22
+ if (typeof options.fromFile === 'string')
23
+ return readJsonFile(options.fromFile);
24
+ return { timeoutMs: options.timeoutMs ? Number(options.timeoutMs) : undefined };
25
+ },
26
+ }));
27
+ command.addCommand(addOutputFlags(new Command('cancel')
28
+ .description('Cancel an invocation')
29
+ .argument('<id>'))
30
+ .action(async (id, options) => {
31
+ await requestAndPrint(factory, 'post', `/invocations/${encodeURIComponent(id)}/cancel`, {
32
+ output: options,
33
+ });
34
+ }));
35
+ return command;
36
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { Factory } from '../../factory.js';
3
+ export declare function createRunCommand(factory: Factory): Command;
@@ -0,0 +1,247 @@
1
+ import { Command } from 'commander';
2
+ import { CliError } from '../../runtime/errors.js';
3
+ import { readJsonFile, writeBinary } from '../shared/io.js';
4
+ import { parsePositiveInteger } from '../shared/options.js';
5
+ import { addOutputFlags } from '../shared/output.js';
6
+ import { addPaginationFlags, createJsonBodyCommand, createListCommand, createViewCommand, requestAndPrint, } from '../shared/rest.js';
7
+ export function createRunCommand(factory) {
8
+ const command = new Command('run').description('Inspect and stream BCTRL run history');
9
+ command.addCommand(createListCommand(factory, {
10
+ description: 'List runs',
11
+ path: '/runs',
12
+ configure: (cmd) => addPaginationFlags(cmd)
13
+ .option('--space <id>', 'Filter by space id')
14
+ .option('--runtime <id>', 'Filter by runtime id')
15
+ .option('--status <status>', 'Filter by run status'),
16
+ query: (options) => ({
17
+ spaceId: typeof options.space === 'string' ? options.space : undefined,
18
+ runtimeId: typeof options.runtime === 'string' ? options.runtime : undefined,
19
+ status: typeof options.status === 'string' ? options.status : undefined,
20
+ limit: typeof options.limit === 'number' ? options.limit : undefined,
21
+ cursor: typeof options.cursor === 'string' ? options.cursor : undefined,
22
+ }),
23
+ }));
24
+ command.addCommand(createViewCommand(factory, {
25
+ description: 'View a run',
26
+ path: '/runs/{id}',
27
+ }));
28
+ command.addCommand(createRunEventsCommand(factory));
29
+ command.addCommand(createRunCommandsCommand(factory));
30
+ command.addCommand(createRunFilesCommand(factory));
31
+ command.addCommand(createRunExportCommand(factory));
32
+ command.addCommand(createRunWatchCommand(factory));
33
+ command.addCommand(createRunWaitCommand(factory));
34
+ command.addCommand(createRunViewerCommand(factory, 'live', 'Mint a live viewer URL'));
35
+ command.addCommand(createRunViewerCommand(factory, 'recording', 'Mint a recording viewer URL'));
36
+ return command;
37
+ }
38
+ function createRunWatchCommand(factory) {
39
+ return new Command('watch')
40
+ .description('Stream run events')
41
+ .argument('<id>')
42
+ .option('--type <type...>', 'Filter by event type')
43
+ .option('--category <category...>', 'Filter by event category')
44
+ .option('--connection <id>', 'Filter by connection id')
45
+ .option('-L, --limit <number>', 'Maximum number of events per poll', parsePositiveInteger)
46
+ .option('--cursor <cursor>', 'Start cursor')
47
+ .action(async (id, options) => {
48
+ const client = await factory.apiClient();
49
+ const stream = await client.streamText(`/runs/${encodeURIComponent(id)}/events/stream`, {
50
+ query: {
51
+ type: options.type?.join(','),
52
+ category: options.category?.join(','),
53
+ connectionId: options.connection,
54
+ limit: options.limit,
55
+ cursor: options.cursor,
56
+ },
57
+ });
58
+ await renderSseStream(factory, stream);
59
+ });
60
+ }
61
+ async function renderSseStream(factory, stream) {
62
+ let buffer = '';
63
+ for await (const chunk of stream) {
64
+ buffer += chunk;
65
+ let frameEnd = buffer.indexOf('\n\n');
66
+ while (frameEnd !== -1) {
67
+ const frame = buffer.slice(0, frameEnd);
68
+ buffer = buffer.slice(frameEnd + 2);
69
+ renderSseFrame(factory, frame);
70
+ frameEnd = buffer.indexOf('\n\n');
71
+ }
72
+ }
73
+ if (buffer.trim())
74
+ renderSseFrame(factory, buffer);
75
+ }
76
+ function renderSseFrame(factory, frame) {
77
+ let eventName = 'message';
78
+ const dataLines = [];
79
+ for (const line of frame.split(/\r?\n/)) {
80
+ if (line.startsWith(':'))
81
+ continue;
82
+ const separator = line.indexOf(':');
83
+ const field = separator === -1 ? line : line.slice(0, separator);
84
+ const value = separator === -1 ? '' : line.slice(separator + 1).trimStart();
85
+ if (field === 'event')
86
+ eventName = value;
87
+ if (field === 'data')
88
+ dataLines.push(value);
89
+ }
90
+ if (eventName === 'heartbeat')
91
+ return;
92
+ const rawData = dataLines.join('\n');
93
+ if (!rawData)
94
+ return;
95
+ let parsed;
96
+ try {
97
+ parsed = JSON.parse(rawData);
98
+ }
99
+ catch {
100
+ factory.io.writeOut(`${rawData}\n`);
101
+ return;
102
+ }
103
+ if (!factory.io.isStdoutTTY()) {
104
+ factory.io.writeOut(`${JSON.stringify(parsed)}\n`);
105
+ return;
106
+ }
107
+ factory.io.writeOut(`${formatRunEvent(parsed)}\n`);
108
+ }
109
+ function formatRunEvent(value) {
110
+ if (!value || typeof value !== 'object')
111
+ return JSON.stringify(value);
112
+ const event = value;
113
+ const time = typeof event.time === 'string' ? event.time : '';
114
+ const label = [event.category, event.type].filter((part) => typeof part === 'string').join('/');
115
+ const status = typeof event.status === 'string' ? ` ${event.status}` : '';
116
+ const name = typeof event.name === 'string' ? ` ${event.name}` : '';
117
+ const suffix = event.data && Object.keys(event.data).length > 0 ? ` ${JSON.stringify(event.data)}` : '';
118
+ return [time, label || 'event'].filter(Boolean).join(' ') + status + name + suffix;
119
+ }
120
+ function createRunEventsCommand(factory) {
121
+ return addOutputFlags(new Command('events')
122
+ .description('List run events')
123
+ .argument('<id>')
124
+ .option('--type <type...>', 'Filter by event type')
125
+ .option('--category <category...>', 'Filter by event category')
126
+ .option('--connection <id>', 'Filter by connection id')
127
+ .option('-L, --limit <number>', 'Maximum number of results to return', parsePositiveInteger)
128
+ .option('--cursor <cursor>', 'Pagination cursor'))
129
+ .action(async (id, options) => {
130
+ await requestAndPrint(factory, 'get', `/runs/${encodeURIComponent(id)}/events`, {
131
+ query: {
132
+ type: options.type?.join(','),
133
+ category: options.category?.join(','),
134
+ connectionId: options.connection,
135
+ limit: options.limit,
136
+ cursor: options.cursor,
137
+ },
138
+ output: options,
139
+ });
140
+ });
141
+ }
142
+ function createRunCommandsCommand(factory) {
143
+ return addOutputFlags(new Command('commands')
144
+ .description('List run commands')
145
+ .argument('<id>')
146
+ .option('--type <type>', 'Filter by command type')
147
+ .option('--connection <id>', 'Filter by connection id')
148
+ .option('-L, --limit <number>', 'Maximum number of results to return', parsePositiveInteger)
149
+ .option('--cursor <cursor>', 'Pagination cursor'))
150
+ .action(async (id, options) => {
151
+ await requestAndPrint(factory, 'get', `/runs/${encodeURIComponent(id)}/commands`, {
152
+ query: {
153
+ type: options.type,
154
+ connectionId: options.connection,
155
+ limit: options.limit,
156
+ cursor: options.cursor,
157
+ },
158
+ output: options,
159
+ });
160
+ });
161
+ }
162
+ function createRunFilesCommand(factory) {
163
+ return addOutputFlags(new Command('files')
164
+ .description('List run files')
165
+ .argument('<id>')
166
+ .option('--type <type>', 'Filter by file type')
167
+ .option('-L, --limit <number>', 'Maximum number of results to return', parsePositiveInteger)
168
+ .option('--cursor <cursor>', 'Pagination cursor'))
169
+ .action(async (id, options) => {
170
+ await requestAndPrint(factory, 'get', `/runs/${encodeURIComponent(id)}/files`, {
171
+ query: { type: options.type, limit: options.limit, cursor: options.cursor },
172
+ output: options,
173
+ });
174
+ });
175
+ }
176
+ function createRunExportCommand(factory) {
177
+ return addOutputFlags(new Command('export')
178
+ .description('Export run files as an archive')
179
+ .argument('<id>')
180
+ .requiredOption('--to <path>', 'Output archive path, or - for stdout')
181
+ .option('--name <name>', 'Export file name', 'run-files.zip')
182
+ .option('--type <type...>', 'Filter exported file types'))
183
+ .action(async (id, options) => {
184
+ const client = await factory.apiClient();
185
+ const result = await client.post(`/runs/${encodeURIComponent(id)}/files/export`, {
186
+ body: {
187
+ format: 'zip',
188
+ name: options.name,
189
+ filter: options.type ? { type: options.type } : undefined,
190
+ },
191
+ });
192
+ const fileId = result.fileId ?? result.id;
193
+ if (!fileId) {
194
+ throw new CliError('Expected run export response to include fileId');
195
+ }
196
+ const data = await client.download(`/files/${encodeURIComponent(fileId)}/content`);
197
+ await writeBinary(options.to, data);
198
+ });
199
+ }
200
+ function createRunWaitCommand(factory) {
201
+ return createJsonBodyCommand(factory, {
202
+ name: 'wait',
203
+ description: 'Wait for a run event',
204
+ method: 'post',
205
+ path: '/runs/{id}/events/wait',
206
+ argNames: ['id'],
207
+ configure: (cmd) => cmd
208
+ .option('--type <type...>', 'Event type')
209
+ .option('--category <category...>', 'Event category')
210
+ .option('--connection <id>', 'Connection id')
211
+ .option('--cursor <cursor>', 'Cursor')
212
+ .option('--timeout-ms <ms>', 'Timeout in milliseconds')
213
+ .option('--from-file <path>', 'Read JSON request body from file, or - for stdin'),
214
+ body: async (_args, options) => {
215
+ if (typeof options.fromFile === 'string')
216
+ return readJsonFile(options.fromFile);
217
+ return {
218
+ type: options.type,
219
+ category: options.category,
220
+ connectionId: options.connection,
221
+ cursor: options.cursor,
222
+ timeoutMs: options.timeoutMs ? Number(options.timeoutMs) : undefined,
223
+ };
224
+ },
225
+ });
226
+ }
227
+ function createRunViewerCommand(factory, name, description) {
228
+ return createJsonBodyCommand(factory, {
229
+ name,
230
+ description,
231
+ method: 'post',
232
+ path: `/runs/{id}/${name}`,
233
+ argNames: ['id'],
234
+ configure: (cmd) => cmd
235
+ .option('--expires-in-seconds <seconds>', 'Viewer expiration in seconds')
236
+ .option('--control <none|input>', 'Live viewer control mode')
237
+ .option('--from-file <path>', 'Read JSON request body from file, or - for stdin'),
238
+ body: async (_args, options) => {
239
+ if (typeof options.fromFile === 'string')
240
+ return readJsonFile(options.fromFile);
241
+ return {
242
+ expiresInSeconds: options.expiresInSeconds ? Number(options.expiresInSeconds) : undefined,
243
+ control: name === 'live' ? options.control : undefined,
244
+ };
245
+ },
246
+ });
247
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { Factory } from '../../factory.js';
3
+ export declare function createRuntimeCommand(factory: Factory): Command;