unbound-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/.github/workflows/publish.yml +27 -0
- package/.github/workflows/version-check.yml +35 -0
- package/LOCAL_DEV.md +126 -0
- package/README.md +128 -0
- package/package.json +27 -0
- package/src/api.js +81 -0
- package/src/auth.js +115 -0
- package/src/commands/login.js +63 -0
- package/src/commands/logout.js +28 -0
- package/src/commands/policy.js +412 -0
- package/src/commands/setup.js +341 -0
- package/src/commands/status.js +59 -0
- package/src/commands/tools.js +176 -0
- package/src/commands/user-groups.js +282 -0
- package/src/commands/users.js +88 -0
- package/src/commands/whoami.js +49 -0
- package/src/config.js +87 -0
- package/src/index.js +129 -0
- package/src/output.js +65 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
const config = require('../config');
|
|
3
|
+
const api = require('../api');
|
|
4
|
+
const output = require('../output');
|
|
5
|
+
|
|
6
|
+
function formatDate(dateStr) {
|
|
7
|
+
if (!dateStr) return '-';
|
|
8
|
+
return new Date(dateStr).toLocaleDateString();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function displayGroup(group) {
|
|
12
|
+
output.keyValue([
|
|
13
|
+
['ID', String(group.id)],
|
|
14
|
+
['Name', group.name],
|
|
15
|
+
['All Org Users', String(group.all_org_users)],
|
|
16
|
+
['Member Count', String(group.member_count ?? '-')],
|
|
17
|
+
['Created', formatDate(group.created_at)],
|
|
18
|
+
['Updated', formatDate(group.updated_at)],
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
if (!group.all_org_users && group.members && group.members.length > 0) {
|
|
22
|
+
console.log('\nMembers:');
|
|
23
|
+
output.table(group.members, [
|
|
24
|
+
{ key: 'id', header: 'ID' },
|
|
25
|
+
{ key: 'email', header: 'Email' },
|
|
26
|
+
{ key: 'first_name', header: 'First Name', format: (v) => v || '-' },
|
|
27
|
+
{ key: 'last_name', header: 'Last Name', format: (v) => v || '-' },
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function confirm(message) {
|
|
33
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
rl.question(`${message} (y/N) `, (answer) => {
|
|
36
|
+
rl.close();
|
|
37
|
+
resolve(answer.toLowerCase() === 'y');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseCommaSeparated(value) {
|
|
43
|
+
if (!value) return undefined;
|
|
44
|
+
return value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function register(program) {
|
|
48
|
+
const userGroups = program
|
|
49
|
+
.command('user-groups')
|
|
50
|
+
.alias('groups')
|
|
51
|
+
.description('Manage user groups. Groups organize users for policy scoping and access control.');
|
|
52
|
+
|
|
53
|
+
// user-groups list
|
|
54
|
+
userGroups
|
|
55
|
+
.command('list')
|
|
56
|
+
.description('List all user groups. Supports searching by name.')
|
|
57
|
+
.option('--search <term>', 'Search groups by name')
|
|
58
|
+
.option('--json', 'Output raw JSON instead of a table')
|
|
59
|
+
.addHelpText('after', `
|
|
60
|
+
Examples:
|
|
61
|
+
$ unbound user-groups list
|
|
62
|
+
$ unbound groups list
|
|
63
|
+
$ unbound user-groups list --search "engineering"
|
|
64
|
+
$ unbound user-groups list --json
|
|
65
|
+
`)
|
|
66
|
+
.action(async (opts) => {
|
|
67
|
+
try {
|
|
68
|
+
if (!config.isLoggedIn()) {
|
|
69
|
+
output.error('Not logged in. Run `unbound login` first.');
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const query = {};
|
|
75
|
+
if (opts.search) query.search = opts.search;
|
|
76
|
+
|
|
77
|
+
const data = await api.get('/api/v1/user_groups/', { query });
|
|
78
|
+
|
|
79
|
+
if (opts.json) {
|
|
80
|
+
output.json(data);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
output.table(data.user_groups, [
|
|
85
|
+
{ key: 'id', header: 'ID' },
|
|
86
|
+
{ key: 'name', header: 'Name' },
|
|
87
|
+
{ key: 'all_org_users', header: 'All Org Users' },
|
|
88
|
+
{ key: 'member_count', header: 'Member Count', format: (v) => (v != null ? String(v) : '-') },
|
|
89
|
+
{ key: 'created_at', header: 'Created', format: (v) => formatDate(v) },
|
|
90
|
+
]);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
output.error(err.message);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// user-groups get
|
|
98
|
+
userGroups
|
|
99
|
+
.command('get <id>')
|
|
100
|
+
.description('Get detailed information about a specific user group, including its members.')
|
|
101
|
+
.option('--json', 'Output raw JSON instead of formatted key-value pairs')
|
|
102
|
+
.addHelpText('after', `
|
|
103
|
+
Examples:
|
|
104
|
+
$ unbound user-groups get 1
|
|
105
|
+
$ unbound groups get 1 --json
|
|
106
|
+
`)
|
|
107
|
+
.action(async (id, opts) => {
|
|
108
|
+
try {
|
|
109
|
+
if (!config.isLoggedIn()) {
|
|
110
|
+
output.error('Not logged in. Run `unbound login` first.');
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const data = await api.get(`/api/v1/user_groups/${id}/`);
|
|
116
|
+
|
|
117
|
+
if (opts.json) {
|
|
118
|
+
output.json(data);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
displayGroup(data.user_group);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
output.error(err.message);
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// user-groups create
|
|
130
|
+
userGroups
|
|
131
|
+
.command('create')
|
|
132
|
+
.description('Create a new user group. By default, the group includes all organization users.')
|
|
133
|
+
.requiredOption('--name <name>', 'Group name (required)')
|
|
134
|
+
.option('--all-org-users', 'Include all organization users (default: true)', true)
|
|
135
|
+
.option('--no-all-org-users', 'Do not include all organization users')
|
|
136
|
+
.option('--user-ids <ids>', 'Comma-separated user IDs to add to this group')
|
|
137
|
+
.addHelpText('after', `
|
|
138
|
+
Examples:
|
|
139
|
+
$ unbound user-groups create --name "Engineering"
|
|
140
|
+
$ unbound groups create --name "Backend Team" --no-all-org-users --user-ids 1,2,3
|
|
141
|
+
`)
|
|
142
|
+
.action(async (opts) => {
|
|
143
|
+
try {
|
|
144
|
+
if (!config.isLoggedIn()) {
|
|
145
|
+
output.error('Not logged in. Run `unbound login` first.');
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const body = {
|
|
151
|
+
name: opts.name,
|
|
152
|
+
all_org_users: opts.allOrgUsers,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (opts.userIds) {
|
|
156
|
+
body.user_ids = parseCommaSeparated(opts.userIds).map(Number);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const data = await api.post('/api/v1/user_groups/', { body });
|
|
160
|
+
output.success('User group created.');
|
|
161
|
+
displayGroup(data.user_group);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
output.error(err.message);
|
|
164
|
+
process.exitCode = 1;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// user-groups update
|
|
169
|
+
userGroups
|
|
170
|
+
.command('update <id>')
|
|
171
|
+
.description('Update an existing user group. Only provided fields will be changed.')
|
|
172
|
+
.option('--name <name>', 'Update group name')
|
|
173
|
+
.option('--all-org-users', 'Include all organization users')
|
|
174
|
+
.option('--no-all-org-users', 'Do not include all organization users')
|
|
175
|
+
.option('--user-ids <ids>', 'Comma-separated user IDs to set as group members')
|
|
176
|
+
.addHelpText('after', `
|
|
177
|
+
Examples:
|
|
178
|
+
$ unbound user-groups update 1 --name "New Name"
|
|
179
|
+
$ unbound groups update 1 --no-all-org-users --user-ids 1,2,3
|
|
180
|
+
$ unbound user-groups update 1 --all-org-users
|
|
181
|
+
`)
|
|
182
|
+
.action(async (id, opts) => {
|
|
183
|
+
try {
|
|
184
|
+
if (!config.isLoggedIn()) {
|
|
185
|
+
output.error('Not logged in. Run `unbound login` first.');
|
|
186
|
+
process.exitCode = 1;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const body = {};
|
|
191
|
+
if (opts.name !== undefined) body.name = opts.name;
|
|
192
|
+
if (opts.allOrgUsers !== undefined) body.all_org_users = opts.allOrgUsers;
|
|
193
|
+
|
|
194
|
+
if (opts.userIds) {
|
|
195
|
+
body.user_ids = parseCommaSeparated(opts.userIds).map(Number);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const data = await api.put(`/api/v1/user_groups/${id}/`, { body });
|
|
199
|
+
output.success('User group updated.');
|
|
200
|
+
displayGroup(data.user_group);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
output.error(err.message);
|
|
203
|
+
process.exitCode = 1;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// user-groups delete
|
|
208
|
+
userGroups
|
|
209
|
+
.command('delete <id>')
|
|
210
|
+
.description('Delete a user group by its ID. Prompts for confirmation unless --yes is provided.')
|
|
211
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
212
|
+
.addHelpText('after', `
|
|
213
|
+
Examples:
|
|
214
|
+
$ unbound user-groups delete 1
|
|
215
|
+
$ unbound groups delete 1 --yes
|
|
216
|
+
`)
|
|
217
|
+
.action(async (id, opts) => {
|
|
218
|
+
try {
|
|
219
|
+
if (!config.isLoggedIn()) {
|
|
220
|
+
output.error('Not logged in. Run `unbound login` first.');
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!opts.yes) {
|
|
226
|
+
const confirmed = await confirm(`Are you sure you want to delete user group ${id}?`);
|
|
227
|
+
if (!confirmed) {
|
|
228
|
+
output.warn('Aborted.');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
await api.del(`/api/v1/user_groups/${id}/`);
|
|
234
|
+
output.success(`User group ${id} deleted.`);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
output.error(err.message);
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// user-groups effective-policies
|
|
242
|
+
userGroups
|
|
243
|
+
.command('effective-policies <id>')
|
|
244
|
+
.description('View the effective policies applied to a specific user group. Shows the resolved set of policies after inheritance and priority.')
|
|
245
|
+
.option('--json', 'Output raw JSON')
|
|
246
|
+
.addHelpText('after', `
|
|
247
|
+
Examples:
|
|
248
|
+
$ unbound user-groups effective-policies 1
|
|
249
|
+
$ unbound groups effective-policies 1 --json
|
|
250
|
+
`)
|
|
251
|
+
.action(async (id, opts) => {
|
|
252
|
+
try {
|
|
253
|
+
if (!config.isLoggedIn()) {
|
|
254
|
+
output.error('Not logged in. Run `unbound login` first.');
|
|
255
|
+
process.exitCode = 1;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const data = await api.get(`/api/v1/user_groups/${id}/effective_policies/`);
|
|
260
|
+
|
|
261
|
+
if (opts.json) {
|
|
262
|
+
output.json(data);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const policies = data.effective_policies || data.policies || [];
|
|
267
|
+
output.table(policies, [
|
|
268
|
+
{ key: 'id', header: 'ID' },
|
|
269
|
+
{ key: 'name', header: 'Name' },
|
|
270
|
+
{ key: 'type', header: 'Type' },
|
|
271
|
+
{ key: 'enabled', header: 'Enabled' },
|
|
272
|
+
{ key: 'priority', header: 'Priority', format: (v) => (v != null ? String(v) : '-') },
|
|
273
|
+
{ key: 'config_summary', header: 'Config Summary', format: (v) => v || '-' },
|
|
274
|
+
]);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
output.error(err.message);
|
|
277
|
+
process.exitCode = 1;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = { register };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const config = require('../config');
|
|
2
|
+
const api = require('../api');
|
|
3
|
+
const output = require('../output');
|
|
4
|
+
|
|
5
|
+
function register(program) {
|
|
6
|
+
const users = program
|
|
7
|
+
.command('users')
|
|
8
|
+
.description('Manage organization users. List members and view their effective policies.');
|
|
9
|
+
|
|
10
|
+
// users list
|
|
11
|
+
users
|
|
12
|
+
.command('list')
|
|
13
|
+
.description('List all members in the organization.')
|
|
14
|
+
.option('--json', 'Output raw JSON instead of a table')
|
|
15
|
+
.addHelpText('after', `
|
|
16
|
+
Examples:
|
|
17
|
+
$ unbound users list
|
|
18
|
+
$ unbound users list --json
|
|
19
|
+
`)
|
|
20
|
+
.action(async (opts) => {
|
|
21
|
+
try {
|
|
22
|
+
if (!config.isLoggedIn()) {
|
|
23
|
+
output.error('Not logged in. Run `unbound login` first.');
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const data = await api.get('/api/v1/user_groups/members/');
|
|
29
|
+
|
|
30
|
+
if (opts.json) {
|
|
31
|
+
output.json(data);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
output.table(data.members, [
|
|
36
|
+
{ key: 'id', header: 'ID' },
|
|
37
|
+
{ key: 'email', header: 'Email' },
|
|
38
|
+
{ key: 'first_name', header: 'First Name', format: (v) => v || '-' },
|
|
39
|
+
{ key: 'last_name', header: 'Last Name', format: (v) => v || '-' },
|
|
40
|
+
]);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
output.error(err.message);
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// users effective-policies
|
|
48
|
+
users
|
|
49
|
+
.command('effective-policies <user-id>')
|
|
50
|
+
.description('View the effective policies applied to a specific user. Shows the resolved set of policies after inheritance and priority.')
|
|
51
|
+
.option('--json', 'Output raw JSON')
|
|
52
|
+
.addHelpText('after', `
|
|
53
|
+
Examples:
|
|
54
|
+
$ unbound users effective-policies 42
|
|
55
|
+
$ unbound users effective-policies 42 --json
|
|
56
|
+
`)
|
|
57
|
+
.action(async (userId, opts) => {
|
|
58
|
+
try {
|
|
59
|
+
if (!config.isLoggedIn()) {
|
|
60
|
+
output.error('Not logged in. Run `unbound login` first.');
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const data = await api.get(`/api/v1/users/${userId}/effective_policies/`);
|
|
66
|
+
|
|
67
|
+
if (opts.json) {
|
|
68
|
+
output.json(data);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const policies = data.effective_policies || data.policies || [];
|
|
73
|
+
output.table(policies, [
|
|
74
|
+
{ key: 'id', header: 'ID' },
|
|
75
|
+
{ key: 'name', header: 'Name' },
|
|
76
|
+
{ key: 'type', header: 'Type' },
|
|
77
|
+
{ key: 'enabled', header: 'Enabled' },
|
|
78
|
+
{ key: 'priority', header: 'Priority', format: (v) => (v != null ? String(v) : '-') },
|
|
79
|
+
{ key: 'config_summary', header: 'Config Summary', format: (v) => v || '-' },
|
|
80
|
+
]);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
output.error(err.message);
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { register };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const config = require('../config');
|
|
2
|
+
const api = require('../api');
|
|
3
|
+
const output = require('../output');
|
|
4
|
+
|
|
5
|
+
function roleFromPrivileges(privileges) {
|
|
6
|
+
if (privileges.is_admin) return 'Admin';
|
|
7
|
+
if (privileges.is_manager) return 'Manager';
|
|
8
|
+
if (privileges.is_member) return 'Member';
|
|
9
|
+
return 'Unknown';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function register(program) {
|
|
13
|
+
program
|
|
14
|
+
.command('whoami')
|
|
15
|
+
.description('Display the currently authenticated user, organization, and role. Requires an active login session.')
|
|
16
|
+
.addHelpText('after', `
|
|
17
|
+
Output fields:
|
|
18
|
+
Email - The authenticated user's email address
|
|
19
|
+
Organization - The organization the user belongs to
|
|
20
|
+
Role - One of: Admin, Manager, Member
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
$ unbound whoami
|
|
24
|
+
`)
|
|
25
|
+
.action(async () => {
|
|
26
|
+
try {
|
|
27
|
+
if (!config.isLoggedIn()) {
|
|
28
|
+
output.error('Not logged in. Run `unbound login` first.');
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const cfg = config.readConfig();
|
|
34
|
+
const privileges = await api.get('/api/v1/users/privileges/');
|
|
35
|
+
const role = roleFromPrivileges(privileges);
|
|
36
|
+
|
|
37
|
+
output.keyValue([
|
|
38
|
+
['Email', cfg.email || '-'],
|
|
39
|
+
['Organization', cfg.org_name || '-'],
|
|
40
|
+
['Role', role],
|
|
41
|
+
]);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
output.error(err.message);
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { register };
|
package/src/config.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), '.unbound');
|
|
6
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
7
|
+
const DEFAULT_BASE_URL = 'https://backend.getunbound.ai';
|
|
8
|
+
const DEFAULT_FRONTEND_URL = 'https://gateway.getunbound.ai';
|
|
9
|
+
|
|
10
|
+
function ensureConfigDir() {
|
|
11
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
12
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readConfig() {
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
19
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// Corrupted config, start fresh
|
|
23
|
+
}
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeConfig(config) {
|
|
28
|
+
ensureConfigDir();
|
|
29
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getApiKey() {
|
|
33
|
+
const config = readConfig();
|
|
34
|
+
return config.api_key || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setApiKey(apiKey) {
|
|
38
|
+
const config = readConfig();
|
|
39
|
+
config.api_key = apiKey;
|
|
40
|
+
writeConfig(config);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getBaseUrl() {
|
|
44
|
+
return process.env.UNBOUND_API_URL || readConfig().base_url || DEFAULT_BASE_URL;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function setBaseUrl(url) {
|
|
48
|
+
const config = readConfig();
|
|
49
|
+
config.base_url = url;
|
|
50
|
+
writeConfig(config);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getFrontendUrl() {
|
|
54
|
+
return process.env.UNBOUND_FRONTEND_URL || readConfig().frontend_url || DEFAULT_FRONTEND_URL;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function setFrontendUrl(url) {
|
|
58
|
+
const config = readConfig();
|
|
59
|
+
config.frontend_url = url;
|
|
60
|
+
writeConfig(config);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function clearConfig() {
|
|
64
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
65
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isLoggedIn() {
|
|
70
|
+
return !!getApiKey();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
CONFIG_DIR,
|
|
75
|
+
CONFIG_FILE,
|
|
76
|
+
ensureConfigDir,
|
|
77
|
+
readConfig,
|
|
78
|
+
writeConfig,
|
|
79
|
+
getApiKey,
|
|
80
|
+
setApiKey,
|
|
81
|
+
getBaseUrl,
|
|
82
|
+
setBaseUrl,
|
|
83
|
+
getFrontendUrl,
|
|
84
|
+
setFrontendUrl,
|
|
85
|
+
clearConfig,
|
|
86
|
+
isLoggedIn,
|
|
87
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const config = require('./config');
|
|
5
|
+
const output = require('./output');
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name('unbound')
|
|
11
|
+
.description('Unbound CLI - Manage your AI Gateway from the command line.\n\nUnbound is an AI gateway that provides centralized policy management,\ncost controls, and security guardrails for AI coding tools like\nClaude Code, Cursor, Gemini CLI, and more.')
|
|
12
|
+
.version('0.1.0')
|
|
13
|
+
.addHelpText('after', `
|
|
14
|
+
Examples:
|
|
15
|
+
$ unbound login Sign in via browser
|
|
16
|
+
$ unbound login --api-key <key> Sign in with an API key
|
|
17
|
+
$ unbound whoami Show current user info
|
|
18
|
+
$ unbound policy list List all policies
|
|
19
|
+
$ unbound policy list --type SECURITY List security policies
|
|
20
|
+
$ unbound users list List organization users
|
|
21
|
+
$ unbound user-groups list List user groups
|
|
22
|
+
$ unbound tools list List connected AI tools
|
|
23
|
+
$ unbound setup cursor Configure Cursor to use Unbound
|
|
24
|
+
$ unbound setup claude-code Configure Claude Code to use Unbound
|
|
25
|
+
$ unbound config set-url http://localhost:8000 Use local backend
|
|
26
|
+
$ unbound config set-frontend-url http://localhost:3000 Use local frontend
|
|
27
|
+
|
|
28
|
+
Environment Variables:
|
|
29
|
+
UNBOUND_API_URL Override the API base URL (e.g. http://localhost:8000)
|
|
30
|
+
UNBOUND_FRONTEND_URL Override the frontend URL (e.g. http://localhost:3000)
|
|
31
|
+
|
|
32
|
+
Configuration:
|
|
33
|
+
Config is stored in ~/.unbound/config.json
|
|
34
|
+
Production backend: https://backend.getunbound.ai
|
|
35
|
+
Production frontend: https://gateway.getunbound.ai
|
|
36
|
+
`);
|
|
37
|
+
|
|
38
|
+
// Register all command modules
|
|
39
|
+
require('./commands/login').register(program);
|
|
40
|
+
require('./commands/logout').register(program);
|
|
41
|
+
require('./commands/whoami').register(program);
|
|
42
|
+
require('./commands/status').register(program);
|
|
43
|
+
require('./commands/policy').register(program);
|
|
44
|
+
require('./commands/users').register(program);
|
|
45
|
+
require('./commands/user-groups').register(program);
|
|
46
|
+
require('./commands/tools').register(program);
|
|
47
|
+
require('./commands/setup').register(program);
|
|
48
|
+
|
|
49
|
+
// config command for managing CLI settings
|
|
50
|
+
const configCmd = program
|
|
51
|
+
.command('config')
|
|
52
|
+
.description('Manage CLI configuration. Set or view the API base URL and other settings.');
|
|
53
|
+
|
|
54
|
+
configCmd
|
|
55
|
+
.command('set-url <url>')
|
|
56
|
+
.description('Set the API base URL. Use http://localhost:8000 for local development.')
|
|
57
|
+
.addHelpText('after', `
|
|
58
|
+
Examples:
|
|
59
|
+
$ unbound config set-url http://localhost:8000 # Local development
|
|
60
|
+
$ unbound config set-url https://backend.getunbound.ai # Production (default)
|
|
61
|
+
`)
|
|
62
|
+
.action((url) => {
|
|
63
|
+
config.setBaseUrl(url);
|
|
64
|
+
output.success(`API base URL set to ${url}`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
configCmd
|
|
68
|
+
.command('get-url')
|
|
69
|
+
.description('Show the current API base URL.')
|
|
70
|
+
.action(() => {
|
|
71
|
+
console.log(config.getBaseUrl());
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
configCmd
|
|
75
|
+
.command('reset-url')
|
|
76
|
+
.description('Reset the API base URL to the default (https://backend.getunbound.ai).')
|
|
77
|
+
.action(() => {
|
|
78
|
+
const cfg = config.readConfig();
|
|
79
|
+
delete cfg.base_url;
|
|
80
|
+
config.writeConfig(cfg);
|
|
81
|
+
output.success(`API base URL reset to ${config.getBaseUrl()}`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
configCmd
|
|
85
|
+
.command('set-frontend-url <url>')
|
|
86
|
+
.description('Set the frontend URL. Use http://localhost:3000 for local development.')
|
|
87
|
+
.addHelpText('after', `
|
|
88
|
+
Examples:
|
|
89
|
+
$ unbound config set-frontend-url http://localhost:3000 # Local development
|
|
90
|
+
$ unbound config set-frontend-url https://gateway.getunbound.ai # Production (default)
|
|
91
|
+
`)
|
|
92
|
+
.action((url) => {
|
|
93
|
+
config.setFrontendUrl(url);
|
|
94
|
+
output.success(`Frontend URL set to ${url}`);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
configCmd
|
|
98
|
+
.command('get-frontend-url')
|
|
99
|
+
.description('Show the current frontend URL.')
|
|
100
|
+
.action(() => {
|
|
101
|
+
console.log(config.getFrontendUrl());
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
configCmd
|
|
105
|
+
.command('reset-frontend-url')
|
|
106
|
+
.description('Reset the frontend URL to the default (https://gateway.getunbound.ai).')
|
|
107
|
+
.action(() => {
|
|
108
|
+
const cfg = config.readConfig();
|
|
109
|
+
delete cfg.frontend_url;
|
|
110
|
+
config.writeConfig(cfg);
|
|
111
|
+
output.success(`Frontend URL reset to ${config.getFrontendUrl()}`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
configCmd
|
|
115
|
+
.command('show')
|
|
116
|
+
.description('Show all current configuration values.')
|
|
117
|
+
.action(() => {
|
|
118
|
+
const cfg = config.readConfig();
|
|
119
|
+
output.keyValue([
|
|
120
|
+
['Config file', config.CONFIG_FILE],
|
|
121
|
+
['API base URL', config.getBaseUrl()],
|
|
122
|
+
['Frontend URL', config.getFrontendUrl()],
|
|
123
|
+
['Logged in', config.isLoggedIn() ? 'Yes' : 'No'],
|
|
124
|
+
['Email', cfg.email || '-'],
|
|
125
|
+
['Organization', cfg.org_name || '-'],
|
|
126
|
+
]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
program.parse(process.argv);
|
package/src/output.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatting utilities for the Unbound CLI.
|
|
3
|
+
* Provides table, JSON, and key-value output for terminal display.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function table(rows, columns) {
|
|
7
|
+
if (!rows || rows.length === 0) {
|
|
8
|
+
console.log('No results found.');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Calculate column widths
|
|
13
|
+
const widths = {};
|
|
14
|
+
for (const col of columns) {
|
|
15
|
+
widths[col.key] = col.header.length;
|
|
16
|
+
}
|
|
17
|
+
for (const row of rows) {
|
|
18
|
+
for (const col of columns) {
|
|
19
|
+
const val = String(col.format ? col.format(row[col.key], row) : row[col.key] ?? '');
|
|
20
|
+
widths[col.key] = Math.max(widths[col.key], val.length);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Header
|
|
25
|
+
const header = columns.map((col) => col.header.padEnd(widths[col.key])).join(' ');
|
|
26
|
+
const separator = columns.map((col) => '-'.repeat(widths[col.key])).join(' ');
|
|
27
|
+
console.log(header);
|
|
28
|
+
console.log(separator);
|
|
29
|
+
|
|
30
|
+
// Rows
|
|
31
|
+
for (const row of rows) {
|
|
32
|
+
const line = columns
|
|
33
|
+
.map((col) => {
|
|
34
|
+
const val = String(col.format ? col.format(row[col.key], row) : row[col.key] ?? '');
|
|
35
|
+
return val.padEnd(widths[col.key]);
|
|
36
|
+
})
|
|
37
|
+
.join(' ');
|
|
38
|
+
console.log(line);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function json(data) {
|
|
43
|
+
console.log(JSON.stringify(data, null, 2));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function keyValue(pairs) {
|
|
47
|
+
const maxKey = Math.max(...pairs.map(([k]) => k.length));
|
|
48
|
+
for (const [key, value] of pairs) {
|
|
49
|
+
console.log(`${key.padEnd(maxKey)} ${value ?? '-'}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function success(msg) {
|
|
54
|
+
console.log(`\u2713 ${msg}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function error(msg) {
|
|
58
|
+
console.error(`Error: ${msg}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function warn(msg) {
|
|
62
|
+
console.error(`Warning: ${msg}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { table, json, keyValue, success, error, warn };
|