@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
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # BCTRL CLI
2
+
3
+ TypeScript command-line interface for BCTRL.
4
+
5
+ This package is intentionally scaffolded around a GitHub CLI-style command architecture:
6
+
7
+ - tiny bin entrypoint
8
+ - testable runtime `main()`
9
+ - lazy factory dependencies
10
+ - IO streams instead of direct `console.log`
11
+ - one module per leaf command
12
+ - `Options + createCommand + run` per command
13
+
14
+ ## Development
15
+
16
+ ```bash
17
+ pnpm -C cli run dev -- --help
18
+ pnpm -C cli run dev -- version
19
+ pnpm -C cli run dev -- auth status
20
+ pnpm -C cli run dev -- space list
21
+ ```
22
+
23
+ ## Command Boundary
24
+
25
+ This package is for public BCTRL API usage:
26
+
27
+ - auth
28
+ - spaces
29
+ - runtimes
30
+ - runs
31
+ - invocations
32
+ - files
33
+ - subaccounts
34
+ - `bctrl version` for current version and future non-blocking update hints
35
+
36
+ Do not add maintainer-only release commands here. CLI publishing belongs in pnpm scripts and CI/CD. Infrastructure rollout commands belong in `@bctrl/machine-cli`.
37
+
38
+ Do not add a public `bctrl update` command initially. The public update surface is `bctrl version`, which can later show a safe upgrade command such as `npm install -g @bctrl/cli@latest`.
39
+
40
+ ## Build
41
+
42
+ ```bash
43
+ pnpm -C cli run build
44
+ ```
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+ export declare const AuthWhoamiSchema: z.ZodObject<{
3
+ authenticated: z.ZodLiteral<true>;
4
+ keyKind: z.ZodEnum<{
5
+ user: "user";
6
+ subaccount: "subaccount";
7
+ }>;
8
+ organizationId: z.ZodString;
9
+ subaccountId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
10
+ userId: z.ZodString;
11
+ apiKeyId: z.ZodString;
12
+ plan: z.ZodString;
13
+ }, z.core.$strict>;
14
+ export type AuthWhoami = z.infer<typeof AuthWhoamiSchema>;
15
+ export declare function validateAuthToken(apiBaseUrl: string, token: string): Promise<AuthWhoami>;
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ import { CliError } from '../runtime/errors.js';
3
+ import { apiErrorFromResponse } from './errors.js';
4
+ export const AuthWhoamiSchema = z
5
+ .object({
6
+ authenticated: z.literal(true),
7
+ keyKind: z.enum(['user', 'subaccount']),
8
+ organizationId: z.string().min(1),
9
+ subaccountId: z.string().min(1).nullable().optional(),
10
+ userId: z.string().min(1),
11
+ apiKeyId: z.string().min(1),
12
+ plan: z.string().min(1),
13
+ })
14
+ .strict();
15
+ export async function validateAuthToken(apiBaseUrl, token) {
16
+ const response = await fetch(new URL(`${apiBaseUrl.replace(/\/+$/, '')}/auth/whoami`), {
17
+ method: 'GET',
18
+ headers: {
19
+ accept: 'application/json',
20
+ authorization: `Bearer ${token}`,
21
+ 'user-agent': 'BCTRL CLI',
22
+ },
23
+ });
24
+ if (!response.ok) {
25
+ throw await apiErrorFromResponse(response, 'BCTRL auth validation');
26
+ }
27
+ const body = AuthWhoamiSchema.safeParse(await response.json());
28
+ if (!body.success) {
29
+ throw new CliError(`BCTRL auth validation returned an unexpected response: ${body.error.message}`);
30
+ }
31
+ return body.data;
32
+ }
@@ -0,0 +1,22 @@
1
+ import type { BctrlConfig } from '../config/config.js';
2
+ export type BctrlApiClient = {
3
+ get: <T>(path: string, options?: RequestOptions) => Promise<T>;
4
+ post: <T>(path: string, options?: JsonRequestOptions) => Promise<T>;
5
+ patch: <T>(path: string, options?: JsonRequestOptions) => Promise<T>;
6
+ put: <T>(path: string, options?: JsonRequestOptions) => Promise<T>;
7
+ delete: <T>(path: string, options?: RequestOptions) => Promise<T>;
8
+ download: (path: string, options?: RequestOptions) => Promise<Uint8Array>;
9
+ streamText: (path: string, options?: RequestOptions) => Promise<AsyncIterable<string>>;
10
+ uploadFile: <T>(path: string, options: RequestOptions & {
11
+ file: Blob;
12
+ fileName: string;
13
+ fields?: Record<string, string>;
14
+ }) => Promise<T>;
15
+ };
16
+ export type RequestOptions = {
17
+ query?: Record<string, string | number | boolean | undefined>;
18
+ };
19
+ export type JsonRequestOptions = RequestOptions & {
20
+ body?: unknown;
21
+ };
22
+ export declare function createBctrlApiClient(config: BctrlConfig): BctrlApiClient;
@@ -0,0 +1,130 @@
1
+ import { AuthError, CliError } from '../runtime/errors.js';
2
+ import { apiErrorFromResponse } from './errors.js';
3
+ export function createBctrlApiClient(config) {
4
+ 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),
13
+ };
14
+ }
15
+ async function requestJson(config, method, path, options) {
16
+ if (!config.activeToken) {
17
+ throw new AuthError();
18
+ }
19
+ const headers = {
20
+ accept: 'application/json',
21
+ authorization: `Bearer ${config.activeToken.token}`,
22
+ 'user-agent': 'BCTRL CLI',
23
+ };
24
+ let body;
25
+ if (options && 'body' in options && options.body !== undefined) {
26
+ headers['content-type'] = 'application/json';
27
+ body = JSON.stringify(options.body);
28
+ }
29
+ const response = await fetch(buildUrl(config.apiBaseUrl, path, options?.query), {
30
+ method,
31
+ headers,
32
+ body,
33
+ });
34
+ if (!response.ok) {
35
+ throw await apiErrorFromResponse(response);
36
+ }
37
+ if (response.status === 204) {
38
+ return { ok: true };
39
+ }
40
+ const text = await response.text();
41
+ return (text ? JSON.parse(text) : { ok: true });
42
+ }
43
+ async function requestBinary(config, method, path, options) {
44
+ if (!config.activeToken) {
45
+ throw new AuthError();
46
+ }
47
+ const response = await fetch(buildUrl(config.apiBaseUrl, path, options?.query), {
48
+ method,
49
+ headers: {
50
+ authorization: `Bearer ${config.activeToken.token}`,
51
+ 'user-agent': 'BCTRL CLI',
52
+ },
53
+ });
54
+ if (!response.ok) {
55
+ throw await apiErrorFromResponse(response);
56
+ }
57
+ return new Uint8Array(await response.arrayBuffer());
58
+ }
59
+ async function requestTextStream(config, path, options) {
60
+ if (!config.activeToken) {
61
+ throw new AuthError();
62
+ }
63
+ const response = await fetch(buildUrl(config.apiBaseUrl, path, options?.query), {
64
+ method: 'GET',
65
+ headers: {
66
+ accept: 'text/event-stream',
67
+ authorization: `Bearer ${config.activeToken.token}`,
68
+ 'user-agent': 'BCTRL CLI',
69
+ },
70
+ });
71
+ if (!response.ok) {
72
+ throw await apiErrorFromResponse(response);
73
+ }
74
+ if (!response.body) {
75
+ throw new CliError('BCTRL API response did not include a stream body');
76
+ }
77
+ const decoder = new TextDecoder();
78
+ const reader = response.body.getReader();
79
+ return {
80
+ async *[Symbol.asyncIterator]() {
81
+ try {
82
+ while (true) {
83
+ const { done, value } = await reader.read();
84
+ if (done)
85
+ break;
86
+ if (value)
87
+ yield decoder.decode(value, { stream: true });
88
+ }
89
+ const rest = decoder.decode();
90
+ if (rest)
91
+ yield rest;
92
+ }
93
+ finally {
94
+ reader.releaseLock();
95
+ }
96
+ },
97
+ };
98
+ }
99
+ async function uploadFile(config, path, options) {
100
+ if (!config.activeToken) {
101
+ throw new AuthError();
102
+ }
103
+ const form = new FormData();
104
+ form.set('file', options.file, options.fileName);
105
+ for (const [key, value] of Object.entries(options.fields ?? {})) {
106
+ form.set(key, value);
107
+ }
108
+ const response = await fetch(buildUrl(config.apiBaseUrl, path, options.query), {
109
+ method: 'POST',
110
+ headers: {
111
+ accept: 'application/json',
112
+ authorization: `Bearer ${config.activeToken.token}`,
113
+ 'user-agent': 'BCTRL CLI',
114
+ },
115
+ body: form,
116
+ });
117
+ if (!response.ok) {
118
+ throw await apiErrorFromResponse(response);
119
+ }
120
+ return (await response.json());
121
+ }
122
+ function buildUrl(baseUrl, path, query) {
123
+ const url = new URL(`${baseUrl}${path.startsWith('/') ? path : `/${path}`}`);
124
+ for (const [key, value] of Object.entries(query ?? {})) {
125
+ if (value !== undefined && value !== '') {
126
+ url.searchParams.set(key, String(value));
127
+ }
128
+ }
129
+ return url.toString();
130
+ }
@@ -0,0 +1,2 @@
1
+ import { CliError } from '../runtime/errors.js';
2
+ export declare function apiErrorFromResponse(response: Response, context?: string): Promise<CliError>;
@@ -0,0 +1,95 @@
1
+ import { CliError } from '../runtime/errors.js';
2
+ export async function apiErrorFromResponse(response, context = 'BCTRL API request') {
3
+ const bodyText = await response.text();
4
+ const parsed = parseErrorBody(bodyText);
5
+ const message = parsed.message ?? parsed.error;
6
+ const humanMessage = typeof message === 'string' && message.trim()
7
+ ? message.trim()
8
+ : bodyText || `${response.status} ${response.statusText}`;
9
+ const apiError = {
10
+ status: response.status,
11
+ code: typeof parsed.code === 'string' ? parsed.code : undefined,
12
+ requestId: typeof parsed.requestId === 'string' ? parsed.requestId : undefined,
13
+ details: isRecord(parsed.details) ? parsed.details : undefined,
14
+ };
15
+ return new CliError(formatApiErrorMessage(context, response.status, humanMessage, apiError), {
16
+ apiError,
17
+ });
18
+ }
19
+ function parseErrorBody(bodyText) {
20
+ if (!bodyText)
21
+ return {};
22
+ try {
23
+ const parsed = JSON.parse(bodyText);
24
+ return isRecord(parsed) ? parsed : { error: bodyText };
25
+ }
26
+ catch {
27
+ return { error: bodyText };
28
+ }
29
+ }
30
+ function formatApiErrorMessage(context, status, message, apiError) {
31
+ const lines = [`${context} failed: ${status} ${message}`];
32
+ if (apiError.code) {
33
+ lines.push(`Code: ${apiError.code}`);
34
+ }
35
+ if (apiError.requestId) {
36
+ lines.push(`Request ID: ${apiError.requestId}`);
37
+ }
38
+ const suggestion = suggestionForApiError(apiError);
39
+ if (suggestion) {
40
+ lines.push('', 'Try:', ` ${suggestion}`);
41
+ }
42
+ return lines.join('\n');
43
+ }
44
+ function suggestionForApiError(error) {
45
+ switch (error.code) {
46
+ case 'auth.required':
47
+ case 'auth.invalid':
48
+ return 'bctrl auth login';
49
+ case 'permission.denied':
50
+ case 'auth.forbidden':
51
+ return 'bctrl auth status';
52
+ case 'space.not_found':
53
+ return 'bctrl space list';
54
+ case 'runtime.not_found':
55
+ return error.details && typeof error.details.spaceId === 'string'
56
+ ? `bctrl runtime list --space ${error.details.spaceId}`
57
+ : 'bctrl runtime list';
58
+ case 'run.not_found':
59
+ return error.details && typeof error.details.runtimeId === 'string'
60
+ ? `bctrl runtime runs ${error.details.runtimeId}`
61
+ : 'bctrl run list';
62
+ case 'file.not_found':
63
+ return error.details && typeof error.details.spaceId === 'string'
64
+ ? `bctrl file list --space ${error.details.spaceId}`
65
+ : 'bctrl file list';
66
+ case 'tool.not_found':
67
+ return commandWithOptionalSpace('bctrl tool list', error.details);
68
+ case 'toolset.not_found':
69
+ return commandWithOptionalSpace('bctrl toolset list', error.details);
70
+ case 'tool_call.not_found':
71
+ return 'bctrl tool-call list';
72
+ case 'vault_secret.not_found':
73
+ return 'bctrl vault list';
74
+ case 'browser_extension.not_found':
75
+ return 'bctrl browser extension list';
76
+ case 'ai_provider.not_found':
77
+ return 'bctrl ai provider list';
78
+ case 'proxy.not_found':
79
+ return 'bctrl ai proxy list';
80
+ case 'subaccount.not_found':
81
+ return 'bctrl subaccount list';
82
+ case 'subaccount_api_key.not_found':
83
+ return error.details && typeof error.details.subaccountId === 'string'
84
+ ? `bctrl subaccount key list ${error.details.subaccountId}`
85
+ : 'bctrl subaccount list';
86
+ default:
87
+ return null;
88
+ }
89
+ }
90
+ function commandWithOptionalSpace(command, details) {
91
+ return details && typeof details.spaceId === 'string' ? `${command} --space ${details.spaceId}` : command;
92
+ }
93
+ function isRecord(value) {
94
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
95
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../runtime/main.js';
3
+ const argv = process.argv.slice(2);
4
+ if (argv[0] === '--')
5
+ argv.shift();
6
+ process.exitCode = await main(argv);
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { Factory } from '../../factory.js';
3
+ export declare function createAiCommand(factory: Factory): Command;
@@ -0,0 +1,130 @@
1
+ import { Command } from 'commander';
2
+ import { readJsonFile } from '../shared/io.js';
3
+ import { addOutputFlags } from '../shared/output.js';
4
+ import { createDeleteCommand, createJsonBodyCommand, createListCommand, createViewCommand, requestAndPrint, } from '../shared/rest.js';
5
+ export function createAiCommand(factory) {
6
+ const command = new Command('ai').description('Manage AI providers and proxies');
7
+ command.addCommand(createAiProviderCommand(factory));
8
+ command.addCommand(createAiProxyCommand(factory));
9
+ return command;
10
+ }
11
+ function createAiProviderCommand(factory) {
12
+ const command = new Command('provider').description('Manage AI providers');
13
+ command.addCommand(createListCommand(factory, {
14
+ description: 'List AI providers',
15
+ path: '/ai-providers',
16
+ configure: (cmd) => cmd
17
+ .option('--provider <provider>', 'Filter by provider')
18
+ .option('--name <name>', 'Filter by name')
19
+ .option('--status <status>', 'Filter by status'),
20
+ query: (options) => ({
21
+ provider: typeof options.provider === 'string' ? options.provider : undefined,
22
+ name: typeof options.name === 'string' ? options.name : undefined,
23
+ status: typeof options.status === 'string' ? options.status : undefined,
24
+ }),
25
+ }));
26
+ command.addCommand(createViewCommand(factory, {
27
+ description: 'View an AI provider',
28
+ path: '/ai-providers/{id}',
29
+ }));
30
+ command.addCommand(createJsonBodyCommand(factory, {
31
+ name: 'create',
32
+ description: 'Create an AI provider',
33
+ method: 'post',
34
+ path: '/ai-providers',
35
+ configure: (cmd) => cmd
36
+ .option('--name <name>', 'Provider name')
37
+ .option('--provider <provider>', 'Provider key')
38
+ .option('--api-key <key>', 'Provider API key')
39
+ .option('--status <status>', 'Provider status')
40
+ .option('--default-model <model>', 'Default model')
41
+ .option('--subaccount <id>', 'Subaccount scope')
42
+ .option('--from-file <path>', 'Read full JSON request body from file, or - for stdin'),
43
+ body: async (_args, options) => {
44
+ if (typeof options.fromFile === 'string')
45
+ return readJsonFile(options.fromFile);
46
+ return {
47
+ name: options.name,
48
+ provider: options.provider,
49
+ apiKey: options.apiKey,
50
+ status: options.status,
51
+ scope: typeof options.subaccount === 'string' ? { subaccountId: options.subaccount } : undefined,
52
+ config: typeof options.defaultModel === 'string' ? { defaultModel: options.defaultModel } : undefined,
53
+ };
54
+ },
55
+ }));
56
+ command.addCommand(createJsonBodyCommand(factory, {
57
+ name: 'edit',
58
+ description: 'Edit an AI provider',
59
+ method: 'patch',
60
+ path: '/ai-providers/{id}',
61
+ argNames: ['id'],
62
+ configure: (cmd) => cmd
63
+ .option('--name <name>', 'Provider name')
64
+ .option('--api-key <key>', 'Provider API key')
65
+ .option('--status <status>', 'Provider status')
66
+ .option('--default-model <model>', 'Default model')
67
+ .option('--from-file <path>', 'Read full JSON request body from file, or - for stdin'),
68
+ body: async (_args, options) => {
69
+ if (typeof options.fromFile === 'string')
70
+ return readJsonFile(options.fromFile);
71
+ return {
72
+ name: options.name,
73
+ apiKey: options.apiKey,
74
+ status: options.status,
75
+ config: typeof options.defaultModel === 'string' ? { defaultModel: options.defaultModel } : undefined,
76
+ };
77
+ },
78
+ }));
79
+ command.addCommand(addOutputFlags(new Command('test')
80
+ .description('Test an AI provider')
81
+ .argument('<id>'))
82
+ .action(async (id, options) => {
83
+ await requestAndPrint(factory, 'post', `/ai-providers/${encodeURIComponent(id)}/test`, {
84
+ output: options,
85
+ });
86
+ }));
87
+ command.addCommand(createDeleteCommand(factory, {
88
+ description: 'Delete an AI provider',
89
+ path: '/ai-providers/{id}',
90
+ }));
91
+ return command;
92
+ }
93
+ function createAiProxyCommand(factory) {
94
+ const command = new Command('proxy').description('Manage AI/browser proxies');
95
+ command.addCommand(createListCommand(factory, {
96
+ description: 'List proxies',
97
+ path: '/proxies',
98
+ configure: (cmd) => cmd,
99
+ }));
100
+ command.addCommand(createViewCommand(factory, {
101
+ description: 'View a proxy',
102
+ path: '/proxies/{id}',
103
+ }));
104
+ command.addCommand(createJsonBodyCommand(factory, {
105
+ name: 'create',
106
+ description: 'Create a proxy',
107
+ method: 'post',
108
+ path: '/proxies',
109
+ }));
110
+ command.addCommand(createJsonBodyCommand(factory, {
111
+ name: 'edit',
112
+ description: 'Edit a proxy',
113
+ method: 'patch',
114
+ path: '/proxies/{id}',
115
+ argNames: ['id'],
116
+ }));
117
+ command.addCommand(addOutputFlags(new Command('test')
118
+ .description('Test a proxy')
119
+ .argument('<id>'))
120
+ .action(async (id, options) => {
121
+ await requestAndPrint(factory, 'post', `/proxies/${encodeURIComponent(id)}/test`, {
122
+ output: options,
123
+ });
124
+ }));
125
+ command.addCommand(createDeleteCommand(factory, {
126
+ description: 'Delete a proxy',
127
+ path: '/proxies/{id}',
128
+ }));
129
+ return command;
130
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { Factory } from '../../factory.js';
3
+ export declare function createAuthCommand(factory: Factory): Command;
@@ -0,0 +1,13 @@
1
+ import { Command } from 'commander';
2
+ import { createAuthLoginCommand } from './login.js';
3
+ import { createAuthLogoutCommand } from './logout.js';
4
+ import { createAuthStatusCommand } from './status.js';
5
+ import { createAuthTokenCommand } from './token.js';
6
+ export function createAuthCommand(factory) {
7
+ const command = new Command('auth').description('Manage BCTRL authentication');
8
+ command.addCommand(createAuthLoginCommand(factory));
9
+ command.addCommand(createAuthLogoutCommand(factory));
10
+ command.addCommand(createAuthStatusCommand(factory));
11
+ command.addCommand(createAuthTokenCommand(factory));
12
+ return command;
13
+ }
@@ -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 AuthLoginOptions = {
6
+ io: IOStreams;
7
+ config: () => Promise<BctrlConfig>;
8
+ withToken?: boolean;
9
+ };
10
+ export declare function createAuthLoginCommand(factory: Factory, run?: (options: AuthLoginOptions) => Promise<void>): Command;
11
+ export declare function authLoginRun(options: AuthLoginOptions): Promise<void>;
@@ -0,0 +1,52 @@
1
+ import { Command } from 'commander';
2
+ import { writeStoredAuth } from '../../config/auth-store.js';
3
+ import { CliError } from '../../runtime/errors.js';
4
+ import { validateAuthToken } from '../../api/auth.js';
5
+ import { readHiddenInput } from '../shared/hidden-input.js';
6
+ export function createAuthLoginCommand(factory, run = authLoginRun) {
7
+ return new Command('login')
8
+ .description('Authenticate with a BCTRL API key')
9
+ .option('--with-token', 'Read API key from standard input')
10
+ .action(async (options) => {
11
+ await run({
12
+ io: factory.io,
13
+ config: factory.config,
14
+ withToken: options.withToken,
15
+ });
16
+ });
17
+ }
18
+ export async function authLoginRun(options) {
19
+ const config = await options.config();
20
+ const token = options.withToken
21
+ ? (await readStreamText(options.io.in)).trim()
22
+ : await readHiddenInput(options.io, 'Paste BCTRL API key: ');
23
+ if (!token) {
24
+ throw new CliError('No API key provided');
25
+ }
26
+ const whoami = await validateAuthToken(config.apiBaseUrl, token);
27
+ const now = new Date().toISOString();
28
+ const path = await writeStoredAuth({
29
+ apiBaseUrl: config.apiBaseUrl,
30
+ token,
31
+ whoami,
32
+ createdAt: now,
33
+ validatedAt: now,
34
+ });
35
+ options.io.writeErr(`Logged in to ${config.apiBaseUrl}\n`);
36
+ options.io.writeErr(`Credentials saved to ${path}\n`);
37
+ options.io.writeErr(`Key kind: ${whoami.keyKind}\n`);
38
+ options.io.writeErr(`Organization: ${whoami.organizationId}\n`);
39
+ if (whoami.subaccountId) {
40
+ options.io.writeErr(`Subaccount: ${whoami.subaccountId}\n`);
41
+ }
42
+ if (config.activeToken?.source === 'BCTRL_API_KEY') {
43
+ options.io.writeErr('BCTRL_API_KEY is set and will take precedence over stored credentials.\n');
44
+ }
45
+ }
46
+ async function readStreamText(stream) {
47
+ const chunks = [];
48
+ for await (const chunk of stream) {
49
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
50
+ }
51
+ return Buffer.concat(chunks).toString('utf8');
52
+ }
@@ -0,0 +1,10 @@
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 AuthLogoutOptions = {
6
+ io: IOStreams;
7
+ config: () => Promise<BctrlConfig>;
8
+ };
9
+ export declare function createAuthLogoutCommand(factory: Factory, run?: (options: AuthLogoutOptions) => Promise<void>): Command;
10
+ export declare function authLogoutRun(options: AuthLogoutOptions): Promise<void>;
@@ -0,0 +1,23 @@
1
+ import { Command } from 'commander';
2
+ import { deleteStoredAuth } from '../../config/auth-store.js';
3
+ export function createAuthLogoutCommand(factory, run = authLogoutRun) {
4
+ return new Command('logout').description('Remove stored BCTRL credentials').action(async () => {
5
+ await run({
6
+ io: factory.io,
7
+ config: factory.config,
8
+ });
9
+ });
10
+ }
11
+ export async function authLogoutRun(options) {
12
+ const config = await options.config();
13
+ const removed = await deleteStoredAuth();
14
+ if (removed) {
15
+ options.io.writeErr('Removed stored BCTRL credentials.\n');
16
+ }
17
+ else {
18
+ options.io.writeErr('No stored BCTRL credentials found.\n');
19
+ }
20
+ if (config.activeToken?.source === 'BCTRL_API_KEY') {
21
+ options.io.writeErr('BCTRL_API_KEY is still set and will continue to authenticate this shell.\n');
22
+ }
23
+ }
@@ -0,0 +1,12 @@
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
+ import { type OutputFlags } from '../shared/output.js';
6
+ export type AuthStatusOptions = {
7
+ io: IOStreams;
8
+ config: () => Promise<BctrlConfig>;
9
+ output?: OutputFlags;
10
+ };
11
+ export declare function createAuthStatusCommand(factory: Factory, run?: (options: AuthStatusOptions) => Promise<void>): Command;
12
+ export declare function authStatusRun(options: AuthStatusOptions): Promise<void>;
@@ -0,0 +1,31 @@
1
+ import { Command } from 'commander';
2
+ import { validateAuthToken } from '../../api/auth.js';
3
+ import { addOutputFlags, outputData } from '../shared/output.js';
4
+ export function createAuthStatusCommand(factory, run = authStatusRun) {
5
+ return addOutputFlags(new Command('status')
6
+ .description('Show BCTRL authentication status'))
7
+ .action(async (options) => {
8
+ await run({
9
+ io: factory.io,
10
+ config: factory.config,
11
+ output: options,
12
+ });
13
+ });
14
+ }
15
+ export async function authStatusRun(options) {
16
+ const config = await options.config();
17
+ if (!config.activeToken) {
18
+ await outputData(options.io, {
19
+ apiBaseUrl: config.apiBaseUrl,
20
+ authenticated: false,
21
+ tokenSource: null,
22
+ }, options.output);
23
+ return;
24
+ }
25
+ const whoami = await validateAuthToken(config.apiBaseUrl, config.activeToken.token);
26
+ await outputData(options.io, {
27
+ apiBaseUrl: config.apiBaseUrl,
28
+ tokenSource: config.activeToken.source,
29
+ ...whoami,
30
+ }, options.output);
31
+ }