@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.
- package/README.md +44 -0
- package/dist/api/auth.d.ts +15 -0
- package/dist/api/auth.js +32 -0
- package/dist/api/client.d.ts +22 -0
- package/dist/api/client.js +130 -0
- package/dist/api/errors.d.ts +2 -0
- package/dist/api/errors.js +95 -0
- package/dist/bin/bctrl.d.ts +2 -0
- package/dist/bin/bctrl.js +6 -0
- package/dist/commands/ai/index.d.ts +3 -0
- package/dist/commands/ai/index.js +130 -0
- package/dist/commands/auth/index.d.ts +3 -0
- package/dist/commands/auth/index.js +13 -0
- package/dist/commands/auth/login.d.ts +11 -0
- package/dist/commands/auth/login.js +52 -0
- package/dist/commands/auth/logout.d.ts +10 -0
- package/dist/commands/auth/logout.js +23 -0
- package/dist/commands/auth/status.d.ts +12 -0
- package/dist/commands/auth/status.js +31 -0
- package/dist/commands/auth/token.d.ts +11 -0
- package/dist/commands/auth/token.js +24 -0
- package/dist/commands/browser/index.d.ts +3 -0
- package/dist/commands/browser/index.js +53 -0
- package/dist/commands/file/index.d.ts +3 -0
- package/dist/commands/file/index.js +88 -0
- package/dist/commands/invocation/index.d.ts +3 -0
- package/dist/commands/invocation/index.js +36 -0
- package/dist/commands/run/index.d.ts +3 -0
- package/dist/commands/run/index.js +247 -0
- package/dist/commands/runtime/index.d.ts +3 -0
- package/dist/commands/runtime/index.js +197 -0
- package/dist/commands/shared/hidden-input.d.ts +2 -0
- package/dist/commands/shared/hidden-input.js +60 -0
- package/dist/commands/shared/io.d.ts +9 -0
- package/dist/commands/shared/io.js +58 -0
- package/dist/commands/shared/options.d.ts +2 -0
- package/dist/commands/shared/options.js +15 -0
- package/dist/commands/shared/output.d.ts +10 -0
- package/dist/commands/shared/output.js +105 -0
- package/dist/commands/shared/rest.d.ts +54 -0
- package/dist/commands/shared/rest.js +121 -0
- package/dist/commands/space/index.d.ts +3 -0
- package/dist/commands/space/index.js +86 -0
- package/dist/commands/space/list.d.ts +13 -0
- package/dist/commands/space/list.js +25 -0
- package/dist/commands/subaccount/index.d.ts +3 -0
- package/dist/commands/subaccount/index.js +162 -0
- package/dist/commands/tool/index.d.ts +3 -0
- package/dist/commands/tool/index.js +40 -0
- package/dist/commands/tool-call/index.d.ts +3 -0
- package/dist/commands/tool-call/index.js +25 -0
- package/dist/commands/toolset/index.d.ts +3 -0
- package/dist/commands/toolset/index.js +33 -0
- package/dist/commands/vault/index.d.ts +3 -0
- package/dist/commands/vault/index.js +96 -0
- package/dist/commands/version/version.d.ts +9 -0
- package/dist/commands/version/version.js +14 -0
- package/dist/config/auth-store.d.ts +37 -0
- package/dist/config/auth-store.js +71 -0
- package/dist/config/config.d.ts +12 -0
- package/dist/config/config.js +35 -0
- package/dist/factory.d.ts +15 -0
- package/dist/factory.js +15 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/io/streams.d.ts +22 -0
- package/dist/io/streams.js +42 -0
- package/dist/root.d.ts +3 -0
- package/dist/root.js +40 -0
- package/dist/runtime/errors.d.ts +19 -0
- package/dist/runtime/errors.js +23 -0
- package/dist/runtime/exit-codes.d.ts +7 -0
- package/dist/runtime/exit-codes.js +6 -0
- package/dist/runtime/main.d.ts +1 -0
- package/dist/runtime/main.js +27 -0
- package/package.json +47 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createJsonBodyCommand, createViewCommand } from '../shared/rest.js';
|
|
3
|
+
import { readJsonFile } from '../shared/io.js';
|
|
4
|
+
import { addOutputFlags, outputData } from '../shared/output.js';
|
|
5
|
+
import { createSpaceListCommand } from './list.js';
|
|
6
|
+
export function createSpaceCommand(factory) {
|
|
7
|
+
const command = new Command('space').description('Manage BCTRL spaces');
|
|
8
|
+
command.addCommand(createSpaceListCommand(factory));
|
|
9
|
+
command.addCommand(createViewCommand(factory, {
|
|
10
|
+
description: 'View a space',
|
|
11
|
+
path: '/spaces/{id}',
|
|
12
|
+
}));
|
|
13
|
+
command.addCommand(createJsonBodyCommand(factory, {
|
|
14
|
+
name: 'create',
|
|
15
|
+
description: 'Create a space',
|
|
16
|
+
method: 'post',
|
|
17
|
+
path: '/spaces',
|
|
18
|
+
configure: (cmd) => cmd
|
|
19
|
+
.option('--name <name>', 'Space name')
|
|
20
|
+
.option('--region <region>', 'Space region')
|
|
21
|
+
.option('--subaccount-id <id>', 'Subaccount id')
|
|
22
|
+
.option('--from-file <path>', 'Read JSON request body from file, or - for stdin'),
|
|
23
|
+
body: async (_args, options) => {
|
|
24
|
+
if (typeof options.fromFile === 'string')
|
|
25
|
+
return readJsonFile(options.fromFile);
|
|
26
|
+
return {
|
|
27
|
+
name: options.name,
|
|
28
|
+
region: options.region,
|
|
29
|
+
subaccountId: options.subaccountId,
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
command.addCommand(createJsonBodyCommand(factory, {
|
|
34
|
+
name: 'edit',
|
|
35
|
+
description: 'Edit a space',
|
|
36
|
+
method: 'patch',
|
|
37
|
+
path: '/spaces/{id}',
|
|
38
|
+
argNames: ['id'],
|
|
39
|
+
configure: (cmd) => cmd
|
|
40
|
+
.option('--name <name>', 'Space name')
|
|
41
|
+
.option('--from-file <path>', 'Read JSON request body from file, or - for stdin'),
|
|
42
|
+
body: async (_args, options) => {
|
|
43
|
+
if (typeof options.fromFile === 'string')
|
|
44
|
+
return readJsonFile(options.fromFile);
|
|
45
|
+
return { name: options.name };
|
|
46
|
+
},
|
|
47
|
+
}));
|
|
48
|
+
command.addCommand(createSpaceEnvironmentCommand(factory));
|
|
49
|
+
command.addCommand(createSpaceAgentContextCommand(factory));
|
|
50
|
+
return command;
|
|
51
|
+
}
|
|
52
|
+
function createSpaceEnvironmentCommand(factory) {
|
|
53
|
+
const command = new Command('env').description('Manage a space environment');
|
|
54
|
+
command.addCommand(createViewCommand(factory, {
|
|
55
|
+
name: 'get',
|
|
56
|
+
description: 'Get a space environment',
|
|
57
|
+
path: '/spaces/{id}/environment',
|
|
58
|
+
}));
|
|
59
|
+
command.addCommand(createJsonBodyCommand(factory, {
|
|
60
|
+
name: 'set',
|
|
61
|
+
description: 'Update a space environment',
|
|
62
|
+
method: 'patch',
|
|
63
|
+
path: '/spaces/{id}/environment',
|
|
64
|
+
argNames: ['id'],
|
|
65
|
+
}));
|
|
66
|
+
return command;
|
|
67
|
+
}
|
|
68
|
+
function createSpaceAgentContextCommand(factory) {
|
|
69
|
+
return addOutputFlags(new Command('agent-context')
|
|
70
|
+
.description('Get machine-readable context for coding agents')
|
|
71
|
+
.argument('<id>')
|
|
72
|
+
.option('--runtime <id>', 'Runtime id')
|
|
73
|
+
.option('--topic <topic>', 'Context topic')
|
|
74
|
+
.option('--detail <level>', 'Detail level'))
|
|
75
|
+
.action(async (id, options) => {
|
|
76
|
+
const client = await factory.apiClient();
|
|
77
|
+
const result = await client.get(`/spaces/${encodeURIComponent(id)}/agent-context`, {
|
|
78
|
+
query: {
|
|
79
|
+
runtimeId: options.runtime,
|
|
80
|
+
topic: options.topic,
|
|
81
|
+
detail: options.detail,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
await outputData(factory.io, result, options);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { BctrlApiClient } from '../../api/client.js';
|
|
3
|
+
import type { Factory } from '../../factory.js';
|
|
4
|
+
import type { IOStreams } from '../../io/streams.js';
|
|
5
|
+
import { type OutputFlags } from '../shared/output.js';
|
|
6
|
+
export type SpaceListOptions = {
|
|
7
|
+
io: IOStreams;
|
|
8
|
+
apiClient: () => Promise<BctrlApiClient>;
|
|
9
|
+
limit?: number;
|
|
10
|
+
output?: OutputFlags;
|
|
11
|
+
};
|
|
12
|
+
export declare function createSpaceListCommand(factory: Factory, run?: (options: SpaceListOptions) => Promise<void>): Command;
|
|
13
|
+
export declare function spaceListRun(options: SpaceListOptions): Promise<void>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { addOutputFlags, outputData } from '../shared/output.js';
|
|
3
|
+
export function createSpaceListCommand(factory, run = spaceListRun) {
|
|
4
|
+
return addOutputFlags(new Command('list')
|
|
5
|
+
.description('List spaces')
|
|
6
|
+
.option('--limit <number>', 'Maximum number of spaces to return', parsePositiveInteger))
|
|
7
|
+
.action(async (options) => {
|
|
8
|
+
await run({
|
|
9
|
+
io: factory.io,
|
|
10
|
+
apiClient: factory.apiClient,
|
|
11
|
+
limit: options.limit,
|
|
12
|
+
output: options,
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export async function spaceListRun(options) {
|
|
17
|
+
const client = await options.apiClient();
|
|
18
|
+
const result = await client.get('/spaces', {
|
|
19
|
+
query: {
|
|
20
|
+
limit: options.limit,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
await outputData(options.io, result, options.output);
|
|
24
|
+
}
|
|
25
|
+
import { parsePositiveInteger } from '../shared/options.js';
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { readJsonFile } from '../shared/io.js';
|
|
3
|
+
import { parsePositiveInteger } from '../shared/options.js';
|
|
4
|
+
import { addOutputFlags } from '../shared/output.js';
|
|
5
|
+
import { createDeleteCommand, createJsonBodyCommand, createListCommand, createViewCommand, requestAndPrint, } from '../shared/rest.js';
|
|
6
|
+
export function createSubaccountCommand(factory) {
|
|
7
|
+
const command = new Command('subaccount').description('Manage subaccounts, keys, limits, and usage');
|
|
8
|
+
command.addCommand(createListCommand(factory, {
|
|
9
|
+
description: 'List subaccounts',
|
|
10
|
+
path: '/subaccounts',
|
|
11
|
+
configure: (cmd) => cmd,
|
|
12
|
+
}));
|
|
13
|
+
command.addCommand(createViewCommand(factory, {
|
|
14
|
+
description: 'View a subaccount',
|
|
15
|
+
path: '/subaccounts/{id}',
|
|
16
|
+
}));
|
|
17
|
+
command.addCommand(createJsonBodyCommand(factory, {
|
|
18
|
+
name: 'create',
|
|
19
|
+
description: 'Create a subaccount',
|
|
20
|
+
method: 'post',
|
|
21
|
+
path: '/subaccounts',
|
|
22
|
+
configure: (cmd) => cmd
|
|
23
|
+
.option('--name <name>', 'Subaccount name')
|
|
24
|
+
.option('--external-id <id>', 'External id')
|
|
25
|
+
.option('--metadata-file <path>', 'Metadata JSON file')
|
|
26
|
+
.option('--from-file <path>', 'Read full JSON request body from file, or - for stdin'),
|
|
27
|
+
body: async (_args, options) => {
|
|
28
|
+
if (typeof options.fromFile === 'string')
|
|
29
|
+
return readJsonFile(options.fromFile);
|
|
30
|
+
return {
|
|
31
|
+
name: options.name,
|
|
32
|
+
externalId: options.externalId,
|
|
33
|
+
metadata: typeof options.metadataFile === 'string'
|
|
34
|
+
? await readJsonFile(options.metadataFile, '--metadata-file')
|
|
35
|
+
: undefined,
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
command.addCommand(createJsonBodyCommand(factory, {
|
|
40
|
+
name: 'edit',
|
|
41
|
+
description: 'Edit a subaccount',
|
|
42
|
+
method: 'patch',
|
|
43
|
+
path: '/subaccounts/{id}',
|
|
44
|
+
argNames: ['id'],
|
|
45
|
+
configure: (cmd) => cmd
|
|
46
|
+
.option('--name <name>', 'Subaccount name')
|
|
47
|
+
.option('--external-id <id>', 'External id')
|
|
48
|
+
.option('--metadata-file <path>', 'Metadata JSON file')
|
|
49
|
+
.option('--from-file <path>', 'Read full JSON request body from file, or - for stdin'),
|
|
50
|
+
body: async (_args, options) => {
|
|
51
|
+
if (typeof options.fromFile === 'string')
|
|
52
|
+
return readJsonFile(options.fromFile);
|
|
53
|
+
return {
|
|
54
|
+
name: options.name,
|
|
55
|
+
externalId: options.externalId,
|
|
56
|
+
metadata: typeof options.metadataFile === 'string'
|
|
57
|
+
? await readJsonFile(options.metadataFile, '--metadata-file')
|
|
58
|
+
: undefined,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
62
|
+
command.addCommand(addOutputFlags(new Command('archive')
|
|
63
|
+
.description('Archive a subaccount')
|
|
64
|
+
.argument('<id>')
|
|
65
|
+
.requiredOption('-y, --yes', 'Confirm archive'))
|
|
66
|
+
.action(async (id, options) => {
|
|
67
|
+
await requestAndPrint(factory, 'post', `/subaccounts/${encodeURIComponent(id)}/archive`, {
|
|
68
|
+
output: options,
|
|
69
|
+
});
|
|
70
|
+
}));
|
|
71
|
+
command.addCommand(createSubaccountUsageCommand(factory));
|
|
72
|
+
command.addCommand(createSubaccountLimitsCommand(factory));
|
|
73
|
+
command.addCommand(createSubaccountKeyCommand(factory));
|
|
74
|
+
return command;
|
|
75
|
+
}
|
|
76
|
+
function createSubaccountUsageCommand(factory) {
|
|
77
|
+
return addOutputFlags(new Command('usage')
|
|
78
|
+
.description('Show subaccount usage')
|
|
79
|
+
.argument('[id]')
|
|
80
|
+
.option('-L, --limit <number>', 'Maximum number of usage records to return', parsePositiveInteger)
|
|
81
|
+
.option('--cursor <cursor>', 'Pagination cursor'))
|
|
82
|
+
.action(async (id, options) => {
|
|
83
|
+
if (id) {
|
|
84
|
+
await requestAndPrint(factory, 'get', `/subaccounts/${encodeURIComponent(id)}/usage`, {
|
|
85
|
+
output: options,
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
await requestAndPrint(factory, 'get', '/subaccounts/usage', {
|
|
90
|
+
query: { limit: options.limit, cursor: options.cursor },
|
|
91
|
+
output: options,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function createSubaccountLimitsCommand(factory) {
|
|
96
|
+
const command = new Command('limits').description('Manage subaccount limits');
|
|
97
|
+
command.addCommand(addOutputFlags(new Command('get')
|
|
98
|
+
.description('View subaccount limits')
|
|
99
|
+
.argument('<id>'))
|
|
100
|
+
.action(async (id, options) => {
|
|
101
|
+
await requestAndPrint(factory, 'get', `/subaccounts/${encodeURIComponent(id)}/limits`, {
|
|
102
|
+
output: options,
|
|
103
|
+
});
|
|
104
|
+
}));
|
|
105
|
+
command.addCommand(createJsonBodyCommand(factory, {
|
|
106
|
+
name: 'set',
|
|
107
|
+
description: 'Update subaccount limits',
|
|
108
|
+
method: 'patch',
|
|
109
|
+
path: '/subaccounts/{id}/limits',
|
|
110
|
+
argNames: ['id'],
|
|
111
|
+
configure: (cmd) => cmd
|
|
112
|
+
.option('--max-spaces <number>', 'Maximum spaces')
|
|
113
|
+
.option('--max-active-runs <number>', 'Maximum active runs')
|
|
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'),
|
|
116
|
+
body: async (_args, options) => {
|
|
117
|
+
if (typeof options.fromFile === 'string')
|
|
118
|
+
return readJsonFile(options.fromFile);
|
|
119
|
+
return {
|
|
120
|
+
spaces: options.maxSpaces ? { max: Number(options.maxSpaces) } : undefined,
|
|
121
|
+
runs: options.maxActiveRuns ? { maxActive: Number(options.maxActiveRuns) } : undefined,
|
|
122
|
+
spend: options.monthlySpendMicroUsd
|
|
123
|
+
? { monthlyLimitMicroUsd: Number(options.monthlySpendMicroUsd) }
|
|
124
|
+
: undefined,
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
}));
|
|
128
|
+
return command;
|
|
129
|
+
}
|
|
130
|
+
function createSubaccountKeyCommand(factory) {
|
|
131
|
+
const command = new Command('key').description('Manage subaccount API keys');
|
|
132
|
+
command.addCommand(addOutputFlags(new Command('list')
|
|
133
|
+
.description('List subaccount API keys')
|
|
134
|
+
.argument('<id>'))
|
|
135
|
+
.action(async (id, options) => {
|
|
136
|
+
await requestAndPrint(factory, 'get', `/subaccounts/${encodeURIComponent(id)}/api-keys`, {
|
|
137
|
+
output: options,
|
|
138
|
+
});
|
|
139
|
+
}));
|
|
140
|
+
command.addCommand(createJsonBodyCommand(factory, {
|
|
141
|
+
name: 'create',
|
|
142
|
+
description: 'Create a subaccount API key',
|
|
143
|
+
method: 'post',
|
|
144
|
+
path: '/subaccounts/{id}/api-keys',
|
|
145
|
+
argNames: ['id'],
|
|
146
|
+
configure: (cmd) => cmd
|
|
147
|
+
.option('--name <name>', 'API key name')
|
|
148
|
+
.option('--from-file <path>', 'Read full JSON request body from file, or - for stdin'),
|
|
149
|
+
body: async (_args, options) => {
|
|
150
|
+
if (typeof options.fromFile === 'string')
|
|
151
|
+
return readJsonFile(options.fromFile);
|
|
152
|
+
return { name: options.name };
|
|
153
|
+
},
|
|
154
|
+
}));
|
|
155
|
+
command.addCommand(createDeleteCommand(factory, {
|
|
156
|
+
name: 'revoke',
|
|
157
|
+
description: 'Revoke a subaccount API key',
|
|
158
|
+
path: '/subaccounts/{id}/api-keys/{keyId}',
|
|
159
|
+
argNames: ['id', 'keyId'],
|
|
160
|
+
}));
|
|
161
|
+
return command;
|
|
162
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createDeleteCommand, createJsonBodyCommand, createListCommand, createViewCommand } from '../shared/rest.js';
|
|
3
|
+
export function createToolCommand(factory) {
|
|
4
|
+
const command = new Command('tool').description('Manage BCTRL tools');
|
|
5
|
+
command.addCommand(createListCommand(factory, {
|
|
6
|
+
description: 'List tools',
|
|
7
|
+
path: '/tools',
|
|
8
|
+
configure: (cmd) => cmd.option('--space <id>', 'Filter by space id'),
|
|
9
|
+
query: (options) => ({ spaceId: typeof options.space === 'string' ? options.space : undefined }),
|
|
10
|
+
}));
|
|
11
|
+
command.addCommand(createViewCommand(factory, {
|
|
12
|
+
description: 'View a tool',
|
|
13
|
+
path: '/tools/{id}',
|
|
14
|
+
}));
|
|
15
|
+
command.addCommand(createJsonBodyCommand(factory, {
|
|
16
|
+
name: 'create',
|
|
17
|
+
description: 'Create a tool',
|
|
18
|
+
method: 'post',
|
|
19
|
+
path: '/tools',
|
|
20
|
+
}));
|
|
21
|
+
command.addCommand(createJsonBodyCommand(factory, {
|
|
22
|
+
name: 'edit',
|
|
23
|
+
description: 'Edit a tool',
|
|
24
|
+
method: 'patch',
|
|
25
|
+
path: '/tools/{id}',
|
|
26
|
+
argNames: ['id'],
|
|
27
|
+
}));
|
|
28
|
+
command.addCommand(createJsonBodyCommand(factory, {
|
|
29
|
+
name: 'test',
|
|
30
|
+
description: 'Test a tool',
|
|
31
|
+
method: 'post',
|
|
32
|
+
path: '/tools/{id}/test',
|
|
33
|
+
argNames: ['id'],
|
|
34
|
+
}));
|
|
35
|
+
command.addCommand(createDeleteCommand(factory, {
|
|
36
|
+
description: 'Delete a tool',
|
|
37
|
+
path: '/tools/{id}',
|
|
38
|
+
}));
|
|
39
|
+
return command;
|
|
40
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createListCommand, createViewCommand } from '../shared/rest.js';
|
|
3
|
+
export function createToolCallCommand(factory) {
|
|
4
|
+
const command = new Command('tool-call').description('Inspect BCTRL tool calls');
|
|
5
|
+
command.addCommand(createListCommand(factory, {
|
|
6
|
+
description: 'List tool calls',
|
|
7
|
+
path: '/tool-calls',
|
|
8
|
+
configure: (cmd) => cmd
|
|
9
|
+
.option('--tool <id>', 'Filter by tool id')
|
|
10
|
+
.option('--run <id>', 'Filter by run id')
|
|
11
|
+
.option('--invocation <id>', 'Filter by invocation id')
|
|
12
|
+
.option('--status <status>', 'Filter by status'),
|
|
13
|
+
query: (options) => ({
|
|
14
|
+
toolId: typeof options.tool === 'string' ? options.tool : undefined,
|
|
15
|
+
runId: typeof options.run === 'string' ? options.run : undefined,
|
|
16
|
+
invocationId: typeof options.invocation === 'string' ? options.invocation : undefined,
|
|
17
|
+
status: typeof options.status === 'string' ? options.status : undefined,
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
command.addCommand(createViewCommand(factory, {
|
|
21
|
+
description: 'View a tool call',
|
|
22
|
+
path: '/tool-calls/{id}',
|
|
23
|
+
}));
|
|
24
|
+
return command;
|
|
25
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createDeleteCommand, createJsonBodyCommand, createListCommand, createViewCommand } from '../shared/rest.js';
|
|
3
|
+
export function createToolsetCommand(factory) {
|
|
4
|
+
const command = new Command('toolset').description('Manage BCTRL toolsets');
|
|
5
|
+
command.addCommand(createListCommand(factory, {
|
|
6
|
+
description: 'List toolsets',
|
|
7
|
+
path: '/toolsets',
|
|
8
|
+
configure: (cmd) => cmd.option('--space <id>', 'Filter by space id'),
|
|
9
|
+
query: (options) => ({ spaceId: typeof options.space === 'string' ? options.space : undefined }),
|
|
10
|
+
}));
|
|
11
|
+
command.addCommand(createViewCommand(factory, {
|
|
12
|
+
description: 'View a toolset',
|
|
13
|
+
path: '/toolsets/{id}',
|
|
14
|
+
}));
|
|
15
|
+
command.addCommand(createJsonBodyCommand(factory, {
|
|
16
|
+
name: 'create',
|
|
17
|
+
description: 'Create a toolset',
|
|
18
|
+
method: 'post',
|
|
19
|
+
path: '/toolsets',
|
|
20
|
+
}));
|
|
21
|
+
command.addCommand(createJsonBodyCommand(factory, {
|
|
22
|
+
name: 'edit',
|
|
23
|
+
description: 'Edit a toolset',
|
|
24
|
+
method: 'patch',
|
|
25
|
+
path: '/toolsets/{id}',
|
|
26
|
+
argNames: ['id'],
|
|
27
|
+
}));
|
|
28
|
+
command.addCommand(createDeleteCommand(factory, {
|
|
29
|
+
description: 'Delete a toolset',
|
|
30
|
+
path: '/toolsets/{id}',
|
|
31
|
+
}));
|
|
32
|
+
return command;
|
|
33
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { CliError } from '../../runtime/errors.js';
|
|
3
|
+
import { readJsonFile, readText } from '../shared/io.js';
|
|
4
|
+
import { parsePositiveInteger } from '../shared/options.js';
|
|
5
|
+
import { addOutputFlags } from '../shared/output.js';
|
|
6
|
+
import { createDeleteCommand, createJsonBodyCommand, createListCommand, createViewCommand, requestAndPrint, } from '../shared/rest.js';
|
|
7
|
+
export function createVaultCommand(factory) {
|
|
8
|
+
const command = new Command('vault').description('Manage vault secrets and TOTP codes');
|
|
9
|
+
command.addCommand(createListCommand(factory, {
|
|
10
|
+
description: 'List vault secret metadata',
|
|
11
|
+
path: '/vault/secrets',
|
|
12
|
+
configure: (cmd) => cmd
|
|
13
|
+
.option('--prefix <prefix>', 'Filter by secret id prefix')
|
|
14
|
+
.option('--origin <origin>', 'Filter by origin')
|
|
15
|
+
.option('--has-totp', 'Only show secrets with TOTP')
|
|
16
|
+
.option('-L, --limit <number>', 'Maximum number of results to return', parsePositiveInteger),
|
|
17
|
+
query: (options) => ({
|
|
18
|
+
prefix: typeof options.prefix === 'string' ? options.prefix : undefined,
|
|
19
|
+
origin: typeof options.origin === 'string' ? options.origin : undefined,
|
|
20
|
+
hasTotp: options.hasTotp === true ? true : undefined,
|
|
21
|
+
limit: typeof options.limit === 'number' ? options.limit : undefined,
|
|
22
|
+
}),
|
|
23
|
+
}));
|
|
24
|
+
command.addCommand(createViewCommand(factory, {
|
|
25
|
+
description: 'View vault secret metadata',
|
|
26
|
+
path: '/vault/secrets/{id}',
|
|
27
|
+
}));
|
|
28
|
+
command.addCommand(createVaultReadCommand(factory));
|
|
29
|
+
command.addCommand(createVaultSetCommand(factory));
|
|
30
|
+
command.addCommand(createDeleteCommand(factory, {
|
|
31
|
+
description: 'Delete a vault secret',
|
|
32
|
+
path: '/vault/secrets/{id}',
|
|
33
|
+
}));
|
|
34
|
+
command.addCommand(createVaultTotpCommand(factory));
|
|
35
|
+
return command;
|
|
36
|
+
}
|
|
37
|
+
function createVaultReadCommand(factory) {
|
|
38
|
+
return addOutputFlags(new Command('read')
|
|
39
|
+
.description('Read a vault secret value')
|
|
40
|
+
.argument('<id>')
|
|
41
|
+
.option('--reveal', 'Allow printing a secret to an interactive terminal'))
|
|
42
|
+
.action(async (id, options) => {
|
|
43
|
+
if (factory.io.isStdoutTTY() && options.reveal !== true) {
|
|
44
|
+
throw new CliError('Refusing to print a secret to a terminal without --reveal');
|
|
45
|
+
}
|
|
46
|
+
await requestAndPrint(factory, 'get', `/vault/secrets/${encodeURIComponent(id)}/value`, {
|
|
47
|
+
output: options,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function createVaultSetCommand(factory) {
|
|
52
|
+
return createJsonBodyCommand(factory, {
|
|
53
|
+
name: 'set',
|
|
54
|
+
description: 'Create or replace a vault secret',
|
|
55
|
+
method: 'put',
|
|
56
|
+
path: '/vault/secrets',
|
|
57
|
+
argNames: ['id'],
|
|
58
|
+
configure: (cmd) => cmd
|
|
59
|
+
.option('--username <value>', 'Credential username')
|
|
60
|
+
.option('--password <value>', 'Credential password')
|
|
61
|
+
.option('--password-from-file <path>', 'Read password from file, or - for stdin')
|
|
62
|
+
.option('--totp <secret>', 'TOTP seed')
|
|
63
|
+
.option('--label <label>', 'Display label')
|
|
64
|
+
.option('--origin <origin...>', 'Allowed origin')
|
|
65
|
+
.option('--origin-pattern <pattern...>', 'Allowed origin pattern')
|
|
66
|
+
.option('--from-file <path>', 'Read full JSON request body from file, or - for stdin'),
|
|
67
|
+
body: async (args, options) => {
|
|
68
|
+
if (typeof options.fromFile === 'string')
|
|
69
|
+
return readJsonFile(options.fromFile);
|
|
70
|
+
const password = typeof options.passwordFromFile === 'string'
|
|
71
|
+
? (await readText(options.passwordFromFile)).trimEnd()
|
|
72
|
+
: options.password;
|
|
73
|
+
return {
|
|
74
|
+
id: args.id,
|
|
75
|
+
value: {
|
|
76
|
+
username: options.username,
|
|
77
|
+
password,
|
|
78
|
+
totp: options.totp,
|
|
79
|
+
},
|
|
80
|
+
metadata: {
|
|
81
|
+
label: options.label,
|
|
82
|
+
origins: options.origin,
|
|
83
|
+
originPatterns: options.originPattern,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function createVaultTotpCommand(factory) {
|
|
90
|
+
return addOutputFlags(new Command('totp')
|
|
91
|
+
.description('Generate the current TOTP code for a vault secret')
|
|
92
|
+
.argument('<id>'))
|
|
93
|
+
.action(async (id, options) => {
|
|
94
|
+
await requestAndPrint(factory, 'post', '/vault/totp', { body: { id }, output: options });
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { Factory } from '../../factory.js';
|
|
3
|
+
import type { IOStreams } from '../../io/streams.js';
|
|
4
|
+
export type VersionOptions = {
|
|
5
|
+
io: IOStreams;
|
|
6
|
+
version: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function createVersionCommand(factory: Factory, run?: (options: VersionOptions) => void): Command;
|
|
9
|
+
export declare function versionRun(options: VersionOptions): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
export function createVersionCommand(factory, run = versionRun) {
|
|
3
|
+
return new Command('version')
|
|
4
|
+
.description('Show bctrl version')
|
|
5
|
+
.action(() => {
|
|
6
|
+
run({
|
|
7
|
+
io: factory.io,
|
|
8
|
+
version: factory.version,
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export function versionRun(options) {
|
|
13
|
+
options.io.writeOut(`bctrl version ${options.version}\n`);
|
|
14
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const StoredWhoamiSchema: 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 StoredWhoami = z.infer<typeof StoredWhoamiSchema>;
|
|
15
|
+
export declare const StoredAuthSchema: z.ZodObject<{
|
|
16
|
+
apiBaseUrl: z.ZodString;
|
|
17
|
+
token: z.ZodString;
|
|
18
|
+
whoami: z.ZodObject<{
|
|
19
|
+
authenticated: z.ZodLiteral<true>;
|
|
20
|
+
keyKind: z.ZodEnum<{
|
|
21
|
+
user: "user";
|
|
22
|
+
subaccount: "subaccount";
|
|
23
|
+
}>;
|
|
24
|
+
organizationId: z.ZodString;
|
|
25
|
+
subaccountId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
26
|
+
userId: z.ZodString;
|
|
27
|
+
apiKeyId: z.ZodString;
|
|
28
|
+
plan: z.ZodString;
|
|
29
|
+
}, z.core.$strict>;
|
|
30
|
+
createdAt: z.ZodString;
|
|
31
|
+
validatedAt: z.ZodString;
|
|
32
|
+
}, z.core.$strict>;
|
|
33
|
+
export type StoredAuth = z.infer<typeof StoredAuthSchema>;
|
|
34
|
+
export declare function getAuthFilePath(env?: NodeJS.ProcessEnv): string;
|
|
35
|
+
export declare function readStoredAuth(env?: NodeJS.ProcessEnv): Promise<StoredAuth | null>;
|
|
36
|
+
export declare function writeStoredAuth(auth: StoredAuth, env?: NodeJS.ProcessEnv): Promise<string>;
|
|
37
|
+
export declare function deleteStoredAuth(env?: NodeJS.ProcessEnv): Promise<boolean>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
const DEFAULT_AUTH_FILE_NAME = 'auth.json';
|
|
6
|
+
export const StoredWhoamiSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
authenticated: z.literal(true),
|
|
9
|
+
keyKind: z.enum(['user', 'subaccount']),
|
|
10
|
+
organizationId: z.string().min(1),
|
|
11
|
+
subaccountId: z.string().min(1).nullable().optional(),
|
|
12
|
+
userId: z.string().min(1),
|
|
13
|
+
apiKeyId: z.string().min(1),
|
|
14
|
+
plan: z.string().min(1),
|
|
15
|
+
})
|
|
16
|
+
.strict();
|
|
17
|
+
export const StoredAuthSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
apiBaseUrl: z.string().url(),
|
|
20
|
+
token: z.string().min(1),
|
|
21
|
+
whoami: StoredWhoamiSchema,
|
|
22
|
+
createdAt: z.string().datetime(),
|
|
23
|
+
validatedAt: z.string().datetime(),
|
|
24
|
+
})
|
|
25
|
+
.strict();
|
|
26
|
+
export function getAuthFilePath(env = process.env) {
|
|
27
|
+
if (env.BCTRL_AUTH_FILE?.trim()) {
|
|
28
|
+
return env.BCTRL_AUTH_FILE.trim();
|
|
29
|
+
}
|
|
30
|
+
const configDir = env.BCTRL_CONFIG_DIR?.trim() ??
|
|
31
|
+
(env.XDG_CONFIG_HOME?.trim()
|
|
32
|
+
? join(env.XDG_CONFIG_HOME.trim(), 'bctrl')
|
|
33
|
+
: join(homedir(), '.config', 'bctrl'));
|
|
34
|
+
return join(configDir, DEFAULT_AUTH_FILE_NAME);
|
|
35
|
+
}
|
|
36
|
+
export async function readStoredAuth(env = process.env) {
|
|
37
|
+
const path = getAuthFilePath(env);
|
|
38
|
+
try {
|
|
39
|
+
const text = await readFile(path, 'utf8');
|
|
40
|
+
return StoredAuthSchema.parse(JSON.parse(text));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (isFileNotFound(error))
|
|
44
|
+
return null;
|
|
45
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
46
|
+
throw new Error(`Failed to read BCTRL auth file at ${path}: ${reason}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function writeStoredAuth(auth, env = process.env) {
|
|
50
|
+
const path = getAuthFilePath(env);
|
|
51
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
52
|
+
await writeFile(path, `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 });
|
|
53
|
+
return path;
|
|
54
|
+
}
|
|
55
|
+
export async function deleteStoredAuth(env = process.env) {
|
|
56
|
+
try {
|
|
57
|
+
await rm(getAuthFilePath(env));
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (isFileNotFound(error))
|
|
62
|
+
return false;
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function isFileNotFound(error) {
|
|
67
|
+
return (typeof error === 'object' &&
|
|
68
|
+
error !== null &&
|
|
69
|
+
'code' in error &&
|
|
70
|
+
error.code === 'ENOENT');
|
|
71
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type StoredAuth } from './auth-store.js';
|
|
2
|
+
export type TokenSource = 'BCTRL_API_KEY' | 'stored' | 'none';
|
|
3
|
+
export type ActiveToken = {
|
|
4
|
+
token: string;
|
|
5
|
+
source: Exclude<TokenSource, 'none'>;
|
|
6
|
+
};
|
|
7
|
+
export type BctrlConfig = {
|
|
8
|
+
apiBaseUrl: string;
|
|
9
|
+
activeToken: ActiveToken | null;
|
|
10
|
+
storedAuth: StoredAuth | null;
|
|
11
|
+
};
|
|
12
|
+
export declare function loadConfig(env?: NodeJS.ProcessEnv): Promise<BctrlConfig>;
|