@agentuity/cli 1.0.41 → 1.0.42
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 +54 -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 +201 -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 +56 -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 +59 -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 +45 -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 +63 -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 +50 -0
- package/dist/cmd/cloud/oidc/users.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 +61 -0
- package/src/cmd/cloud/oidc/create.ts +232 -0
- package/src/cmd/cloud/oidc/delete.ts +63 -0
- package/src/cmd/cloud/oidc/get.ts +65 -0
- package/src/cmd/cloud/oidc/index.ts +35 -0
- package/src/cmd/cloud/oidc/list.ts +50 -0
- package/src/cmd/cloud/oidc/rotate-secret.ts +77 -0
- package/src/cmd/cloud/oidc/users.ts +57 -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,375 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
getMonitorNode,
|
|
4
|
+
listDistressedNodes,
|
|
5
|
+
listMonitorNodeContainers,
|
|
6
|
+
listMonitorNodes,
|
|
7
|
+
MonitorWebSocketClient,
|
|
8
|
+
type MachineMonitorState,
|
|
9
|
+
type MonitorMessage,
|
|
10
|
+
type MonitorScope,
|
|
11
|
+
} from '@agentuity/core';
|
|
12
|
+
import { getAPIBaseURL } from '../../api';
|
|
13
|
+
import { getCommand } from '../../command-prefix';
|
|
14
|
+
import { createSubcommand } from '../../types';
|
|
15
|
+
import * as tui from '../../tui';
|
|
16
|
+
|
|
17
|
+
const monitorOptionsSchema = z.object({
|
|
18
|
+
machine: z.string().optional().describe('Monitor a specific machine id'),
|
|
19
|
+
deployment: z.string().optional().describe('Monitor machines for a deployment id'),
|
|
20
|
+
distressed: z.boolean().optional().describe('Only include distressed machines'),
|
|
21
|
+
snapshot: z.boolean().optional().describe('One-shot snapshot (no stream watch)'),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const monitorSubcommand = createSubcommand({
|
|
25
|
+
name: 'monitor',
|
|
26
|
+
description: 'Monitor infrastructure machines in real time',
|
|
27
|
+
tags: ['read-only', 'slow', 'requires-auth'],
|
|
28
|
+
requires: { auth: true, apiClient: true },
|
|
29
|
+
optional: { org: true },
|
|
30
|
+
idempotent: true,
|
|
31
|
+
examples: [
|
|
32
|
+
{ command: getCommand('cloud monitor --snapshot'), description: 'Show a monitor snapshot' },
|
|
33
|
+
{
|
|
34
|
+
command: getCommand('cloud monitor --distressed'),
|
|
35
|
+
description: 'Watch distressed machines',
|
|
36
|
+
},
|
|
37
|
+
{ command: getCommand('cloud monitor --machine mach_123'), description: 'Watch one machine' },
|
|
38
|
+
],
|
|
39
|
+
schema: {
|
|
40
|
+
options: monitorOptionsSchema,
|
|
41
|
+
},
|
|
42
|
+
webUrl: '/infrastructure/monitoring',
|
|
43
|
+
|
|
44
|
+
async handler(ctx) {
|
|
45
|
+
const { apiClient, options, opts, auth, config, orgId } = ctx;
|
|
46
|
+
|
|
47
|
+
if (opts.machine && opts.distressed) {
|
|
48
|
+
ctx.logger.fatal('--machine and --distressed are mutually exclusive.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (opts.deployment && opts.distressed) {
|
|
52
|
+
ctx.logger.fatal('--deployment and --distressed are mutually exclusive.');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (opts.snapshot || options.json) {
|
|
56
|
+
const machines = await getSnapshotMachines({
|
|
57
|
+
apiClient,
|
|
58
|
+
machineId: opts.machine,
|
|
59
|
+
deploymentId: opts.deployment,
|
|
60
|
+
distressed: opts.distressed,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (options.json) {
|
|
64
|
+
console.log(JSON.stringify(machines, null, 2));
|
|
65
|
+
return machines;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
renderMachineTable(machines);
|
|
69
|
+
return machines;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const machineMap = new Map<string, MachineMonitorState>();
|
|
73
|
+
|
|
74
|
+
const initialMachines = await getSnapshotMachines({
|
|
75
|
+
apiClient,
|
|
76
|
+
machineId: opts.machine,
|
|
77
|
+
deploymentId: opts.deployment,
|
|
78
|
+
distressed: opts.distressed,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
for (const machine of initialMachines) {
|
|
82
|
+
machineMap.set(machine.machineId, machine);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
renderWatchTable(machineMap, {
|
|
86
|
+
mode: 'snapshot',
|
|
87
|
+
machineId: opts.machine,
|
|
88
|
+
deploymentId: opts.deployment,
|
|
89
|
+
distressed: opts.distressed,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
tui.info('Connecting to monitoring stream...');
|
|
93
|
+
|
|
94
|
+
let processingChain = Promise.resolve();
|
|
95
|
+
let resolveWait: (() => void) | null = null;
|
|
96
|
+
|
|
97
|
+
const monitorClient = new MonitorWebSocketClient({
|
|
98
|
+
baseUrl: getAPIBaseURL(config),
|
|
99
|
+
token: auth.apiKey,
|
|
100
|
+
orgId,
|
|
101
|
+
scope: toMonitorScope(opts.machine, opts.deployment),
|
|
102
|
+
onOpen: () => {
|
|
103
|
+
tui.success('Connected to monitoring stream');
|
|
104
|
+
},
|
|
105
|
+
onError: (error) => {
|
|
106
|
+
tui.error(`Monitoring stream error: ${error.message}`);
|
|
107
|
+
},
|
|
108
|
+
onClose: () => {
|
|
109
|
+
tui.info('Monitoring stream disconnected');
|
|
110
|
+
resolveWait?.();
|
|
111
|
+
},
|
|
112
|
+
onMessage: (message) => {
|
|
113
|
+
processingChain = processingChain.then(async () => {
|
|
114
|
+
await applyMonitorMessage(machineMap, message, apiClient, opts.deployment);
|
|
115
|
+
renderWatchTable(machineMap, {
|
|
116
|
+
mode: 'watch',
|
|
117
|
+
machineId: opts.machine,
|
|
118
|
+
deploymentId: opts.deployment,
|
|
119
|
+
distressed: opts.distressed,
|
|
120
|
+
lastMessage: message,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
monitorClient.connect();
|
|
127
|
+
|
|
128
|
+
await new Promise<void>((resolve) => {
|
|
129
|
+
resolveWait = resolve;
|
|
130
|
+
const onSigInt = () => {
|
|
131
|
+
monitorClient.close();
|
|
132
|
+
process.off('SIGINT', onSigInt);
|
|
133
|
+
resolve();
|
|
134
|
+
};
|
|
135
|
+
process.on('SIGINT', onSigInt);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return Array.from(machineMap.values());
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
async function getSnapshotMachines(params: {
|
|
143
|
+
apiClient: Parameters<typeof listMonitorNodes>[0];
|
|
144
|
+
machineId?: string;
|
|
145
|
+
deploymentId?: string;
|
|
146
|
+
distressed?: boolean;
|
|
147
|
+
}): Promise<MachineMonitorState[]> {
|
|
148
|
+
const { apiClient, machineId, deploymentId, distressed } = params;
|
|
149
|
+
|
|
150
|
+
let machines: MachineMonitorState[];
|
|
151
|
+
if (distressed) {
|
|
152
|
+
machines = await listDistressedNodes(apiClient);
|
|
153
|
+
} else if (machineId) {
|
|
154
|
+
machines = [await getMonitorNode(apiClient, machineId)];
|
|
155
|
+
} else {
|
|
156
|
+
machines = await listMonitorNodes(apiClient);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!deploymentId) {
|
|
160
|
+
return machines;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const results = await Promise.all(
|
|
164
|
+
machines.map(async (machine) => {
|
|
165
|
+
const containers = await listMonitorNodeContainers(apiClient, machine.machineId);
|
|
166
|
+
const hasDeployment = containers.some(
|
|
167
|
+
(container) => container.deploymentId === deploymentId
|
|
168
|
+
);
|
|
169
|
+
return hasDeployment ? machine : null;
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
return results.filter((m): m is MachineMonitorState => m !== null);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function toMonitorScope(machineId?: string, deploymentId?: string): MonitorScope {
|
|
176
|
+
if (machineId) {
|
|
177
|
+
return { scope: 'machine', machineId };
|
|
178
|
+
}
|
|
179
|
+
if (deploymentId) {
|
|
180
|
+
return { scope: 'deployment', deploymentId };
|
|
181
|
+
}
|
|
182
|
+
return { scope: 'org' };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function applyMonitorMessage(
|
|
186
|
+
machineMap: Map<string, MachineMonitorState>,
|
|
187
|
+
message: MonitorMessage,
|
|
188
|
+
apiClient: Parameters<typeof listMonitorNodes>[0],
|
|
189
|
+
deploymentId?: string
|
|
190
|
+
) {
|
|
191
|
+
if (message.type === 'snapshot') {
|
|
192
|
+
machineMap.clear();
|
|
193
|
+
for (const machine of message.machines) {
|
|
194
|
+
machineMap.set(machine.machineId, machine);
|
|
195
|
+
}
|
|
196
|
+
if (deploymentId) {
|
|
197
|
+
await filterMapByDeployment(machineMap, apiClient, deploymentId);
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (message.type === 'update') {
|
|
203
|
+
const existing = machineMap.get(message.machineId);
|
|
204
|
+
const next: MachineMonitorState = {
|
|
205
|
+
machineId: message.machineId,
|
|
206
|
+
orgId: existing?.orgId ?? '',
|
|
207
|
+
report: message.report,
|
|
208
|
+
compositeScore: message.report.capacity?.compositeScore ?? existing?.compositeScore ?? 0,
|
|
209
|
+
health: message.health,
|
|
210
|
+
reportedAt: usecToISO(message.report.reportedAtUs) ?? existing?.reportedAt ?? '',
|
|
211
|
+
updatedAt: new Date().toISOString(),
|
|
212
|
+
gravity: existing?.gravity ?? '',
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if (deploymentId) {
|
|
216
|
+
const containers = await listMonitorNodeContainers(apiClient, message.machineId);
|
|
217
|
+
const include = containers.some((container) => container.deploymentId === deploymentId);
|
|
218
|
+
if (!include) {
|
|
219
|
+
machineMap.delete(message.machineId);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
machineMap.set(message.machineId, next);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const existing = machineMap.get(message.machineId);
|
|
229
|
+
if (existing) {
|
|
230
|
+
existing.health = message.health;
|
|
231
|
+
existing.updatedAt = new Date().toISOString();
|
|
232
|
+
machineMap.set(message.machineId, existing);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function filterMapByDeployment(
|
|
237
|
+
machineMap: Map<string, MachineMonitorState>,
|
|
238
|
+
apiClient: Parameters<typeof listMonitorNodes>[0],
|
|
239
|
+
deploymentId: string
|
|
240
|
+
) {
|
|
241
|
+
const entries = Array.from(machineMap.keys());
|
|
242
|
+
const results = await Promise.all(
|
|
243
|
+
entries.map(async (machineId) => {
|
|
244
|
+
const containers = await listMonitorNodeContainers(apiClient, machineId);
|
|
245
|
+
const include = containers.some((container) => container.deploymentId === deploymentId);
|
|
246
|
+
return { machineId, include };
|
|
247
|
+
})
|
|
248
|
+
);
|
|
249
|
+
for (const { machineId, include } of results) {
|
|
250
|
+
if (!include) {
|
|
251
|
+
machineMap.delete(machineId);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function renderWatchTable(
|
|
257
|
+
machineMap: Map<string, MachineMonitorState>,
|
|
258
|
+
params: {
|
|
259
|
+
mode: 'snapshot' | 'watch';
|
|
260
|
+
machineId?: string;
|
|
261
|
+
deploymentId?: string;
|
|
262
|
+
distressed?: boolean;
|
|
263
|
+
lastMessage?: MonitorMessage;
|
|
264
|
+
}
|
|
265
|
+
) {
|
|
266
|
+
console.clear();
|
|
267
|
+
|
|
268
|
+
const subtitle = [
|
|
269
|
+
params.mode === 'watch' ? 'Live mode' : 'Snapshot mode',
|
|
270
|
+
params.machineId ? `machine=${params.machineId}` : undefined,
|
|
271
|
+
params.deploymentId ? `deployment=${params.deploymentId}` : undefined,
|
|
272
|
+
params.distressed ? 'distressed=true' : undefined,
|
|
273
|
+
]
|
|
274
|
+
.filter(Boolean)
|
|
275
|
+
.join(' • ');
|
|
276
|
+
|
|
277
|
+
tui.header('Cloud Monitor');
|
|
278
|
+
if (subtitle) {
|
|
279
|
+
tui.info(subtitle);
|
|
280
|
+
}
|
|
281
|
+
if (params.lastMessage && params.lastMessage.type === 'state_change') {
|
|
282
|
+
tui.warning(
|
|
283
|
+
`${params.lastMessage.machineId}: ${params.lastMessage.previousHealth} -> ${params.lastMessage.health}`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
renderMachineTable(Array.from(machineMap.values()));
|
|
288
|
+
tui.info('Press Ctrl+C to stop watching.');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function renderMachineTable(machines: MachineMonitorState[]) {
|
|
292
|
+
if (machines.length === 0) {
|
|
293
|
+
tui.info('No machines found');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const rows = machines
|
|
298
|
+
.slice()
|
|
299
|
+
.sort((a, b) => a.machineId.localeCompare(b.machineId))
|
|
300
|
+
.map((machine) => ({
|
|
301
|
+
Machine: machine.machineId,
|
|
302
|
+
Health: formatHealth(machine.health),
|
|
303
|
+
CPU: formatPercent(machine.report?.host?.cpu?.usagePercent),
|
|
304
|
+
Memory: formatPercent(machine.report?.host?.memory?.usagePercent),
|
|
305
|
+
Disk: formatPercent(maxDiskUsage(machine.report?.host?.disks)),
|
|
306
|
+
Pressure: formatScore(machine.compositeScore),
|
|
307
|
+
Containers: `${machine.report?.capacity?.runningContainers ?? 0}/${machine.report?.capacity?.totalContainers ?? 0}`,
|
|
308
|
+
'Last Report': formatAge(machine.reportedAt),
|
|
309
|
+
}));
|
|
310
|
+
|
|
311
|
+
tui.table(rows, [
|
|
312
|
+
{ name: 'Machine' },
|
|
313
|
+
{ name: 'Health' },
|
|
314
|
+
{ name: 'CPU', alignment: 'right' },
|
|
315
|
+
{ name: 'Memory', alignment: 'right' },
|
|
316
|
+
{ name: 'Disk', alignment: 'right' },
|
|
317
|
+
{ name: 'Pressure', alignment: 'right' },
|
|
318
|
+
{ name: 'Containers', alignment: 'right' },
|
|
319
|
+
{ name: 'Last Report' },
|
|
320
|
+
]);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function formatHealth(health: string): string {
|
|
324
|
+
if (health === 'CONNECTED') return '● CONNECTED';
|
|
325
|
+
if (health === 'STALE') return '◌ STALE';
|
|
326
|
+
if (health === 'DISCONNECTED') return '○ DISCONNECTED';
|
|
327
|
+
return health;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function formatPercent(value?: number): string {
|
|
331
|
+
if (value === undefined || value === null || Number.isNaN(value)) {
|
|
332
|
+
return '-';
|
|
333
|
+
}
|
|
334
|
+
return `${value.toFixed(1)}%`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function formatScore(score?: number): string {
|
|
338
|
+
if (score === undefined || score === null || Number.isNaN(score)) {
|
|
339
|
+
return '-';
|
|
340
|
+
}
|
|
341
|
+
if (score >= 0.85) {
|
|
342
|
+
return `${score.toFixed(2)} ⚠`;
|
|
343
|
+
}
|
|
344
|
+
return score.toFixed(2);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function maxDiskUsage(disks?: Array<{ usagePercent: number }>): number | undefined {
|
|
348
|
+
if (!disks || disks.length === 0) {
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
return Math.max(...disks.map((d) => d.usagePercent));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function formatAge(timestamp: string): string {
|
|
355
|
+
const date = new Date(timestamp);
|
|
356
|
+
const time = date.getTime();
|
|
357
|
+
if (Number.isNaN(time)) {
|
|
358
|
+
return '-';
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const diff = Date.now() - time;
|
|
362
|
+
if (diff < 60_000) return `${Math.max(0, Math.floor(diff / 1000))}s ago`;
|
|
363
|
+
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
|
|
364
|
+
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
|
|
365
|
+
return `${Math.floor(diff / 86_400_000)}d ago`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function usecToISO(us?: number): string | undefined {
|
|
369
|
+
if (us === undefined || us <= 0 || Number.isNaN(us)) {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
return new Date(Math.floor(us / 1000)).toISOString();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export default monitorSubcommand;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { oauthClientActivity, type APIClient } 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
|
+
|
|
7
|
+
const OAuthClientActivityResponseSchema = z.array(
|
|
8
|
+
z.object({
|
|
9
|
+
activity_date: z.string(),
|
|
10
|
+
total_access: z.number(),
|
|
11
|
+
unique_users: z.number(),
|
|
12
|
+
})
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const activitySubcommand = createSubcommand({
|
|
16
|
+
name: 'activity',
|
|
17
|
+
description: 'Show activity for an OAuth application',
|
|
18
|
+
tags: ['read-only', 'requires-auth'],
|
|
19
|
+
examples: [
|
|
20
|
+
{ command: getCommand('cloud oidc activity <id>'), description: 'Show OAuth activity' },
|
|
21
|
+
{
|
|
22
|
+
command: getCommand('cloud oidc activity <id> --days=30'),
|
|
23
|
+
description: 'Show OAuth activity for last 30 days',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
requires: { auth: true, apiClient: true },
|
|
27
|
+
idempotent: true,
|
|
28
|
+
schema: {
|
|
29
|
+
args: z.object({
|
|
30
|
+
id: z.string().describe('the OAuth client id'),
|
|
31
|
+
}),
|
|
32
|
+
options: z.object({
|
|
33
|
+
days: z.coerce.number().int().min(1).max(365).default(7).describe('Number of days'),
|
|
34
|
+
}),
|
|
35
|
+
response: OAuthClientActivityResponseSchema,
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async handler(ctx) {
|
|
39
|
+
const { args, opts, apiClient, options } = ctx;
|
|
40
|
+
|
|
41
|
+
const activity = await tui.spinner('Fetching OAuth activity', () => {
|
|
42
|
+
return oauthClientActivity(apiClient as APIClient, args.id, opts.days);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!options.json) {
|
|
46
|
+
if (activity.length === 0) {
|
|
47
|
+
tui.info('No OAuth activity found');
|
|
48
|
+
} else {
|
|
49
|
+
const rows = activity.map((item) => ({
|
|
50
|
+
activity_date: item.activity_date,
|
|
51
|
+
total_access: item.total_access,
|
|
52
|
+
unique_users: item.unique_users,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
tui.table(rows);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return activity;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import {
|
|
2
|
+
oauthClientCreate,
|
|
3
|
+
oauthScopes,
|
|
4
|
+
type APIClient,
|
|
5
|
+
type OAuthClientCreateRequest,
|
|
6
|
+
} from '@agentuity/core';
|
|
7
|
+
import enquirer from 'enquirer';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { getCommand } from '../../../command-prefix';
|
|
10
|
+
import * as tui from '../../../tui';
|
|
11
|
+
import { createSubcommand as createSubcommandHelper } from '../../../types';
|
|
12
|
+
|
|
13
|
+
const OAuthClientCreateResponseSchema = z.object({
|
|
14
|
+
client: z.object({
|
|
15
|
+
id: z.string(),
|
|
16
|
+
name: z.string(),
|
|
17
|
+
description: z.string(),
|
|
18
|
+
homepage_url: z.string(),
|
|
19
|
+
client_type: z.enum(['public', 'confidential']),
|
|
20
|
+
redirect_uris: z.array(z.string()),
|
|
21
|
+
scopes: z.array(z.string()),
|
|
22
|
+
created_at: z.string(),
|
|
23
|
+
updated_at: z.string(),
|
|
24
|
+
}),
|
|
25
|
+
client_secret: z.string(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function parseCsv(value?: string): string[] {
|
|
29
|
+
if (!value) return [];
|
|
30
|
+
return value
|
|
31
|
+
.split(',')
|
|
32
|
+
.map((part) => part.trim())
|
|
33
|
+
.filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const createSubcommand = createSubcommandHelper({
|
|
37
|
+
name: 'create',
|
|
38
|
+
aliases: ['new'],
|
|
39
|
+
description: 'Create a new OAuth application',
|
|
40
|
+
tags: ['creates-resource', 'slow', 'requires-auth'],
|
|
41
|
+
examples: [
|
|
42
|
+
{
|
|
43
|
+
command: getCommand(
|
|
44
|
+
'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"'
|
|
45
|
+
),
|
|
46
|
+
description: 'Create OAuth application non-interactively',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
command: getCommand('cloud oidc create'),
|
|
50
|
+
description: 'Create OAuth application interactively',
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
requires: { auth: true, apiClient: true },
|
|
54
|
+
idempotent: false,
|
|
55
|
+
schema: {
|
|
56
|
+
options: z.object({
|
|
57
|
+
name: z.string().optional().describe('the OAuth application name'),
|
|
58
|
+
description: z.string().optional().describe('the OAuth application description'),
|
|
59
|
+
'homepage-url': z.string().optional().describe('the homepage URL'),
|
|
60
|
+
type: z
|
|
61
|
+
.enum(['public', 'confidential'])
|
|
62
|
+
.optional()
|
|
63
|
+
.describe('OAuth client type: public or confidential'),
|
|
64
|
+
'redirect-uris': z
|
|
65
|
+
.string()
|
|
66
|
+
.optional()
|
|
67
|
+
.describe('comma-separated redirect URIs (e.g. https://app/callback,https://app/alt)'),
|
|
68
|
+
scopes: z
|
|
69
|
+
.string()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe('comma-separated OAuth scopes (e.g. openid,profile,email)'),
|
|
72
|
+
}),
|
|
73
|
+
response: OAuthClientCreateResponseSchema,
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async handler(ctx) {
|
|
77
|
+
const { opts, apiClient, options } = ctx;
|
|
78
|
+
|
|
79
|
+
const availableScopes = await tui.spinner('Fetching available OAuth scopes', () => {
|
|
80
|
+
return oauthScopes(apiClient as APIClient);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const nonInteractive = !process.stdin.isTTY || !process.stdout.isTTY;
|
|
84
|
+
|
|
85
|
+
let name = opts?.name?.trim() || '';
|
|
86
|
+
let description = opts?.description?.trim() || '';
|
|
87
|
+
let homepageUrl = opts?.['homepage-url']?.trim() || '';
|
|
88
|
+
let clientType = opts?.type;
|
|
89
|
+
let redirectUris = parseCsv(opts?.['redirect-uris']);
|
|
90
|
+
let scopes = parseCsv(opts?.scopes);
|
|
91
|
+
|
|
92
|
+
if (!name) {
|
|
93
|
+
if (nonInteractive) {
|
|
94
|
+
tui.fatal('--name is required in non-interactive mode');
|
|
95
|
+
}
|
|
96
|
+
const answer = await enquirer.prompt<{ name: string }>({
|
|
97
|
+
type: 'input',
|
|
98
|
+
name: 'name',
|
|
99
|
+
message: 'Application name:',
|
|
100
|
+
});
|
|
101
|
+
name = answer.name?.trim() || '';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!description && !nonInteractive) {
|
|
105
|
+
const answer = await enquirer.prompt<{ description: string }>({
|
|
106
|
+
type: 'input',
|
|
107
|
+
name: 'description',
|
|
108
|
+
message: 'Description:',
|
|
109
|
+
});
|
|
110
|
+
description = answer.description?.trim() || '';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!homepageUrl) {
|
|
114
|
+
if (nonInteractive) {
|
|
115
|
+
tui.fatal('--homepage-url is required in non-interactive mode');
|
|
116
|
+
}
|
|
117
|
+
const answer = await enquirer.prompt<{ homepageUrl: string }>({
|
|
118
|
+
type: 'input',
|
|
119
|
+
name: 'homepageUrl',
|
|
120
|
+
message: 'Homepage URL:',
|
|
121
|
+
});
|
|
122
|
+
homepageUrl = answer.homepageUrl?.trim() || '';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!clientType) {
|
|
126
|
+
if (nonInteractive) {
|
|
127
|
+
tui.fatal('--type is required in non-interactive mode');
|
|
128
|
+
}
|
|
129
|
+
const answer = await enquirer.prompt<{ clientType: 'public' | 'confidential' }>({
|
|
130
|
+
type: 'select',
|
|
131
|
+
name: 'clientType',
|
|
132
|
+
message: 'Client type:',
|
|
133
|
+
choices: [
|
|
134
|
+
{ name: 'public', message: 'public' },
|
|
135
|
+
{ name: 'confidential', message: 'confidential' },
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
clientType = answer.clientType;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (redirectUris.length === 0) {
|
|
142
|
+
if (nonInteractive) {
|
|
143
|
+
tui.fatal('--redirect-uris is required in non-interactive mode');
|
|
144
|
+
}
|
|
145
|
+
const answer = await enquirer.prompt<{ redirectUris: string }>({
|
|
146
|
+
type: 'input',
|
|
147
|
+
name: 'redirectUris',
|
|
148
|
+
message: 'Redirect URIs (comma-separated):',
|
|
149
|
+
});
|
|
150
|
+
redirectUris = parseCsv(answer.redirectUris);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (scopes.length === 0) {
|
|
154
|
+
if (nonInteractive) {
|
|
155
|
+
tui.fatal('--scopes is required in non-interactive mode');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const choices = availableScopes.scopes.map((scope) => ({
|
|
159
|
+
name: scope.name,
|
|
160
|
+
message: `${scope.name} — ${scope.description}`,
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
const answer = await enquirer.prompt<{ scopes: string[] }>({
|
|
164
|
+
type: 'multiselect',
|
|
165
|
+
name: 'scopes',
|
|
166
|
+
message: 'Select OAuth scopes:',
|
|
167
|
+
choices,
|
|
168
|
+
});
|
|
169
|
+
scopes = answer.scopes;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!name) {
|
|
173
|
+
tui.fatal('Name is required');
|
|
174
|
+
}
|
|
175
|
+
if (!homepageUrl) {
|
|
176
|
+
tui.fatal('Homepage URL is required');
|
|
177
|
+
}
|
|
178
|
+
if (!clientType) {
|
|
179
|
+
tui.fatal('Client type is required');
|
|
180
|
+
}
|
|
181
|
+
if (redirectUris.length === 0) {
|
|
182
|
+
tui.fatal('At least one redirect URI is required');
|
|
183
|
+
}
|
|
184
|
+
if (scopes.length === 0) {
|
|
185
|
+
tui.fatal('At least one scope is required');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const availableScopeNames = new Set(availableScopes.scopes.map((scope) => scope.name));
|
|
189
|
+
const invalidScopes = scopes.filter((scope) => !availableScopeNames.has(scope));
|
|
190
|
+
if (invalidScopes.length > 0) {
|
|
191
|
+
tui.fatal(`Invalid scopes: ${invalidScopes.join(', ')}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const request: OAuthClientCreateRequest = {
|
|
195
|
+
name,
|
|
196
|
+
description,
|
|
197
|
+
homepage_url: homepageUrl,
|
|
198
|
+
client_type: clientType,
|
|
199
|
+
redirect_uris: redirectUris,
|
|
200
|
+
scopes,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const result = await tui.spinner('Creating OAuth application', () => {
|
|
204
|
+
return oauthClientCreate(apiClient as APIClient, request);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (!options.json) {
|
|
208
|
+
tui.newline();
|
|
209
|
+
tui.success('OAuth application created successfully!');
|
|
210
|
+
tui.newline();
|
|
211
|
+
tui.warning('Copy the client secret now. It will only be shown once.');
|
|
212
|
+
tui.newline();
|
|
213
|
+
|
|
214
|
+
tui.table(
|
|
215
|
+
[
|
|
216
|
+
{
|
|
217
|
+
ID: result.client.id,
|
|
218
|
+
Name: result.client.name,
|
|
219
|
+
Type: result.client.client_type,
|
|
220
|
+
'Client Secret': result.client_secret,
|
|
221
|
+
'Redirect URIs': result.client.redirect_uris.join(', '),
|
|
222
|
+
Scopes: result.client.scopes.join(', '),
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
undefined,
|
|
226
|
+
{ layout: 'vertical' }
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return result;
|
|
231
|
+
},
|
|
232
|
+
});
|