@output.ai/cli 0.3.0-dev.pr156.c8e7f40 → 0.3.1-dev.pr156.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/generated/api.d.ts +34 -34
- package/dist/api/generated/api.js +9 -0
- package/dist/assets/docker/docker-compose-dev.yml +2 -1
- package/dist/commands/workflow/debug.d.ts +9 -3
- package/dist/commands/workflow/debug.js +167 -25
- package/dist/commands/workflow/debug.test.js +53 -21
- package/dist/utils/s3_downloader.d.ts +49 -0
- package/dist/utils/s3_downloader.js +154 -0
- package/dist/utils/trace_formatter.d.ts +61 -27
- package/dist/utils/trace_formatter.js +239 -447
- package/package.json +1 -1
- package/dist/services/trace_reader.d.ts +0 -8
- package/dist/services/trace_reader.js +0 -51
- package/dist/services/trace_reader.test.d.ts +0 -1
- package/dist/services/trace_reader.test.js +0 -163
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
export class S3Downloader {
|
|
6
|
+
s3Client = null;
|
|
7
|
+
cacheDir;
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.cacheDir = options.cacheDir || path.join(process.cwd(), '.output', 'traces');
|
|
10
|
+
this.initializeS3Client();
|
|
11
|
+
}
|
|
12
|
+
initializeS3Client() {
|
|
13
|
+
const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1';
|
|
14
|
+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
|
|
15
|
+
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
|
|
16
|
+
if (!accessKeyId || !secretAccessKey) {
|
|
17
|
+
// S3 client not available without credentials
|
|
18
|
+
this.s3Client = null;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
this.s3Client = new S3Client({
|
|
22
|
+
region,
|
|
23
|
+
credentials: {
|
|
24
|
+
accessKeyId,
|
|
25
|
+
secretAccessKey
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse S3 URL to extract bucket and key
|
|
31
|
+
* Supports formats:
|
|
32
|
+
* - https://bucket.s3.amazonaws.com/path/to/file
|
|
33
|
+
* - https://bucket.s3.region.amazonaws.com/path/to/file
|
|
34
|
+
* - s3://bucket/path/to/file
|
|
35
|
+
*/
|
|
36
|
+
parseS3Url(url) {
|
|
37
|
+
// Handle s3:// protocol
|
|
38
|
+
if (url.startsWith('s3://')) {
|
|
39
|
+
const withoutProtocol = url.slice(5);
|
|
40
|
+
const firstSlash = withoutProtocol.indexOf('/');
|
|
41
|
+
if (firstSlash === -1) {
|
|
42
|
+
throw new Error(`Invalid S3 URL format: ${url}`);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
bucket: withoutProtocol.substring(0, firstSlash),
|
|
46
|
+
key: withoutProtocol.substring(firstSlash + 1)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Handle https:// URLs
|
|
50
|
+
if (url.startsWith('https://')) {
|
|
51
|
+
const urlObj = new URL(url);
|
|
52
|
+
const hostname = urlObj.hostname;
|
|
53
|
+
// Extract bucket from hostname
|
|
54
|
+
// Format: bucket.s3.amazonaws.com or bucket.s3.region.amazonaws.com
|
|
55
|
+
const s3Match = hostname.match(/^([^.]+)\.s3(?:\.[^.]+)?\.amazonaws\.com$/);
|
|
56
|
+
if (!s3Match) {
|
|
57
|
+
throw new Error(`Invalid S3 URL format: ${url}`);
|
|
58
|
+
}
|
|
59
|
+
const bucket = s3Match[1];
|
|
60
|
+
const key = urlObj.pathname.startsWith('/') ? urlObj.pathname.slice(1) : urlObj.pathname;
|
|
61
|
+
return { bucket, key };
|
|
62
|
+
}
|
|
63
|
+
throw new Error(`Unsupported S3 URL format: ${url}`);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get cached file path for a given S3 URL
|
|
67
|
+
*/
|
|
68
|
+
getCachePath(url) {
|
|
69
|
+
const { key } = this.parseS3Url(url);
|
|
70
|
+
// Use the last part of the key as filename to preserve the original name
|
|
71
|
+
const filename = path.basename(key);
|
|
72
|
+
return path.join(this.cacheDir, filename);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Download a file from S3
|
|
76
|
+
*/
|
|
77
|
+
async download(s3Url, options = {}) {
|
|
78
|
+
if (!this.s3Client) {
|
|
79
|
+
throw new Error('AWS credentials not configured. Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.');
|
|
80
|
+
}
|
|
81
|
+
// Check cache first
|
|
82
|
+
const cachePath = this.getCachePath(s3Url);
|
|
83
|
+
if (!options.forceDownload && existsSync(cachePath)) {
|
|
84
|
+
const content = await fs.readFile(cachePath, 'utf-8');
|
|
85
|
+
return content;
|
|
86
|
+
}
|
|
87
|
+
// Parse S3 URL
|
|
88
|
+
const { bucket, key } = this.parseS3Url(s3Url);
|
|
89
|
+
try {
|
|
90
|
+
// Download from S3
|
|
91
|
+
const command = new GetObjectCommand({
|
|
92
|
+
Bucket: bucket,
|
|
93
|
+
Key: key
|
|
94
|
+
});
|
|
95
|
+
const response = await this.s3Client.send(command);
|
|
96
|
+
if (!response.Body) {
|
|
97
|
+
throw new Error('Empty response from S3');
|
|
98
|
+
}
|
|
99
|
+
// Convert stream to string
|
|
100
|
+
const bodyContents = await response.Body.transformToString();
|
|
101
|
+
// Cache the file
|
|
102
|
+
await this.ensureCacheDir();
|
|
103
|
+
await fs.writeFile(cachePath, bodyContents, 'utf-8');
|
|
104
|
+
return bodyContents;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error instanceof Error) {
|
|
108
|
+
throw new Error(`Failed to download from S3: ${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Check if S3 client is available (credentials configured)
|
|
115
|
+
*/
|
|
116
|
+
isAvailable() {
|
|
117
|
+
return this.s3Client !== null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Ensure cache directory exists
|
|
121
|
+
*/
|
|
122
|
+
async ensureCacheDir() {
|
|
123
|
+
await fs.mkdir(this.cacheDir, { recursive: true });
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Clear the cache directory
|
|
127
|
+
*/
|
|
128
|
+
async clearCache() {
|
|
129
|
+
if (existsSync(this.cacheDir)) {
|
|
130
|
+
await fs.rm(this.cacheDir, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get the size of the cache directory in bytes
|
|
135
|
+
*/
|
|
136
|
+
async getCacheSize() {
|
|
137
|
+
if (!existsSync(this.cacheDir)) {
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
const files = await fs.readdir(this.cacheDir);
|
|
141
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
142
|
+
let totalSize = 0;
|
|
143
|
+
for (const file of files) {
|
|
144
|
+
const filePath = path.join(this.cacheDir, file);
|
|
145
|
+
const stats = await fs.stat(filePath);
|
|
146
|
+
totalSize += stats.size;
|
|
147
|
+
}
|
|
148
|
+
return totalSize;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Create a singleton instance for convenience
|
|
153
|
+
*/
|
|
154
|
+
export const s3Downloader = new S3Downloader();
|
|
@@ -1,27 +1,61 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
export declare class TraceFormatter {
|
|
2
|
+
/**
|
|
3
|
+
* Format trace data based on the requested format
|
|
4
|
+
*/
|
|
5
|
+
format(traceData: string | object, format?: 'json' | 'text'): string;
|
|
6
|
+
/**
|
|
7
|
+
* Format trace as human-readable text with tree structure
|
|
8
|
+
*/
|
|
9
|
+
private formatAsText;
|
|
10
|
+
/**
|
|
11
|
+
* Format the header with workflow information
|
|
12
|
+
*/
|
|
13
|
+
private formatHeader;
|
|
14
|
+
/**
|
|
15
|
+
* Format events as a timeline table
|
|
16
|
+
*/
|
|
17
|
+
private formatEventsTable;
|
|
18
|
+
/**
|
|
19
|
+
* Format trace as a tree structure
|
|
20
|
+
*/
|
|
21
|
+
private formatTree;
|
|
22
|
+
/**
|
|
23
|
+
* Get a readable name for an event
|
|
24
|
+
*/
|
|
25
|
+
private getEventName;
|
|
26
|
+
/**
|
|
27
|
+
* Format the phase with icons
|
|
28
|
+
*/
|
|
29
|
+
private formatPhase;
|
|
30
|
+
/**
|
|
31
|
+
* Format duration in human-readable format
|
|
32
|
+
*/
|
|
33
|
+
private formatDuration;
|
|
34
|
+
/**
|
|
35
|
+
* Format error for display
|
|
36
|
+
*/
|
|
37
|
+
private formatError;
|
|
38
|
+
/**
|
|
39
|
+
* Format details for table display
|
|
40
|
+
*/
|
|
41
|
+
private formatDetails;
|
|
42
|
+
/**
|
|
43
|
+
* Format details for tree display
|
|
44
|
+
*/
|
|
45
|
+
private formatTreeDetails;
|
|
46
|
+
/**
|
|
47
|
+
* Truncate long values for display
|
|
48
|
+
*/
|
|
49
|
+
private truncateValue;
|
|
50
|
+
/**
|
|
51
|
+
* Get summary statistics from trace
|
|
52
|
+
*/
|
|
53
|
+
getSummary(traceData: string | object): {
|
|
54
|
+
totalDuration: number;
|
|
55
|
+
totalEvents: number;
|
|
56
|
+
totalSteps: number;
|
|
57
|
+
totalActivities: number;
|
|
58
|
+
hasErrors: boolean;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export declare const traceFormatter: TraceFormatter;
|