@bctrl/cli 0.1.5 → 0.1.7

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 (62) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +109 -95
  3. package/dist/api/auth.d.ts +10 -1
  4. package/dist/api/auth.js +10 -1
  5. package/dist/api/client.d.ts +2 -1
  6. package/dist/api/client.js +37 -37
  7. package/dist/api/device-auth.d.ts +39 -0
  8. package/dist/api/device-auth.js +93 -0
  9. package/dist/api/errors.d.ts +1 -0
  10. package/dist/api/errors.js +8 -0
  11. package/dist/bin/bctrl.js +0 -0
  12. package/dist/commands/ai/index.js +10 -11
  13. package/dist/commands/api-key/index.js +3 -8
  14. package/dist/commands/auth/device-login.d.ts +6 -0
  15. package/dist/commands/auth/device-login.js +19 -0
  16. package/dist/commands/auth/login.d.ts +11 -0
  17. package/dist/commands/auth/login.js +140 -20
  18. package/dist/commands/auth/logout.d.ts +2 -0
  19. package/dist/commands/auth/logout.js +19 -2
  20. package/dist/commands/auth/status.d.ts +1 -0
  21. package/dist/commands/auth/status.js +22 -5
  22. package/dist/commands/auth/token.js +6 -0
  23. package/dist/commands/browser-extension/index.js +7 -15
  24. package/dist/commands/file/index.js +8 -7
  25. package/dist/commands/help/index.js +5 -4
  26. package/dist/commands/notification-recipient/index.d.ts +3 -0
  27. package/dist/commands/notification-recipient/index.js +72 -0
  28. package/dist/commands/proxy/index.js +6 -4
  29. package/dist/commands/run/index.js +31 -32
  30. package/dist/commands/runtime/index.js +186 -107
  31. package/dist/commands/shared/help.d.ts +26 -3
  32. package/dist/commands/shared/help.js +87 -23
  33. package/dist/commands/shared/io.d.ts +1 -0
  34. package/dist/commands/shared/io.js +4 -2
  35. package/dist/commands/shared/operation.d.ts +50 -3
  36. package/dist/commands/shared/operation.js +171 -25
  37. package/dist/commands/shared/output.js +3 -7
  38. package/dist/commands/space/index.js +35 -32
  39. package/dist/commands/space/list.js +6 -5
  40. package/dist/commands/subaccount/index.js +13 -15
  41. package/dist/commands/usage/index.js +9 -5
  42. package/dist/commands/vault/index.js +15 -21
  43. package/dist/config/auth-store.d.ts +75 -9
  44. package/dist/config/auth-store.js +133 -27
  45. package/dist/config/config.d.ts +2 -2
  46. package/dist/config/config.js +9 -17
  47. package/dist/config/paths.d.ts +16 -0
  48. package/dist/config/paths.js +34 -0
  49. package/dist/config/pending-auth.d.ts +19 -0
  50. package/dist/config/pending-auth.js +72 -0
  51. package/dist/config/secret-store.d.ts +24 -0
  52. package/dist/config/secret-store.js +123 -0
  53. package/dist/factory.js +2 -1
  54. package/dist/generated/help.d.ts +7218 -2535
  55. package/dist/generated/help.js +10057 -3928
  56. package/dist/generated/openapi-routes.d.ts +52 -8
  57. package/dist/generated/openapi-routes.js +20 -9
  58. package/dist/generated/openapi-types.d.ts +2729 -849
  59. package/dist/root.js +2 -0
  60. package/dist/version.d.ts +1 -0
  61. package/dist/version.js +4 -0
  62. package/package.json +56 -47
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 BCTRL
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose
6
+ with or without fee is hereby granted, provided that the above copyright notice
7
+ and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
11
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
13
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
14
+ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
15
+ THIS SOFTWARE.
package/README.md CHANGED
@@ -1,95 +1,109 @@
1
- # BCTRL CLI
2
-
3
- CLI for controlling BCTRL runtimes, files, runs, vault secrets, tools, and account resources from scripts or AI agents.
4
-
5
- ## Install
6
-
7
- ```bash
8
- npm install -g @bctrl/cli
9
- ```
10
-
11
- ## Auth
12
-
13
- The CLI authenticates with a BCTRL API key.
14
-
15
- Use an API key for the current shell:
16
-
17
- ```bash
18
- export BCTRL_API_KEY="bctrl_..."
19
- ```
20
-
21
- Or save it locally:
22
-
23
- ```bash
24
- bctrl auth login
25
- ```
26
-
27
- If `BCTRL_API_KEY` is already set, `auth login` saves that key. Otherwise, it prompts you to paste an API key.
28
-
29
- Check the active auth:
30
-
31
- ```bash
32
- bctrl auth status
33
- ```
34
-
35
- ## Browser Runtime Flow
36
-
37
- ```bash
38
- bctrl space list
39
- bctrl runtime create --space sp_123 --name checkout-test
40
- bctrl runtime start rt_123
41
- bctrl runtime stop rt_123
42
- ```
43
-
44
- ## JSON Input
45
-
46
- Use `--input` for full request bodies:
47
-
48
- ```bash
49
- bctrl runtime create --input runtime.json
50
- ```
51
-
52
- Use `-` to read from stdin:
53
-
54
- ```bash
55
- echo '{"type":"browser","spaceId":"sp_123","name":"agent-runtime"}' \
56
- | bctrl runtime create --input -
57
- ```
58
-
59
- ## Output
60
-
61
- Print full JSON:
62
-
63
- ```bash
64
- bctrl runtime list --space sp_123 --json
65
- ```
66
-
67
- Print selected JSON fields:
68
-
69
- ```bash
70
- bctrl runtime list --space sp_123 --json id,status,name
71
- ```
72
-
73
- Filter with `jq`:
74
-
75
- ```bash
76
- bctrl runtime list --space sp_123 --json --jq '.data[] | select(.status == "active")'
77
- ```
78
-
79
- Render with a template:
80
-
81
- ```bash
82
- bctrl runtime list \
83
- --space sp_123 \
84
- --json \
85
- --template '{{#each data}}{{id}} {{status}}{{newline}}{{/each}}'
86
- ```
87
-
88
- ## Help
89
-
90
- Use command help for exact input and output fields:
91
-
92
- ```bash
93
- bctrl runtime create --help
94
- bctrl runtime start --help
95
- ```
1
+ # BCTRL CLI
2
+
3
+ CLI for controlling BCTRL runtimes, files, runs, vault secrets, tools, and account resources from scripts or AI agents.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @bctrl/cli
9
+ ```
10
+
11
+ ## Auth
12
+
13
+ The CLI authenticates with either a browser-approved CLI session or a BCTRL API key.
14
+
15
+ For a local terminal, start a browser approval and let the CLI poll until you approve it:
16
+
17
+ ```bash
18
+ bctrl auth login --url --wait
19
+ ```
20
+
21
+ For agents that need to hand the URL to a human, split the flow into two commands:
22
+
23
+ ```bash
24
+ bctrl auth login --url
25
+ # approve the printed URL
26
+ bctrl auth login
27
+ ```
28
+
29
+ Use an API key for CI or a single shell:
30
+
31
+ ```bash
32
+ export BCTRL_API_KEY="bctrl_..."
33
+ ```
34
+
35
+ `BCTRL_API_KEY` takes precedence over stored credentials.
36
+
37
+ Check the active auth:
38
+
39
+ ```bash
40
+ bctrl auth status
41
+ ```
42
+
43
+ Point contributors at a local or staging stack with `BCTRL_API_URL`:
44
+
45
+ ```bash
46
+ BCTRL_API_URL=http://127.0.0.1:8787/v1 bctrl auth login --url --wait
47
+ ```
48
+
49
+ ## Browser Runtime Flow
50
+
51
+ ```bash
52
+ bctrl space list
53
+ bctrl runtime create --space sp_123 --name checkout-test
54
+ bctrl runtime start rt_123
55
+ bctrl runtime stop rt_123
56
+ ```
57
+
58
+ ## JSON Input
59
+
60
+ Use `--input` for full request bodies:
61
+
62
+ ```bash
63
+ bctrl runtime create --input runtime.json
64
+ ```
65
+
66
+ Use `-` to read from stdin:
67
+
68
+ ```bash
69
+ echo '{"type":"browser","spaceId":"sp_123","name":"agent-runtime"}' \
70
+ | bctrl runtime create --input -
71
+ ```
72
+
73
+ ## Output
74
+
75
+ Print full JSON:
76
+
77
+ ```bash
78
+ bctrl runtime list --space sp_123 --json
79
+ ```
80
+
81
+ Print selected JSON fields:
82
+
83
+ ```bash
84
+ bctrl runtime list --space sp_123 --json id,status,name
85
+ ```
86
+
87
+ Filter with `jq`:
88
+
89
+ ```bash
90
+ bctrl runtime list --space sp_123 --json --jq '.data[] | select(.status == "active")'
91
+ ```
92
+
93
+ Render with a template:
94
+
95
+ ```bash
96
+ bctrl runtime list \
97
+ --space sp_123 \
98
+ --json \
99
+ --template '{{#each data}}{{id}} {{status}}{{newline}}{{/each}}'
100
+ ```
101
+
102
+ ## Help
103
+
104
+ Use command help for exact input and output fields:
105
+
106
+ ```bash
107
+ bctrl runtime create --help
108
+ bctrl runtime start --help
109
+ ```
@@ -7,7 +7,16 @@ export declare const AuthWhoamiSchema: z.ZodObject<{
7
7
  }>;
8
8
  organizationId: z.ZodString;
9
9
  subaccountId: z.ZodNullable<z.ZodString>;
10
- defaultSpaceId: z.ZodString;
10
+ defaultSpaceId: z.ZodNullable<z.ZodString>;
11
+ effectiveScope: z.ZodObject<{
12
+ scope: z.ZodEnum<{
13
+ organization: "organization";
14
+ subaccount: "subaccount";
15
+ }>;
16
+ organizationId: z.ZodString;
17
+ subaccountId: z.ZodNullable<z.ZodString>;
18
+ defaultSpaceId: z.ZodNullable<z.ZodString>;
19
+ }, z.core.$strict>;
11
20
  keyId: z.ZodString;
12
21
  plan: z.ZodString;
13
22
  }, z.core.$strict>;
package/dist/api/auth.js CHANGED
@@ -1,13 +1,22 @@
1
1
  import { z } from 'zod';
2
2
  import { CliError } from '../runtime/errors.js';
3
3
  import { apiErrorFromResponse } from './errors.js';
4
+ const AuthEffectiveScopeSchema = z
5
+ .object({
6
+ scope: z.enum(['organization', 'subaccount']),
7
+ organizationId: z.string().min(1),
8
+ subaccountId: z.string().min(1).nullable(),
9
+ defaultSpaceId: z.string().min(1).nullable(),
10
+ })
11
+ .strict();
4
12
  export const AuthWhoamiSchema = z
5
13
  .object({
6
14
  email: z.string().email().nullable().optional(),
7
15
  scope: z.enum(['organization', 'subaccount']),
8
16
  organizationId: z.string().min(1),
9
17
  subaccountId: z.string().min(1).nullable(),
10
- defaultSpaceId: z.string().min(1),
18
+ defaultSpaceId: z.string().min(1).nullable(),
19
+ effectiveScope: AuthEffectiveScopeSchema,
11
20
  keyId: z.string().min(1),
12
21
  plan: z.string().min(1),
13
22
  })
@@ -16,8 +16,9 @@ export type BctrlApiClient = {
16
16
  export type RequestOptions = {
17
17
  query?: Record<string, string | number | boolean | undefined>;
18
18
  idempotencyKey?: string;
19
+ actingSubaccountId?: string;
19
20
  };
20
21
  export type JsonRequestOptions = RequestOptions & {
21
22
  body?: unknown;
22
23
  };
23
- export declare function createBctrlApiClient(config: BctrlConfig): BctrlApiClient;
24
+ export declare function createBctrlApiClient(config: BctrlConfig, env?: NodeJS.ProcessEnv): BctrlApiClient;
@@ -1,26 +1,23 @@
1
1
  import { AuthError, CliError } from '../runtime/errors.js';
2
- import { apiErrorFromResponse } from './errors.js';
3
- export function createBctrlApiClient(config) {
2
+ import { clearCredential } from '../config/auth-store.js';
3
+ import { apiErrorFromResponse, isUnauthorizedApiError } from './errors.js';
4
+ export function createBctrlApiClient(config, env = process.env) {
4
5
  return {
5
- get: (path, options) => requestJson(config, 'GET', path, options),
6
- post: (path, options) => requestJson(config, 'POST', path, options),
7
- patch: (path, options) => requestJson(config, 'PATCH', path, options),
8
- put: (path, options) => requestJson(config, 'PUT', path, options),
9
- delete: (path, options) => requestJson(config, 'DELETE', path, options),
10
- download: (path, options) => requestBinary(config, 'GET', path, options),
11
- streamText: (path, options) => requestTextStream(config, path, options),
12
- uploadFile: (path, options) => uploadFile(config, path, options),
6
+ get: (path, options) => requestJson(config, env, 'GET', path, options),
7
+ post: (path, options) => requestJson(config, env, 'POST', path, options),
8
+ patch: (path, options) => requestJson(config, env, 'PATCH', path, options),
9
+ put: (path, options) => requestJson(config, env, 'PUT', path, options),
10
+ delete: (path, options) => requestJson(config, env, 'DELETE', path, options),
11
+ download: (path, options) => requestBinary(config, env, 'GET', path, options),
12
+ streamText: (path, options) => requestTextStream(config, env, path, options),
13
+ uploadFile: (path, options) => uploadFile(config, env, path, options),
13
14
  };
14
15
  }
15
- async function requestJson(config, method, path, options) {
16
+ async function requestJson(config, env, method, path, options) {
16
17
  if (!config.activeToken) {
17
18
  throw new AuthError();
18
19
  }
19
- const headers = {
20
- accept: 'application/json',
21
- authorization: `Bearer ${config.activeToken.token}`,
22
- 'user-agent': 'BCTRL CLI',
23
- };
20
+ const headers = requestHeaders(config.activeToken.token, options, 'application/json');
24
21
  if (options?.idempotencyKey) {
25
22
  headers['idempotency-key'] = options.idempotencyKey;
26
23
  }
@@ -35,7 +32,7 @@ async function requestJson(config, method, path, options) {
35
32
  body,
36
33
  });
37
34
  if (!response.ok) {
38
- throw await apiErrorFromResponse(response);
35
+ throw await reconcileStoredCredentialAuthError(config, env, await apiErrorFromResponse(response));
39
36
  }
40
37
  if (response.status === 204) {
41
38
  return { ok: true };
@@ -43,36 +40,29 @@ async function requestJson(config, method, path, options) {
43
40
  const text = await response.text();
44
41
  return (text ? JSON.parse(text) : { ok: true });
45
42
  }
46
- async function requestBinary(config, method, path, options) {
43
+ async function requestBinary(config, env, method, path, options) {
47
44
  if (!config.activeToken) {
48
45
  throw new AuthError();
49
46
  }
50
47
  const response = await fetch(buildUrl(config.apiBaseUrl, path, options?.query), {
51
48
  method,
52
- headers: {
53
- authorization: `Bearer ${config.activeToken.token}`,
54
- 'user-agent': 'BCTRL CLI',
55
- },
49
+ headers: requestHeaders(config.activeToken.token, options),
56
50
  });
57
51
  if (!response.ok) {
58
- throw await apiErrorFromResponse(response);
52
+ throw await reconcileStoredCredentialAuthError(config, env, await apiErrorFromResponse(response));
59
53
  }
60
54
  return new Uint8Array(await response.arrayBuffer());
61
55
  }
62
- async function requestTextStream(config, path, options) {
56
+ async function requestTextStream(config, env, path, options) {
63
57
  if (!config.activeToken) {
64
58
  throw new AuthError();
65
59
  }
66
60
  const response = await fetch(buildUrl(config.apiBaseUrl, path, options?.query), {
67
61
  method: 'GET',
68
- headers: {
69
- accept: 'text/event-stream',
70
- authorization: `Bearer ${config.activeToken.token}`,
71
- 'user-agent': 'BCTRL CLI',
72
- },
62
+ headers: requestHeaders(config.activeToken.token, options, 'text/event-stream'),
73
63
  });
74
64
  if (!response.ok) {
75
- throw await apiErrorFromResponse(response);
65
+ throw await reconcileStoredCredentialAuthError(config, env, await apiErrorFromResponse(response));
76
66
  }
77
67
  if (!response.body) {
78
68
  throw new CliError('BCTRL API response did not include a stream body');
@@ -99,7 +89,7 @@ async function requestTextStream(config, path, options) {
99
89
  },
100
90
  };
101
91
  }
102
- async function uploadFile(config, path, options) {
92
+ async function uploadFile(config, env, path, options) {
103
93
  if (!config.activeToken) {
104
94
  throw new AuthError();
105
95
  }
@@ -110,18 +100,28 @@ async function uploadFile(config, path, options) {
110
100
  }
111
101
  const response = await fetch(buildUrl(config.apiBaseUrl, path, options.query), {
112
102
  method: 'POST',
113
- headers: {
114
- accept: 'application/json',
115
- authorization: `Bearer ${config.activeToken.token}`,
116
- 'user-agent': 'BCTRL CLI',
117
- },
103
+ headers: requestHeaders(config.activeToken.token, options, 'application/json'),
118
104
  body: form,
119
105
  });
120
106
  if (!response.ok) {
121
- throw await apiErrorFromResponse(response);
107
+ throw await reconcileStoredCredentialAuthError(config, env, await apiErrorFromResponse(response));
122
108
  }
123
109
  return (await response.json());
124
110
  }
111
+ async function reconcileStoredCredentialAuthError(config, env, error) {
112
+ if (config.activeToken?.source === 'stored' && isUnauthorizedApiError(error)) {
113
+ await clearCredential(env, config.apiBaseUrl);
114
+ }
115
+ return error;
116
+ }
117
+ function requestHeaders(token, options, accept) {
118
+ return {
119
+ ...(accept ? { accept } : {}),
120
+ authorization: `Bearer ${token}`,
121
+ 'user-agent': 'BCTRL CLI',
122
+ ...(options?.actingSubaccountId ? { 'BCTRL-Subaccount-Id': options.actingSubaccountId } : {}),
123
+ };
124
+ }
125
125
  function buildUrl(baseUrl, path, query) {
126
126
  const url = new URL(`${baseUrl}${path.startsWith('/') ? path : `/${path}`}`);
127
127
  for (const [key, value] of Object.entries(query ?? {})) {
@@ -0,0 +1,39 @@
1
+ import { z } from 'zod';
2
+ declare const DeviceStartResponseSchema: z.ZodObject<{
3
+ deviceCode: z.ZodString;
4
+ userCode: z.ZodString;
5
+ verificationUri: z.ZodString;
6
+ verificationUriComplete: z.ZodString;
7
+ expiresIn: z.ZodNumber;
8
+ interval: z.ZodNumber;
9
+ }, z.core.$strict>;
10
+ declare const DeviceSessionRevokeResponseSchema: z.ZodObject<{
11
+ revoked: z.ZodBoolean;
12
+ }, z.core.$strict>;
13
+ export type DeviceStartResponse = z.infer<typeof DeviceStartResponseSchema>;
14
+ type DeviceSessionRevokeResponse = z.infer<typeof DeviceSessionRevokeResponseSchema>;
15
+ /**
16
+ * Poll outcome. The control-plane returns the RFC 8628 polling states as a 200
17
+ * status union; a 429 (rate limiter) is surfaced as `rate_limited` so the poll
18
+ * loop can back off rather than treating it as a hard failure.
19
+ */
20
+ export type DevicePollResult = {
21
+ status: 'authorization_pending';
22
+ } | {
23
+ status: 'access_denied';
24
+ } | {
25
+ status: 'expired_token';
26
+ } | {
27
+ status: 'rate_limited';
28
+ } | {
29
+ status: 'complete';
30
+ token: string;
31
+ };
32
+ export declare function startDeviceAuth(apiBaseUrl: string, body?: {
33
+ clientName?: string;
34
+ clientKind?: 'cli' | 'mcp';
35
+ deviceName?: string;
36
+ }): Promise<DeviceStartResponse>;
37
+ export declare function pollDeviceAuth(apiBaseUrl: string, deviceCode: string): Promise<DevicePollResult>;
38
+ export declare function revokeDeviceSession(apiBaseUrl: string, token: string): Promise<DeviceSessionRevokeResponse>;
39
+ export {};
@@ -0,0 +1,93 @@
1
+ import { z } from 'zod';
2
+ import { CliError } from '../runtime/errors.js';
3
+ import { apiErrorFromResponse } from './errors.js';
4
+ // Hand-written client for the OAuth 2.0 device-authorization endpoints (RFC 8628).
5
+ // These are unauthenticated (the CLI has no credential yet) and intentionally kept
6
+ // out of the generated public OpenAPI/SDK (docs-hidden).
7
+ const DeviceStartResponseSchema = z
8
+ .object({
9
+ deviceCode: z.string().min(1),
10
+ userCode: z.string().min(1),
11
+ verificationUri: z.string().min(1),
12
+ verificationUriComplete: z.string().min(1),
13
+ expiresIn: z.number().int().positive(),
14
+ interval: z.number().int().positive(),
15
+ })
16
+ .strict();
17
+ const DeviceTokenResponseSchema = z
18
+ .object({
19
+ status: z.enum(['authorization_pending', 'access_denied', 'expired_token', 'complete']),
20
+ token: z.string().optional(),
21
+ })
22
+ .strict();
23
+ const DeviceSessionRevokeResponseSchema = z.object({ revoked: z.boolean() }).strict();
24
+ function deviceEndpoint(apiBaseUrl, path) {
25
+ return new URL(`${apiBaseUrl.replace(/\/+$/, '')}${path}`);
26
+ }
27
+ export async function startDeviceAuth(apiBaseUrl, body = {}) {
28
+ const response = await fetch(deviceEndpoint(apiBaseUrl, '/auth/device'), {
29
+ method: 'POST',
30
+ headers: {
31
+ accept: 'application/json',
32
+ 'content-type': 'application/json',
33
+ 'user-agent': 'BCTRL CLI',
34
+ },
35
+ body: JSON.stringify(body),
36
+ });
37
+ if (!response.ok) {
38
+ throw await apiErrorFromResponse(response, 'BCTRL device login');
39
+ }
40
+ const parsed = DeviceStartResponseSchema.safeParse(await response.json());
41
+ if (!parsed.success) {
42
+ throw new CliError(`BCTRL device login returned an unexpected response: ${parsed.error.message}`);
43
+ }
44
+ return parsed.data;
45
+ }
46
+ export async function pollDeviceAuth(apiBaseUrl, deviceCode) {
47
+ const response = await fetch(deviceEndpoint(apiBaseUrl, '/auth/device/token'), {
48
+ method: 'POST',
49
+ headers: {
50
+ accept: 'application/json',
51
+ 'content-type': 'application/json',
52
+ 'user-agent': 'BCTRL CLI',
53
+ },
54
+ body: JSON.stringify({ deviceCode }),
55
+ });
56
+ // The poll route is rate-limited; a 429 is expected under fast polling and just
57
+ // means "back off and keep waiting", not a terminal error.
58
+ if (response.status === 429) {
59
+ return { status: 'rate_limited' };
60
+ }
61
+ if (!response.ok) {
62
+ throw await apiErrorFromResponse(response, 'BCTRL device login');
63
+ }
64
+ const parsed = DeviceTokenResponseSchema.safeParse(await response.json());
65
+ if (!parsed.success) {
66
+ throw new CliError(`BCTRL device login returned an unexpected response: ${parsed.error.message}`);
67
+ }
68
+ if (parsed.data.status === 'complete') {
69
+ if (!parsed.data.token) {
70
+ throw new CliError('BCTRL device login completed without returning a token.');
71
+ }
72
+ return { status: 'complete', token: parsed.data.token };
73
+ }
74
+ return { status: parsed.data.status };
75
+ }
76
+ export async function revokeDeviceSession(apiBaseUrl, token) {
77
+ const response = await fetch(deviceEndpoint(apiBaseUrl, '/auth/device/session'), {
78
+ method: 'DELETE',
79
+ headers: {
80
+ accept: 'application/json',
81
+ authorization: `Bearer ${token}`,
82
+ 'user-agent': 'BCTRL CLI',
83
+ },
84
+ });
85
+ if (!response.ok) {
86
+ throw await apiErrorFromResponse(response, 'BCTRL device logout');
87
+ }
88
+ const parsed = DeviceSessionRevokeResponseSchema.safeParse(await response.json());
89
+ if (!parsed.success) {
90
+ throw new CliError(`BCTRL device logout returned an unexpected response: ${parsed.error.message}`);
91
+ }
92
+ return parsed.data;
93
+ }
@@ -1,2 +1,3 @@
1
1
  import { CliError } from '../runtime/errors.js';
2
2
  export declare function apiErrorFromResponse(response: Response, context?: string): Promise<CliError>;
3
+ export declare function isUnauthorizedApiError(error: unknown): boolean;
@@ -16,6 +16,14 @@ export async function apiErrorFromResponse(response, context = 'BCTRL API reques
16
16
  apiError,
17
17
  });
18
18
  }
19
+ export function isUnauthorizedApiError(error) {
20
+ if (!(error instanceof CliError))
21
+ return false;
22
+ return (error.apiError?.status === 401 &&
23
+ (error.apiError.code === 'auth.required' ||
24
+ error.apiError.code === 'auth.invalid' ||
25
+ error.apiError.code === undefined));
26
+ }
19
27
  function parseErrorBody(bodyText) {
20
28
  if (!bodyText)
21
29
  return {};
package/dist/bin/bctrl.js CHANGED
File without changes
@@ -1,7 +1,6 @@
1
1
  import { Command } from 'commander';
2
- import { readJsonFile } from '../shared/io.js';
3
2
  import { addOutputFlags } from '../shared/output.js';
4
- import { createOperationDeleteCommand, createOperationJsonBodyCommand, createOperationListCommand, createOperationViewCommand, outputFlags, requestOperationAndPrint, } from '../shared/operation.js';
3
+ import { actingSubaccountOption, buildOperationInput, createOperationDeleteCommand, createOperationJsonBodyCommand, createOperationListCommand, createOperationViewCommand, outputFlags, requestOperationAndPrint, } from '../shared/operation.js';
5
4
  export function createAiCommand(factory) {
6
5
  const command = new Command('ai').description('Manage AI models and credentials');
7
6
  command.addCommand(createAiModelsCommand(factory));
@@ -50,11 +49,13 @@ function createAiCredentialsCommand(factory) {
50
49
  }));
51
50
  command.addCommand(createAiCredentialWriteCommand(factory, 'create', 'ai.credentials.create'));
52
51
  command.addCommand(createAiCredentialWriteCommand(factory, 'patch', 'ai.credentials.update', ['credentialId']));
53
- command.addCommand(addOutputFlags(new Command('test').description('Test an AI credential').argument('<credentialId>')).action(async (credentialId, options) => {
54
- await requestOperationAndPrint(factory, 'ai.credentials.test', {
52
+ command.addCommand(addOutputFlags(new Command('test').description('Test an AI credential').argument('<credentialId>'))
53
+ .option('--params <json>', 'Path/query parameters as a JSON object (inline, @file, or - for stdin)')
54
+ .action(async (credentialId, options) => {
55
+ await requestOperationAndPrint(factory, 'ai.credentials.test', await buildOperationInput('ai.credentials.test', options, {
55
56
  pathParams: { credentialId },
56
57
  output: outputFlags(options),
57
- });
58
+ }));
58
59
  }));
59
60
  command.addCommand(createOperationDeleteCommand(factory, {
60
61
  operationId: 'ai.credentials.delete',
@@ -76,22 +77,20 @@ function createAiCredentialWriteCommand(factory, name, operationId, argNames = [
76
77
  .option('--status <status>', 'Credential status')
77
78
  .option('--default-model <model>', 'Default model')
78
79
  .option('--base-url <url>', 'Custom provider base URL')
79
- .option('--subaccount-id <id>', 'Subaccount scope when using a parent/org key')
80
- .option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
80
+ .option('--subaccount-id <id>', 'Subaccount scope when using a parent/org key'),
81
81
  body: async (_args, options) => {
82
- if (typeof options.input === 'string') {
83
- return (await readJsonFile(options.input));
84
- }
85
82
  return {
86
83
  name: options.name,
87
84
  provider: options.provider,
88
85
  apiKey: options.apiKey,
89
86
  status: options.status,
90
- subaccountId: options.subaccountId,
91
87
  defaultModel: options.defaultModel,
92
88
  baseUrl: options.baseUrl,
93
89
  };
94
90
  },
91
+ actingSubaccountId: operationId === 'ai.credentials.create'
92
+ ? (_args, options) => actingSubaccountOption()(options)
93
+ : undefined,
95
94
  });
96
95
  }
97
96
  function parseBooleanFlag(value) {
@@ -1,5 +1,4 @@
1
1
  import { Command } from 'commander';
2
- import { readJsonFile } from '../shared/io.js';
3
2
  import { createOperationDeleteCommand, createOperationJsonBodyCommand, createOperationListCommand, } from '../shared/operation.js';
4
3
  export function createApiKeyCommand(factory) {
5
4
  const command = new Command('api-key').description('Manage API keys');
@@ -8,12 +7,12 @@ export function createApiKeyCommand(factory) {
8
7
  description: 'List API keys',
9
8
  configure: (cmd) => cmd
10
9
  .option('--subaccount-id <id>', 'Filter by subaccount id')
11
- .option('--kind <kind>', 'Filter by key kind: organization or subaccount')
10
+ .option('--type <type>', 'Filter by API key type: organization or subaccount')
12
11
  .option('-L, --limit <number>', 'Maximum number of results to return')
13
12
  .option('--cursor <cursor>', 'Pagination cursor'),
14
13
  query: (options) => ({
15
14
  subaccountId: typeof options.subaccountId === 'string' ? options.subaccountId : undefined,
16
- keyKind: typeof options.kind === 'string' ? options.kind : undefined,
15
+ type: typeof options.type === 'string' ? options.type : undefined,
17
16
  limit: typeof options.limit === 'string' ? Number(options.limit) : undefined,
18
17
  cursor: typeof options.cursor === 'string' ? options.cursor : undefined,
19
18
  }),
@@ -25,12 +24,8 @@ export function createApiKeyCommand(factory) {
25
24
  configure: (cmd) => cmd
26
25
  .option('--name <name>', 'API key name')
27
26
  .option('--subaccount-id <id>', 'Create a subaccount-scoped key')
28
- .option('--expires-at <iso>', 'Expiration timestamp')
29
- .option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
27
+ .option('--expires-at <iso>', 'Expiration timestamp'),
30
28
  body: async (_args, options) => {
31
- if (typeof options.input === 'string') {
32
- return (await readJsonFile(options.input));
33
- }
34
29
  return {
35
30
  name: options.name,
36
31
  subaccountId: options.subaccountId,
@@ -0,0 +1,6 @@
1
+ import { startDeviceAuth as defaultStartDeviceAuth, type DeviceStartResponse } from '../../api/device-auth.js';
2
+ export type DeviceLoginStartDeps = {
3
+ apiBaseUrl: string;
4
+ startDeviceAuth?: typeof defaultStartDeviceAuth;
5
+ };
6
+ export declare function startDeviceLoginSession(deps: DeviceLoginStartDeps): Promise<DeviceStartResponse>;