@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,197 @@
1
+ import { Command } from 'commander';
2
+ import { CliError } from '../../runtime/errors.js';
3
+ import { readBlob, readJsonFile, writeBinary } from '../shared/io.js';
4
+ import { parsePositiveInteger } from '../shared/options.js';
5
+ import { addOutputFlags, outputData } from '../shared/output.js';
6
+ import { addPaginationFlags, createJsonBodyCommand, createListCommand, createViewCommand, pathTemplate, requestAndPrint, } from '../shared/rest.js';
7
+ export function createRuntimeCommand(factory) {
8
+ const command = new Command('runtime').description('Launch and manage BCTRL runtimes');
9
+ command.addCommand(createListCommand(factory, {
10
+ description: 'List runtimes',
11
+ path: '/runtimes',
12
+ configure: (cmd) => addPaginationFlags(cmd)
13
+ .option('--space <id>', 'Filter by space id')
14
+ .option('--status <status>', 'Filter by runtime status'),
15
+ query: (options) => ({
16
+ spaceId: typeof options.space === 'string' ? options.space : undefined,
17
+ status: typeof options.status === 'string' ? options.status : undefined,
18
+ limit: typeof options.limit === 'number' ? options.limit : undefined,
19
+ cursor: typeof options.cursor === 'string' ? options.cursor : undefined,
20
+ }),
21
+ }));
22
+ command.addCommand(createViewCommand(factory, {
23
+ description: 'View a runtime',
24
+ path: '/runtimes/{id}',
25
+ }));
26
+ command.addCommand(createJsonBodyCommand(factory, {
27
+ name: 'create',
28
+ description: 'Create a runtime',
29
+ method: 'post',
30
+ path: '/runtimes',
31
+ configure: (cmd) => cmd
32
+ .requiredOption('--space <id>', 'Space id')
33
+ .option('--name <name>', 'Runtime name')
34
+ .option('--config-file <path>', 'Runtime config JSON file')
35
+ .option('--metadata-file <path>', 'Runtime metadata JSON file')
36
+ .option('--from-file <path>', 'Read full JSON request body from file, or - for stdin'),
37
+ body: async (_args, options) => {
38
+ if (typeof options.fromFile === 'string')
39
+ return readJsonFile(options.fromFile);
40
+ return {
41
+ spaceId: options.space,
42
+ name: options.name,
43
+ config: typeof options.configFile === 'string' ? await readJsonFile(options.configFile, '--config-file') : undefined,
44
+ metadata: typeof options.metadataFile === 'string'
45
+ ? await readJsonFile(options.metadataFile, '--metadata-file')
46
+ : undefined,
47
+ };
48
+ },
49
+ }));
50
+ command.addCommand(addOutputFlags(new Command('stop')
51
+ .description('Stop a runtime')
52
+ .argument('<id>'))
53
+ .action(async (id, options) => {
54
+ await requestAndPrint(factory, 'post', `/runtimes/${encodeURIComponent(id)}/stop`, {
55
+ output: options,
56
+ });
57
+ }));
58
+ command.addCommand(createRuntimeConnectionCommand(factory));
59
+ command.addCommand(createRuntimeFileCommand(factory));
60
+ command.addCommand(addOutputFlags(new Command('runs')
61
+ .description('List runs for a runtime')
62
+ .argument('<id>')
63
+ .option('-L, --limit <number>', 'Maximum number of results to return', parsePositiveInteger)
64
+ .option('--cursor <cursor>', 'Pagination cursor'))
65
+ .action(async (id, options) => {
66
+ await requestAndPrint(factory, 'get', `/runtimes/${encodeURIComponent(id)}/runs`, {
67
+ query: { limit: options.limit, cursor: options.cursor },
68
+ output: options,
69
+ });
70
+ }));
71
+ return command;
72
+ }
73
+ function createRuntimeConnectionCommand(factory) {
74
+ const command = new Command('connection').description('Manage runtime connections');
75
+ command.addCommand(addOutputFlags(new Command('list')
76
+ .description('List runtime connections')
77
+ .argument('<runtime>')
78
+ .option('--status <status>', 'Filter by connection status')
79
+ .option('-L, --limit <number>', 'Maximum number of results to return', parsePositiveInteger))
80
+ .action(async (runtime, options) => {
81
+ await requestAndPrint(factory, 'get', `/runtimes/${encodeURIComponent(runtime)}/connections`, {
82
+ query: { status: options.status, limit: options.limit },
83
+ output: options,
84
+ });
85
+ }));
86
+ command.addCommand(createJsonBodyCommand(factory, {
87
+ name: 'create',
88
+ description: 'Create a runtime connection',
89
+ method: 'post',
90
+ path: '/runtimes/{runtime}/connections',
91
+ argNames: ['runtime'],
92
+ configure: (cmd) => cmd
93
+ .option('--protocol <protocol>', 'Connection protocol', 'cdp')
94
+ .option('--ttl-seconds <seconds>', 'Connection TTL in seconds')
95
+ .option('--from-file <path>', 'Read JSON request body from file, or - for stdin'),
96
+ body: async (_args, options) => {
97
+ if (typeof options.fromFile === 'string')
98
+ return readJsonFile(options.fromFile);
99
+ return {
100
+ protocol: options.protocol,
101
+ ttlSeconds: options.ttlSeconds ? Number(options.ttlSeconds) : undefined,
102
+ };
103
+ },
104
+ }));
105
+ command.addCommand(addOutputFlags(new Command('delete')
106
+ .description('Revoke a runtime connection')
107
+ .argument('<runtime>')
108
+ .argument('<connection>'))
109
+ .action(async (runtime, connection, options) => {
110
+ await requestAndPrint(factory, 'delete', pathTemplate('/runtimes/{runtime}/connections/{connection}', { runtime, connection }), { output: options });
111
+ }));
112
+ return command;
113
+ }
114
+ function createRuntimeFileCommand(factory) {
115
+ const command = new Command('file').description('Move files into and out of runtimes');
116
+ command.addCommand(addOutputFlags(new Command('push')
117
+ .description('Upload and stage a local file into a runtime')
118
+ .argument('<runtime>')
119
+ .argument('<local-path>')
120
+ .option('--storage-path <path>', 'Advanced: durable BCTRL storage path')
121
+ .option('--name <name>', 'Display name')
122
+ .option('--metadata-file <path>', 'Metadata JSON file'))
123
+ .action(async (runtime, localPath, options) => {
124
+ const client = await factory.apiClient();
125
+ const file = await readBlob(localPath);
126
+ const metadata = typeof options.metadataFile === 'string'
127
+ ? JSON.stringify(await readJsonFile(options.metadataFile, '--metadata-file'))
128
+ : undefined;
129
+ const result = await client.uploadFile(`/runtimes/${encodeURIComponent(runtime)}/files/upload`, {
130
+ file: file.blob,
131
+ fileName: options.name ?? file.fileName,
132
+ fields: {
133
+ ...(options.storagePath ? { storagePath: options.storagePath } : {}),
134
+ ...(options.name ? { name: options.name } : {}),
135
+ ...(metadata ? { metadata } : {}),
136
+ },
137
+ });
138
+ await outputData(factory.io, result, options);
139
+ }));
140
+ command.addCommand(addOutputFlags(new Command('pull')
141
+ .description('Collect a runtime file and download it locally')
142
+ .argument('<runtime>')
143
+ .argument('<runtime-path>')
144
+ .requiredOption('--to <path>', 'Output path, or - for stdout')
145
+ .option('--name <name>', 'Durable file name')
146
+ .option('--storage-path <path>', 'Advanced: durable BCTRL storage path'))
147
+ .action(async (runtime, runtimePath, options) => {
148
+ const client = await factory.apiClient();
149
+ const collected = await client.post(`/runtimes/${encodeURIComponent(runtime)}/files/collect`, {
150
+ body: {
151
+ path: runtimePath,
152
+ name: options.name,
153
+ storagePath: options.storagePath,
154
+ },
155
+ });
156
+ if (!collected.id) {
157
+ throw new CliError('Expected runtime file collect response to include id');
158
+ }
159
+ const data = await client.download(`/files/${encodeURIComponent(collected.id)}/content`);
160
+ await writeBinary(options.to, data);
161
+ }));
162
+ command.addCommand(createJsonBodyCommand(factory, {
163
+ name: 'stage',
164
+ description: 'Stage a durable file into a runtime',
165
+ method: 'post',
166
+ path: '/runtimes/{runtime}/files/stage',
167
+ argNames: ['runtime'],
168
+ configure: (cmd) => cmd
169
+ .option('--file <id>', 'Durable file id')
170
+ .option('--storage-path <path>', 'Advanced: durable BCTRL storage path')
171
+ .option('--name <name>', 'Runtime-local name')
172
+ .option('--from-file <path>', 'Read JSON request body from file, or - for stdin'),
173
+ body: async (_args, options) => {
174
+ if (typeof options.fromFile === 'string')
175
+ return readJsonFile(options.fromFile);
176
+ return { fileId: options.file, storagePath: options.storagePath, name: options.name };
177
+ },
178
+ }));
179
+ command.addCommand(createJsonBodyCommand(factory, {
180
+ name: 'collect',
181
+ description: 'Collect a runtime-local file into durable storage',
182
+ method: 'post',
183
+ path: '/runtimes/{runtime}/files/collect',
184
+ argNames: ['runtime'],
185
+ configure: (cmd) => cmd
186
+ .requiredOption('--path <path>', 'Runtime-local path')
187
+ .option('--name <name>', 'Durable file name')
188
+ .option('--storage-path <path>', 'Advanced: durable BCTRL storage path')
189
+ .option('--from-file <path>', 'Read JSON request body from file, or - for stdin'),
190
+ body: async (_args, options) => {
191
+ if (typeof options.fromFile === 'string')
192
+ return readJsonFile(options.fromFile);
193
+ return { path: options.path, name: options.name, storagePath: options.storagePath };
194
+ },
195
+ }));
196
+ return command;
197
+ }
@@ -0,0 +1,2 @@
1
+ import type { IOStreams } from '../../io/streams.js';
2
+ export declare function readHiddenInput(io: IOStreams, prompt: string): Promise<string>;
@@ -0,0 +1,60 @@
1
+ import { CliError } from '../../runtime/errors.js';
2
+ export async function readHiddenInput(io, prompt) {
3
+ if (!io.canPrompt()) {
4
+ throw new CliError('Cannot prompt for an API key in this terminal. Use: bctrl auth login --with-token');
5
+ }
6
+ const input = io.in;
7
+ if (typeof input.setRawMode !== 'function') {
8
+ throw new CliError('This terminal does not support hidden input. Use: bctrl auth login --with-token');
9
+ }
10
+ const setRawMode = input.setRawMode.bind(input);
11
+ io.writeErr(prompt);
12
+ return new Promise((resolve, reject) => {
13
+ let value = '';
14
+ let settled = false;
15
+ let onData;
16
+ const cleanup = () => {
17
+ input.off('data', onData);
18
+ setRawMode(false);
19
+ input.pause();
20
+ io.writeErr('\n');
21
+ };
22
+ const finish = (result) => {
23
+ if (settled)
24
+ return;
25
+ settled = true;
26
+ cleanup();
27
+ resolve(result);
28
+ };
29
+ const fail = (error) => {
30
+ if (settled)
31
+ return;
32
+ settled = true;
33
+ cleanup();
34
+ reject(error);
35
+ };
36
+ onData = (chunk) => {
37
+ const text = chunk.toString('utf8');
38
+ for (const char of text) {
39
+ if (char === '\u0003') {
40
+ fail(new CliError('Authentication cancelled'));
41
+ return;
42
+ }
43
+ if (char === '\r' || char === '\n') {
44
+ finish(value.trim());
45
+ return;
46
+ }
47
+ if (char === '\u007f' || char === '\b') {
48
+ value = value.slice(0, -1);
49
+ continue;
50
+ }
51
+ if (char >= ' ') {
52
+ value += char;
53
+ }
54
+ }
55
+ };
56
+ setRawMode(true);
57
+ input.resume();
58
+ input.on('data', onData);
59
+ });
60
+ }
@@ -0,0 +1,9 @@
1
+ export declare function readText(path: string): Promise<string>;
2
+ export declare function readJsonFile(path: string, label?: string): Promise<unknown>;
3
+ export declare function readStdinText(): Promise<string>;
4
+ export declare function writeBinary(path: string, data: Uint8Array): Promise<void>;
5
+ export declare function readBlob(path: string): Promise<{
6
+ blob: Blob;
7
+ fileName: string;
8
+ }>;
9
+ export declare function parseKeyValueList(values: string[] | undefined): Record<string, unknown> | undefined;
@@ -0,0 +1,58 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { basename } from 'node:path';
3
+ import { stdin as processStdin } from 'node:process';
4
+ import { CliError } from '../../runtime/errors.js';
5
+ export async function readText(path) {
6
+ if (path === '-') {
7
+ return readStdinText();
8
+ }
9
+ return readFile(path, 'utf8');
10
+ }
11
+ export async function readJsonFile(path, label = '--from-file') {
12
+ const text = await readText(path);
13
+ try {
14
+ return JSON.parse(text);
15
+ }
16
+ catch (error) {
17
+ const reason = error instanceof Error ? error.message : String(error);
18
+ throw new CliError(`Invalid JSON in ${label}: ${reason}`);
19
+ }
20
+ }
21
+ export async function readStdinText() {
22
+ const chunks = [];
23
+ for await (const chunk of processStdin) {
24
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
25
+ }
26
+ return Buffer.concat(chunks).toString('utf8');
27
+ }
28
+ export async function writeBinary(path, data) {
29
+ if (path === '-') {
30
+ process.stdout.write(data);
31
+ return;
32
+ }
33
+ await writeFile(path, data);
34
+ }
35
+ export async function readBlob(path) {
36
+ if (path === '-') {
37
+ const chunks = [];
38
+ for await (const chunk of processStdin) {
39
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
40
+ }
41
+ return { blob: new Blob([Buffer.concat(chunks)]), fileName: 'stdin' };
42
+ }
43
+ const data = await readFile(path);
44
+ return { blob: new Blob([data]), fileName: basename(path) };
45
+ }
46
+ export function parseKeyValueList(values) {
47
+ if (!values || values.length === 0)
48
+ return undefined;
49
+ const result = {};
50
+ for (const item of values) {
51
+ const eq = item.indexOf('=');
52
+ if (eq === -1) {
53
+ throw new CliError(`Expected KEY=VALUE, got ${item}`);
54
+ }
55
+ result[item.slice(0, eq)] = item.slice(eq + 1);
56
+ }
57
+ return result;
58
+ }
@@ -0,0 +1,2 @@
1
+ export declare function parsePositiveInteger(value: string): number;
2
+ export declare function parsePositiveNumber(value: string): number;
@@ -0,0 +1,15 @@
1
+ import { InvalidArgumentError } from 'commander';
2
+ export function parsePositiveInteger(value) {
3
+ const parsed = Number(value);
4
+ if (!Number.isInteger(parsed) || parsed < 1) {
5
+ throw new InvalidArgumentError(`invalid positive integer: ${value}`);
6
+ }
7
+ return parsed;
8
+ }
9
+ export function parsePositiveNumber(value) {
10
+ const parsed = Number(value);
11
+ if (!Number.isFinite(parsed) || parsed <= 0) {
12
+ throw new InvalidArgumentError(`invalid positive number: ${value}`);
13
+ }
14
+ return parsed;
15
+ }
@@ -0,0 +1,10 @@
1
+ import { Command } from 'commander';
2
+ import type { IOStreams } from '../../io/streams.js';
3
+ export type OutputFlags = {
4
+ json?: boolean | string;
5
+ jq?: string;
6
+ template?: string;
7
+ };
8
+ export declare function addOutputFlags(command: Command): Command;
9
+ export declare function outputData(io: IOStreams, value: unknown, flags?: OutputFlags | undefined): Promise<void>;
10
+ export declare function writeJson(io: IOStreams, value: unknown): void;
@@ -0,0 +1,105 @@
1
+ import Handlebars from 'handlebars';
2
+ import jqPromise from 'jq-web';
3
+ import { CliError } from '../../runtime/errors.js';
4
+ Handlebars.registerHelper('json', (value) => JSON.stringify(value));
5
+ Handlebars.registerHelper('newline', () => '\n');
6
+ Handlebars.registerHelper('tab', () => '\t');
7
+ export function addOutputFlags(command) {
8
+ return command
9
+ .option('--json [fields]', 'Output JSON with optional comma-separated fields')
10
+ .option('--jq <expression>', 'Filter JSON output using a jq expression')
11
+ .option('--template <template>', 'Format JSON output using a template');
12
+ }
13
+ export async function outputData(io, value, flags = {}) {
14
+ const output = resolveOutput(flags);
15
+ const projected = projectFields(value, output.fields);
16
+ if (output.jq) {
17
+ const jq = await jqPromise;
18
+ const result = jq.raw(JSON.stringify(projected), output.jq, ['-r']);
19
+ io.writeOut(result.endsWith('\n') ? result : `${result}\n`);
20
+ return;
21
+ }
22
+ if (output.template) {
23
+ io.writeOut(renderTemplate(projected, output.template));
24
+ return;
25
+ }
26
+ writeJson(io, projected);
27
+ }
28
+ export function writeJson(io, value) {
29
+ io.writeOut(`${JSON.stringify(value, null, 2)}\n`);
30
+ }
31
+ function resolveOutput(flags) {
32
+ const jsonChanged = flags.json !== undefined;
33
+ if (flags.jq && !jsonChanged) {
34
+ throw new CliError('cannot use --jq without specifying --json');
35
+ }
36
+ if (flags.template && !jsonChanged) {
37
+ throw new CliError('cannot use --template without specifying --json');
38
+ }
39
+ return {
40
+ fields: parseJsonFields(flags.json),
41
+ jq: flags.jq,
42
+ template: flags.template,
43
+ };
44
+ }
45
+ function parseJsonFields(value) {
46
+ if (value === undefined || value === true || value === false)
47
+ return undefined;
48
+ return value
49
+ .split(',')
50
+ .map((field) => field.trim())
51
+ .filter(Boolean);
52
+ }
53
+ function projectFields(value, fields) {
54
+ if (!fields || fields.length === 0)
55
+ return value;
56
+ if (Array.isArray(value)) {
57
+ return value.map((item) => projectObject(item, fields));
58
+ }
59
+ if (isRecord(value)) {
60
+ if (Array.isArray(value.data)) {
61
+ return {
62
+ ...value,
63
+ data: value.data.map((item) => projectObject(item, fields)),
64
+ };
65
+ }
66
+ if (isRecord(value.data)) {
67
+ return {
68
+ ...value,
69
+ data: projectObject(value.data, fields),
70
+ };
71
+ }
72
+ return projectObject(value, fields);
73
+ }
74
+ return value;
75
+ }
76
+ function projectObject(value, fields) {
77
+ if (!isRecord(value))
78
+ return value;
79
+ const projected = {};
80
+ for (const field of fields) {
81
+ projected[field] = getPath(value, field);
82
+ }
83
+ return projected;
84
+ }
85
+ function getPath(value, path) {
86
+ let current = value;
87
+ for (const part of path.split('.')) {
88
+ if (!isRecord(current))
89
+ return undefined;
90
+ current = current[part];
91
+ }
92
+ return current;
93
+ }
94
+ function renderTemplate(value, template) {
95
+ try {
96
+ return Handlebars.compile(template, { noEscape: true })(value);
97
+ }
98
+ catch (error) {
99
+ const reason = error instanceof Error ? error.message : String(error);
100
+ throw new CliError(`Template rendering failed: ${reason}`);
101
+ }
102
+ }
103
+ function isRecord(value) {
104
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
105
+ }
@@ -0,0 +1,54 @@
1
+ import { Command } from 'commander';
2
+ import type { BctrlApiClient } from '../../api/client.js';
3
+ import type { Factory } from '../../factory.js';
4
+ import type { IOStreams } from '../../io/streams.js';
5
+ import { type OutputFlags } from './output.js';
6
+ type ClientMethod = 'get' | 'post' | 'patch' | 'put' | 'delete';
7
+ export type ApiDeps = {
8
+ io: IOStreams;
9
+ apiClient: () => Promise<BctrlApiClient>;
10
+ };
11
+ export declare function pathTemplate(template: string, params: Record<string, string>): string;
12
+ export declare function addPaginationFlags(command: Command): Command;
13
+ export declare function addJsonBodyFlag(command: Command): Command;
14
+ export declare function requestAndPrint(deps: ApiDeps, method: ClientMethod, path: string, options?: {
15
+ query?: Record<string, string | number | boolean | undefined>;
16
+ body?: unknown;
17
+ output?: OutputFlags;
18
+ }): Promise<void>;
19
+ export declare function bodyFromFileOption(path: string | undefined): Promise<unknown>;
20
+ export declare function createListCommand(factory: Factory, config: {
21
+ name?: string;
22
+ description: string;
23
+ path: string;
24
+ query?: (options: Record<string, unknown>) => Record<string, string | number | boolean | undefined>;
25
+ configure?: (command: Command) => Command;
26
+ }): Command;
27
+ export declare function createViewCommand(factory: Factory, config: {
28
+ name?: string;
29
+ description: string;
30
+ path: string;
31
+ argName?: string;
32
+ configure?: (command: Command) => Command;
33
+ query?: (id: string, options: Record<string, unknown>) => Record<string, string | number | boolean | undefined>;
34
+ }): Command;
35
+ export declare function createDeleteCommand(factory: Factory, config: {
36
+ name?: string;
37
+ description: string;
38
+ path: string;
39
+ argNames?: string[];
40
+ configure?: (command: Command) => Command;
41
+ query?: (args: Record<string, string>, options: Record<string, unknown>) => Record<string, string | number | boolean | undefined>;
42
+ }): Command;
43
+ export declare function createJsonBodyCommand(factory: Factory, config: {
44
+ name: string;
45
+ description: string;
46
+ method: Exclude<ClientMethod, 'get' | 'delete'>;
47
+ path: string;
48
+ argNames?: string[];
49
+ configure?: (command: Command) => Command;
50
+ body?: (args: Record<string, string>, options: Record<string, unknown>) => Promise<unknown>;
51
+ query?: (args: Record<string, string>, options: Record<string, unknown>) => Record<string, string | number | boolean | undefined>;
52
+ }): Command;
53
+ export declare function outputFlags(options: Record<string, unknown>): OutputFlags;
54
+ export {};
@@ -0,0 +1,121 @@
1
+ import { Command } from 'commander';
2
+ import { CliError } from '../../runtime/errors.js';
3
+ import { readJsonFile } from './io.js';
4
+ import { parsePositiveInteger } from './options.js';
5
+ import { addOutputFlags, outputData } from './output.js';
6
+ export function pathTemplate(template, params) {
7
+ return template.replace(/\{(\w+)\}/g, (_, key) => encodeURIComponent(params[key] ?? ''));
8
+ }
9
+ export function addPaginationFlags(command) {
10
+ return command
11
+ .option('-L, --limit <number>', 'Maximum number of results to return', parsePositiveInteger)
12
+ .option('--cursor <cursor>', 'Pagination cursor');
13
+ }
14
+ export function addJsonBodyFlag(command) {
15
+ return command.option('--from-file <path>', 'Read JSON request body from file, or - for stdin');
16
+ }
17
+ export async function requestAndPrint(deps, method, path, options) {
18
+ const client = await deps.apiClient();
19
+ const result = method === 'get'
20
+ ? await client.get(path, options)
21
+ : method === 'post'
22
+ ? await client.post(path, options)
23
+ : method === 'patch'
24
+ ? await client.patch(path, options)
25
+ : method === 'put'
26
+ ? await client.put(path, options)
27
+ : await client.delete(path, options);
28
+ await outputData(deps.io, result, options?.output);
29
+ }
30
+ export async function bodyFromFileOption(path) {
31
+ return path ? readJsonFile(path) : {};
32
+ }
33
+ export function createListCommand(factory, config) {
34
+ let command = new Command(config.name ?? 'list').description(config.description);
35
+ command = config.configure ? config.configure(command) : addPaginationFlags(command);
36
+ command = addOutputFlags(command);
37
+ return command.action(async (options) => {
38
+ await requestAndPrint(factory, 'get', config.path, {
39
+ query: config.query ? config.query(options) : defaultListQuery(options),
40
+ output: outputFlags(options),
41
+ });
42
+ });
43
+ }
44
+ export function createViewCommand(factory, config) {
45
+ const argName = config.argName ?? 'id';
46
+ let command = new Command(config.name ?? 'view')
47
+ .description(config.description)
48
+ .argument(`<${argName}>`);
49
+ command = config.configure ? config.configure(command) : command;
50
+ command = addOutputFlags(command);
51
+ return command.action(async (id, options) => {
52
+ await requestAndPrint(factory, 'get', pathTemplate(config.path, { [argName]: id, id }), {
53
+ query: config.query ? config.query(id, options) : undefined,
54
+ output: outputFlags(options),
55
+ });
56
+ });
57
+ }
58
+ export function createDeleteCommand(factory, config) {
59
+ const argNames = config.argNames ?? ['id'];
60
+ let command = new Command(config.name ?? 'delete').description(config.description);
61
+ for (const arg of argNames)
62
+ command = command.argument(`<${arg}>`);
63
+ command = command.option('-y, --yes', 'Confirm deletion');
64
+ command = config.configure ? config.configure(command) : command;
65
+ command = addOutputFlags(command);
66
+ return command.action(async (...args) => {
67
+ const options = getActionOptions(args);
68
+ if (options.yes !== true) {
69
+ throw new CliError('Refusing to delete without --yes');
70
+ }
71
+ const params = {};
72
+ for (let i = 0; i < argNames.length; i += 1)
73
+ params[argNames[i]] = String(args[i]);
74
+ await requestAndPrint(factory, 'delete', pathTemplate(config.path, params), {
75
+ query: config.query ? config.query(params, options) : undefined,
76
+ output: outputFlags(options),
77
+ });
78
+ });
79
+ }
80
+ export function createJsonBodyCommand(factory, config) {
81
+ const argNames = config.argNames ?? [];
82
+ let command = new Command(config.name).description(config.description);
83
+ for (const arg of argNames)
84
+ command = command.argument(`<${arg}>`);
85
+ command = config.configure ? config.configure(command) : addJsonBodyFlag(command);
86
+ command = addOutputFlags(command);
87
+ return command.action(async (...actionArgs) => {
88
+ const options = getActionOptions(actionArgs);
89
+ const args = {};
90
+ for (let i = 0; i < argNames.length; i += 1)
91
+ args[argNames[i]] = String(actionArgs[i]);
92
+ const body = config.body ? await config.body(args, options) : await bodyFromFileOption(String(options.fromFile ?? ''));
93
+ await requestAndPrint(factory, config.method, pathTemplate(config.path, args), {
94
+ body,
95
+ query: config.query ? config.query(args, options) : undefined,
96
+ output: outputFlags(options),
97
+ });
98
+ });
99
+ }
100
+ function defaultListQuery(options) {
101
+ return {
102
+ limit: typeof options.limit === 'number' ? options.limit : undefined,
103
+ cursor: typeof options.cursor === 'string' ? options.cursor : undefined,
104
+ };
105
+ }
106
+ export function outputFlags(options) {
107
+ return {
108
+ ...(typeof options.json === 'string' || typeof options.json === 'boolean'
109
+ ? { json: options.json }
110
+ : {}),
111
+ ...(typeof options.jq === 'string' ? { jq: options.jq } : {}),
112
+ ...(typeof options.template === 'string' ? { template: options.template } : {}),
113
+ };
114
+ }
115
+ function getActionOptions(args) {
116
+ const candidate = args.at(-2);
117
+ return isRecord(candidate) ? candidate : {};
118
+ }
119
+ function isRecord(value) {
120
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
121
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { Factory } from '../../factory.js';
3
+ export declare function createSpaceCommand(factory: Factory): Command;