@agentuity/cli 0.1.42 → 0.1.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cmd/build/entry-generator.d.ts.map +1 -1
- package/dist/cmd/build/entry-generator.js +137 -1
- package/dist/cmd/build/entry-generator.js.map +1 -1
- package/dist/cmd/build/vite/metadata-generator.d.ts.map +1 -1
- package/dist/cmd/build/vite/metadata-generator.js +19 -9
- package/dist/cmd/build/vite/metadata-generator.js.map +1 -1
- package/dist/cmd/build/vite/public-asset-path-plugin.d.ts +25 -13
- package/dist/cmd/build/vite/public-asset-path-plugin.d.ts.map +1 -1
- package/dist/cmd/build/vite/public-asset-path-plugin.js +66 -40
- package/dist/cmd/build/vite/public-asset-path-plugin.js.map +1 -1
- package/dist/cmd/build/vite/vite-asset-server-config.d.ts.map +1 -1
- package/dist/cmd/build/vite/vite-asset-server-config.js +6 -5
- package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
- package/dist/cmd/build/vite/vite-asset-server.d.ts.map +1 -1
- package/dist/cmd/build/vite/vite-asset-server.js +1 -1
- package/dist/cmd/build/vite/vite-asset-server.js.map +1 -1
- package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
- package/dist/cmd/build/vite/vite-builder.js +12 -11
- package/dist/cmd/build/vite/vite-builder.js.map +1 -1
- package/dist/cmd/support/report.d.ts.map +1 -1
- package/dist/cmd/support/report.js +58 -23
- package/dist/cmd/support/report.js.map +1 -1
- package/dist/internal-logger.d.ts +7 -0
- package/dist/internal-logger.d.ts.map +1 -1
- package/dist/internal-logger.js +82 -28
- package/dist/internal-logger.js.map +1 -1
- package/package.json +8 -7
- package/src/cmd/build/entry-generator.ts +137 -1
- package/src/cmd/build/vite/metadata-generator.ts +20 -9
- package/src/cmd/build/vite/public-asset-path-plugin.ts +76 -46
- package/src/cmd/build/vite/vite-asset-server-config.ts +6 -5
- package/src/cmd/build/vite/vite-asset-server.ts +3 -1
- package/src/cmd/build/vite/vite-builder.ts +21 -20
- package/src/cmd/support/report.ts +82 -28
- package/src/internal-logger.ts +91 -27
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
import { createSubcommand } from '../../types';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
4
|
-
import { join } from 'node:path';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { join, basename } from 'node:path';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
|
-
import {
|
|
6
|
+
import { getLogSessionsInCurrentWindow } from '../../internal-logger';
|
|
7
7
|
import * as tui from '../../tui';
|
|
8
8
|
import { randomBytes } from 'node:crypto';
|
|
9
9
|
import AdmZip from 'adm-zip';
|
|
10
10
|
import { APIResponseSchema } from '@agentuity/server';
|
|
11
|
+
import { StructuredError } from '@agentuity/core';
|
|
12
|
+
|
|
13
|
+
// Structured errors for this module
|
|
14
|
+
const NoSessionDirectoriesError = StructuredError(
|
|
15
|
+
'NoSessionDirectoriesError',
|
|
16
|
+
'No session directories provided'
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const ReportUploadError = StructuredError('ReportUploadError')<{
|
|
20
|
+
statusText: string;
|
|
21
|
+
status?: number;
|
|
22
|
+
}>();
|
|
23
|
+
|
|
24
|
+
const UploadUrlCreationError = StructuredError('UploadUrlCreationError');
|
|
25
|
+
|
|
26
|
+
const BrowserOpenError = StructuredError(
|
|
27
|
+
'BrowserOpenError',
|
|
28
|
+
'Failed to open browser. Please open the URL manually.'
|
|
29
|
+
)<{
|
|
30
|
+
exitCode?: number | null;
|
|
31
|
+
}>();
|
|
11
32
|
|
|
12
33
|
const argsSchema = z.object({});
|
|
13
34
|
|
|
@@ -33,22 +54,34 @@ const ReportUploadDataSchema = z.object({
|
|
|
33
54
|
const ReportUploadResponseSchema = APIResponseSchema(ReportUploadDataSchema);
|
|
34
55
|
|
|
35
56
|
/**
|
|
36
|
-
* Create a zip file containing session and logs
|
|
57
|
+
* Create a zip file containing session and logs from multiple session directories
|
|
37
58
|
*/
|
|
38
|
-
async function createReportZip(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (!existsSync(sessionFile) || !existsSync(logsFile)) {
|
|
43
|
-
throw new Error('Session files not found');
|
|
59
|
+
async function createReportZip(sessionDirs: string[]): Promise<string> {
|
|
60
|
+
if (sessionDirs.length === 0) {
|
|
61
|
+
throw NoSessionDirectoriesError();
|
|
44
62
|
}
|
|
45
63
|
|
|
46
64
|
// Create zip in temp directory
|
|
47
65
|
const tempZip = join(tmpdir(), `agentuity-report-${randomBytes(8).toString('hex')}.zip`);
|
|
48
66
|
|
|
49
67
|
const zip = new AdmZip();
|
|
50
|
-
|
|
51
|
-
|
|
68
|
+
|
|
69
|
+
for (const sessionDir of sessionDirs) {
|
|
70
|
+
const sessionFile = join(sessionDir, 'session.json');
|
|
71
|
+
const logsFile = join(sessionDir, 'logs.jsonl');
|
|
72
|
+
|
|
73
|
+
// Extract session ID from directory name cross-platform
|
|
74
|
+
const sessionId = basename(sessionDir) || 'unknown';
|
|
75
|
+
|
|
76
|
+
// Add files with session ID prefix to avoid conflicts
|
|
77
|
+
if (await Bun.file(sessionFile).exists()) {
|
|
78
|
+
zip.addLocalFile(sessionFile, sessionId);
|
|
79
|
+
}
|
|
80
|
+
if (await Bun.file(logsFile).exists()) {
|
|
81
|
+
zip.addLocalFile(logsFile, sessionId);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
52
85
|
zip.writeZip(tempZip);
|
|
53
86
|
|
|
54
87
|
return tempZip;
|
|
@@ -76,7 +109,11 @@ async function uploadReport(
|
|
|
76
109
|
if (!response.ok) {
|
|
77
110
|
const errorText = await response.text();
|
|
78
111
|
logger.error('Upload failed', { status: response.status, error: errorText });
|
|
79
|
-
throw new
|
|
112
|
+
throw new ReportUploadError({
|
|
113
|
+
message: `Upload failed: ${response.statusText}`,
|
|
114
|
+
statusText: response.statusText,
|
|
115
|
+
status: response.status,
|
|
116
|
+
});
|
|
80
117
|
}
|
|
81
118
|
}
|
|
82
119
|
|
|
@@ -161,11 +198,11 @@ async function openBrowser(url: string, logger: import('../../types').Logger): P
|
|
|
161
198
|
await proc.exited;
|
|
162
199
|
|
|
163
200
|
if (proc.exitCode !== 0) {
|
|
164
|
-
throw new
|
|
201
|
+
throw new BrowserOpenError({ exitCode: proc.exitCode });
|
|
165
202
|
}
|
|
166
203
|
} catch (error) {
|
|
167
204
|
logger.error('Failed to open browser', { error });
|
|
168
|
-
throw new
|
|
205
|
+
throw new BrowserOpenError({ exitCode: null, cause: error });
|
|
169
206
|
}
|
|
170
207
|
}
|
|
171
208
|
|
|
@@ -184,9 +221,9 @@ export default createSubcommand({
|
|
|
184
221
|
const { opts, logger, apiClient } = ctx;
|
|
185
222
|
const isJsonMode = ctx.options.json;
|
|
186
223
|
|
|
187
|
-
// Get the
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
224
|
+
// Get all log sessions in the current time window (current + previous bucket)
|
|
225
|
+
const sessionDirs = getLogSessionsInCurrentWindow();
|
|
226
|
+
if (sessionDirs.length === 0) {
|
|
190
227
|
if (isJsonMode) {
|
|
191
228
|
console.log(JSON.stringify({ success: false, error: 'No CLI logs found' }));
|
|
192
229
|
} else {
|
|
@@ -196,10 +233,27 @@ export default createSubcommand({
|
|
|
196
233
|
return;
|
|
197
234
|
}
|
|
198
235
|
|
|
199
|
-
//
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
|
|
236
|
+
// Use the first (most recent) session for metadata
|
|
237
|
+
const primarySessionDir = sessionDirs[0]!;
|
|
238
|
+
const sessionFile = join(primarySessionDir, 'session.json');
|
|
239
|
+
|
|
240
|
+
// Safely read session data with fallback for corrupt/missing session.json
|
|
241
|
+
let sessionData: SessionData = {};
|
|
242
|
+
let cliVersion = 'unknown';
|
|
243
|
+
try {
|
|
244
|
+
if (await Bun.file(sessionFile).exists()) {
|
|
245
|
+
sessionData = JSON.parse(readFileSync(sessionFile, 'utf-8'));
|
|
246
|
+
cliVersion = sessionData.cli?.version || 'unknown';
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
// Fall back to defaults if session.json is corrupt or unreadable
|
|
250
|
+
logger.trace('Failed to read session.json, using defaults');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Log how many sessions we're including
|
|
254
|
+
if (!isJsonMode && sessionDirs.length > 1) {
|
|
255
|
+
tui.info(`Found ${sessionDirs.length} session(s) in the current time window`);
|
|
256
|
+
}
|
|
203
257
|
|
|
204
258
|
// Get issue description from:
|
|
205
259
|
// 1. --description flag
|
|
@@ -281,11 +335,11 @@ export default createSubcommand({
|
|
|
281
335
|
// Debug: log the response
|
|
282
336
|
logger.debug('Upload response received', { uploadResponse });
|
|
283
337
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
338
|
+
if (!uploadResponse.success) {
|
|
339
|
+
const errorMsg = uploadResponse.message || 'Failed to create upload URL';
|
|
340
|
+
logger.error('Upload URL creation failed', { uploadResponse, errorMsg });
|
|
341
|
+
throw new UploadUrlCreationError({ message: errorMsg });
|
|
342
|
+
}
|
|
289
343
|
|
|
290
344
|
const { presigned_url, url: reportUrl, report_id: reportId } = uploadResponse.data;
|
|
291
345
|
|
|
@@ -294,7 +348,7 @@ export default createSubcommand({
|
|
|
294
348
|
tui.info('Creating report archive...');
|
|
295
349
|
}
|
|
296
350
|
|
|
297
|
-
const zipPath = await createReportZip(
|
|
351
|
+
const zipPath = await createReportZip(sessionDirs);
|
|
298
352
|
|
|
299
353
|
// Step 3: Upload to S3
|
|
300
354
|
if (!isJsonMode) {
|
package/src/internal-logger.ts
CHANGED
|
@@ -45,6 +45,9 @@ const SENSITIVE_ENV_PATTERNS = [
|
|
|
45
45
|
|
|
46
46
|
interface SessionMetadata {
|
|
47
47
|
sessionId: string;
|
|
48
|
+
bucket: number;
|
|
49
|
+
pid: number;
|
|
50
|
+
ppid: number;
|
|
48
51
|
command: string;
|
|
49
52
|
args: string[];
|
|
50
53
|
timestamp: string;
|
|
@@ -107,9 +110,30 @@ function getLogsDir(): string {
|
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
/**
|
|
110
|
-
*
|
|
113
|
+
* Calculate the current 5-minute bucket number
|
|
114
|
+
* Each bucket represents a 5-minute window (300000ms)
|
|
111
115
|
*/
|
|
112
|
-
function
|
|
116
|
+
function getCurrentBucket(): number {
|
|
117
|
+
return Math.floor(Date.now() / 300000);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Parse bucket number from directory name
|
|
122
|
+
* Directory format: {bucket}-{uuid}
|
|
123
|
+
* Returns null for legacy directories (uuid-only format)
|
|
124
|
+
*/
|
|
125
|
+
function parseBucketFromDirName(dirName: string): number | null {
|
|
126
|
+
const match = dirName.match(/^(\d+)-/);
|
|
127
|
+
if (match && match[1]) {
|
|
128
|
+
return parseInt(match[1], 10);
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Clean up old log directories, keeping only directories in the current bucket
|
|
135
|
+
*/
|
|
136
|
+
function cleanupOldLogs(currentBucket: number): void {
|
|
113
137
|
// Skip cleanup when inheriting a parent's session ID to avoid
|
|
114
138
|
// deleting the parent's session directory (race condition).
|
|
115
139
|
// This applies to forked deploy processes and any subprocess
|
|
@@ -125,19 +149,24 @@ function cleanupOldLogs(currentSessionId: string): void {
|
|
|
125
149
|
|
|
126
150
|
try {
|
|
127
151
|
const entries = readdirSync(logsDir, { withFileTypes: true });
|
|
128
|
-
const dirs = entries
|
|
129
|
-
.filter((e) => e.isDirectory())
|
|
130
|
-
.map((e) => e.name)
|
|
131
|
-
.filter((name) => name !== currentSessionId);
|
|
152
|
+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
132
153
|
|
|
133
|
-
// Remove
|
|
154
|
+
// Remove directories with bucket < currentBucket (old buckets)
|
|
155
|
+
// Also remove legacy directories (no bucket prefix) for backward compatibility
|
|
134
156
|
for (const dir of dirs) {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
157
|
+
const bucket = parseBucketFromDirName(dir);
|
|
158
|
+
|
|
159
|
+
// Delete if:
|
|
160
|
+
// 1. Legacy directory (no bucket prefix) - clean up old format
|
|
161
|
+
// 2. Bucket is older than current bucket
|
|
162
|
+
if (bucket === null || bucket < currentBucket) {
|
|
163
|
+
const dirPath = join(logsDir, dir);
|
|
164
|
+
try {
|
|
165
|
+
rmSync(dirPath, { recursive: true, force: true });
|
|
166
|
+
} catch (err) {
|
|
167
|
+
// Ignore errors during cleanup
|
|
168
|
+
console.debug(`Failed to remove old log directory ${dir}: ${err}`);
|
|
169
|
+
}
|
|
141
170
|
}
|
|
142
171
|
}
|
|
143
172
|
} catch (err) {
|
|
@@ -154,6 +183,7 @@ export class InternalLogger implements Logger {
|
|
|
154
183
|
private sessionDir: string;
|
|
155
184
|
private sessionFile: string;
|
|
156
185
|
private logsFile: string;
|
|
186
|
+
private bucket: number;
|
|
157
187
|
private initialized = false;
|
|
158
188
|
private disabled = false;
|
|
159
189
|
|
|
@@ -161,16 +191,23 @@ export class InternalLogger implements Logger {
|
|
|
161
191
|
private cliVersion: string,
|
|
162
192
|
private cliName: string
|
|
163
193
|
) {
|
|
194
|
+
// Calculate current 5-minute bucket
|
|
195
|
+
this.bucket = getCurrentBucket();
|
|
196
|
+
|
|
164
197
|
// When a parent session ID is set in the environment, use it to ensure
|
|
165
198
|
// all CLI invocations (parent and any subprocesses) write to the same log file.
|
|
166
199
|
// This prevents race conditions where child processes delete parent's logs.
|
|
167
200
|
const parentSessionId = process.env.AGENTUITY_INTERNAL_SESSION_ID;
|
|
168
201
|
if (parentSessionId) {
|
|
169
202
|
this.sessionId = parentSessionId;
|
|
203
|
+
// Parent session ID already includes bucket prefix, use as-is for directory name
|
|
204
|
+
this.sessionDir = join(getLogsDir(), parentSessionId);
|
|
170
205
|
} else {
|
|
171
|
-
|
|
206
|
+
// Generate new session ID with bucket prefix: {bucket}-{uuid}
|
|
207
|
+
const uuid = randomUUID();
|
|
208
|
+
this.sessionId = `${this.bucket}-${uuid}`;
|
|
209
|
+
this.sessionDir = join(getLogsDir(), this.sessionId);
|
|
172
210
|
}
|
|
173
|
-
this.sessionDir = join(getLogsDir(), this.sessionId);
|
|
174
211
|
this.sessionFile = join(this.sessionDir, 'session.json');
|
|
175
212
|
this.logsFile = join(this.sessionDir, 'logs.jsonl');
|
|
176
213
|
}
|
|
@@ -189,9 +226,9 @@ export class InternalLogger implements Logger {
|
|
|
189
226
|
// Create logs directory (may already exist if we're a child process)
|
|
190
227
|
mkdirSync(this.sessionDir, { recursive: true, mode: 0o700 });
|
|
191
228
|
|
|
192
|
-
// Clean up old logs (
|
|
229
|
+
// Clean up old logs (directories with bucket < current bucket)
|
|
193
230
|
// This is skipped for child processes to avoid deleting parent's session
|
|
194
|
-
cleanupOldLogs(this.
|
|
231
|
+
cleanupOldLogs(this.bucket);
|
|
195
232
|
|
|
196
233
|
// When inheriting a parent's session ID, skip session.json creation
|
|
197
234
|
// (parent already created it) but enable logging
|
|
@@ -232,6 +269,9 @@ export class InternalLogger implements Logger {
|
|
|
232
269
|
// Gather session metadata
|
|
233
270
|
const sessionMetadata: SessionMetadata = {
|
|
234
271
|
sessionId: this.sessionId,
|
|
272
|
+
bucket: this.bucket,
|
|
273
|
+
pid: process.pid,
|
|
274
|
+
ppid: process.ppid,
|
|
235
275
|
command,
|
|
236
276
|
args,
|
|
237
277
|
timestamp: new Date().toISOString(),
|
|
@@ -422,30 +462,54 @@ export function createInternalLogger(cliVersion: string, cliName: string): Inter
|
|
|
422
462
|
}
|
|
423
463
|
|
|
424
464
|
/**
|
|
425
|
-
* Get
|
|
465
|
+
* Get all log session directories in the current time window
|
|
466
|
+
* Returns directories from current bucket AND previous bucket (to handle boundary cases)
|
|
426
467
|
*/
|
|
427
|
-
export function
|
|
468
|
+
export function getLogSessionsInCurrentWindow(): string[] {
|
|
428
469
|
const logsDir = getLogsDir();
|
|
429
470
|
if (!existsSync(logsDir)) {
|
|
430
|
-
return
|
|
471
|
+
return [];
|
|
431
472
|
}
|
|
432
473
|
|
|
433
474
|
try {
|
|
475
|
+
const currentBucket = getCurrentBucket();
|
|
476
|
+
const previousBucket = currentBucket - 1;
|
|
477
|
+
|
|
434
478
|
const entries = readdirSync(logsDir, { withFileTypes: true });
|
|
435
479
|
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
436
480
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
481
|
+
// Filter to directories in current or previous bucket
|
|
482
|
+
const validDirs = dirs.filter((dir) => {
|
|
483
|
+
const bucket = parseBucketFromDirName(dir);
|
|
484
|
+
// Include current bucket, previous bucket, and legacy directories (for backward compat)
|
|
485
|
+
return bucket === currentBucket || bucket === previousBucket || bucket === null;
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Sort by bucket (descending) then by name to get most recent first
|
|
489
|
+
validDirs.sort((a, b) => {
|
|
490
|
+
const bucketA = parseBucketFromDirName(a) ?? 0;
|
|
491
|
+
const bucketB = parseBucketFromDirName(b) ?? 0;
|
|
492
|
+
if (bucketA !== bucketB) {
|
|
493
|
+
return bucketB - bucketA; // Higher bucket first
|
|
494
|
+
}
|
|
495
|
+
return b.localeCompare(a); // Then by name descending
|
|
496
|
+
});
|
|
441
497
|
|
|
442
|
-
|
|
443
|
-
return join(logsDir, firstDir);
|
|
498
|
+
return validDirs.map((dir) => join(logsDir, dir));
|
|
444
499
|
} catch {
|
|
445
|
-
return
|
|
500
|
+
return [];
|
|
446
501
|
}
|
|
447
502
|
}
|
|
448
503
|
|
|
504
|
+
/**
|
|
505
|
+
* Get the latest log session directory (if any)
|
|
506
|
+
* For backward compatibility, returns the first session in the current window
|
|
507
|
+
*/
|
|
508
|
+
export function getLatestLogSession(): string | null {
|
|
509
|
+
const sessions = getLogSessionsInCurrentWindow();
|
|
510
|
+
return sessions[0] ?? null;
|
|
511
|
+
}
|
|
512
|
+
|
|
449
513
|
/**
|
|
450
514
|
* Get the logs directory path (exported for external use)
|
|
451
515
|
*/
|