@agentuity/cli 0.1.42 → 0.1.44
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/auth.d.ts +2 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +7 -5
- package/dist/auth.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +24 -12
- package/dist/cli.js.map +1 -1
- package/dist/cmd/build/entry-generator.d.ts.map +1 -1
- package/dist/cmd/build/entry-generator.js +163 -18
- 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 +24 -15
- package/dist/cmd/build/vite/public-asset-path-plugin.d.ts.map +1 -1
- package/dist/cmd/build/vite/public-asset-path-plugin.js +92 -47
- 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 +9 -6
- 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/cloud/env/org-util.d.ts +2 -1
- package/dist/cmd/cloud/env/org-util.d.ts.map +1 -1
- package/dist/cmd/cloud/env/org-util.js +4 -2
- package/dist/cmd/cloud/env/org-util.js.map +1 -1
- package/dist/cmd/cloud/stream/create.d.ts +3 -0
- package/dist/cmd/cloud/stream/create.d.ts.map +1 -0
- package/dist/cmd/cloud/stream/create.js +227 -0
- package/dist/cmd/cloud/stream/create.js.map +1 -0
- package/dist/cmd/cloud/stream/delete.d.ts.map +1 -1
- package/dist/cmd/cloud/stream/delete.js +2 -1
- package/dist/cmd/cloud/stream/delete.js.map +1 -1
- package/dist/cmd/cloud/stream/get.d.ts.map +1 -1
- package/dist/cmd/cloud/stream/get.js +2 -1
- package/dist/cmd/cloud/stream/get.js.map +1 -1
- package/dist/cmd/cloud/stream/index.d.ts.map +1 -1
- package/dist/cmd/cloud/stream/index.js +10 -1
- package/dist/cmd/cloud/stream/index.js.map +1 -1
- package/dist/cmd/cloud/stream/list.d.ts.map +1 -1
- package/dist/cmd/cloud/stream/list.js +2 -1
- package/dist/cmd/cloud/stream/list.js.map +1 -1
- package/dist/cmd/cloud/stream/util.d.ts +6 -5
- package/dist/cmd/cloud/stream/util.d.ts.map +1 -1
- package/dist/cmd/cloud/stream/util.js +26 -5
- package/dist/cmd/cloud/stream/util.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/cmd/upgrade/index.d.ts.map +1 -1
- package/dist/cmd/upgrade/index.js +23 -0
- package/dist/cmd/upgrade/index.js.map +1 -1
- package/dist/cmd/upgrade/npm-availability.d.ts +44 -0
- package/dist/cmd/upgrade/npm-availability.d.ts.map +1 -0
- package/dist/cmd/upgrade/npm-availability.js +73 -0
- package/dist/cmd/upgrade/npm-availability.js.map +1 -0
- 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/dist/tui.d.ts +9 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +39 -14
- package/dist/tui.js.map +1 -1
- package/dist/version-check.d.ts.map +1 -1
- package/dist/version-check.js +13 -2
- package/dist/version-check.js.map +1 -1
- package/package.json +8 -7
- package/src/auth.ts +9 -5
- package/src/cli.ts +44 -12
- package/src/cmd/build/entry-generator.ts +163 -18
- package/src/cmd/build/vite/metadata-generator.ts +20 -9
- package/src/cmd/build/vite/public-asset-path-plugin.ts +105 -53
- package/src/cmd/build/vite/vite-asset-server-config.ts +9 -6
- package/src/cmd/build/vite/vite-asset-server.ts +3 -1
- package/src/cmd/build/vite/vite-builder.ts +21 -20
- package/src/cmd/cloud/env/org-util.ts +5 -2
- package/src/cmd/cloud/stream/create.ts +248 -0
- package/src/cmd/cloud/stream/delete.ts +2 -1
- package/src/cmd/cloud/stream/get.ts +2 -1
- package/src/cmd/cloud/stream/index.ts +10 -1
- package/src/cmd/cloud/stream/list.ts +2 -1
- package/src/cmd/cloud/stream/util.ts +39 -12
- package/src/cmd/support/report.ts +82 -28
- package/src/cmd/upgrade/index.ts +25 -0
- package/src/cmd/upgrade/npm-availability.ts +105 -0
- package/src/internal-logger.ts +91 -27
- package/src/tui.ts +42 -14
- package/src/version-check.ts +19 -3
|
@@ -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/cmd/upgrade/index.ts
CHANGED
|
@@ -185,6 +185,31 @@ export const command = createCommand({
|
|
|
185
185
|
};
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
// Verify the version is available on npm before proceeding
|
|
189
|
+
const isAvailable = await tui.spinner({
|
|
190
|
+
message: 'Verifying npm availability...',
|
|
191
|
+
clearOnSuccess: true,
|
|
192
|
+
callback: async () => {
|
|
193
|
+
const { waitForNpmAvailability } = await import('./npm-availability');
|
|
194
|
+
return await waitForNpmAvailability(latestVersion, {
|
|
195
|
+
maxAttempts: 6,
|
|
196
|
+
initialDelayMs: 2000,
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (!isAvailable) {
|
|
202
|
+
tui.warning('The new version is not yet available on npm.');
|
|
203
|
+
tui.info('This can happen right after a release. Please try again in a few minutes.');
|
|
204
|
+
tui.info(`You can also run: ${tui.muted('bun add -g @agentuity/cli@latest')}`);
|
|
205
|
+
return {
|
|
206
|
+
upgraded: false,
|
|
207
|
+
from: currentVersion,
|
|
208
|
+
to: latestVersion,
|
|
209
|
+
message: 'Version not yet available on npm',
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
188
213
|
// Show version info
|
|
189
214
|
if (!force) {
|
|
190
215
|
tui.info(`Current version: ${tui.muted(normalizedCurrent)}`);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm registry availability checking utilities.
|
|
3
|
+
* Used to verify a version is available on npm before attempting upgrade.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const NPM_REGISTRY_URL = 'https://registry.npmjs.org';
|
|
7
|
+
const PACKAGE_NAME = '@agentuity/cli';
|
|
8
|
+
|
|
9
|
+
/** Default timeout for quick checks (implicit version check) */
|
|
10
|
+
const QUICK_CHECK_TIMEOUT_MS = 1000;
|
|
11
|
+
|
|
12
|
+
/** Default timeout for explicit upgrade command */
|
|
13
|
+
const EXPLICIT_CHECK_TIMEOUT_MS = 5000;
|
|
14
|
+
|
|
15
|
+
export interface CheckNpmOptions {
|
|
16
|
+
/** Timeout in milliseconds (default: 5000 for explicit, 1000 for quick) */
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if a specific version of @agentuity/cli is available on npm registry.
|
|
22
|
+
* Uses the npm registry API directly for faster response than `npm view`.
|
|
23
|
+
*
|
|
24
|
+
* @param version - Version to check (with or without 'v' prefix)
|
|
25
|
+
* @param options - Optional configuration
|
|
26
|
+
* @returns true if version is available, false otherwise
|
|
27
|
+
*/
|
|
28
|
+
export async function isVersionAvailableOnNpm(
|
|
29
|
+
version: string,
|
|
30
|
+
options: CheckNpmOptions = {}
|
|
31
|
+
): Promise<boolean> {
|
|
32
|
+
const { timeoutMs = EXPLICIT_CHECK_TIMEOUT_MS } = options;
|
|
33
|
+
const normalizedVersion = version.replace(/^v/, '');
|
|
34
|
+
const url = `${NPM_REGISTRY_URL}/${encodeURIComponent(PACKAGE_NAME)}/${normalizedVersion}`;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(url, {
|
|
38
|
+
method: 'HEAD', // Only need to check existence, not full metadata
|
|
39
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
40
|
+
headers: {
|
|
41
|
+
Accept: 'application/json',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
return response.ok;
|
|
45
|
+
} catch {
|
|
46
|
+
// Network error or timeout - assume not available
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Quick check if a version is available on npm with a short timeout.
|
|
53
|
+
* Used for implicit version checks (auto-upgrade flow) to avoid blocking the user's command.
|
|
54
|
+
*
|
|
55
|
+
* @param version - Version to check (with or without 'v' prefix)
|
|
56
|
+
* @returns true if version is available, false if unavailable or timeout
|
|
57
|
+
*/
|
|
58
|
+
export async function isVersionAvailableOnNpmQuick(version: string): Promise<boolean> {
|
|
59
|
+
return isVersionAvailableOnNpm(version, { timeoutMs: QUICK_CHECK_TIMEOUT_MS });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface WaitForNpmOptions {
|
|
63
|
+
/** Maximum number of attempts (default: 6) */
|
|
64
|
+
maxAttempts?: number;
|
|
65
|
+
/** Initial delay between attempts in ms (default: 2000) */
|
|
66
|
+
initialDelayMs?: number;
|
|
67
|
+
/** Maximum delay between attempts in ms (default: 10000) */
|
|
68
|
+
maxDelayMs?: number;
|
|
69
|
+
/** Callback called before each retry */
|
|
70
|
+
onRetry?: (attempt: number, delayMs: number) => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Wait for a version to become available on npm with exponential backoff.
|
|
75
|
+
*
|
|
76
|
+
* @param version - Version to wait for (with or without 'v' prefix)
|
|
77
|
+
* @param options - Configuration options
|
|
78
|
+
* @returns true if version became available, false if timed out
|
|
79
|
+
*/
|
|
80
|
+
export async function waitForNpmAvailability(
|
|
81
|
+
version: string,
|
|
82
|
+
options: WaitForNpmOptions = {}
|
|
83
|
+
): Promise<boolean> {
|
|
84
|
+
const { maxAttempts = 6, initialDelayMs = 2000, maxDelayMs = 10000, onRetry } = options;
|
|
85
|
+
|
|
86
|
+
// First check - no delay
|
|
87
|
+
if (await isVersionAvailableOnNpm(version)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Retry with exponential backoff
|
|
92
|
+
let delay = initialDelayMs;
|
|
93
|
+
for (let attempt = 1; attempt < maxAttempts; attempt++) {
|
|
94
|
+
onRetry?.(attempt, delay);
|
|
95
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
96
|
+
|
|
97
|
+
if (await isVersionAvailableOnNpm(version)) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
delay = Math.min(Math.round(delay * 1.5), maxDelayMs);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return false;
|
|
105
|
+
}
|
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
|
*/
|
package/src/tui.ts
CHANGED
|
@@ -1764,9 +1764,18 @@ export async function prompt(message: string): Promise<string> {
|
|
|
1764
1764
|
});
|
|
1765
1765
|
}
|
|
1766
1766
|
|
|
1767
|
+
/**
|
|
1768
|
+
* Select an organization from a list.
|
|
1769
|
+
*
|
|
1770
|
+
* @param orgs - List of organizations to choose from
|
|
1771
|
+
* @param initial - Preferred org ID to pre-select (from saved preferences)
|
|
1772
|
+
* @param autoSelect - If true, auto-select preferred org without prompting (for --confirm or non-interactive)
|
|
1773
|
+
* @returns The selected organization ID
|
|
1774
|
+
*/
|
|
1767
1775
|
export async function selectOrganization(
|
|
1768
1776
|
orgs: OrganizationList,
|
|
1769
|
-
initial?: string
|
|
1777
|
+
initial?: string,
|
|
1778
|
+
autoSelect?: boolean
|
|
1770
1779
|
): Promise<string> {
|
|
1771
1780
|
if (orgs.length === 0) {
|
|
1772
1781
|
fatal(
|
|
@@ -1775,6 +1784,7 @@ export async function selectOrganization(
|
|
|
1775
1784
|
);
|
|
1776
1785
|
}
|
|
1777
1786
|
|
|
1787
|
+
// 1. Environment variable always takes precedence
|
|
1778
1788
|
if (process.env.AGENTUITY_CLOUD_ORG_ID) {
|
|
1779
1789
|
const org = orgs.find((o) => o.id === process.env.AGENTUITY_CLOUD_ORG_ID);
|
|
1780
1790
|
if (org) {
|
|
@@ -1782,41 +1792,59 @@ export async function selectOrganization(
|
|
|
1782
1792
|
}
|
|
1783
1793
|
}
|
|
1784
1794
|
|
|
1785
|
-
// Auto-select if only one org (regardless of TTY mode)
|
|
1795
|
+
// 2. Auto-select if only one org (regardless of TTY mode or autoSelect)
|
|
1786
1796
|
if (orgs.length === 1 && orgs[0]) {
|
|
1787
1797
|
return orgs[0].id;
|
|
1788
1798
|
}
|
|
1789
1799
|
|
|
1790
|
-
//
|
|
1791
|
-
//
|
|
1792
|
-
if (
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1800
|
+
// 3. Auto-select mode (--confirm flag or explicit autoSelect)
|
|
1801
|
+
// Use preferred org if set, otherwise fall back to first org
|
|
1802
|
+
if (autoSelect) {
|
|
1803
|
+
if (initial) {
|
|
1804
|
+
const initialOrg = orgs.find((o) => o.id === initial);
|
|
1805
|
+
if (initialOrg) {
|
|
1806
|
+
return initialOrg.id;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
// Fall back to first org with warning
|
|
1810
|
+
const firstOrg = orgs[0];
|
|
1811
|
+
if (firstOrg) {
|
|
1812
|
+
warning(
|
|
1813
|
+
`Multiple organizations found. Auto-selecting first org: ${firstOrg.name}. ` +
|
|
1814
|
+
`Set AGENTUITY_CLOUD_ORG_ID, use --org-id, or run 'agentuity auth org select' to set a default.`
|
|
1815
|
+
);
|
|
1816
|
+
return firstOrg.id;
|
|
1796
1817
|
}
|
|
1797
1818
|
}
|
|
1798
1819
|
|
|
1799
|
-
// Check for non-interactive environment (check both stdin and stdout)
|
|
1820
|
+
// 4. Check for non-interactive environment (check both stdin and stdout)
|
|
1800
1821
|
const isNonInteractive = !process.stdin.isTTY || !process.stdout.isTTY;
|
|
1801
1822
|
if (isNonInteractive) {
|
|
1802
|
-
// In non-interactive mode
|
|
1803
|
-
|
|
1823
|
+
// In non-interactive mode, use preferred org if set
|
|
1824
|
+
if (initial) {
|
|
1825
|
+
const initialOrg = orgs.find((o) => o.id === initial);
|
|
1826
|
+
if (initialOrg) {
|
|
1827
|
+
return initialOrg.id;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
// Fall back to first org with warning
|
|
1804
1831
|
const firstOrg = orgs[0];
|
|
1805
1832
|
if (firstOrg) {
|
|
1806
1833
|
warning(
|
|
1807
1834
|
`Multiple organizations found. Auto-selecting first org: ${firstOrg.name}. ` +
|
|
1808
|
-
`Set AGENTUITY_CLOUD_ORG_ID
|
|
1835
|
+
`Set AGENTUITY_CLOUD_ORG_ID, use --org-id, or run 'agentuity auth org select' to set a default.`
|
|
1809
1836
|
);
|
|
1810
1837
|
return firstOrg.id;
|
|
1811
1838
|
}
|
|
1812
1839
|
}
|
|
1813
1840
|
|
|
1814
|
-
// Interactive mode
|
|
1841
|
+
// 5. Interactive mode - show selector with preferred org pre-selected
|
|
1842
|
+
const initialIndex = initial ? orgs.findIndex((o) => o.id === initial) : 0;
|
|
1815
1843
|
const response = await enquirer.prompt<{ action: string }>({
|
|
1816
1844
|
type: 'select',
|
|
1817
1845
|
name: 'action',
|
|
1818
1846
|
message: 'Select an organization',
|
|
1819
|
-
initial: 0,
|
|
1847
|
+
initial: initialIndex >= 0 ? initialIndex : 0,
|
|
1820
1848
|
choices: orgs.map((o) => ({ message: o.name, name: o.id })),
|
|
1821
1849
|
});
|
|
1822
1850
|
|
package/src/version-check.ts
CHANGED
|
@@ -233,18 +233,34 @@ export async function checkForUpdates(
|
|
|
233
233
|
const currentVersion = getVersion();
|
|
234
234
|
const latestVersion = await fetchLatestVersion();
|
|
235
235
|
|
|
236
|
-
// Update the timestamp since we successfully checked
|
|
237
|
-
await updateCheckTimestamp(config, logger);
|
|
238
|
-
|
|
239
236
|
// Compare versions
|
|
240
237
|
const normalizedCurrent = currentVersion.replace(/^v/, '');
|
|
241
238
|
const normalizedLatest = latestVersion.replace(/^v/, '');
|
|
242
239
|
|
|
243
240
|
if (normalizedCurrent === normalizedLatest) {
|
|
241
|
+
// Update timestamp - we confirmed we're on latest version
|
|
242
|
+
await updateCheckTimestamp(config, logger);
|
|
244
243
|
logger.trace('Already on latest version: %s', currentVersion);
|
|
245
244
|
return;
|
|
246
245
|
}
|
|
247
246
|
|
|
247
|
+
// Quick npm availability check before prompting (short timeout, no retries)
|
|
248
|
+
// This avoids blocking the user's command if npm is slow or version not yet available
|
|
249
|
+
const { isVersionAvailableOnNpmQuick } = await import('./cmd/upgrade/npm-availability');
|
|
250
|
+
const isAvailable = await isVersionAvailableOnNpmQuick(latestVersion);
|
|
251
|
+
|
|
252
|
+
if (!isAvailable) {
|
|
253
|
+
// Don't update timestamp - we want to check again soon since npm may propagate
|
|
254
|
+
logger.debug(
|
|
255
|
+
'Version %s not yet available on npm, skipping upgrade prompt',
|
|
256
|
+
latestVersion
|
|
257
|
+
);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Update timestamp - npm availability confirmed, we can proceed with prompt
|
|
262
|
+
await updateCheckTimestamp(config, logger);
|
|
263
|
+
|
|
248
264
|
// New version available - prompt user
|
|
249
265
|
const shouldUpgrade = await promptUpgrade(currentVersion, latestVersion);
|
|
250
266
|
|