@bctrl/cli 0.1.0 → 0.1.3
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.
- package/README.md +78 -27
- package/dist/api/auth.d.ts +1 -0
- package/dist/api/auth.js +1 -0
- package/dist/bin/bctrl.js +0 -0
- package/dist/commands/ai/index.js +25 -17
- package/dist/commands/auth/login.d.ts +3 -0
- package/dist/commands/auth/login.js +19 -5
- package/dist/commands/auth/logout.d.ts +1 -0
- package/dist/commands/auth/logout.js +1 -1
- package/dist/commands/auth/status.d.ts +2 -0
- package/dist/commands/auth/status.js +49 -5
- package/dist/commands/browser/index.js +3 -3
- package/dist/commands/file/index.js +3 -3
- package/dist/commands/invocation/index.js +3 -3
- package/dist/commands/run/index.js +6 -6
- package/dist/commands/runtime/index.js +31 -67
- package/dist/commands/shared/help.d.ts +24 -0
- package/dist/commands/shared/help.js +72 -0
- package/dist/commands/shared/io.js +1 -1
- package/dist/commands/shared/rest.d.ts +1 -1
- package/dist/commands/shared/rest.js +3 -3
- package/dist/commands/space/index.js +6 -6
- package/dist/commands/subaccount/index.js +12 -12
- package/dist/commands/vault/index.js +3 -3
- package/dist/config/auth-store.d.ts +2 -0
- package/dist/config/auth-store.js +1 -0
- package/dist/config/config.js +1 -1
- package/dist/factory.js +1 -1
- package/dist/generated/help.d.ts +134 -0
- package/dist/generated/help.js +179 -0
- package/package.json +10 -12
package/README.md
CHANGED
|
@@ -1,44 +1,95 @@
|
|
|
1
1
|
# BCTRL CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CLI for controlling BCTRL runtimes, files, runs, vault secrets, tools, and account resources from scripts or AI agents.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
```
|
|
13
34
|
|
|
14
|
-
##
|
|
35
|
+
## Browser Runtime Flow
|
|
15
36
|
|
|
16
37
|
```bash
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
21
42
|
```
|
|
22
43
|
|
|
23
|
-
##
|
|
44
|
+
## JSON Input
|
|
45
|
+
|
|
46
|
+
Use `--input` for full request bodies:
|
|
24
47
|
|
|
25
|
-
|
|
48
|
+
```bash
|
|
49
|
+
bctrl runtime create --input runtime.json
|
|
50
|
+
```
|
|
26
51
|
|
|
27
|
-
|
|
28
|
-
- spaces
|
|
29
|
-
- runtimes
|
|
30
|
-
- runs
|
|
31
|
-
- invocations
|
|
32
|
-
- files
|
|
33
|
-
- subaccounts
|
|
34
|
-
- `bctrl version` for current version and future non-blocking update hints
|
|
52
|
+
Use `-` to read from stdin:
|
|
35
53
|
|
|
36
|
-
|
|
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
|
+
```
|
|
37
87
|
|
|
38
|
-
|
|
88
|
+
## Help
|
|
39
89
|
|
|
40
|
-
|
|
90
|
+
Use command help for exact input and output fields:
|
|
41
91
|
|
|
42
92
|
```bash
|
|
43
|
-
|
|
93
|
+
bctrl runtime create --help
|
|
94
|
+
bctrl runtime start --help
|
|
44
95
|
```
|
package/dist/api/auth.d.ts
CHANGED
package/dist/api/auth.js
CHANGED
|
@@ -4,6 +4,7 @@ import { apiErrorFromResponse } from './errors.js';
|
|
|
4
4
|
export const AuthWhoamiSchema = z
|
|
5
5
|
.object({
|
|
6
6
|
authenticated: z.literal(true),
|
|
7
|
+
email: z.string().email().nullable().optional(),
|
|
7
8
|
keyKind: z.enum(['user', 'subaccount']),
|
|
8
9
|
organizationId: z.string().min(1),
|
|
9
10
|
subaccountId: z.string().min(1).nullable().optional(),
|
package/dist/bin/bctrl.js
CHANGED
|
File without changes
|
|
@@ -38,18 +38,26 @@ function createAiProviderCommand(factory) {
|
|
|
38
38
|
.option('--api-key <key>', 'Provider API key')
|
|
39
39
|
.option('--status <status>', 'Provider status')
|
|
40
40
|
.option('--default-model <model>', 'Default model')
|
|
41
|
+
.option('--base-url <url>', 'OpenAI-compatible provider base URL')
|
|
41
42
|
.option('--subaccount <id>', 'Subaccount scope')
|
|
42
|
-
.option('--
|
|
43
|
+
.option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
|
|
43
44
|
body: async (_args, options) => {
|
|
44
|
-
if (typeof options.
|
|
45
|
-
return readJsonFile(options.
|
|
45
|
+
if (typeof options.input === 'string')
|
|
46
|
+
return readJsonFile(options.input);
|
|
46
47
|
return {
|
|
47
48
|
name: options.name,
|
|
48
49
|
provider: options.provider,
|
|
49
50
|
apiKey: options.apiKey,
|
|
50
51
|
status: options.status,
|
|
51
|
-
scope: typeof options.subaccount === 'string'
|
|
52
|
-
|
|
52
|
+
scope: typeof options.subaccount === 'string'
|
|
53
|
+
? { subaccountId: options.subaccount }
|
|
54
|
+
: undefined,
|
|
55
|
+
config: typeof options.defaultModel === 'string' || typeof options.baseUrl === 'string'
|
|
56
|
+
? {
|
|
57
|
+
defaultModel: typeof options.defaultModel === 'string' ? options.defaultModel : undefined,
|
|
58
|
+
baseUrl: typeof options.baseUrl === 'string' ? options.baseUrl : undefined,
|
|
59
|
+
}
|
|
60
|
+
: undefined,
|
|
53
61
|
};
|
|
54
62
|
},
|
|
55
63
|
}));
|
|
@@ -64,22 +72,25 @@ function createAiProviderCommand(factory) {
|
|
|
64
72
|
.option('--api-key <key>', 'Provider API key')
|
|
65
73
|
.option('--status <status>', 'Provider status')
|
|
66
74
|
.option('--default-model <model>', 'Default model')
|
|
67
|
-
.option('--
|
|
75
|
+
.option('--base-url <url>', 'OpenAI-compatible provider base URL')
|
|
76
|
+
.option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
|
|
68
77
|
body: async (_args, options) => {
|
|
69
|
-
if (typeof options.
|
|
70
|
-
return readJsonFile(options.
|
|
78
|
+
if (typeof options.input === 'string')
|
|
79
|
+
return readJsonFile(options.input);
|
|
71
80
|
return {
|
|
72
81
|
name: options.name,
|
|
73
82
|
apiKey: options.apiKey,
|
|
74
83
|
status: options.status,
|
|
75
|
-
config: typeof options.defaultModel === 'string'
|
|
84
|
+
config: typeof options.defaultModel === 'string' || typeof options.baseUrl === 'string'
|
|
85
|
+
? {
|
|
86
|
+
defaultModel: typeof options.defaultModel === 'string' ? options.defaultModel : undefined,
|
|
87
|
+
baseUrl: typeof options.baseUrl === 'string' ? options.baseUrl : undefined,
|
|
88
|
+
}
|
|
89
|
+
: undefined,
|
|
76
90
|
};
|
|
77
91
|
},
|
|
78
92
|
}));
|
|
79
|
-
command.addCommand(addOutputFlags(new Command('test')
|
|
80
|
-
.description('Test an AI provider')
|
|
81
|
-
.argument('<id>'))
|
|
82
|
-
.action(async (id, options) => {
|
|
93
|
+
command.addCommand(addOutputFlags(new Command('test').description('Test an AI provider').argument('<id>')).action(async (id, options) => {
|
|
83
94
|
await requestAndPrint(factory, 'post', `/ai-providers/${encodeURIComponent(id)}/test`, {
|
|
84
95
|
output: options,
|
|
85
96
|
});
|
|
@@ -114,10 +125,7 @@ function createAiProxyCommand(factory) {
|
|
|
114
125
|
path: '/proxies/{id}',
|
|
115
126
|
argNames: ['id'],
|
|
116
127
|
}));
|
|
117
|
-
command.addCommand(addOutputFlags(new Command('test')
|
|
118
|
-
.description('Test a proxy')
|
|
119
|
-
.argument('<id>'))
|
|
120
|
-
.action(async (id, options) => {
|
|
128
|
+
command.addCommand(addOutputFlags(new Command('test').description('Test a proxy').argument('<id>')).action(async (id, options) => {
|
|
121
129
|
await requestAndPrint(factory, 'post', `/proxies/${encodeURIComponent(id)}/test`, {
|
|
122
130
|
output: options,
|
|
123
131
|
});
|
|
@@ -2,10 +2,13 @@ import { Command } from 'commander';
|
|
|
2
2
|
import type { BctrlConfig } from '../../config/config.js';
|
|
3
3
|
import type { Factory } from '../../factory.js';
|
|
4
4
|
import type { IOStreams } from '../../io/streams.js';
|
|
5
|
+
import { validateAuthToken } from '../../api/auth.js';
|
|
5
6
|
export type AuthLoginOptions = {
|
|
6
7
|
io: IOStreams;
|
|
7
8
|
config: () => Promise<BctrlConfig>;
|
|
8
9
|
withToken?: boolean;
|
|
10
|
+
validateToken?: typeof validateAuthToken;
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
9
12
|
};
|
|
10
13
|
export declare function createAuthLoginCommand(factory: Factory, run?: (options: AuthLoginOptions) => Promise<void>): Command;
|
|
11
14
|
export declare function authLoginRun(options: AuthLoginOptions): Promise<void>;
|
|
@@ -17,13 +17,12 @@ export function createAuthLoginCommand(factory, run = authLoginRun) {
|
|
|
17
17
|
}
|
|
18
18
|
export async function authLoginRun(options) {
|
|
19
19
|
const config = await options.config();
|
|
20
|
-
const token = options
|
|
21
|
-
? (await readStreamText(options.io.in)).trim()
|
|
22
|
-
: await readHiddenInput(options.io, 'Paste BCTRL API key: ');
|
|
20
|
+
const { token, source } = await resolveLoginToken(options, config);
|
|
23
21
|
if (!token) {
|
|
24
22
|
throw new CliError('No API key provided');
|
|
25
23
|
}
|
|
26
|
-
const
|
|
24
|
+
const validateToken = options.validateToken ?? validateAuthToken;
|
|
25
|
+
const whoami = await validateToken(config.apiBaseUrl, token);
|
|
27
26
|
const now = new Date().toISOString();
|
|
28
27
|
const path = await writeStoredAuth({
|
|
29
28
|
apiBaseUrl: config.apiBaseUrl,
|
|
@@ -31,7 +30,10 @@ export async function authLoginRun(options) {
|
|
|
31
30
|
whoami,
|
|
32
31
|
createdAt: now,
|
|
33
32
|
validatedAt: now,
|
|
34
|
-
});
|
|
33
|
+
}, options.env);
|
|
34
|
+
if (source === 'env') {
|
|
35
|
+
options.io.writeErr('Using BCTRL_API_KEY from environment.\n');
|
|
36
|
+
}
|
|
35
37
|
options.io.writeErr(`Logged in to ${config.apiBaseUrl}\n`);
|
|
36
38
|
options.io.writeErr(`Credentials saved to ${path}\n`);
|
|
37
39
|
options.io.writeErr(`Key kind: ${whoami.keyKind}\n`);
|
|
@@ -43,6 +45,18 @@ export async function authLoginRun(options) {
|
|
|
43
45
|
options.io.writeErr('BCTRL_API_KEY is set and will take precedence over stored credentials.\n');
|
|
44
46
|
}
|
|
45
47
|
}
|
|
48
|
+
async function resolveLoginToken(options, config) {
|
|
49
|
+
if (options.withToken) {
|
|
50
|
+
return { token: (await readStreamText(options.io.in)).trim(), source: 'stdin' };
|
|
51
|
+
}
|
|
52
|
+
if (config.activeToken?.source === 'BCTRL_API_KEY') {
|
|
53
|
+
return { token: config.activeToken.token, source: 'env' };
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
token: await readHiddenInput(options.io, 'Paste BCTRL API key: '),
|
|
57
|
+
source: 'prompt',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
46
60
|
async function readStreamText(stream) {
|
|
47
61
|
const chunks = [];
|
|
48
62
|
for await (const chunk of stream) {
|
|
@@ -5,6 +5,7 @@ import type { IOStreams } from '../../io/streams.js';
|
|
|
5
5
|
export type AuthLogoutOptions = {
|
|
6
6
|
io: IOStreams;
|
|
7
7
|
config: () => Promise<BctrlConfig>;
|
|
8
|
+
env?: NodeJS.ProcessEnv;
|
|
8
9
|
};
|
|
9
10
|
export declare function createAuthLogoutCommand(factory: Factory, run?: (options: AuthLogoutOptions) => Promise<void>): Command;
|
|
10
11
|
export declare function authLogoutRun(options: AuthLogoutOptions): Promise<void>;
|
|
@@ -10,7 +10,7 @@ export function createAuthLogoutCommand(factory, run = authLogoutRun) {
|
|
|
10
10
|
}
|
|
11
11
|
export async function authLogoutRun(options) {
|
|
12
12
|
const config = await options.config();
|
|
13
|
-
const removed = await deleteStoredAuth();
|
|
13
|
+
const removed = await deleteStoredAuth(options.env);
|
|
14
14
|
if (removed) {
|
|
15
15
|
options.io.writeErr('Removed stored BCTRL credentials.\n');
|
|
16
16
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { validateAuthToken } from '../../api/auth.js';
|
|
2
3
|
import type { BctrlConfig } from '../../config/config.js';
|
|
3
4
|
import type { Factory } from '../../factory.js';
|
|
4
5
|
import type { IOStreams } from '../../io/streams.js';
|
|
@@ -7,6 +8,7 @@ export type AuthStatusOptions = {
|
|
|
7
8
|
io: IOStreams;
|
|
8
9
|
config: () => Promise<BctrlConfig>;
|
|
9
10
|
output?: OutputFlags;
|
|
11
|
+
validateToken?: typeof validateAuthToken;
|
|
10
12
|
};
|
|
11
13
|
export declare function createAuthStatusCommand(factory: Factory, run?: (options: AuthStatusOptions) => Promise<void>): Command;
|
|
12
14
|
export declare function authStatusRun(options: AuthStatusOptions): Promise<void>;
|
|
@@ -15,17 +15,61 @@ export function createAuthStatusCommand(factory, run = authStatusRun) {
|
|
|
15
15
|
export async function authStatusRun(options) {
|
|
16
16
|
const config = await options.config();
|
|
17
17
|
if (!config.activeToken) {
|
|
18
|
-
|
|
18
|
+
const status = {
|
|
19
19
|
apiBaseUrl: config.apiBaseUrl,
|
|
20
20
|
authenticated: false,
|
|
21
21
|
tokenSource: null,
|
|
22
|
-
}
|
|
22
|
+
};
|
|
23
|
+
if (usesStructuredOutput(options.output)) {
|
|
24
|
+
await outputData(options.io, status, options.output);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
writeHumanAuthStatus(options.io, status);
|
|
28
|
+
}
|
|
23
29
|
return;
|
|
24
30
|
}
|
|
25
|
-
const
|
|
26
|
-
await
|
|
31
|
+
const validateToken = options.validateToken ?? validateAuthToken;
|
|
32
|
+
const whoami = await validateToken(config.apiBaseUrl, config.activeToken.token);
|
|
33
|
+
const status = {
|
|
27
34
|
apiBaseUrl: config.apiBaseUrl,
|
|
28
35
|
tokenSource: config.activeToken.source,
|
|
29
36
|
...whoami,
|
|
30
|
-
}
|
|
37
|
+
};
|
|
38
|
+
if (usesStructuredOutput(options.output)) {
|
|
39
|
+
await outputData(options.io, status, options.output);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
writeHumanAuthStatus(options.io, status);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function usesStructuredOutput(output) {
|
|
46
|
+
return Boolean(output?.json !== undefined || output?.jq || output?.template);
|
|
47
|
+
}
|
|
48
|
+
function writeHumanAuthStatus(io, status) {
|
|
49
|
+
if (!status.authenticated) {
|
|
50
|
+
io.writeOut([
|
|
51
|
+
'Authenticated: no',
|
|
52
|
+
`API: ${status.apiBaseUrl}`,
|
|
53
|
+
'',
|
|
54
|
+
'Run:',
|
|
55
|
+
' bctrl auth login',
|
|
56
|
+
'',
|
|
57
|
+
].join('\n'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
io.writeOut([
|
|
61
|
+
'Authenticated: yes',
|
|
62
|
+
`Email: ${status.email ?? '-'}`,
|
|
63
|
+
`Plan: ${status.plan}`,
|
|
64
|
+
`Auth: ${formatTokenSource(status.tokenSource)}`,
|
|
65
|
+
`API: ${status.apiBaseUrl}`,
|
|
66
|
+
'',
|
|
67
|
+
].join('\n'));
|
|
68
|
+
}
|
|
69
|
+
function formatTokenSource(source) {
|
|
70
|
+
if (source === 'BCTRL_API_KEY')
|
|
71
|
+
return 'environment API key';
|
|
72
|
+
if (source === 'stored')
|
|
73
|
+
return 'saved API key';
|
|
74
|
+
return source;
|
|
31
75
|
}
|
|
@@ -37,10 +37,10 @@ function createBrowserExtensionCommand(factory) {
|
|
|
37
37
|
path: '/browser-extensions/import',
|
|
38
38
|
configure: (cmd) => cmd
|
|
39
39
|
.option('--url <url>', 'Extension URL')
|
|
40
|
-
.option('--
|
|
40
|
+
.option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
|
|
41
41
|
body: async (_args, options) => {
|
|
42
|
-
if (typeof options.
|
|
43
|
-
return readJsonFile(options.
|
|
42
|
+
if (typeof options.input === 'string') {
|
|
43
|
+
return readJsonFile(options.input);
|
|
44
44
|
}
|
|
45
45
|
return { source: { type: 'url', url: options.url } };
|
|
46
46
|
},
|
|
@@ -67,10 +67,10 @@ export function createFileCommand(factory) {
|
|
|
67
67
|
configure: (cmd) => cmd
|
|
68
68
|
.option('--name <name>', 'Display name')
|
|
69
69
|
.option('--metadata-file <path>', 'Metadata JSON file')
|
|
70
|
-
.option('--
|
|
70
|
+
.option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
|
|
71
71
|
body: async (_args, options) => {
|
|
72
|
-
if (typeof options.
|
|
73
|
-
return readJsonFile(options.
|
|
72
|
+
if (typeof options.input === 'string')
|
|
73
|
+
return readJsonFile(options.input);
|
|
74
74
|
return {
|
|
75
75
|
name: options.name,
|
|
76
76
|
metadata: typeof options.metadataFile === 'string'
|
|
@@ -17,10 +17,10 @@ export function createInvocationCommand(factory) {
|
|
|
17
17
|
argNames: ['id'],
|
|
18
18
|
configure: (cmd) => cmd
|
|
19
19
|
.option('--timeout-ms <ms>', 'Timeout in milliseconds')
|
|
20
|
-
.option('--
|
|
20
|
+
.option('--input <path>', 'Read JSON request body from file, or - for stdin'),
|
|
21
21
|
body: async (_args, options) => {
|
|
22
|
-
if (typeof options.
|
|
23
|
-
return readJsonFile(options.
|
|
22
|
+
if (typeof options.input === 'string')
|
|
23
|
+
return readJsonFile(options.input);
|
|
24
24
|
return { timeoutMs: options.timeoutMs ? Number(options.timeoutMs) : undefined };
|
|
25
25
|
},
|
|
26
26
|
}));
|
|
@@ -210,10 +210,10 @@ function createRunWaitCommand(factory) {
|
|
|
210
210
|
.option('--connection <id>', 'Connection id')
|
|
211
211
|
.option('--cursor <cursor>', 'Cursor')
|
|
212
212
|
.option('--timeout-ms <ms>', 'Timeout in milliseconds')
|
|
213
|
-
.option('--
|
|
213
|
+
.option('--input <path>', 'Read JSON request body from file, or - for stdin'),
|
|
214
214
|
body: async (_args, options) => {
|
|
215
|
-
if (typeof options.
|
|
216
|
-
return readJsonFile(options.
|
|
215
|
+
if (typeof options.input === 'string')
|
|
216
|
+
return readJsonFile(options.input);
|
|
217
217
|
return {
|
|
218
218
|
type: options.type,
|
|
219
219
|
category: options.category,
|
|
@@ -234,10 +234,10 @@ function createRunViewerCommand(factory, name, description) {
|
|
|
234
234
|
configure: (cmd) => cmd
|
|
235
235
|
.option('--expires-in-seconds <seconds>', 'Viewer expiration in seconds')
|
|
236
236
|
.option('--control <none|input>', 'Live viewer control mode')
|
|
237
|
-
.option('--
|
|
237
|
+
.option('--input <path>', 'Read JSON request body from file, or - for stdin'),
|
|
238
238
|
body: async (_args, options) => {
|
|
239
|
-
if (typeof options.
|
|
240
|
-
return readJsonFile(options.
|
|
239
|
+
if (typeof options.input === 'string')
|
|
240
|
+
return readJsonFile(options.input);
|
|
241
241
|
return {
|
|
242
242
|
expiresInSeconds: options.expiresInSeconds ? Number(options.expiresInSeconds) : undefined,
|
|
243
243
|
control: name === 'live' ? options.control : undefined,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { CliError } from '../../runtime/errors.js';
|
|
3
|
+
import { addCliHelp } from '../shared/help.js';
|
|
3
4
|
import { readBlob, readJsonFile, writeBinary } from '../shared/io.js';
|
|
4
5
|
import { parsePositiveInteger } from '../shared/options.js';
|
|
5
6
|
import { addOutputFlags, outputData } from '../shared/output.js';
|
|
6
|
-
import { addPaginationFlags, createJsonBodyCommand, createListCommand, createViewCommand,
|
|
7
|
+
import { addPaginationFlags, createJsonBodyCommand, createListCommand, createViewCommand, requestAndPrint, } from '../shared/rest.js';
|
|
7
8
|
export function createRuntimeCommand(factory) {
|
|
8
|
-
const command = new Command('runtime').description('
|
|
9
|
+
const command = new Command('runtime').description('Create, start, stop, and manage BCTRL runtimes');
|
|
9
10
|
command.addCommand(createListCommand(factory, {
|
|
10
11
|
description: 'List runtimes',
|
|
11
12
|
path: '/runtimes',
|
|
@@ -23,46 +24,52 @@ export function createRuntimeCommand(factory) {
|
|
|
23
24
|
description: 'View a runtime',
|
|
24
25
|
path: '/runtimes/{id}',
|
|
25
26
|
}));
|
|
26
|
-
command.addCommand(createJsonBodyCommand(factory, {
|
|
27
|
+
command.addCommand(addCliHelp(createJsonBodyCommand(factory, {
|
|
27
28
|
name: 'create',
|
|
28
29
|
description: 'Create a runtime',
|
|
29
30
|
method: 'post',
|
|
30
31
|
path: '/runtimes',
|
|
31
32
|
configure: (cmd) => cmd
|
|
32
|
-
.
|
|
33
|
+
.option('--space <id>', 'Space id')
|
|
33
34
|
.option('--name <name>', 'Runtime name')
|
|
34
35
|
.option('--config-file <path>', 'Runtime config JSON file')
|
|
35
36
|
.option('--metadata-file <path>', 'Runtime metadata JSON file')
|
|
36
|
-
.option('--
|
|
37
|
+
.option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
|
|
37
38
|
body: async (_args, options) => {
|
|
38
|
-
if (typeof options.
|
|
39
|
-
return readJsonFile(options.
|
|
39
|
+
if (typeof options.input === 'string')
|
|
40
|
+
return readJsonFile(options.input);
|
|
41
|
+
if (typeof options.space !== 'string' || options.space.length === 0) {
|
|
42
|
+
throw new CliError('runtime create requires --space unless --input is used');
|
|
43
|
+
}
|
|
40
44
|
return {
|
|
45
|
+
type: 'browser',
|
|
41
46
|
spaceId: options.space,
|
|
42
47
|
name: options.name,
|
|
43
|
-
config: typeof options.configFile === 'string'
|
|
48
|
+
config: typeof options.configFile === 'string'
|
|
49
|
+
? await readJsonFile(options.configFile, '--config-file')
|
|
50
|
+
: undefined,
|
|
44
51
|
metadata: typeof options.metadataFile === 'string'
|
|
45
52
|
? await readJsonFile(options.metadataFile, '--metadata-file')
|
|
46
53
|
: undefined,
|
|
47
54
|
};
|
|
48
55
|
},
|
|
49
|
-
}));
|
|
50
|
-
command.addCommand(addOutputFlags(new Command('
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
}), 'runtime.create'));
|
|
57
|
+
command.addCommand(addCliHelp(addOutputFlags(new Command('start').description('Start a runtime').argument('<id>')).action(async (id, options) => {
|
|
58
|
+
await requestAndPrint(factory, 'post', `/runtimes/${encodeURIComponent(id)}/start`, {
|
|
59
|
+
output: options,
|
|
60
|
+
});
|
|
61
|
+
}), 'runtime.start'));
|
|
62
|
+
command.addCommand(addOutputFlags(new Command('stop').description('Stop a runtime').argument('<id>')).action(async (id, options) => {
|
|
54
63
|
await requestAndPrint(factory, 'post', `/runtimes/${encodeURIComponent(id)}/stop`, {
|
|
55
64
|
output: options,
|
|
56
65
|
});
|
|
57
66
|
}));
|
|
58
|
-
command.addCommand(createRuntimeConnectionCommand(factory));
|
|
59
67
|
command.addCommand(createRuntimeFileCommand(factory));
|
|
60
68
|
command.addCommand(addOutputFlags(new Command('runs')
|
|
61
69
|
.description('List runs for a runtime')
|
|
62
70
|
.argument('<id>')
|
|
63
71
|
.option('-L, --limit <number>', 'Maximum number of results to return', parsePositiveInteger)
|
|
64
|
-
.option('--cursor <cursor>', 'Pagination cursor'))
|
|
65
|
-
.action(async (id, options) => {
|
|
72
|
+
.option('--cursor <cursor>', 'Pagination cursor')).action(async (id, options) => {
|
|
66
73
|
await requestAndPrint(factory, 'get', `/runtimes/${encodeURIComponent(id)}/runs`, {
|
|
67
74
|
query: { limit: options.limit, cursor: options.cursor },
|
|
68
75
|
output: options,
|
|
@@ -70,47 +77,6 @@ export function createRuntimeCommand(factory) {
|
|
|
70
77
|
}));
|
|
71
78
|
return command;
|
|
72
79
|
}
|
|
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
80
|
function createRuntimeFileCommand(factory) {
|
|
115
81
|
const command = new Command('file').description('Move files into and out of runtimes');
|
|
116
82
|
command.addCommand(addOutputFlags(new Command('push')
|
|
@@ -119,8 +85,7 @@ function createRuntimeFileCommand(factory) {
|
|
|
119
85
|
.argument('<local-path>')
|
|
120
86
|
.option('--storage-path <path>', 'Advanced: durable BCTRL storage path')
|
|
121
87
|
.option('--name <name>', 'Display name')
|
|
122
|
-
.option('--metadata-file <path>', 'Metadata JSON file'))
|
|
123
|
-
.action(async (runtime, localPath, options) => {
|
|
88
|
+
.option('--metadata-file <path>', 'Metadata JSON file')).action(async (runtime, localPath, options) => {
|
|
124
89
|
const client = await factory.apiClient();
|
|
125
90
|
const file = await readBlob(localPath);
|
|
126
91
|
const metadata = typeof options.metadataFile === 'string'
|
|
@@ -143,8 +108,7 @@ function createRuntimeFileCommand(factory) {
|
|
|
143
108
|
.argument('<runtime-path>')
|
|
144
109
|
.requiredOption('--to <path>', 'Output path, or - for stdout')
|
|
145
110
|
.option('--name <name>', 'Durable file name')
|
|
146
|
-
.option('--storage-path <path>', 'Advanced: durable BCTRL storage path'))
|
|
147
|
-
.action(async (runtime, runtimePath, options) => {
|
|
111
|
+
.option('--storage-path <path>', 'Advanced: durable BCTRL storage path')).action(async (runtime, runtimePath, options) => {
|
|
148
112
|
const client = await factory.apiClient();
|
|
149
113
|
const collected = await client.post(`/runtimes/${encodeURIComponent(runtime)}/files/collect`, {
|
|
150
114
|
body: {
|
|
@@ -169,10 +133,10 @@ function createRuntimeFileCommand(factory) {
|
|
|
169
133
|
.option('--file <id>', 'Durable file id')
|
|
170
134
|
.option('--storage-path <path>', 'Advanced: durable BCTRL storage path')
|
|
171
135
|
.option('--name <name>', 'Runtime-local name')
|
|
172
|
-
.option('--
|
|
136
|
+
.option('--input <path>', 'Read JSON request body from file, or - for stdin'),
|
|
173
137
|
body: async (_args, options) => {
|
|
174
|
-
if (typeof options.
|
|
175
|
-
return readJsonFile(options.
|
|
138
|
+
if (typeof options.input === 'string')
|
|
139
|
+
return readJsonFile(options.input);
|
|
176
140
|
return { fileId: options.file, storagePath: options.storagePath, name: options.name };
|
|
177
141
|
},
|
|
178
142
|
}));
|
|
@@ -186,10 +150,10 @@ function createRuntimeFileCommand(factory) {
|
|
|
186
150
|
.requiredOption('--path <path>', 'Runtime-local path')
|
|
187
151
|
.option('--name <name>', 'Durable file name')
|
|
188
152
|
.option('--storage-path <path>', 'Advanced: durable BCTRL storage path')
|
|
189
|
-
.option('--
|
|
153
|
+
.option('--input <path>', 'Read JSON request body from file, or - for stdin'),
|
|
190
154
|
body: async (_args, options) => {
|
|
191
|
-
if (typeof options.
|
|
192
|
-
return readJsonFile(options.
|
|
155
|
+
if (typeof options.input === 'string')
|
|
156
|
+
return readJsonFile(options.input);
|
|
193
157
|
return { path: options.path, name: options.name, storagePath: options.storagePath };
|
|
194
158
|
},
|
|
195
159
|
}));
|