@agentuity/cli 1.0.41 → 1.0.43
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/dist/cmd/build/ast.d.ts.map +1 -1
- package/dist/cmd/build/ast.js +3 -3
- package/dist/cmd/build/ast.js.map +1 -1
- package/dist/cmd/build/typecheck.d.ts.map +1 -1
- package/dist/cmd/build/typecheck.js +52 -1
- package/dist/cmd/build/typecheck.js.map +1 -1
- package/dist/cmd/build/vite/static-renderer.d.ts.map +1 -1
- package/dist/cmd/build/vite/static-renderer.js +22 -8
- package/dist/cmd/build/vite/static-renderer.js.map +1 -1
- package/dist/cmd/cloud/index.d.ts.map +1 -1
- package/dist/cmd/cloud/index.js +4 -0
- package/dist/cmd/cloud/index.js.map +1 -1
- package/dist/cmd/cloud/monitor.d.ts +3 -0
- package/dist/cmd/cloud/monitor.d.ts.map +1 -0
- package/dist/cmd/cloud/monitor.js +300 -0
- package/dist/cmd/cloud/monitor.js.map +1 -0
- package/dist/cmd/cloud/oidc/activity.d.ts +2 -0
- package/dist/cmd/cloud/oidc/activity.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/activity.js +57 -0
- package/dist/cmd/cloud/oidc/activity.js.map +1 -0
- package/dist/cmd/cloud/oidc/create.d.ts +2 -0
- package/dist/cmd/cloud/oidc/create.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/create.js +204 -0
- package/dist/cmd/cloud/oidc/create.js.map +1 -0
- package/dist/cmd/cloud/oidc/delete.d.ts +2 -0
- package/dist/cmd/cloud/oidc/delete.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/delete.js +59 -0
- package/dist/cmd/cloud/oidc/delete.js.map +1 -0
- package/dist/cmd/cloud/oidc/get.d.ts +2 -0
- package/dist/cmd/cloud/oidc/get.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/get.js +62 -0
- package/dist/cmd/cloud/oidc/get.js.map +1 -0
- package/dist/cmd/cloud/oidc/index.d.ts +3 -0
- package/dist/cmd/cloud/oidc/index.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/index.js +32 -0
- package/dist/cmd/cloud/oidc/index.js.map +1 -0
- package/dist/cmd/cloud/oidc/list.d.ts +2 -0
- package/dist/cmd/cloud/oidc/list.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/list.js +48 -0
- package/dist/cmd/cloud/oidc/list.js.map +1 -0
- package/dist/cmd/cloud/oidc/rotate-secret.d.ts +2 -0
- package/dist/cmd/cloud/oidc/rotate-secret.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/rotate-secret.js +66 -0
- package/dist/cmd/cloud/oidc/rotate-secret.js.map +1 -0
- package/dist/cmd/cloud/oidc/users.d.ts +2 -0
- package/dist/cmd/cloud/oidc/users.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/users.js +53 -0
- package/dist/cmd/cloud/oidc/users.js.map +1 -0
- package/dist/cmd/cloud/oidc/util.d.ts +10 -0
- package/dist/cmd/cloud/oidc/util.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/util.js +13 -0
- package/dist/cmd/cloud/oidc/util.js.map +1 -0
- package/dist/cmd/coder/hub-url.d.ts +1 -0
- package/dist/cmd/coder/hub-url.d.ts.map +1 -1
- package/dist/cmd/coder/hub-url.js +4 -1
- package/dist/cmd/coder/hub-url.js.map +1 -1
- package/dist/cmd/coder/start.d.ts.map +1 -1
- package/dist/cmd/coder/start.js +14 -8
- package/dist/cmd/coder/start.js.map +1 -1
- package/dist/cmd/coder/tui-init.d.ts +9 -0
- package/dist/cmd/coder/tui-init.d.ts.map +1 -0
- package/dist/cmd/coder/tui-init.js +56 -0
- package/dist/cmd/coder/tui-init.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -5
- package/dist/config.js.map +1 -1
- package/dist/utils/jsonc.d.ts +13 -0
- package/dist/utils/jsonc.d.ts.map +1 -0
- package/dist/utils/jsonc.js +63 -0
- package/dist/utils/jsonc.js.map +1 -0
- package/dist/utils/route-migration.d.ts +2 -1
- package/dist/utils/route-migration.d.ts.map +1 -1
- package/dist/utils/route-migration.js +23 -32
- package/dist/utils/route-migration.js.map +1 -1
- package/dist/utils/zip.d.ts.map +1 -1
- package/dist/utils/zip.js +18 -2
- package/dist/utils/zip.js.map +1 -1
- package/package.json +6 -7
- package/src/cmd/build/ast.ts +6 -3
- package/src/cmd/build/typecheck.ts +60 -1
- package/src/cmd/build/vite/static-renderer.ts +24 -8
- package/src/cmd/cloud/index.ts +4 -0
- package/src/cmd/cloud/monitor.ts +375 -0
- package/src/cmd/cloud/oidc/activity.ts +64 -0
- package/src/cmd/cloud/oidc/create.ts +230 -0
- package/src/cmd/cloud/oidc/delete.ts +66 -0
- package/src/cmd/cloud/oidc/get.ts +68 -0
- package/src/cmd/cloud/oidc/index.ts +35 -0
- package/src/cmd/cloud/oidc/list.ts +53 -0
- package/src/cmd/cloud/oidc/rotate-secret.ts +80 -0
- package/src/cmd/cloud/oidc/users.ts +60 -0
- package/src/cmd/cloud/oidc/util.ts +28 -0
- package/src/cmd/coder/hub-url.ts +5 -1
- package/src/cmd/coder/start.ts +22 -8
- package/src/cmd/coder/tui-init.ts +75 -0
- package/src/config.ts +16 -5
- package/src/utils/jsonc.ts +67 -0
- package/src/utils/route-migration.ts +29 -40
- package/src/utils/zip.ts +17 -2
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { oauthClientCreate, oauthScopes, type OAuthClientCreateRequest } from '@agentuity/core';
|
|
2
|
+
import enquirer from 'enquirer';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { getCommand } from '../../../command-prefix';
|
|
5
|
+
import * as tui from '../../../tui';
|
|
6
|
+
import { createSubcommand as createSubcommandHelper } from '../../../types';
|
|
7
|
+
import { createOAuthClient } from './util';
|
|
8
|
+
|
|
9
|
+
const OAuthClientCreateResponseSchema = z.object({
|
|
10
|
+
client: z.object({
|
|
11
|
+
id: z.string(),
|
|
12
|
+
name: z.string(),
|
|
13
|
+
description: z.string(),
|
|
14
|
+
homepage_url: z.string(),
|
|
15
|
+
client_type: z.enum(['public', 'confidential']),
|
|
16
|
+
redirect_uris: z.array(z.string()),
|
|
17
|
+
scopes: z.array(z.string()),
|
|
18
|
+
created_at: z.string(),
|
|
19
|
+
updated_at: z.string(),
|
|
20
|
+
}),
|
|
21
|
+
client_secret: z.string(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function parseCsv(value?: string): string[] {
|
|
25
|
+
if (!value) return [];
|
|
26
|
+
return value
|
|
27
|
+
.split(',')
|
|
28
|
+
.map((part) => part.trim())
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const createSubcommand = createSubcommandHelper({
|
|
33
|
+
name: 'create',
|
|
34
|
+
aliases: ['new'],
|
|
35
|
+
description: 'Create a new OAuth application',
|
|
36
|
+
tags: ['creates-resource', 'slow', 'requires-auth'],
|
|
37
|
+
examples: [
|
|
38
|
+
{
|
|
39
|
+
command: getCommand(
|
|
40
|
+
'cloud oidc create --name "My App" --description "OAuth app" --homepage-url "https://example.com" --type confidential --redirect-uris "https://example.com/callback" --scopes "openid,profile,email"'
|
|
41
|
+
),
|
|
42
|
+
description: 'Create OAuth application non-interactively',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
command: getCommand('cloud oidc create'),
|
|
46
|
+
description: 'Create OAuth application interactively',
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
requires: { auth: true },
|
|
50
|
+
idempotent: false,
|
|
51
|
+
webUrl: '/settings/oauth-apps',
|
|
52
|
+
schema: {
|
|
53
|
+
options: z.object({
|
|
54
|
+
name: z.string().optional().describe('the OAuth application name'),
|
|
55
|
+
description: z.string().optional().describe('the OAuth application description'),
|
|
56
|
+
'homepage-url': z.string().optional().describe('the homepage URL'),
|
|
57
|
+
type: z
|
|
58
|
+
.enum(['public', 'confidential'])
|
|
59
|
+
.optional()
|
|
60
|
+
.describe('OAuth client type: public or confidential'),
|
|
61
|
+
'redirect-uris': z
|
|
62
|
+
.string()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe('comma-separated redirect URIs (e.g. https://app/callback,https://app/alt)'),
|
|
65
|
+
scopes: z
|
|
66
|
+
.string()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe('comma-separated OAuth scopes (e.g. openid,profile,email)'),
|
|
69
|
+
}),
|
|
70
|
+
response: OAuthClientCreateResponseSchema,
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async handler(ctx) {
|
|
74
|
+
const { opts, options } = ctx;
|
|
75
|
+
const catalystClient = await createOAuthClient(ctx);
|
|
76
|
+
|
|
77
|
+
const availableScopes = await tui.spinner('Fetching available OAuth scopes', () => {
|
|
78
|
+
return oauthScopes(catalystClient);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const nonInteractive = !process.stdin.isTTY || !process.stdout.isTTY;
|
|
82
|
+
|
|
83
|
+
let name = opts?.name?.trim() || '';
|
|
84
|
+
let description = opts?.description?.trim() || '';
|
|
85
|
+
let homepageUrl = opts?.['homepage-url']?.trim() || '';
|
|
86
|
+
let clientType = opts?.type;
|
|
87
|
+
let redirectUris = parseCsv(opts?.['redirect-uris']);
|
|
88
|
+
let scopes = parseCsv(opts?.scopes);
|
|
89
|
+
|
|
90
|
+
if (!name) {
|
|
91
|
+
if (nonInteractive) {
|
|
92
|
+
tui.fatal('--name is required in non-interactive mode');
|
|
93
|
+
}
|
|
94
|
+
const answer = await enquirer.prompt<{ name: string }>({
|
|
95
|
+
type: 'input',
|
|
96
|
+
name: 'name',
|
|
97
|
+
message: 'Application name:',
|
|
98
|
+
});
|
|
99
|
+
name = answer.name?.trim() || '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!description && !nonInteractive) {
|
|
103
|
+
const answer = await enquirer.prompt<{ description: string }>({
|
|
104
|
+
type: 'input',
|
|
105
|
+
name: 'description',
|
|
106
|
+
message: 'Description:',
|
|
107
|
+
});
|
|
108
|
+
description = answer.description?.trim() || '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!homepageUrl) {
|
|
112
|
+
if (nonInteractive) {
|
|
113
|
+
tui.fatal('--homepage-url is required in non-interactive mode');
|
|
114
|
+
}
|
|
115
|
+
const answer = await enquirer.prompt<{ homepageUrl: string }>({
|
|
116
|
+
type: 'input',
|
|
117
|
+
name: 'homepageUrl',
|
|
118
|
+
message: 'Homepage URL:',
|
|
119
|
+
});
|
|
120
|
+
homepageUrl = answer.homepageUrl?.trim() || '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!clientType) {
|
|
124
|
+
if (nonInteractive) {
|
|
125
|
+
tui.fatal('--type is required in non-interactive mode');
|
|
126
|
+
}
|
|
127
|
+
const answer = await enquirer.prompt<{ clientType: 'public' | 'confidential' }>({
|
|
128
|
+
type: 'select',
|
|
129
|
+
name: 'clientType',
|
|
130
|
+
message: 'Client type:',
|
|
131
|
+
choices: [
|
|
132
|
+
{ name: 'public', message: 'public' },
|
|
133
|
+
{ name: 'confidential', message: 'confidential' },
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
clientType = answer.clientType;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (redirectUris.length === 0) {
|
|
140
|
+
if (nonInteractive) {
|
|
141
|
+
tui.fatal('--redirect-uris is required in non-interactive mode');
|
|
142
|
+
}
|
|
143
|
+
const answer = await enquirer.prompt<{ redirectUris: string }>({
|
|
144
|
+
type: 'input',
|
|
145
|
+
name: 'redirectUris',
|
|
146
|
+
message: 'Redirect URIs (comma-separated):',
|
|
147
|
+
});
|
|
148
|
+
redirectUris = parseCsv(answer.redirectUris);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (scopes.length === 0) {
|
|
152
|
+
if (nonInteractive) {
|
|
153
|
+
tui.fatal('--scopes is required in non-interactive mode');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const choices = availableScopes.scopes.map((scope) => ({
|
|
157
|
+
name: scope.name,
|
|
158
|
+
message: `${scope.name} — ${scope.description}`,
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
const answer = await enquirer.prompt<{ scopes: string[] }>({
|
|
162
|
+
type: 'multiselect',
|
|
163
|
+
name: 'scopes',
|
|
164
|
+
message: 'Select OAuth scopes:',
|
|
165
|
+
choices,
|
|
166
|
+
});
|
|
167
|
+
scopes = answer.scopes;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!name) {
|
|
171
|
+
tui.fatal('Name is required');
|
|
172
|
+
}
|
|
173
|
+
if (!homepageUrl) {
|
|
174
|
+
tui.fatal('Homepage URL is required');
|
|
175
|
+
}
|
|
176
|
+
if (!clientType) {
|
|
177
|
+
tui.fatal('Client type is required');
|
|
178
|
+
}
|
|
179
|
+
if (redirectUris.length === 0) {
|
|
180
|
+
tui.fatal('At least one redirect URI is required');
|
|
181
|
+
}
|
|
182
|
+
if (scopes.length === 0) {
|
|
183
|
+
tui.fatal('At least one scope is required');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const availableScopeNames = new Set(availableScopes.scopes.map((scope) => scope.name));
|
|
187
|
+
const invalidScopes = scopes.filter((scope) => !availableScopeNames.has(scope));
|
|
188
|
+
if (invalidScopes.length > 0) {
|
|
189
|
+
tui.fatal(`Invalid scopes: ${invalidScopes.join(', ')}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const request: OAuthClientCreateRequest = {
|
|
193
|
+
name,
|
|
194
|
+
description,
|
|
195
|
+
homepage_url: homepageUrl,
|
|
196
|
+
client_type: clientType,
|
|
197
|
+
redirect_uris: redirectUris,
|
|
198
|
+
scopes,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const result = await tui.spinner('Creating OAuth application', () => {
|
|
202
|
+
return oauthClientCreate(catalystClient, request);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (!options.json) {
|
|
206
|
+
tui.newline();
|
|
207
|
+
tui.success('OAuth application created successfully!');
|
|
208
|
+
tui.newline();
|
|
209
|
+
tui.warning('Copy the client secret now. It will only be shown once.');
|
|
210
|
+
tui.newline();
|
|
211
|
+
|
|
212
|
+
tui.table(
|
|
213
|
+
[
|
|
214
|
+
{
|
|
215
|
+
ID: result.client.id,
|
|
216
|
+
Name: result.client.name,
|
|
217
|
+
Type: result.client.client_type,
|
|
218
|
+
'Client Secret': result.client_secret,
|
|
219
|
+
'Redirect URIs': result.client.redirect_uris.join(', '),
|
|
220
|
+
Scopes: result.client.scopes.join(', '),
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
undefined,
|
|
224
|
+
{ layout: 'vertical' }
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return result;
|
|
229
|
+
},
|
|
230
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { oauthClientDelete } from '@agentuity/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getCommand } from '../../../command-prefix';
|
|
4
|
+
import { ErrorCode } from '../../../errors';
|
|
5
|
+
import * as tui from '../../../tui';
|
|
6
|
+
import { createSubcommand } from '../../../types';
|
|
7
|
+
import { createOAuthClient } from './util';
|
|
8
|
+
|
|
9
|
+
const OAuthClientDeleteResponseSchema = z.object({
|
|
10
|
+
success: z.boolean().describe('Whether the operation succeeded'),
|
|
11
|
+
id: z.string().describe('OAuth client id that was deleted'),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const deleteSubcommand = createSubcommand({
|
|
15
|
+
name: 'delete',
|
|
16
|
+
aliases: ['del', 'rm'],
|
|
17
|
+
description: 'Delete an OAuth application',
|
|
18
|
+
tags: ['destructive', 'deletes-resource', 'slow', 'requires-auth'],
|
|
19
|
+
idempotent: true,
|
|
20
|
+
examples: [
|
|
21
|
+
{ command: getCommand('cloud oidc delete <id>'), description: 'Delete OAuth application' },
|
|
22
|
+
{
|
|
23
|
+
command: getCommand('cloud oidc delete <id> --force'),
|
|
24
|
+
description: 'Delete OAuth application without confirmation',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
requires: { auth: true },
|
|
28
|
+
webUrl: '/settings/oauth-apps',
|
|
29
|
+
schema: {
|
|
30
|
+
args: z.object({
|
|
31
|
+
id: z.string().describe('the OAuth client id to delete'),
|
|
32
|
+
}),
|
|
33
|
+
options: z.object({
|
|
34
|
+
force: z.boolean().optional().default(false).describe('Skip confirmation prompt'),
|
|
35
|
+
yes: z.boolean().optional().default(false).describe('Skip confirmation prompt'),
|
|
36
|
+
}),
|
|
37
|
+
response: OAuthClientDeleteResponseSchema,
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async handler(ctx) {
|
|
41
|
+
const { args, opts, options } = ctx;
|
|
42
|
+
const catalystClient = await createOAuthClient(ctx);
|
|
43
|
+
|
|
44
|
+
const skipConfirm = opts.force || opts.yes;
|
|
45
|
+
|
|
46
|
+
if (!skipConfirm) {
|
|
47
|
+
const confirmed = await tui.confirm(`Delete OAuth application "${args.id}"?`, false);
|
|
48
|
+
if (!confirmed) {
|
|
49
|
+
tui.fatal('Operation cancelled', ErrorCode.USER_CANCELLED);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await tui.spinner('Deleting OAuth application', () => {
|
|
54
|
+
return oauthClientDelete(catalystClient, args.id);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!options.json) {
|
|
58
|
+
tui.success(`OAuth application '${args.id}' deleted successfully`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
success: true,
|
|
63
|
+
id: args.id,
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { oauthClientGet } from '@agentuity/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getCommand } from '../../../command-prefix';
|
|
4
|
+
import { ErrorCode } from '../../../errors';
|
|
5
|
+
import * as tui from '../../../tui';
|
|
6
|
+
import { createSubcommand } from '../../../types';
|
|
7
|
+
import { createOAuthClient } from './util';
|
|
8
|
+
|
|
9
|
+
export const getSubcommand = createSubcommand({
|
|
10
|
+
name: 'get',
|
|
11
|
+
description: 'Get a specific OAuth application',
|
|
12
|
+
tags: ['read-only', 'fast', 'requires-auth'],
|
|
13
|
+
examples: [
|
|
14
|
+
{ command: getCommand('cloud oidc get <id>'), description: 'Get OAuth application details' },
|
|
15
|
+
],
|
|
16
|
+
requires: { auth: true },
|
|
17
|
+
idempotent: true,
|
|
18
|
+
webUrl: (ctx) => `/settings/oauth-apps/${encodeURIComponent(ctx.args.id)}`,
|
|
19
|
+
schema: {
|
|
20
|
+
args: z.object({
|
|
21
|
+
id: z.string().describe('the OAuth client id'),
|
|
22
|
+
}),
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async handler(ctx) {
|
|
26
|
+
const { args, options } = ctx;
|
|
27
|
+
const catalystClient = await createOAuthClient(ctx);
|
|
28
|
+
|
|
29
|
+
let client: Awaited<ReturnType<typeof oauthClientGet>>;
|
|
30
|
+
try {
|
|
31
|
+
client = await tui.spinner('Fetching OAuth application', () => {
|
|
32
|
+
return oauthClientGet(catalystClient, args.id);
|
|
33
|
+
});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error instanceof Error && error.message.includes('not found')) {
|
|
36
|
+
tui.fatal(`OAuth application '${args.id}' not found`, ErrorCode.RESOURCE_NOT_FOUND);
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!options.json) {
|
|
42
|
+
if (process.stdout.isTTY) {
|
|
43
|
+
tui.newline();
|
|
44
|
+
tui.success('OAuth Application Details:');
|
|
45
|
+
tui.newline();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const rows = [
|
|
49
|
+
{
|
|
50
|
+
ID: client.id,
|
|
51
|
+
Name: client.name,
|
|
52
|
+
Description: client.description || '-',
|
|
53
|
+
Type: client.client_type,
|
|
54
|
+
'Homepage URL': client.homepage_url || '-',
|
|
55
|
+
'Redirect URIs':
|
|
56
|
+
client.redirect_uris.length > 0 ? client.redirect_uris.join('\n') : '-',
|
|
57
|
+
Scopes: client.scopes.length > 0 ? client.scopes.join(', ') : '-',
|
|
58
|
+
Created: new Date(client.created_at).toLocaleString(),
|
|
59
|
+
Updated: new Date(client.updated_at).toLocaleString(),
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
tui.table(rows, undefined, { layout: 'vertical' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return client;
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createCommand } from '../../../types';
|
|
2
|
+
import { getCommand } from '../../../command-prefix';
|
|
3
|
+
import { listSubcommand } from './list';
|
|
4
|
+
import { getSubcommand } from './get';
|
|
5
|
+
import { createSubcommand } from './create';
|
|
6
|
+
import { deleteSubcommand } from './delete';
|
|
7
|
+
import { rotateSecretSubcommand } from './rotate-secret';
|
|
8
|
+
import { activitySubcommand } from './activity';
|
|
9
|
+
import { usersSubcommand } from './users';
|
|
10
|
+
|
|
11
|
+
export const command = createCommand({
|
|
12
|
+
name: 'oidc',
|
|
13
|
+
description: 'Manage OAuth applications',
|
|
14
|
+
tags: ['fast', 'requires-auth'],
|
|
15
|
+
examples: [
|
|
16
|
+
{ command: getCommand('cloud oidc list'), description: 'List all OAuth applications' },
|
|
17
|
+
{
|
|
18
|
+
command: getCommand(
|
|
19
|
+
'cloud oidc create --name "My App" --type confidential --redirect-uris "https://example.com/callback"'
|
|
20
|
+
),
|
|
21
|
+
description: 'Create a new OAuth application',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
subcommands: [
|
|
25
|
+
createSubcommand,
|
|
26
|
+
listSubcommand,
|
|
27
|
+
getSubcommand,
|
|
28
|
+
deleteSubcommand,
|
|
29
|
+
rotateSecretSubcommand,
|
|
30
|
+
activitySubcommand,
|
|
31
|
+
usersSubcommand,
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export default command;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { oauthClientList } from '@agentuity/core';
|
|
2
|
+
import { getCommand } from '../../../command-prefix';
|
|
3
|
+
import * as tui from '../../../tui';
|
|
4
|
+
import { createSubcommand } from '../../../types';
|
|
5
|
+
import { createOAuthClient } from './util';
|
|
6
|
+
|
|
7
|
+
export const listSubcommand = createSubcommand({
|
|
8
|
+
name: 'list',
|
|
9
|
+
aliases: ['ls'],
|
|
10
|
+
description: 'List all OAuth applications',
|
|
11
|
+
tags: ['read-only', 'fast', 'requires-auth'],
|
|
12
|
+
examples: [
|
|
13
|
+
{ command: getCommand('cloud oidc list'), description: 'List OAuth applications' },
|
|
14
|
+
{ command: getCommand('cloud oidc ls'), description: 'List OAuth applications' },
|
|
15
|
+
],
|
|
16
|
+
requires: { auth: true },
|
|
17
|
+
idempotent: true,
|
|
18
|
+
webUrl: '/settings/oauth-apps',
|
|
19
|
+
|
|
20
|
+
async handler(ctx) {
|
|
21
|
+
const { options } = ctx;
|
|
22
|
+
const catalystClient = await createOAuthClient(ctx);
|
|
23
|
+
|
|
24
|
+
const clients = await tui.spinner('Fetching OAuth applications', () => {
|
|
25
|
+
return oauthClientList(catalystClient);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!options.json) {
|
|
29
|
+
if (clients.length === 0) {
|
|
30
|
+
tui.info('No OAuth applications found');
|
|
31
|
+
} else {
|
|
32
|
+
if (process.stdout.isTTY) {
|
|
33
|
+
tui.newline();
|
|
34
|
+
tui.success(`OAuth Applications (${clients.length}):`);
|
|
35
|
+
tui.newline();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const rows = clients.map((client) => ({
|
|
39
|
+
ID: client.id,
|
|
40
|
+
Name: client.name,
|
|
41
|
+
Type: client.client_type,
|
|
42
|
+
Scopes: client.scopes.length,
|
|
43
|
+
Users: client.user_count,
|
|
44
|
+
Created: new Date(client.created_at).toLocaleString(),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
tui.table(rows);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return clients;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { oauthClientRotateSecret } from '@agentuity/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getCommand } from '../../../command-prefix';
|
|
4
|
+
import { ErrorCode } from '../../../errors';
|
|
5
|
+
import * as tui from '../../../tui';
|
|
6
|
+
import { createSubcommand } from '../../../types';
|
|
7
|
+
import { createOAuthClient } from './util';
|
|
8
|
+
|
|
9
|
+
const OAuthClientRotateSecretResponseSchema = z.object({
|
|
10
|
+
client_id: z.string(),
|
|
11
|
+
client_secret: z.string(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const rotateSecretSubcommand = createSubcommand({
|
|
15
|
+
name: 'rotate-secret',
|
|
16
|
+
description: 'Rotate the client secret for an OAuth application',
|
|
17
|
+
tags: ['destructive', 'requires-auth'],
|
|
18
|
+
examples: [
|
|
19
|
+
{
|
|
20
|
+
command: getCommand('cloud oidc rotate-secret <id>'),
|
|
21
|
+
description: 'Rotate OAuth client secret',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
command: getCommand('cloud oidc rotate-secret <id> --force'),
|
|
25
|
+
description: 'Rotate OAuth client secret without confirmation',
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
requires: { auth: true },
|
|
29
|
+
idempotent: false,
|
|
30
|
+
webUrl: (ctx) => `/settings/oauth-apps/${encodeURIComponent(ctx.args.id)}`,
|
|
31
|
+
schema: {
|
|
32
|
+
args: z.object({
|
|
33
|
+
id: z.string().describe('the OAuth client id'),
|
|
34
|
+
}),
|
|
35
|
+
options: z.object({
|
|
36
|
+
force: z.boolean().optional().default(false).describe('Skip confirmation prompt'),
|
|
37
|
+
}),
|
|
38
|
+
response: OAuthClientRotateSecretResponseSchema,
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async handler(ctx) {
|
|
42
|
+
const { args, opts, options } = ctx;
|
|
43
|
+
const catalystClient = await createOAuthClient(ctx);
|
|
44
|
+
|
|
45
|
+
if (!opts.force) {
|
|
46
|
+
const confirmed = await tui.confirm(
|
|
47
|
+
`Rotate secret for OAuth application "${args.id}"?`,
|
|
48
|
+
false
|
|
49
|
+
);
|
|
50
|
+
if (!confirmed) {
|
|
51
|
+
tui.fatal('Operation cancelled', ErrorCode.USER_CANCELLED);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = await tui.spinner('Rotating OAuth client secret', () => {
|
|
56
|
+
return oauthClientRotateSecret(catalystClient, args.id);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!options.json) {
|
|
60
|
+
tui.newline();
|
|
61
|
+
tui.success('OAuth client secret rotated successfully!');
|
|
62
|
+
tui.newline();
|
|
63
|
+
tui.warning('Copy the new client secret now. It will only be shown once.');
|
|
64
|
+
tui.newline();
|
|
65
|
+
|
|
66
|
+
tui.table(
|
|
67
|
+
[
|
|
68
|
+
{
|
|
69
|
+
'Client ID': result.client_id,
|
|
70
|
+
'Client Secret': result.client_secret,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
undefined,
|
|
74
|
+
{ layout: 'vertical' }
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
},
|
|
80
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { oauthClientUsers } from '@agentuity/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getCommand } from '../../../command-prefix';
|
|
4
|
+
import * as tui from '../../../tui';
|
|
5
|
+
import { createSubcommand } from '../../../types';
|
|
6
|
+
import { createOAuthClient } from './util';
|
|
7
|
+
|
|
8
|
+
const OAuthClientUsersResponseSchema = z.array(
|
|
9
|
+
z.object({
|
|
10
|
+
user_id: z.string(),
|
|
11
|
+
scopes: z.array(z.string()),
|
|
12
|
+
created_at: z.string(),
|
|
13
|
+
})
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
export const usersSubcommand = createSubcommand({
|
|
17
|
+
name: 'users',
|
|
18
|
+
description: 'List connected users for an OAuth application',
|
|
19
|
+
tags: ['read-only', 'requires-auth'],
|
|
20
|
+
examples: [
|
|
21
|
+
{
|
|
22
|
+
command: getCommand('cloud oidc users <id>'),
|
|
23
|
+
description: 'List connected users for OAuth application',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
requires: { auth: true },
|
|
27
|
+
idempotent: true,
|
|
28
|
+
webUrl: (ctx) => `/settings/oauth-apps/${encodeURIComponent(ctx.args.id)}`,
|
|
29
|
+
schema: {
|
|
30
|
+
args: z.object({
|
|
31
|
+
id: z.string().describe('the OAuth client id'),
|
|
32
|
+
}),
|
|
33
|
+
response: OAuthClientUsersResponseSchema,
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
async handler(ctx) {
|
|
37
|
+
const { args, options } = ctx;
|
|
38
|
+
const catalystClient = await createOAuthClient(ctx);
|
|
39
|
+
|
|
40
|
+
const users = await tui.spinner('Fetching connected OAuth users', () => {
|
|
41
|
+
return oauthClientUsers(catalystClient, args.id);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!options.json) {
|
|
45
|
+
if (users.length === 0) {
|
|
46
|
+
tui.info('No connected users found');
|
|
47
|
+
} else {
|
|
48
|
+
const rows = users.map((user) => ({
|
|
49
|
+
user_id: user.user_id,
|
|
50
|
+
scopes: user.scopes.join(', '),
|
|
51
|
+
connected_at: new Date(user.created_at).toLocaleString(),
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
tui.table(rows);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return users;
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Logger } from '@agentuity/core';
|
|
2
|
+
import { getGlobalCatalystAPIClient } from '../../../config';
|
|
3
|
+
import * as tui from '../../../tui';
|
|
4
|
+
import type { AuthData, Config, GlobalOptions, ProjectConfig } from '../../../types';
|
|
5
|
+
|
|
6
|
+
export async function createOAuthClient(
|
|
7
|
+
ctx: {
|
|
8
|
+
logger: Logger;
|
|
9
|
+
auth: AuthData;
|
|
10
|
+
project?: ProjectConfig;
|
|
11
|
+
config: Config | null;
|
|
12
|
+
options: GlobalOptions;
|
|
13
|
+
},
|
|
14
|
+
explicitOrgId?: string
|
|
15
|
+
) {
|
|
16
|
+
const orgId =
|
|
17
|
+
explicitOrgId ??
|
|
18
|
+
ctx.project?.orgId ??
|
|
19
|
+
ctx.options.orgId ??
|
|
20
|
+
(process.env.AGENTUITY_CLOUD_ORG_ID || ctx.config?.preferences?.orgId);
|
|
21
|
+
if (!orgId) {
|
|
22
|
+
tui.fatal(
|
|
23
|
+
'Organization ID is required. Either run from a project directory or use --org-id flag.'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return getGlobalCatalystAPIClient(ctx.logger, ctx.auth, ctx.config?.name, orgId, ctx.config);
|
|
28
|
+
}
|
package/src/cmd/coder/hub-url.ts
CHANGED
|
@@ -41,7 +41,11 @@ export async function resolveHubUrl(flagUrl?: string): Promise<string | null> {
|
|
|
41
41
|
export async function resolveHubWsUrl(flagUrl?: string): Promise<string | null> {
|
|
42
42
|
const httpUrl = await resolveHubUrl(flagUrl);
|
|
43
43
|
if (!httpUrl) return null;
|
|
44
|
-
return
|
|
44
|
+
return toHubWsUrl(httpUrl);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function toHubWsUrl(hubHttpUrl: string): string {
|
|
48
|
+
return normalizeToWs(hubHttpUrl);
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
/**
|
package/src/cmd/coder/start.ts
CHANGED
|
@@ -5,7 +5,8 @@ import { createSubcommand } from '../../types';
|
|
|
5
5
|
import * as tui from '../../tui';
|
|
6
6
|
import { getCommand } from '../../command-prefix';
|
|
7
7
|
import { ErrorCode } from '../../errors';
|
|
8
|
-
import {
|
|
8
|
+
import { toHubWsUrl, resolveHubUrl, hubFetchHeaders } from './hub-url';
|
|
9
|
+
import { probeTuiInitAccess } from './tui-init';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Resolve the Coder extension path.
|
|
@@ -129,14 +130,32 @@ export const startSubcommand = createSubcommand({
|
|
|
129
130
|
const { opts, options } = ctx;
|
|
130
131
|
|
|
131
132
|
// Resolve Hub URL
|
|
132
|
-
const
|
|
133
|
-
if (!
|
|
133
|
+
const hubHttpUrl = await resolveHubUrl(opts?.hubUrl);
|
|
134
|
+
if (!hubHttpUrl) {
|
|
134
135
|
tui.fatal(
|
|
135
136
|
'Could not find a running Coder Hub.\n\nEither:\n - Start the Hub with: bun run dev\n - Set AGENTUITY_CODER_HUB_URL environment variable\n - Pass --hub-url flag',
|
|
136
137
|
ErrorCode.NETWORK_ERROR
|
|
137
138
|
);
|
|
138
139
|
return;
|
|
139
140
|
}
|
|
141
|
+
const hubWsUrl = toHubWsUrl(hubHttpUrl);
|
|
142
|
+
|
|
143
|
+
const tuiInitProbe = await probeTuiInitAccess(hubHttpUrl);
|
|
144
|
+
if (!tuiInitProbe.ok) {
|
|
145
|
+
if (tuiInitProbe.code === 'unauthorized') {
|
|
146
|
+
tui.fatal(
|
|
147
|
+
`Coder Hub at ${hubHttpUrl} requires authentication.\n\nSet AGENTUITY_CODER_API_KEY in your shell and retry.\n\nServer said: ${tuiInitProbe.message}`,
|
|
148
|
+
ErrorCode.NETWORK_ERROR
|
|
149
|
+
);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
tui.fatal(
|
|
154
|
+
`Could not bootstrap the Coder Hub at ${hubHttpUrl}: ${tuiInitProbe.message}`,
|
|
155
|
+
ErrorCode.NETWORK_ERROR
|
|
156
|
+
);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
140
159
|
|
|
141
160
|
// Resolve extension path
|
|
142
161
|
const extensionPath = resolveExtensionPath(opts?.extension);
|
|
@@ -160,11 +179,6 @@ export const startSubcommand = createSubcommand({
|
|
|
160
179
|
remoteSessionId = remoteValue;
|
|
161
180
|
} else {
|
|
162
181
|
// No session ID — fetch connectable sessions and show picker
|
|
163
|
-
const hubHttpUrl = await resolveHubUrl(opts?.hubUrl);
|
|
164
|
-
if (!hubHttpUrl) {
|
|
165
|
-
tui.fatal('Could not find Hub URL for session picker.', ErrorCode.NETWORK_ERROR);
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
182
|
try {
|
|
169
183
|
type SessionInfo = {
|
|
170
184
|
id: string;
|