@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.
Files changed (99) hide show
  1. package/dist/cmd/build/ast.d.ts.map +1 -1
  2. package/dist/cmd/build/ast.js +3 -3
  3. package/dist/cmd/build/ast.js.map +1 -1
  4. package/dist/cmd/build/typecheck.d.ts.map +1 -1
  5. package/dist/cmd/build/typecheck.js +52 -1
  6. package/dist/cmd/build/typecheck.js.map +1 -1
  7. package/dist/cmd/build/vite/static-renderer.d.ts.map +1 -1
  8. package/dist/cmd/build/vite/static-renderer.js +22 -8
  9. package/dist/cmd/build/vite/static-renderer.js.map +1 -1
  10. package/dist/cmd/cloud/index.d.ts.map +1 -1
  11. package/dist/cmd/cloud/index.js +4 -0
  12. package/dist/cmd/cloud/index.js.map +1 -1
  13. package/dist/cmd/cloud/monitor.d.ts +3 -0
  14. package/dist/cmd/cloud/monitor.d.ts.map +1 -0
  15. package/dist/cmd/cloud/monitor.js +300 -0
  16. package/dist/cmd/cloud/monitor.js.map +1 -0
  17. package/dist/cmd/cloud/oidc/activity.d.ts +2 -0
  18. package/dist/cmd/cloud/oidc/activity.d.ts.map +1 -0
  19. package/dist/cmd/cloud/oidc/activity.js +57 -0
  20. package/dist/cmd/cloud/oidc/activity.js.map +1 -0
  21. package/dist/cmd/cloud/oidc/create.d.ts +2 -0
  22. package/dist/cmd/cloud/oidc/create.d.ts.map +1 -0
  23. package/dist/cmd/cloud/oidc/create.js +204 -0
  24. package/dist/cmd/cloud/oidc/create.js.map +1 -0
  25. package/dist/cmd/cloud/oidc/delete.d.ts +2 -0
  26. package/dist/cmd/cloud/oidc/delete.d.ts.map +1 -0
  27. package/dist/cmd/cloud/oidc/delete.js +59 -0
  28. package/dist/cmd/cloud/oidc/delete.js.map +1 -0
  29. package/dist/cmd/cloud/oidc/get.d.ts +2 -0
  30. package/dist/cmd/cloud/oidc/get.d.ts.map +1 -0
  31. package/dist/cmd/cloud/oidc/get.js +62 -0
  32. package/dist/cmd/cloud/oidc/get.js.map +1 -0
  33. package/dist/cmd/cloud/oidc/index.d.ts +3 -0
  34. package/dist/cmd/cloud/oidc/index.d.ts.map +1 -0
  35. package/dist/cmd/cloud/oidc/index.js +32 -0
  36. package/dist/cmd/cloud/oidc/index.js.map +1 -0
  37. package/dist/cmd/cloud/oidc/list.d.ts +2 -0
  38. package/dist/cmd/cloud/oidc/list.d.ts.map +1 -0
  39. package/dist/cmd/cloud/oidc/list.js +48 -0
  40. package/dist/cmd/cloud/oidc/list.js.map +1 -0
  41. package/dist/cmd/cloud/oidc/rotate-secret.d.ts +2 -0
  42. package/dist/cmd/cloud/oidc/rotate-secret.d.ts.map +1 -0
  43. package/dist/cmd/cloud/oidc/rotate-secret.js +66 -0
  44. package/dist/cmd/cloud/oidc/rotate-secret.js.map +1 -0
  45. package/dist/cmd/cloud/oidc/users.d.ts +2 -0
  46. package/dist/cmd/cloud/oidc/users.d.ts.map +1 -0
  47. package/dist/cmd/cloud/oidc/users.js +53 -0
  48. package/dist/cmd/cloud/oidc/users.js.map +1 -0
  49. package/dist/cmd/cloud/oidc/util.d.ts +10 -0
  50. package/dist/cmd/cloud/oidc/util.d.ts.map +1 -0
  51. package/dist/cmd/cloud/oidc/util.js +13 -0
  52. package/dist/cmd/cloud/oidc/util.js.map +1 -0
  53. package/dist/cmd/coder/hub-url.d.ts +1 -0
  54. package/dist/cmd/coder/hub-url.d.ts.map +1 -1
  55. package/dist/cmd/coder/hub-url.js +4 -1
  56. package/dist/cmd/coder/hub-url.js.map +1 -1
  57. package/dist/cmd/coder/start.d.ts.map +1 -1
  58. package/dist/cmd/coder/start.js +14 -8
  59. package/dist/cmd/coder/start.js.map +1 -1
  60. package/dist/cmd/coder/tui-init.d.ts +9 -0
  61. package/dist/cmd/coder/tui-init.d.ts.map +1 -0
  62. package/dist/cmd/coder/tui-init.js +56 -0
  63. package/dist/cmd/coder/tui-init.js.map +1 -0
  64. package/dist/config.d.ts.map +1 -1
  65. package/dist/config.js +14 -5
  66. package/dist/config.js.map +1 -1
  67. package/dist/utils/jsonc.d.ts +13 -0
  68. package/dist/utils/jsonc.d.ts.map +1 -0
  69. package/dist/utils/jsonc.js +63 -0
  70. package/dist/utils/jsonc.js.map +1 -0
  71. package/dist/utils/route-migration.d.ts +2 -1
  72. package/dist/utils/route-migration.d.ts.map +1 -1
  73. package/dist/utils/route-migration.js +23 -32
  74. package/dist/utils/route-migration.js.map +1 -1
  75. package/dist/utils/zip.d.ts.map +1 -1
  76. package/dist/utils/zip.js +18 -2
  77. package/dist/utils/zip.js.map +1 -1
  78. package/package.json +6 -7
  79. package/src/cmd/build/ast.ts +6 -3
  80. package/src/cmd/build/typecheck.ts +60 -1
  81. package/src/cmd/build/vite/static-renderer.ts +24 -8
  82. package/src/cmd/cloud/index.ts +4 -0
  83. package/src/cmd/cloud/monitor.ts +375 -0
  84. package/src/cmd/cloud/oidc/activity.ts +64 -0
  85. package/src/cmd/cloud/oidc/create.ts +230 -0
  86. package/src/cmd/cloud/oidc/delete.ts +66 -0
  87. package/src/cmd/cloud/oidc/get.ts +68 -0
  88. package/src/cmd/cloud/oidc/index.ts +35 -0
  89. package/src/cmd/cloud/oidc/list.ts +53 -0
  90. package/src/cmd/cloud/oidc/rotate-secret.ts +80 -0
  91. package/src/cmd/cloud/oidc/users.ts +60 -0
  92. package/src/cmd/cloud/oidc/util.ts +28 -0
  93. package/src/cmd/coder/hub-url.ts +5 -1
  94. package/src/cmd/coder/start.ts +22 -8
  95. package/src/cmd/coder/tui-init.ts +75 -0
  96. package/src/config.ts +16 -5
  97. package/src/utils/jsonc.ts +67 -0
  98. package/src/utils/route-migration.ts +29 -40
  99. 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
+ }
@@ -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 normalizeToWs(httpUrl);
44
+ return toHubWsUrl(httpUrl);
45
+ }
46
+
47
+ export function toHubWsUrl(hubHttpUrl: string): string {
48
+ return normalizeToWs(hubHttpUrl);
45
49
  }
46
50
 
47
51
  /**
@@ -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 { resolveHubWsUrl, resolveHubUrl, hubFetchHeaders } from './hub-url';
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 hubWsUrl = await resolveHubWsUrl(opts?.hubUrl);
133
- if (!hubWsUrl) {
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;