@bctrl/cli 0.1.0 → 0.1.2

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.
@@ -0,0 +1,72 @@
1
+ import { CLI_HELP_COMMANDS } from '../../generated/help.js';
2
+ export function addStructuredHelp(command, spec) {
3
+ command.addHelpText('after', `\n${renderStructuredHelp(spec)}`);
4
+ return command;
5
+ }
6
+ export function addCliHelp(command, commandId) {
7
+ return addStructuredHelp(command, toCommandHelpSpec(CLI_HELP_COMMANDS[commandId]));
8
+ }
9
+ function toCommandHelpSpec(help) {
10
+ const raw = help;
11
+ return {
12
+ purpose: raw.summary,
13
+ flags: raw.flags?.map((flag) => ({ ...flag })),
14
+ input: raw.input?.fields.map((field) => ({ ...field })),
15
+ output: raw.output?.fields.map((field) => ({ ...field })),
16
+ examples: raw.examples?.map(formatHelpItem).filter((value) => Boolean(value)),
17
+ next: raw.next?.map(formatHelpItem).filter((value) => Boolean(value)),
18
+ };
19
+ }
20
+ function formatHelpItem(value) {
21
+ if (typeof value === 'string')
22
+ return value;
23
+ if (!value || typeof value !== 'object' || Array.isArray(value))
24
+ return undefined;
25
+ const record = value;
26
+ if (typeof record.command === 'string')
27
+ return record.command;
28
+ if (typeof record.topic === 'string')
29
+ return record.topic;
30
+ return undefined;
31
+ }
32
+ function renderStructuredHelp(spec) {
33
+ const sections = [
34
+ renderPurpose(spec.purpose),
35
+ spec.flags && spec.flags.length > 0 ? renderFlags(spec.flags) : '',
36
+ spec.input && spec.input.length > 0 ? renderFields('Input', spec.input) : '',
37
+ spec.output && spec.output.length > 0 ? renderFields('Output', spec.output) : '',
38
+ spec.examples && spec.examples.length > 0 ? renderList('Examples', spec.examples) : '',
39
+ spec.next && spec.next.length > 0 ? renderList('Next', spec.next) : '',
40
+ ].filter(Boolean);
41
+ return sections.join('\n\n');
42
+ }
43
+ function renderPurpose(purpose) {
44
+ return `Purpose:\n ${purpose}`;
45
+ }
46
+ function renderFlags(flags) {
47
+ const rows = flags.map((flag) => {
48
+ const left = [flag.name, flag.value].filter(Boolean).join(' ');
49
+ const suffix = flag.mapsTo ? ` Maps to ${flag.mapsTo}.` : '';
50
+ return { left, right: `${flag.description}${suffix}` };
51
+ });
52
+ return renderRows('Flags', rows);
53
+ }
54
+ function renderFields(title, fields) {
55
+ const rows = fields.map((field) => ({
56
+ left: field.name,
57
+ right: [
58
+ field.type,
59
+ field.required === true ? 'required' : 'optional',
60
+ field.description,
61
+ ].filter(Boolean).join(' '),
62
+ }));
63
+ return renderRows(title, rows);
64
+ }
65
+ function renderRows(title, rows) {
66
+ const width = Math.max(...rows.map((row) => row.left.length));
67
+ const lines = rows.map((row) => ` ${row.left.padEnd(width)} ${row.right}`);
68
+ return `${title}:\n${lines.join('\n')}`;
69
+ }
70
+ function renderList(title, lines) {
71
+ return `${title}:\n${lines.map((line) => ` ${line}`).join('\n')}`;
72
+ }
@@ -8,7 +8,7 @@ export async function readText(path) {
8
8
  }
9
9
  return readFile(path, 'utf8');
10
10
  }
11
- export async function readJsonFile(path, label = '--from-file') {
11
+ export async function readJsonFile(path, label = '--input') {
12
12
  const text = await readText(path);
13
13
  try {
14
14
  return JSON.parse(text);
@@ -16,7 +16,7 @@ export declare function requestAndPrint(deps: ApiDeps, method: ClientMethod, pat
16
16
  body?: unknown;
17
17
  output?: OutputFlags;
18
18
  }): Promise<void>;
19
- export declare function bodyFromFileOption(path: string | undefined): Promise<unknown>;
19
+ export declare function bodyFromInputOption(path: string | undefined): Promise<unknown>;
20
20
  export declare function createListCommand(factory: Factory, config: {
21
21
  name?: string;
22
22
  description: string;
@@ -12,7 +12,7 @@ export function addPaginationFlags(command) {
12
12
  .option('--cursor <cursor>', 'Pagination cursor');
13
13
  }
14
14
  export function addJsonBodyFlag(command) {
15
- return command.option('--from-file <path>', 'Read JSON request body from file, or - for stdin');
15
+ return command.option('--input <path>', 'Read JSON request body from file, or - for stdin');
16
16
  }
17
17
  export async function requestAndPrint(deps, method, path, options) {
18
18
  const client = await deps.apiClient();
@@ -27,7 +27,7 @@ export async function requestAndPrint(deps, method, path, options) {
27
27
  : await client.delete(path, options);
28
28
  await outputData(deps.io, result, options?.output);
29
29
  }
30
- export async function bodyFromFileOption(path) {
30
+ export async function bodyFromInputOption(path) {
31
31
  return path ? readJsonFile(path) : {};
32
32
  }
33
33
  export function createListCommand(factory, config) {
@@ -89,7 +89,7 @@ export function createJsonBodyCommand(factory, config) {
89
89
  const args = {};
90
90
  for (let i = 0; i < argNames.length; i += 1)
91
91
  args[argNames[i]] = String(actionArgs[i]);
92
- const body = config.body ? await config.body(args, options) : await bodyFromFileOption(String(options.fromFile ?? ''));
92
+ const body = config.body ? await config.body(args, options) : await bodyFromInputOption(String(options.input ?? ''));
93
93
  await requestAndPrint(factory, config.method, pathTemplate(config.path, args), {
94
94
  body,
95
95
  query: config.query ? config.query(args, options) : undefined,
@@ -19,10 +19,10 @@ export function createSpaceCommand(factory) {
19
19
  .option('--name <name>', 'Space name')
20
20
  .option('--region <region>', 'Space region')
21
21
  .option('--subaccount-id <id>', 'Subaccount id')
22
- .option('--from-file <path>', 'Read JSON request body from file, or - for stdin'),
22
+ .option('--input <path>', 'Read JSON request body from file, or - for stdin'),
23
23
  body: async (_args, options) => {
24
- if (typeof options.fromFile === 'string')
25
- return readJsonFile(options.fromFile);
24
+ if (typeof options.input === 'string')
25
+ return readJsonFile(options.input);
26
26
  return {
27
27
  name: options.name,
28
28
  region: options.region,
@@ -38,10 +38,10 @@ export function createSpaceCommand(factory) {
38
38
  argNames: ['id'],
39
39
  configure: (cmd) => cmd
40
40
  .option('--name <name>', 'Space name')
41
- .option('--from-file <path>', 'Read JSON request body from file, or - for stdin'),
41
+ .option('--input <path>', 'Read JSON request body from file, or - for stdin'),
42
42
  body: async (_args, options) => {
43
- if (typeof options.fromFile === 'string')
44
- return readJsonFile(options.fromFile);
43
+ if (typeof options.input === 'string')
44
+ return readJsonFile(options.input);
45
45
  return { name: options.name };
46
46
  },
47
47
  }));
@@ -23,10 +23,10 @@ export function createSubaccountCommand(factory) {
23
23
  .option('--name <name>', 'Subaccount name')
24
24
  .option('--external-id <id>', 'External id')
25
25
  .option('--metadata-file <path>', 'Metadata JSON file')
26
- .option('--from-file <path>', 'Read full JSON request body from file, or - for stdin'),
26
+ .option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
27
27
  body: async (_args, options) => {
28
- if (typeof options.fromFile === 'string')
29
- return readJsonFile(options.fromFile);
28
+ if (typeof options.input === 'string')
29
+ return readJsonFile(options.input);
30
30
  return {
31
31
  name: options.name,
32
32
  externalId: options.externalId,
@@ -46,10 +46,10 @@ export function createSubaccountCommand(factory) {
46
46
  .option('--name <name>', 'Subaccount name')
47
47
  .option('--external-id <id>', 'External id')
48
48
  .option('--metadata-file <path>', 'Metadata JSON file')
49
- .option('--from-file <path>', 'Read full JSON request body from file, or - for stdin'),
49
+ .option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
50
50
  body: async (_args, options) => {
51
- if (typeof options.fromFile === 'string')
52
- return readJsonFile(options.fromFile);
51
+ if (typeof options.input === 'string')
52
+ return readJsonFile(options.input);
53
53
  return {
54
54
  name: options.name,
55
55
  externalId: options.externalId,
@@ -112,10 +112,10 @@ function createSubaccountLimitsCommand(factory) {
112
112
  .option('--max-spaces <number>', 'Maximum spaces')
113
113
  .option('--max-active-runs <number>', 'Maximum active runs')
114
114
  .option('--monthly-spend-micro-usd <number>', 'Monthly spend limit in micro USD')
115
- .option('--from-file <path>', 'Read full JSON request body from file, or - for stdin'),
115
+ .option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
116
116
  body: async (_args, options) => {
117
- if (typeof options.fromFile === 'string')
118
- return readJsonFile(options.fromFile);
117
+ if (typeof options.input === 'string')
118
+ return readJsonFile(options.input);
119
119
  return {
120
120
  spaces: options.maxSpaces ? { max: Number(options.maxSpaces) } : undefined,
121
121
  runs: options.maxActiveRuns ? { maxActive: Number(options.maxActiveRuns) } : undefined,
@@ -145,10 +145,10 @@ function createSubaccountKeyCommand(factory) {
145
145
  argNames: ['id'],
146
146
  configure: (cmd) => cmd
147
147
  .option('--name <name>', 'API key name')
148
- .option('--from-file <path>', 'Read full JSON request body from file, or - for stdin'),
148
+ .option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
149
149
  body: async (_args, options) => {
150
- if (typeof options.fromFile === 'string')
151
- return readJsonFile(options.fromFile);
150
+ if (typeof options.input === 'string')
151
+ return readJsonFile(options.input);
152
152
  return { name: options.name };
153
153
  },
154
154
  }));
@@ -63,10 +63,10 @@ function createVaultSetCommand(factory) {
63
63
  .option('--label <label>', 'Display label')
64
64
  .option('--origin <origin...>', 'Allowed origin')
65
65
  .option('--origin-pattern <pattern...>', 'Allowed origin pattern')
66
- .option('--from-file <path>', 'Read full JSON request body from file, or - for stdin'),
66
+ .option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
67
67
  body: async (args, options) => {
68
- if (typeof options.fromFile === 'string')
69
- return readJsonFile(options.fromFile);
68
+ if (typeof options.input === 'string')
69
+ return readJsonFile(options.input);
70
70
  const password = typeof options.passwordFromFile === 'string'
71
71
  ? (await readText(options.passwordFromFile)).trimEnd()
72
72
  : options.password;
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  export declare const StoredWhoamiSchema: z.ZodObject<{
3
3
  authenticated: z.ZodLiteral<true>;
4
+ email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
4
5
  keyKind: z.ZodEnum<{
5
6
  user: "user";
6
7
  subaccount: "subaccount";
@@ -17,6 +18,7 @@ export declare const StoredAuthSchema: z.ZodObject<{
17
18
  token: z.ZodString;
18
19
  whoami: z.ZodObject<{
19
20
  authenticated: z.ZodLiteral<true>;
21
+ email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
20
22
  keyKind: z.ZodEnum<{
21
23
  user: "user";
22
24
  subaccount: "subaccount";
@@ -6,6 +6,7 @@ const DEFAULT_AUTH_FILE_NAME = 'auth.json';
6
6
  export const StoredWhoamiSchema = z
7
7
  .object({
8
8
  authenticated: z.literal(true),
9
+ email: z.string().email().nullable().optional(),
9
10
  keyKind: z.enum(['user', 'subaccount']),
10
11
  organizationId: z.string().min(1),
11
12
  subaccountId: z.string().min(1).nullable().optional(),
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { readStoredAuth } from './auth-store.js';
3
- const DEFAULT_API_BASE_URL = 'https://api.bctrl.ai/v2';
3
+ const DEFAULT_API_BASE_URL = 'https://api.bctrl.ai/v1';
4
4
  const EnvSchema = z.object({
5
5
  BCTRL_API_KEY: z.string().optional(),
6
6
  BCTRL_API_URL: z.string().url().optional(),
package/dist/factory.js CHANGED
@@ -7,7 +7,7 @@ export function createFactory(options) {
7
7
  return configPromise;
8
8
  };
9
9
  return {
10
- version: options.version ?? '0.1.0',
10
+ version: options.version ?? '0.1.2',
11
11
  io: options.io,
12
12
  config,
13
13
  apiClient: async () => createBctrlApiClient(await config()),
@@ -0,0 +1,205 @@
1
+ export declare const CLI_HELP_COMMANDS: {
2
+ readonly "runtime.create": {
3
+ readonly kind: "command";
4
+ readonly command: "runtime.create";
5
+ readonly summary: "Create a browser runtime definition. This does not start the browser.";
6
+ readonly usage: "bctrl runtime create [flags]";
7
+ readonly flags: readonly [{
8
+ readonly name: "--space";
9
+ readonly value: "<id>";
10
+ readonly description: "Owning space id.";
11
+ readonly mapsTo: "input.spaceId";
12
+ }, {
13
+ readonly name: "--name";
14
+ readonly value: "<name>";
15
+ readonly description: "Runtime name.";
16
+ readonly mapsTo: "input.name";
17
+ }, {
18
+ readonly name: "--config-file";
19
+ readonly value: "<path>";
20
+ readonly description: "Read runtime config JSON from a file.";
21
+ readonly mapsTo: "input.config";
22
+ }, {
23
+ readonly name: "--metadata-file";
24
+ readonly value: "<path>";
25
+ readonly description: "Read runtime metadata JSON from a file.";
26
+ readonly mapsTo: "input.metadata";
27
+ }, {
28
+ readonly name: "--input";
29
+ readonly value: "<path>";
30
+ readonly description: "Read the full JSON request body from a file, or - for stdin.";
31
+ }];
32
+ readonly input: {
33
+ readonly fields: readonly [{
34
+ readonly name: "spaceId";
35
+ readonly type: "uuid";
36
+ readonly required: true;
37
+ }, {
38
+ readonly name: "type";
39
+ readonly type: "\"browser\"";
40
+ readonly values: readonly ["\"browser\""];
41
+ readonly required: true;
42
+ }, {
43
+ readonly name: "name";
44
+ readonly type: "string";
45
+ readonly required: false;
46
+ }, {
47
+ readonly name: "metadata";
48
+ readonly type: "object";
49
+ readonly required: false;
50
+ }, {
51
+ readonly name: "config";
52
+ readonly type: "object";
53
+ readonly required: false;
54
+ }];
55
+ };
56
+ readonly output: {
57
+ readonly fields: readonly [{
58
+ readonly name: "id";
59
+ readonly type: "uuid";
60
+ readonly required: true;
61
+ }, {
62
+ readonly name: "name";
63
+ readonly type: "string";
64
+ readonly required: true;
65
+ }, {
66
+ readonly name: "type";
67
+ readonly type: "\"browser\" | \"desktop\" | \"spreadsheet\"";
68
+ readonly values: readonly ["\"browser\"", "\"desktop\"", "\"spreadsheet\""];
69
+ readonly required: true;
70
+ }, {
71
+ readonly name: "status";
72
+ readonly type: "\"active\" | \"stopped\" | \"failed\"";
73
+ readonly values: readonly ["\"active\"", "\"stopped\"", "\"failed\""];
74
+ readonly required: true;
75
+ }, {
76
+ readonly name: "activeRunId";
77
+ readonly type: "uuid | null";
78
+ readonly required: true;
79
+ }, {
80
+ readonly name: "createdAt";
81
+ readonly type: "string";
82
+ readonly required: true;
83
+ }, {
84
+ readonly name: "updatedAt";
85
+ readonly type: "string";
86
+ readonly required: true;
87
+ }, {
88
+ readonly name: "config";
89
+ readonly type: "object";
90
+ readonly required: false;
91
+ }, {
92
+ readonly name: "metadata";
93
+ readonly type: "object | null";
94
+ readonly required: false;
95
+ }];
96
+ };
97
+ readonly examples: readonly [{
98
+ readonly command: "bctrl runtime create --space sp_123 --name checkout-test";
99
+ }, {
100
+ readonly command: "bctrl runtime create --input runtime.json";
101
+ }];
102
+ readonly next: readonly [{
103
+ readonly command: "bctrl runtime start <runtimeId>";
104
+ }];
105
+ };
106
+ readonly "runtime.start": {
107
+ readonly kind: "command";
108
+ readonly command: "runtime.start";
109
+ readonly summary: "Start a runtime and open its active run.";
110
+ readonly usage: "bctrl runtime start <runtimeId>";
111
+ readonly output: {
112
+ readonly fields: readonly [{
113
+ readonly name: "runtime";
114
+ readonly type: "object";
115
+ readonly required: true;
116
+ }, {
117
+ readonly name: "run";
118
+ readonly type: "object";
119
+ readonly required: true;
120
+ }];
121
+ };
122
+ readonly examples: readonly [{
123
+ readonly command: "bctrl runtime start rt_123";
124
+ }];
125
+ readonly next: readonly [{
126
+ readonly command: "bctrl runtime connection create <runtimeId>";
127
+ }];
128
+ };
129
+ readonly "runtime.connection.create": {
130
+ readonly kind: "command";
131
+ readonly command: "runtime.connection.create";
132
+ readonly summary: "Create a short-lived CDP connection lease for a started runtime.";
133
+ readonly usage: "bctrl runtime connection create <runtimeId> [flags]";
134
+ readonly flags: readonly [{
135
+ readonly name: "--protocol";
136
+ readonly value: "<protocol>";
137
+ readonly description: "Connection protocol. Defaults to cdp.";
138
+ readonly mapsTo: "input.protocol";
139
+ }, {
140
+ readonly name: "--input";
141
+ readonly value: "<path>";
142
+ readonly description: "Read the full JSON request body from a file, or - for stdin.";
143
+ }];
144
+ readonly input: {
145
+ readonly fields: readonly [{
146
+ readonly name: "protocol";
147
+ readonly type: "\"cdp\"";
148
+ readonly values: readonly ["\"cdp\""];
149
+ readonly required: true;
150
+ }, {
151
+ readonly name: "options";
152
+ readonly type: "object";
153
+ readonly required: false;
154
+ }];
155
+ };
156
+ readonly output: {
157
+ readonly fields: readonly [{
158
+ readonly name: "id";
159
+ readonly type: "uuid";
160
+ readonly required: true;
161
+ }, {
162
+ readonly name: "protocol";
163
+ readonly type: "\"cdp\" | \"cua\" | \"rdp\" | \"vnc\" | \"mcp\" | \"http\"";
164
+ readonly values: readonly ["\"cdp\"", "\"cua\"", "\"rdp\"", "\"vnc\"", "\"mcp\"", "\"http\""];
165
+ readonly required: true;
166
+ }, {
167
+ readonly name: "status";
168
+ readonly type: "\"active\" | \"expired\" | \"revoked\" | \"closed\"";
169
+ readonly values: readonly ["\"active\"", "\"expired\"", "\"revoked\"", "\"closed\""];
170
+ readonly required: true;
171
+ }, {
172
+ readonly name: "runId";
173
+ readonly type: "uuid";
174
+ readonly required: true;
175
+ }, {
176
+ readonly name: "createdAt";
177
+ readonly type: "datetime";
178
+ readonly required: true;
179
+ }, {
180
+ readonly name: "expiresAt";
181
+ readonly type: "datetime | null";
182
+ readonly required: true;
183
+ }, {
184
+ readonly name: "closedAt";
185
+ readonly type: "datetime | null";
186
+ readonly required: false;
187
+ }, {
188
+ readonly name: "revokedAt";
189
+ readonly type: "datetime | null";
190
+ readonly required: false;
191
+ }, {
192
+ readonly name: "endpoint";
193
+ readonly type: "object";
194
+ readonly required: true;
195
+ }];
196
+ };
197
+ readonly examples: readonly [{
198
+ readonly command: "bctrl runtime connection create rt_123";
199
+ }];
200
+ readonly next: readonly [{
201
+ readonly command: "bctrl run events <runId>";
202
+ }];
203
+ };
204
+ };
205
+ export type CliHelpCommandId = keyof typeof CLI_HELP_COMMANDS;