@agentuity/cli 0.1.16 → 0.1.18
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/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -1
- package/dist/cli.js.map +1 -1
- package/dist/cmd/build/ast.d.ts.map +1 -1
- package/dist/cmd/build/ast.js +68 -2
- package/dist/cmd/build/ast.js.map +1 -1
- package/dist/cmd/build/vite/registry-generator.d.ts.map +1 -1
- package/dist/cmd/build/vite/registry-generator.js +112 -23
- package/dist/cmd/build/vite/registry-generator.js.map +1 -1
- package/dist/cmd/build/vite/route-discovery.d.ts +4 -0
- package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -1
- package/dist/cmd/build/vite/route-discovery.js +4 -0
- package/dist/cmd/build/vite/route-discovery.js.map +1 -1
- package/dist/cmd/cloud/env/delete.d.ts.map +1 -1
- package/dist/cmd/cloud/env/delete.js +8 -2
- package/dist/cmd/cloud/env/delete.js.map +1 -1
- package/dist/cmd/cloud/env/get.d.ts.map +1 -1
- package/dist/cmd/cloud/env/get.js +4 -1
- package/dist/cmd/cloud/env/get.js.map +1 -1
- package/dist/cmd/cloud/env/import.d.ts.map +1 -1
- package/dist/cmd/cloud/env/import.js +5 -8
- package/dist/cmd/cloud/env/import.js.map +1 -1
- package/dist/cmd/cloud/env/list.d.ts.map +1 -1
- package/dist/cmd/cloud/env/list.js +11 -6
- package/dist/cmd/cloud/env/list.js.map +1 -1
- package/dist/cmd/cloud/env/pull.d.ts.map +1 -1
- package/dist/cmd/cloud/env/pull.js.map +1 -1
- package/dist/cmd/cloud/env/push.d.ts.map +1 -1
- package/dist/cmd/cloud/env/push.js +1 -7
- package/dist/cmd/cloud/env/push.js.map +1 -1
- package/dist/cmd/cloud/env/set.d.ts.map +1 -1
- package/dist/cmd/cloud/env/set.js +4 -1
- package/dist/cmd/cloud/env/set.js.map +1 -1
- package/dist/cmd/cloud/index.d.ts.map +1 -1
- package/dist/cmd/cloud/index.js +2 -0
- package/dist/cmd/cloud/index.js.map +1 -1
- package/dist/cmd/cloud/queue/ack.d.ts +3 -0
- package/dist/cmd/cloud/queue/ack.d.ts.map +1 -0
- package/dist/cmd/cloud/queue/ack.js +45 -0
- package/dist/cmd/cloud/queue/ack.js.map +1 -0
- package/dist/cmd/cloud/queue/create.d.ts +3 -0
- package/dist/cmd/cloud/queue/create.d.ts.map +1 -0
- package/dist/cmd/cloud/queue/create.js +80 -0
- package/dist/cmd/cloud/queue/create.js.map +1 -0
- package/dist/cmd/cloud/queue/delete.d.ts +3 -0
- package/dist/cmd/cloud/queue/delete.d.ts.map +1 -0
- package/dist/cmd/cloud/queue/delete.js +50 -0
- package/dist/cmd/cloud/queue/delete.js.map +1 -0
- package/dist/cmd/cloud/queue/destinations.d.ts +3 -0
- package/dist/cmd/cloud/queue/destinations.d.ts.map +1 -0
- package/dist/cmd/cloud/queue/destinations.js +232 -0
- package/dist/cmd/cloud/queue/destinations.js.map +1 -0
- package/dist/cmd/cloud/queue/dlq.d.ts +3 -0
- package/dist/cmd/cloud/queue/dlq.d.ts.map +1 -0
- package/dist/cmd/cloud/queue/dlq.js +168 -0
- package/dist/cmd/cloud/queue/dlq.js.map +1 -0
- package/dist/cmd/cloud/queue/get.d.ts +3 -0
- package/dist/cmd/cloud/queue/get.d.ts.map +1 -0
- package/dist/cmd/cloud/queue/get.js +130 -0
- package/dist/cmd/cloud/queue/get.js.map +1 -0
- package/dist/cmd/cloud/queue/index.d.ts +3 -0
- package/dist/cmd/cloud/queue/index.d.ts.map +1 -0
- package/dist/cmd/cloud/queue/index.js +65 -0
- package/dist/cmd/cloud/queue/index.js.map +1 -0
- package/dist/cmd/cloud/queue/list.d.ts +3 -0
- package/dist/cmd/cloud/queue/list.d.ts.map +1 -0
- package/dist/cmd/cloud/queue/list.js +71 -0
- package/dist/cmd/cloud/queue/list.js.map +1 -0
- package/dist/cmd/cloud/queue/messages.d.ts +3 -0
- package/dist/cmd/cloud/queue/messages.d.ts.map +1 -0
- package/dist/cmd/cloud/queue/messages.js +137 -0
- package/dist/cmd/cloud/queue/messages.js.map +1 -0
- package/dist/cmd/cloud/queue/nack.d.ts +3 -0
- package/dist/cmd/cloud/queue/nack.d.ts.map +1 -0
- package/dist/cmd/cloud/queue/nack.js +45 -0
- package/dist/cmd/cloud/queue/nack.js.map +1 -0
- package/dist/cmd/cloud/queue/pause.d.ts +3 -0
- package/dist/cmd/cloud/queue/pause.d.ts.map +1 -0
- package/dist/cmd/cloud/queue/pause.js +36 -0
- package/dist/cmd/cloud/queue/pause.js.map +1 -0
- package/dist/cmd/cloud/queue/publish.d.ts +3 -0
- package/dist/cmd/cloud/queue/publish.d.ts.map +1 -0
- package/dist/cmd/cloud/queue/publish.js +76 -0
- package/dist/cmd/cloud/queue/publish.js.map +1 -0
- package/dist/cmd/cloud/queue/receive.d.ts +3 -0
- package/dist/cmd/cloud/queue/receive.d.ts.map +1 -0
- package/dist/cmd/cloud/queue/receive.js +67 -0
- package/dist/cmd/cloud/queue/receive.js.map +1 -0
- package/dist/cmd/cloud/queue/resume.d.ts +3 -0
- package/dist/cmd/cloud/queue/resume.d.ts.map +1 -0
- package/dist/cmd/cloud/queue/resume.js +35 -0
- package/dist/cmd/cloud/queue/resume.js.map +1 -0
- package/dist/cmd/cloud/queue/sources.d.ts +3 -0
- package/dist/cmd/cloud/queue/sources.d.ts.map +1 -0
- package/dist/cmd/cloud/queue/sources.js +290 -0
- package/dist/cmd/cloud/queue/sources.js.map +1 -0
- package/dist/cmd/cloud/queue/stats.d.ts +3 -0
- package/dist/cmd/cloud/queue/stats.d.ts.map +1 -0
- package/dist/cmd/cloud/queue/stats.js +239 -0
- package/dist/cmd/cloud/queue/stats.js.map +1 -0
- package/dist/cmd/cloud/queue/util.d.ts +26 -0
- package/dist/cmd/cloud/queue/util.d.ts.map +1 -0
- package/dist/cmd/cloud/queue/util.js +19 -0
- package/dist/cmd/cloud/queue/util.js.map +1 -0
- package/dist/cmd/cloud/sandbox/snapshot/build.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/build.js +152 -30
- package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/create.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/create.js +19 -7
- package/dist/cmd/cloud/sandbox/snapshot/create.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/get.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/get.js +20 -0
- package/dist/cmd/cloud/sandbox/snapshot/get.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/list.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/list.js +4 -0
- package/dist/cmd/cloud/sandbox/snapshot/list.js.map +1 -1
- package/dist/cmd/cloud/vector/stats.d.ts.map +1 -1
- package/dist/cmd/cloud/vector/stats.js +8 -0
- package/dist/cmd/cloud/vector/stats.js.map +1 -1
- package/dist/cmd/project/template-flow.d.ts.map +1 -1
- package/dist/cmd/project/template-flow.js.map +1 -1
- package/dist/env-util.d.ts +6 -1
- package/dist/env-util.d.ts.map +1 -1
- package/dist/env-util.js +16 -2
- package/dist/env-util.js.map +1 -1
- package/dist/errors.d.ts +4 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +6 -0
- package/dist/errors.js.map +1 -1
- package/dist/schema-parser.d.ts.map +1 -1
- package/dist/schema-parser.js +2 -2
- package/dist/schema-parser.js.map +1 -1
- package/dist/tui/box.d.ts +8 -0
- package/dist/tui/box.d.ts.map +1 -1
- package/dist/tui/box.js +78 -0
- package/dist/tui/box.js.map +1 -1
- package/dist/tui.d.ts +11 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +16 -8
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +6 -6
- package/src/cli.ts +5 -1
- package/src/cmd/build/ast.ts +88 -2
- package/src/cmd/build/vite/registry-generator.ts +120 -24
- package/src/cmd/build/vite/route-discovery.ts +16 -0
- package/src/cmd/cloud/env/delete.ts +18 -5
- package/src/cmd/cloud/env/get.ts +10 -3
- package/src/cmd/cloud/env/import.ts +10 -11
- package/src/cmd/cloud/env/list.ts +19 -9
- package/src/cmd/cloud/env/org-util.ts +1 -1
- package/src/cmd/cloud/env/pull.ts +9 -4
- package/src/cmd/cloud/env/push.ts +5 -9
- package/src/cmd/cloud/env/set.ts +10 -3
- package/src/cmd/cloud/index.ts +2 -0
- package/src/cmd/cloud/queue/ack.ts +50 -0
- package/src/cmd/cloud/queue/create.ts +91 -0
- package/src/cmd/cloud/queue/delete.ts +57 -0
- package/src/cmd/cloud/queue/destinations.ts +287 -0
- package/src/cmd/cloud/queue/dlq.ts +203 -0
- package/src/cmd/cloud/queue/get.ts +158 -0
- package/src/cmd/cloud/queue/index.ts +66 -0
- package/src/cmd/cloud/queue/list.ts +81 -0
- package/src/cmd/cloud/queue/messages.ts +160 -0
- package/src/cmd/cloud/queue/nack.ts +50 -0
- package/src/cmd/cloud/queue/pause.ts +41 -0
- package/src/cmd/cloud/queue/publish.ts +88 -0
- package/src/cmd/cloud/queue/receive.ts +76 -0
- package/src/cmd/cloud/queue/resume.ts +40 -0
- package/src/cmd/cloud/queue/sources.ts +352 -0
- package/src/cmd/cloud/queue/stats.ts +297 -0
- package/src/cmd/cloud/queue/util.ts +34 -0
- package/src/cmd/cloud/sandbox/snapshot/build.ts +186 -31
- package/src/cmd/cloud/sandbox/snapshot/create.ts +24 -7
- package/src/cmd/cloud/sandbox/snapshot/get.ts +20 -0
- package/src/cmd/cloud/sandbox/snapshot/list.ts +4 -0
- package/src/cmd/cloud/vector/stats.ts +9 -0
- package/src/cmd/project/template-flow.ts +1 -3
- package/src/env-util.ts +17 -2
- package/src/errors.ts +8 -0
- package/src/schema-parser.ts +6 -3
- package/src/tui/box.ts +104 -0
- package/src/tui.ts +28 -8
- package/src/types.ts +0 -1
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createCommand } from '../../../types';
|
|
3
|
+
import * as tui from '../../../tui';
|
|
4
|
+
import { createQueueAPIClient, getQueueApiOptions } from './util';
|
|
5
|
+
import { getCommand } from '../../../command-prefix';
|
|
6
|
+
import {
|
|
7
|
+
getOrgAnalytics,
|
|
8
|
+
getQueueAnalytics,
|
|
9
|
+
streamOrgAnalytics,
|
|
10
|
+
streamQueueAnalytics,
|
|
11
|
+
type OrgAnalytics,
|
|
12
|
+
type QueueAnalytics,
|
|
13
|
+
type SSEStatsEvent,
|
|
14
|
+
} from '@agentuity/server';
|
|
15
|
+
|
|
16
|
+
const StatsResponseSchema = z.union([
|
|
17
|
+
z.object({ type: z.literal('org'), analytics: z.unknown() }),
|
|
18
|
+
z.object({ type: z.literal('queue'), analytics: z.unknown() }),
|
|
19
|
+
z.object({ type: z.literal('stream'), events: z.array(z.unknown()) }),
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function formatNumber(n: number): string {
|
|
23
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
24
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
25
|
+
return String(n);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatPercent(n: number): string {
|
|
29
|
+
return `${n.toFixed(2)}%`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatLatency(ms: number): string {
|
|
33
|
+
if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`;
|
|
34
|
+
return `${Math.round(ms)}ms`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatDuration(start: string, end: string): string {
|
|
38
|
+
const startDate = new Date(start);
|
|
39
|
+
const endDate = new Date(end);
|
|
40
|
+
const hours = Math.round((endDate.getTime() - startDate.getTime()) / 3600000);
|
|
41
|
+
if (hours >= 24) return `${Math.round(hours / 24)}d`;
|
|
42
|
+
return `${hours}h`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function displayOrgAnalytics(analytics: OrgAnalytics): void {
|
|
46
|
+
const { summary, queues, period } = analytics;
|
|
47
|
+
|
|
48
|
+
tui.info(`Organization Analytics (${formatDuration(period.start, period.end)})`);
|
|
49
|
+
tui.newline();
|
|
50
|
+
console.log(tui.colorPrimary('Summary:'));
|
|
51
|
+
console.log(` ${tui.muted('Total Queues:')} ${summary.total_queues}`);
|
|
52
|
+
console.log(
|
|
53
|
+
` ${tui.muted('Published:')} ${formatNumber(summary.total_messages_published)}`
|
|
54
|
+
);
|
|
55
|
+
console.log(
|
|
56
|
+
` ${tui.muted('Delivered:')} ${formatNumber(summary.total_messages_delivered)}`
|
|
57
|
+
);
|
|
58
|
+
console.log(
|
|
59
|
+
` ${tui.muted('Acknowledged:')} ${formatNumber(summary.total_messages_acknowledged)}`
|
|
60
|
+
);
|
|
61
|
+
console.log(` ${tui.muted('DLQ Messages:')} ${formatNumber(summary.total_dlq_messages)}`);
|
|
62
|
+
console.log(` ${tui.muted('Avg Latency:')} ${formatLatency(summary.avg_latency_ms)}`);
|
|
63
|
+
console.log(` ${tui.muted('P95 Latency:')} ${formatLatency(summary.p95_latency_ms)}`);
|
|
64
|
+
console.log(` ${tui.muted('Error Rate:')} ${formatPercent(summary.error_rate_percent)}`);
|
|
65
|
+
|
|
66
|
+
if (queues.length > 0) {
|
|
67
|
+
tui.newline();
|
|
68
|
+
console.log(tui.colorPrimary('Queues:'));
|
|
69
|
+
const tableData = queues.map((q) => ({
|
|
70
|
+
Name: q.name,
|
|
71
|
+
Type: q.queue_type,
|
|
72
|
+
Published: formatNumber(q.messages_published),
|
|
73
|
+
Delivered: formatNumber(q.messages_delivered),
|
|
74
|
+
Backlog: formatNumber(q.backlog),
|
|
75
|
+
DLQ: formatNumber(q.dlq_count),
|
|
76
|
+
'Avg Latency': formatLatency(q.avg_latency_ms),
|
|
77
|
+
'Error %': formatPercent(q.error_rate_percent),
|
|
78
|
+
}));
|
|
79
|
+
tui.table(tableData);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function displayQueueAnalytics(analytics: QueueAnalytics): void {
|
|
84
|
+
const { queue_name, queue_type, period, current, period_stats, latency, consumer_latency } =
|
|
85
|
+
analytics;
|
|
86
|
+
|
|
87
|
+
tui.info(`Queue: ${queue_name} (${queue_type})`);
|
|
88
|
+
console.log(tui.colorWarning(`Period: ${formatDuration(period.start, period.end)}`));
|
|
89
|
+
tui.newline();
|
|
90
|
+
|
|
91
|
+
console.log(tui.colorPrimary('Current State:'));
|
|
92
|
+
console.log(` ${tui.muted('Backlog:')} ${formatNumber(current.backlog)}`);
|
|
93
|
+
console.log(` ${tui.muted('In-Flight:')} ${formatNumber(current.messages_in_flight)}`);
|
|
94
|
+
console.log(` ${tui.muted('DLQ:')} ${formatNumber(current.dlq_count)}`);
|
|
95
|
+
console.log(` ${tui.muted('Consumers:')} ${current.active_consumers}`);
|
|
96
|
+
if (current.oldest_message_age_seconds != null) {
|
|
97
|
+
console.log(` ${tui.muted('Oldest Msg Age:')} ${current.oldest_message_age_seconds}s`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
tui.newline();
|
|
101
|
+
console.log(tui.colorPrimary('Period Stats:'));
|
|
102
|
+
console.log(
|
|
103
|
+
` ${tui.muted('Published:')} ${formatNumber(period_stats.messages_published)}`
|
|
104
|
+
);
|
|
105
|
+
console.log(
|
|
106
|
+
` ${tui.muted('Delivered:')} ${formatNumber(period_stats.messages_delivered)}`
|
|
107
|
+
);
|
|
108
|
+
console.log(
|
|
109
|
+
` ${tui.muted('Acknowledged:')} ${formatNumber(period_stats.messages_acknowledged)}`
|
|
110
|
+
);
|
|
111
|
+
console.log(
|
|
112
|
+
` ${tui.muted('Failed:')} ${formatNumber(period_stats.messages_failed)}`
|
|
113
|
+
);
|
|
114
|
+
console.log(
|
|
115
|
+
` ${tui.muted('Replayed:')} ${formatNumber(period_stats.messages_replayed)}`
|
|
116
|
+
);
|
|
117
|
+
console.log(
|
|
118
|
+
` ${tui.muted('Bytes Published:')} ${tui.formatBytes(period_stats.bytes_published)}`
|
|
119
|
+
);
|
|
120
|
+
console.log(
|
|
121
|
+
` ${tui.muted('Delivery Attempts:')} ${formatNumber(period_stats.delivery_attempts)}`
|
|
122
|
+
);
|
|
123
|
+
console.log(` ${tui.muted('Retries:')} ${formatNumber(period_stats.retry_count)}`);
|
|
124
|
+
|
|
125
|
+
tui.newline();
|
|
126
|
+
console.log(tui.colorPrimary('Delivery Latency:'));
|
|
127
|
+
console.log(` ${tui.muted('Average:')} ${formatLatency(latency.avg_ms)}`);
|
|
128
|
+
if (latency.p50_ms != null)
|
|
129
|
+
console.log(` ${tui.muted('Median (P50):')} ${formatLatency(latency.p50_ms)}`);
|
|
130
|
+
if (latency.p95_ms != null)
|
|
131
|
+
console.log(` ${tui.muted('P95:')} ${formatLatency(latency.p95_ms)}`);
|
|
132
|
+
if (latency.p99_ms != null)
|
|
133
|
+
console.log(` ${tui.muted('P99:')} ${formatLatency(latency.p99_ms)}`);
|
|
134
|
+
if (latency.max_ms != null)
|
|
135
|
+
console.log(` ${tui.muted('Max:')} ${formatLatency(latency.max_ms)}`);
|
|
136
|
+
|
|
137
|
+
tui.newline();
|
|
138
|
+
console.log(tui.colorPrimary('Consumer Latency:'));
|
|
139
|
+
console.log(` ${tui.muted('Average:')} ${formatLatency(consumer_latency.avg_ms)}`);
|
|
140
|
+
if (consumer_latency.p50_ms != null)
|
|
141
|
+
console.log(` ${tui.muted('Median (P50):')} ${formatLatency(consumer_latency.p50_ms)}`);
|
|
142
|
+
if (consumer_latency.p95_ms != null)
|
|
143
|
+
console.log(` ${tui.muted('P95:')} ${formatLatency(consumer_latency.p95_ms)}`);
|
|
144
|
+
if (consumer_latency.p99_ms != null)
|
|
145
|
+
console.log(` ${tui.muted('P99:')} ${formatLatency(consumer_latency.p99_ms)}`);
|
|
146
|
+
|
|
147
|
+
if (analytics.destinations && analytics.destinations.length > 0) {
|
|
148
|
+
tui.newline();
|
|
149
|
+
tui.info('Destinations:');
|
|
150
|
+
const destData = analytics.destinations.map((d) => {
|
|
151
|
+
const total = d.success_count + d.failure_count;
|
|
152
|
+
const errorRate = total > 0 ? (d.failure_count / total) * 100 : 0;
|
|
153
|
+
return {
|
|
154
|
+
ID: d.id.slice(0, 12) + '...',
|
|
155
|
+
URL: d.url.length > 40 ? d.url.slice(0, 37) + '...' : d.url,
|
|
156
|
+
Success: formatNumber(d.success_count),
|
|
157
|
+
Failed: formatNumber(d.failure_count),
|
|
158
|
+
'Avg Response': d.avg_response_time_ms ? formatLatency(d.avg_response_time_ms) : '-',
|
|
159
|
+
'Error %': formatPercent(errorRate),
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
tui.table(destData);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function displayStreamEvent(event: SSEStatsEvent, queueName?: string): void {
|
|
167
|
+
const time = new Date(event.timestamp).toLocaleTimeString();
|
|
168
|
+
const prefix = queueName ? `[${queueName}]` : '[org]';
|
|
169
|
+
|
|
170
|
+
process.stdout.write('\x1b[2K\r');
|
|
171
|
+
process.stdout.write(
|
|
172
|
+
`${tui.colorMuted(time)} ${prefix} ` +
|
|
173
|
+
`Backlog: ${tui.colorInfo(formatNumber(event.backlog))} | ` +
|
|
174
|
+
`In-Flight: ${tui.colorInfo(formatNumber(event.messages_in_flight))} | ` +
|
|
175
|
+
`Throughput: ${tui.colorSuccess(formatNumber(event.throughput_1m))}/min | ` +
|
|
176
|
+
`Latency: ${formatLatency(event.avg_latency_ms)} | ` +
|
|
177
|
+
`Errors: ${event.error_rate_1m > 0 ? tui.colorError(String(event.error_rate_1m)) : '0'}/min`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export const statsSubcommand = createCommand({
|
|
182
|
+
name: 'stats',
|
|
183
|
+
description: 'View queue analytics and statistics',
|
|
184
|
+
tags: ['read-only', 'requires-auth'],
|
|
185
|
+
requires: { auth: true, org: true },
|
|
186
|
+
examples: [
|
|
187
|
+
{
|
|
188
|
+
command: getCommand('cloud queue stats'),
|
|
189
|
+
description: 'View org-level analytics for all queues',
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
command: getCommand('cloud queue stats --name my-queue'),
|
|
193
|
+
description: 'View detailed analytics for a specific queue',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
command: getCommand('cloud queue stats --live'),
|
|
197
|
+
description: 'Stream real-time stats (Ctrl+C to exit)',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
command: getCommand('cloud queue stats --name my-queue --live --interval 10'),
|
|
201
|
+
description: 'Stream queue stats every 10 seconds',
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
schema: {
|
|
205
|
+
args: z.object({
|
|
206
|
+
name: z.string().optional().describe('Queue name (omit for org-level stats)'),
|
|
207
|
+
}),
|
|
208
|
+
options: z.object({
|
|
209
|
+
live: z.boolean().default(false).describe('Stream real-time stats'),
|
|
210
|
+
interval: z.number().default(5).describe('Refresh interval in seconds (for --live)'),
|
|
211
|
+
start: z.string().optional().describe('Start time (ISO 8601)'),
|
|
212
|
+
end: z.string().optional().describe('End time (ISO 8601)'),
|
|
213
|
+
granularity: z.enum(['minute', 'hour', 'day']).optional().describe('Time granularity'),
|
|
214
|
+
}),
|
|
215
|
+
response: StatsResponseSchema,
|
|
216
|
+
},
|
|
217
|
+
idempotent: true,
|
|
218
|
+
|
|
219
|
+
async handler(ctx) {
|
|
220
|
+
const { args, opts, options } = ctx;
|
|
221
|
+
const client = await createQueueAPIClient(ctx);
|
|
222
|
+
const apiOptions = getQueueApiOptions(ctx);
|
|
223
|
+
|
|
224
|
+
const analyticsOptions = {
|
|
225
|
+
...apiOptions,
|
|
226
|
+
start: opts.start,
|
|
227
|
+
end: opts.end,
|
|
228
|
+
granularity: opts.granularity,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if (opts.live) {
|
|
232
|
+
const events: SSEStatsEvent[] = [];
|
|
233
|
+
|
|
234
|
+
tui.info(
|
|
235
|
+
`Streaming stats every ${opts.interval}s... ${tui.colorMuted('(Ctrl+C to exit)')}`
|
|
236
|
+
);
|
|
237
|
+
tui.info('');
|
|
238
|
+
|
|
239
|
+
const handleInterrupt = () => {
|
|
240
|
+
tui.info('');
|
|
241
|
+
tui.info('Stream stopped.');
|
|
242
|
+
process.exit(0);
|
|
243
|
+
};
|
|
244
|
+
process.on('SIGINT', handleInterrupt);
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
if (args.name) {
|
|
248
|
+
const stream = streamQueueAnalytics(client, args.name, {
|
|
249
|
+
interval: opts.interval,
|
|
250
|
+
orgId: apiOptions?.orgId,
|
|
251
|
+
});
|
|
252
|
+
for await (const event of stream) {
|
|
253
|
+
events.push(event);
|
|
254
|
+
if (!options.json) {
|
|
255
|
+
displayStreamEvent(event, args.name);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
const stream = streamOrgAnalytics(client, {
|
|
260
|
+
interval: opts.interval,
|
|
261
|
+
orgId: apiOptions?.orgId,
|
|
262
|
+
});
|
|
263
|
+
for await (const event of stream) {
|
|
264
|
+
events.push(event);
|
|
265
|
+
if (!options.json) {
|
|
266
|
+
displayStreamEvent(event);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} finally {
|
|
271
|
+
process.off('SIGINT', handleInterrupt);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { type: 'stream' as const, events };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (args.name) {
|
|
278
|
+
const analytics = await getQueueAnalytics(client, args.name, analyticsOptions);
|
|
279
|
+
|
|
280
|
+
if (!options.json) {
|
|
281
|
+
displayQueueAnalytics(analytics);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { type: 'queue' as const, analytics };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const analytics = await getOrgAnalytics(client, analyticsOptions);
|
|
288
|
+
|
|
289
|
+
if (!options.json) {
|
|
290
|
+
displayOrgAnalytics(analytics);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return { type: 'org' as const, analytics };
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
export default statsSubcommand;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Logger } from '@agentuity/core';
|
|
2
|
+
import { APIClient, type QueueApiOptions } from '@agentuity/server';
|
|
3
|
+
import { getGlobalCatalystAPIClient } from '../../../config';
|
|
4
|
+
import type { AuthData, Config, GlobalOptions } from '../../../types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Context required for queue API operations.
|
|
8
|
+
*/
|
|
9
|
+
export interface QueueContext {
|
|
10
|
+
logger: Logger;
|
|
11
|
+
auth: AuthData;
|
|
12
|
+
config: Config | null;
|
|
13
|
+
options: GlobalOptions;
|
|
14
|
+
orgId?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates an API client for queue operations.
|
|
19
|
+
*
|
|
20
|
+
* Queues are global resources that don't require a project context.
|
|
21
|
+
* Uses the global Catalyst API client with user authentication.
|
|
22
|
+
*/
|
|
23
|
+
export async function createQueueAPIClient(ctx: QueueContext): Promise<APIClient> {
|
|
24
|
+
return getGlobalCatalystAPIClient(ctx.logger, ctx.auth, ctx.config?.name);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates QueueApiOptions from the CLI context.
|
|
29
|
+
* Prioritizes explicit orgId on context, then falls back to global --org-id option.
|
|
30
|
+
*/
|
|
31
|
+
export function getQueueApiOptions(ctx: QueueContext): QueueApiOptions | undefined {
|
|
32
|
+
const orgId = ctx.orgId ?? ctx.options.orgId;
|
|
33
|
+
return orgId ? { orgId } : undefined;
|
|
34
|
+
}
|
|
@@ -5,10 +5,12 @@ import { YAML } from 'bun';
|
|
|
5
5
|
import * as tar from 'tar';
|
|
6
6
|
import { createCommand } from '../../../../types';
|
|
7
7
|
import * as tui from '../../../../tui';
|
|
8
|
+
import { ErrorCode } from '../../../../errors';
|
|
8
9
|
import { getCommand } from '../../../../command-prefix';
|
|
9
10
|
import {
|
|
10
11
|
snapshotBuildInit,
|
|
11
12
|
snapshotBuildFinalize,
|
|
13
|
+
snapshotUpload,
|
|
12
14
|
SnapshotBuildFileSchema,
|
|
13
15
|
} from '@agentuity/server';
|
|
14
16
|
import type { SnapshotFileInfo } from '@agentuity/server';
|
|
@@ -35,8 +37,13 @@ const SnapshotBuildResponseSchema = z.object({
|
|
|
35
37
|
.record(z.string(), z.string())
|
|
36
38
|
.optional()
|
|
37
39
|
.describe('User-defined metadata key-value pairs'),
|
|
40
|
+
error: z.string().optional().describe('Error message if build failed'),
|
|
41
|
+
malwareDetected: z.boolean().optional().describe('True if malware was detected'),
|
|
42
|
+
virusName: z.string().optional().describe('Name of detected virus'),
|
|
38
43
|
});
|
|
39
44
|
|
|
45
|
+
const MALWARE_REGEX = /malware detected \(([^)]+)\)/i;
|
|
46
|
+
|
|
40
47
|
interface FileEntry {
|
|
41
48
|
path: string;
|
|
42
49
|
absolutePath: string;
|
|
@@ -229,6 +236,33 @@ async function createTarGzArchive(
|
|
|
229
236
|
);
|
|
230
237
|
}
|
|
231
238
|
|
|
239
|
+
function createProgressStream(
|
|
240
|
+
file: ReturnType<typeof Bun.file>,
|
|
241
|
+
totalSize: number,
|
|
242
|
+
onProgress: (percent: number) => void
|
|
243
|
+
): ReadableStream<Uint8Array> {
|
|
244
|
+
let bytesRead = 0;
|
|
245
|
+
const reader = file.stream().getReader();
|
|
246
|
+
|
|
247
|
+
return new ReadableStream<Uint8Array>({
|
|
248
|
+
async pull(controller) {
|
|
249
|
+
const { done, value } = await reader.read();
|
|
250
|
+
if (done) {
|
|
251
|
+
controller.close();
|
|
252
|
+
onProgress(100);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
bytesRead += value.byteLength;
|
|
256
|
+
const percent = Math.min(99, Math.floor((bytesRead / totalSize) * 100));
|
|
257
|
+
onProgress(percent);
|
|
258
|
+
controller.enqueue(value);
|
|
259
|
+
},
|
|
260
|
+
cancel() {
|
|
261
|
+
reader.cancel();
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
232
266
|
async function generateContentHash(params: {
|
|
233
267
|
runtime: string;
|
|
234
268
|
description?: string;
|
|
@@ -236,6 +270,7 @@ async function generateContentHash(params: {
|
|
|
236
270
|
files: SnapshotFileInfo[];
|
|
237
271
|
fileHashes: Map<string, string>;
|
|
238
272
|
env?: Record<string, string>;
|
|
273
|
+
isPublic?: boolean;
|
|
239
274
|
}): Promise<string> {
|
|
240
275
|
const hash = createHash('sha256');
|
|
241
276
|
|
|
@@ -254,7 +289,8 @@ async function generateContentHash(params: {
|
|
|
254
289
|
const sortedFiles = [...params.files].sort((a, b) => a.path.localeCompare(b.path));
|
|
255
290
|
for (const file of sortedFiles) {
|
|
256
291
|
const contentHash = params.fileHashes.get(file.path) ?? '';
|
|
257
|
-
|
|
292
|
+
const mode = file.mode.toString(8).padStart(4, '0');
|
|
293
|
+
hash.update(`file:${file.path}:${file.size}:${contentHash}:${mode}:${file.contentType}\n`);
|
|
258
294
|
}
|
|
259
295
|
}
|
|
260
296
|
|
|
@@ -265,6 +301,8 @@ async function generateContentHash(params: {
|
|
|
265
301
|
}
|
|
266
302
|
}
|
|
267
303
|
|
|
304
|
+
hash.update(`access:${params.isPublic ? 'public' : 'private'}\n`);
|
|
305
|
+
|
|
268
306
|
return hash.digest('hex');
|
|
269
307
|
}
|
|
270
308
|
|
|
@@ -315,6 +353,14 @@ export const buildSubcommand = createCommand({
|
|
|
315
353
|
description: z.string().optional().describe('Snapshot description (overrides build file)'),
|
|
316
354
|
metadata: z.array(z.string()).optional().describe('Metadata key-value pairs (KEY=VALUE)'),
|
|
317
355
|
force: z.boolean().optional().describe('Force rebuild even if content is unchanged'),
|
|
356
|
+
public: z
|
|
357
|
+
.boolean()
|
|
358
|
+
.optional()
|
|
359
|
+
.describe('Make snapshot public (enables virus scanning, no encryption)'),
|
|
360
|
+
confirm: z
|
|
361
|
+
.boolean()
|
|
362
|
+
.optional()
|
|
363
|
+
.describe('Confirm public snapshot publishing (required for --public)'),
|
|
318
364
|
}),
|
|
319
365
|
response: SnapshotBuildResponseSchema,
|
|
320
366
|
},
|
|
@@ -323,6 +369,37 @@ export const buildSubcommand = createCommand({
|
|
|
323
369
|
const { args, opts, options, auth, region, config, logger, orgId } = ctx;
|
|
324
370
|
|
|
325
371
|
const dryRun = options.dryRun === true;
|
|
372
|
+
const isPublic = opts.public === true;
|
|
373
|
+
|
|
374
|
+
if (isPublic && !dryRun) {
|
|
375
|
+
if (!opts.confirm) {
|
|
376
|
+
if (!tui.isTTYLike()) {
|
|
377
|
+
logger.fatal(
|
|
378
|
+
`Publishing a public snapshot requires confirmation.\n\n` +
|
|
379
|
+
`Public snapshots make all environment variables and files publicly accessible.\n\n` +
|
|
380
|
+
`To proceed, add the --confirm flag:\n` +
|
|
381
|
+
` ${getCommand('cloud sandbox snapshot build . --public --confirm')}\n\n` +
|
|
382
|
+
`To preview what will be published, use --dry-run first:\n` +
|
|
383
|
+
` ${getCommand('cloud sandbox snapshot build . --public --dry-run')}`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
tui.warningBox(
|
|
388
|
+
'Public Snapshot',
|
|
389
|
+
`You are publishing a public snapshot.\n\n` +
|
|
390
|
+
`This will make all environment variables and\n` +
|
|
391
|
+
`files in the snapshot publicly accessible.\n\n` +
|
|
392
|
+
`Run with --dry-run to preview the contents.`
|
|
393
|
+
);
|
|
394
|
+
console.log('');
|
|
395
|
+
|
|
396
|
+
const confirmed = await tui.confirm('Proceed with public snapshot?', false);
|
|
397
|
+
|
|
398
|
+
if (!confirmed) {
|
|
399
|
+
logger.fatal('Aborted');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
326
403
|
|
|
327
404
|
const directory = resolve(args.directory);
|
|
328
405
|
if (!existsSync(directory)) {
|
|
@@ -457,21 +534,35 @@ export const buildSubcommand = createCommand({
|
|
|
457
534
|
files = await resolveFileGlobs(directory, buildConfig.files);
|
|
458
535
|
}
|
|
459
536
|
|
|
460
|
-
const
|
|
461
|
-
path: f.path,
|
|
462
|
-
size: f.size,
|
|
463
|
-
}));
|
|
464
|
-
const totalSize = fileList.reduce((sum, f) => sum + f.size, 0);
|
|
465
|
-
|
|
466
|
-
const fileHashes = new Map<string, string>();
|
|
537
|
+
const fileMetadata = new Map<string, { sha256: string; contentType: string; mode: number }>();
|
|
467
538
|
for (const file of files.values()) {
|
|
468
539
|
const fullPath = join(directory, file.path);
|
|
469
540
|
const bunFile = Bun.file(fullPath);
|
|
470
541
|
const content = await bunFile.arrayBuffer();
|
|
471
542
|
const hash = createHash('sha256').update(Buffer.from(content)).digest('hex');
|
|
472
|
-
|
|
543
|
+
const contentType = bunFile.type || 'application/octet-stream';
|
|
544
|
+
const stat = statSync(fullPath);
|
|
545
|
+
const mode = stat.mode & 0o7777; // Extract permission bits only
|
|
546
|
+
fileMetadata.set(file.path, { sha256: hash, contentType, mode });
|
|
473
547
|
}
|
|
474
548
|
|
|
549
|
+
const fileHashes = new Map<string, string>();
|
|
550
|
+
for (const [path, meta] of fileMetadata) {
|
|
551
|
+
fileHashes.set(path, meta.sha256);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const fileList: SnapshotFileInfo[] = Array.from(files.values()).map((f) => {
|
|
555
|
+
const meta = fileMetadata.get(f.path);
|
|
556
|
+
return {
|
|
557
|
+
path: f.path,
|
|
558
|
+
size: f.size,
|
|
559
|
+
sha256: meta?.sha256 ?? '',
|
|
560
|
+
contentType: meta?.contentType ?? 'application/octet-stream',
|
|
561
|
+
mode: meta?.mode ?? 0o644,
|
|
562
|
+
};
|
|
563
|
+
});
|
|
564
|
+
const totalSize = fileList.reduce((sum, f) => sum + f.size, 0);
|
|
565
|
+
|
|
475
566
|
const contentHash = await generateContentHash({
|
|
476
567
|
runtime: buildConfig.runtime,
|
|
477
568
|
description: finalDescription,
|
|
@@ -479,6 +570,7 @@ export const buildSubcommand = createCommand({
|
|
|
479
570
|
files: fileList,
|
|
480
571
|
fileHashes,
|
|
481
572
|
env: finalEnv,
|
|
573
|
+
isPublic,
|
|
482
574
|
});
|
|
483
575
|
|
|
484
576
|
if (dryRun) {
|
|
@@ -492,11 +584,12 @@ export const buildSubcommand = createCommand({
|
|
|
492
584
|
Description: finalDescription ?? '-',
|
|
493
585
|
Runtime: buildConfig.runtime,
|
|
494
586
|
Tag: opts.tag ?? 'latest',
|
|
587
|
+
Access: isPublic ? 'public' : 'private',
|
|
495
588
|
Size: tui.formatBytes(totalSize),
|
|
496
589
|
Files: fileList.length.toFixed(),
|
|
497
590
|
},
|
|
498
591
|
],
|
|
499
|
-
['Name', 'Description', 'Runtime', 'Tag', 'Size', 'Files'],
|
|
592
|
+
['Name', 'Description', 'Runtime', 'Tag', 'Access', 'Size', 'Files'],
|
|
500
593
|
{ layout: 'vertical', padStart: ' ' }
|
|
501
594
|
);
|
|
502
595
|
|
|
@@ -572,11 +665,12 @@ export const buildSubcommand = createCommand({
|
|
|
572
665
|
return await snapshotBuildInit(client, {
|
|
573
666
|
runtime: buildConfig.runtime,
|
|
574
667
|
name: finalName,
|
|
575
|
-
tag: opts.tag,
|
|
668
|
+
tag: opts.tag ?? 'latest',
|
|
576
669
|
description: finalDescription,
|
|
577
670
|
contentHash,
|
|
578
671
|
force: opts.force,
|
|
579
|
-
encrypt:
|
|
672
|
+
encrypt: !isPublic,
|
|
673
|
+
public: isPublic,
|
|
580
674
|
orgId,
|
|
581
675
|
});
|
|
582
676
|
},
|
|
@@ -611,7 +705,7 @@ export const buildSubcommand = createCommand({
|
|
|
611
705
|
};
|
|
612
706
|
}
|
|
613
707
|
|
|
614
|
-
// Encrypt the archive if public key is provided
|
|
708
|
+
// Encrypt the archive if public key is provided (private snapshots only)
|
|
615
709
|
let uploadPath = archivePath;
|
|
616
710
|
let uploadSize = archiveSize;
|
|
617
711
|
|
|
@@ -646,28 +740,89 @@ export const buildSubcommand = createCommand({
|
|
|
646
740
|
uploadSize = Bun.file(encryptedPath).size;
|
|
647
741
|
}
|
|
648
742
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
743
|
+
if (initResult.uploadUrl) {
|
|
744
|
+
// Private snapshot: upload directly to S3
|
|
745
|
+
// Use Bun.file() directly as body - Bun sets Content-Length automatically from file size
|
|
746
|
+
await tui.spinner({
|
|
747
|
+
message: 'Uploading snapshot...',
|
|
748
|
+
clearOnSuccess: true,
|
|
749
|
+
callback: async () => {
|
|
750
|
+
const uploadFile = Bun.file(uploadPath);
|
|
751
|
+
const response = await fetch(initResult.uploadUrl!, {
|
|
752
|
+
method: 'PUT',
|
|
753
|
+
headers: {
|
|
754
|
+
'Content-Type': 'application/gzip',
|
|
755
|
+
},
|
|
756
|
+
body: uploadFile,
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
if (!response.ok) {
|
|
760
|
+
throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
} else {
|
|
765
|
+
// Public snapshot: upload via Catalyst (with virus scanning)
|
|
766
|
+
try {
|
|
767
|
+
await tui.spinner({
|
|
768
|
+
message: 'Uploading and scanning snapshot...',
|
|
769
|
+
type: 'progress',
|
|
770
|
+
clearOnSuccess: true,
|
|
771
|
+
clearOnError: true,
|
|
772
|
+
callback: async (updateProgress) => {
|
|
773
|
+
const uploadFile = Bun.file(uploadPath);
|
|
774
|
+
const progressStream = createProgressStream(uploadFile, uploadSize, updateProgress);
|
|
775
|
+
await snapshotUpload(client, {
|
|
776
|
+
snapshotId: initResult.snapshotId!,
|
|
777
|
+
body: progressStream,
|
|
778
|
+
contentLength: uploadSize,
|
|
779
|
+
orgId,
|
|
780
|
+
});
|
|
660
781
|
},
|
|
661
|
-
body: uploadFile,
|
|
662
782
|
});
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
783
|
+
} catch (err) {
|
|
784
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
785
|
+
const malwareMatch = MALWARE_REGEX.exec(errorMessage);
|
|
786
|
+
|
|
787
|
+
if (malwareMatch) {
|
|
788
|
+
const virusName = malwareMatch[1];
|
|
789
|
+
|
|
790
|
+
if (options.json) {
|
|
791
|
+
console.log(
|
|
792
|
+
JSON.stringify(
|
|
793
|
+
{
|
|
794
|
+
snapshotId: '',
|
|
795
|
+
name: finalName ?? '',
|
|
796
|
+
tag: opts.tag ?? 'latest',
|
|
797
|
+
runtime: buildConfig.runtime,
|
|
798
|
+
sizeBytes: totalSize,
|
|
799
|
+
fileCount: fileList.length,
|
|
800
|
+
createdAt: new Date().toISOString(),
|
|
801
|
+
error: errorMessage,
|
|
802
|
+
malwareDetected: true,
|
|
803
|
+
virusName,
|
|
804
|
+
},
|
|
805
|
+
null,
|
|
806
|
+
2
|
|
807
|
+
)
|
|
808
|
+
);
|
|
809
|
+
process.exit(ErrorCode.MALWARE_DETECTED);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
console.log('');
|
|
813
|
+
tui.errorBox(
|
|
814
|
+
'Malware Detected',
|
|
815
|
+
`Your snapshot was rejected because it contains malware.\n\nVirus: ${virusName}\n\nPlease remove the infected files and try again.`
|
|
816
|
+
);
|
|
817
|
+
tui.fatal(
|
|
818
|
+
'Snapshot build failed due to malware detection',
|
|
819
|
+
ErrorCode.MALWARE_DETECTED
|
|
820
|
+
);
|
|
666
821
|
}
|
|
667
822
|
|
|
668
|
-
|
|
669
|
-
}
|
|
670
|
-
}
|
|
823
|
+
throw err;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
671
826
|
|
|
672
827
|
const snapshot = await tui.spinner({
|
|
673
828
|
message: 'Finalizing snapshot...',
|