@agentuity/cli 0.1.43 → 0.1.45
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 +26 -17
- package/dist/cmd/build/entry-generator.js.map +1 -1
- package/dist/cmd/build/vite/public-asset-path-plugin.d.ts +17 -20
- package/dist/cmd/build/vite/public-asset-path-plugin.d.ts.map +1 -1
- package/dist/cmd/build/vite/public-asset-path-plugin.js +62 -43
- 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 +3 -1
- package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
- package/dist/cmd/build/vite/vite-builder.js +1 -1
- package/dist/cmd/build/vite/vite-builder.js.map +1 -1
- package/dist/cmd/canary/index.js +1 -1
- package/dist/cmd/canary/index.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/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/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 +6 -6
- package/src/auth.ts +9 -5
- package/src/cli.ts +44 -12
- package/src/cmd/build/entry-generator.ts +26 -17
- package/src/cmd/build/vite/public-asset-path-plugin.ts +73 -51
- package/src/cmd/build/vite/vite-asset-server-config.ts +3 -1
- package/src/cmd/build/vite/vite-builder.ts +1 -1
- package/src/cmd/canary/index.ts +1 -1
- 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/upgrade/index.ts +25 -0
- package/src/cmd/upgrade/npm-availability.ts +105 -0
- package/src/tui.ts +42 -14
- package/src/version-check.ts +19 -3
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { basename } from 'path';
|
|
3
|
+
import { createCommand } from '../../../types';
|
|
4
|
+
import * as tui from '../../../tui';
|
|
5
|
+
import { createStorageAdapter } from './util';
|
|
6
|
+
import { getCommand } from '../../../command-prefix';
|
|
7
|
+
|
|
8
|
+
const StreamCreateResponseSchema = z.object({
|
|
9
|
+
id: z.string().describe('Stream ID'),
|
|
10
|
+
namespace: z.string().describe('Stream namespace'),
|
|
11
|
+
url: z.string().describe('Public URL'),
|
|
12
|
+
sizeBytes: z.number().describe('Size in bytes'),
|
|
13
|
+
metadata: z.record(z.string(), z.string()).describe('Stream metadata'),
|
|
14
|
+
expiresAt: z.string().optional().describe('Expiration timestamp'),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const createSubcommand = createCommand({
|
|
18
|
+
name: 'create',
|
|
19
|
+
aliases: ['new'],
|
|
20
|
+
description: 'Create a new stream and upload content',
|
|
21
|
+
tags: ['mutating', 'creates-resource', 'slow', 'requires-auth', 'uses-stdin'],
|
|
22
|
+
requires: { auth: true, region: true },
|
|
23
|
+
optional: { project: true },
|
|
24
|
+
idempotent: false,
|
|
25
|
+
examples: [
|
|
26
|
+
{
|
|
27
|
+
command: getCommand('cloud stream create memory-share ./notes.md'),
|
|
28
|
+
description: 'Create stream from file',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
command: getCommand(
|
|
32
|
+
'cloud stream create memory-share ./data.json --content-type application/json'
|
|
33
|
+
),
|
|
34
|
+
description: 'Create stream with explicit content type',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
command: `cat summary.md | ${getCommand('cloud stream create memory-share -')}`,
|
|
38
|
+
description: 'Create stream from stdin',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
command: getCommand('cloud stream create memory-share ./notes.md --ttl 3600'),
|
|
42
|
+
description: 'Create stream with 1 hour TTL',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
command: getCommand(
|
|
46
|
+
'cloud stream create memory-share ./notes.md --metadata type=summary,source=session'
|
|
47
|
+
),
|
|
48
|
+
description: 'Create stream with metadata',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
command: getCommand('cloud stream create memory-share ./large.json --compress'),
|
|
52
|
+
description: 'Create compressed stream',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
schema: {
|
|
56
|
+
args: z.object({
|
|
57
|
+
namespace: z.string().min(1).max(254).describe('Stream namespace (1-254 characters)'),
|
|
58
|
+
filename: z.string().describe('File path to upload or "-" for STDIN'),
|
|
59
|
+
}),
|
|
60
|
+
options: z.object({
|
|
61
|
+
metadata: z
|
|
62
|
+
.string()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe('Metadata key=value pairs (comma-separated: key1=value1,key2=value2)'),
|
|
65
|
+
contentType: z
|
|
66
|
+
.string()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe('Content type (auto-detected from extension if not provided)'),
|
|
69
|
+
compress: z.boolean().optional().describe('Enable gzip compression'),
|
|
70
|
+
ttl: z.coerce
|
|
71
|
+
.number()
|
|
72
|
+
.optional()
|
|
73
|
+
.describe('TTL in seconds (60-7776000, or 0/null for never expires)'),
|
|
74
|
+
}),
|
|
75
|
+
response: StreamCreateResponseSchema,
|
|
76
|
+
},
|
|
77
|
+
webUrl: '/services/stream',
|
|
78
|
+
|
|
79
|
+
async handler(ctx) {
|
|
80
|
+
const { args, opts, options } = ctx;
|
|
81
|
+
const started = Date.now();
|
|
82
|
+
const storage = await createStorageAdapter(ctx);
|
|
83
|
+
|
|
84
|
+
// Parse metadata if provided
|
|
85
|
+
let metadata: Record<string, string> | undefined;
|
|
86
|
+
if (opts.metadata) {
|
|
87
|
+
const validPairs: Record<string, string> = {};
|
|
88
|
+
const malformed: string[] = [];
|
|
89
|
+
const pairs = opts.metadata.split(',');
|
|
90
|
+
|
|
91
|
+
for (const pair of pairs) {
|
|
92
|
+
const trimmedPair = pair.trim();
|
|
93
|
+
if (!trimmedPair) continue;
|
|
94
|
+
|
|
95
|
+
const firstEqualIdx = trimmedPair.indexOf('=');
|
|
96
|
+
if (firstEqualIdx === -1) {
|
|
97
|
+
malformed.push(trimmedPair);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const key = trimmedPair.substring(0, firstEqualIdx).trim();
|
|
102
|
+
const value = trimmedPair.substring(firstEqualIdx + 1).trim();
|
|
103
|
+
|
|
104
|
+
if (!key || !value) {
|
|
105
|
+
malformed.push(trimmedPair);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
validPairs[key] = value;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (malformed.length > 0) {
|
|
113
|
+
ctx.logger.warn(`Skipping malformed metadata pairs: ${malformed.join(', ')}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (Object.keys(validPairs).length > 0) {
|
|
117
|
+
metadata = validPairs;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Determine content type
|
|
122
|
+
let contentType = opts.contentType;
|
|
123
|
+
if (!contentType) {
|
|
124
|
+
// Auto-detect from filename extension
|
|
125
|
+
const filename = args.filename === '-' ? 'stdin' : args.filename;
|
|
126
|
+
const dotIndex = filename.lastIndexOf('.');
|
|
127
|
+
const ext = dotIndex > 0 ? filename.substring(dotIndex + 1).toLowerCase() : undefined;
|
|
128
|
+
// Text-based types should include charset=utf-8 for proper browser rendering
|
|
129
|
+
const mimeTypes: Record<string, string> = {
|
|
130
|
+
txt: 'text/plain; charset=utf-8',
|
|
131
|
+
md: 'text/markdown; charset=utf-8',
|
|
132
|
+
html: 'text/html; charset=utf-8',
|
|
133
|
+
css: 'text/css; charset=utf-8',
|
|
134
|
+
yaml: 'application/x-yaml; charset=utf-8',
|
|
135
|
+
yml: 'application/x-yaml; charset=utf-8',
|
|
136
|
+
js: 'application/javascript; charset=utf-8',
|
|
137
|
+
ts: 'application/typescript; charset=utf-8',
|
|
138
|
+
json: 'application/json; charset=utf-8',
|
|
139
|
+
xml: 'application/xml; charset=utf-8',
|
|
140
|
+
pdf: 'application/pdf',
|
|
141
|
+
zip: 'application/zip',
|
|
142
|
+
jpg: 'image/jpeg',
|
|
143
|
+
jpeg: 'image/jpeg',
|
|
144
|
+
png: 'image/png',
|
|
145
|
+
gif: 'image/gif',
|
|
146
|
+
svg: 'image/svg+xml; charset=utf-8',
|
|
147
|
+
mp4: 'video/mp4',
|
|
148
|
+
mp3: 'audio/mpeg',
|
|
149
|
+
};
|
|
150
|
+
contentType = ext ? mimeTypes[ext] : 'application/octet-stream';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Create the stream
|
|
154
|
+
const stream = await storage.create(args.namespace, {
|
|
155
|
+
metadata,
|
|
156
|
+
contentType,
|
|
157
|
+
compress: opts.compress ? true : undefined,
|
|
158
|
+
ttl: opts.ttl,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Read and write content
|
|
162
|
+
let inputStream: ReadableStream<Uint8Array>;
|
|
163
|
+
|
|
164
|
+
if (args.filename === '-') {
|
|
165
|
+
// Stream from STDIN
|
|
166
|
+
inputStream = Bun.stdin.stream();
|
|
167
|
+
} else {
|
|
168
|
+
// Stream from file
|
|
169
|
+
const file = Bun.file(args.filename);
|
|
170
|
+
if (!(await file.exists())) {
|
|
171
|
+
tui.fatal(`File not found: ${args.filename}`);
|
|
172
|
+
}
|
|
173
|
+
inputStream = file.stream();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Write content to stream in chunks
|
|
177
|
+
const reader = inputStream.getReader();
|
|
178
|
+
const MAX_CHUNK_SIZE = 5 * 1024 * 1024; // 5MB max per write
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
while (true) {
|
|
182
|
+
const { done, value } = await reader.read();
|
|
183
|
+
if (done) break;
|
|
184
|
+
|
|
185
|
+
// If chunk is larger than 5MB, split it
|
|
186
|
+
if (value.length > MAX_CHUNK_SIZE) {
|
|
187
|
+
let offset = 0;
|
|
188
|
+
while (offset < value.length) {
|
|
189
|
+
const chunk = value.slice(offset, offset + MAX_CHUNK_SIZE);
|
|
190
|
+
await stream.write(chunk);
|
|
191
|
+
offset += MAX_CHUNK_SIZE;
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
await stream.write(value);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} finally {
|
|
198
|
+
reader.releaseLock();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Close the stream
|
|
202
|
+
await stream.close();
|
|
203
|
+
|
|
204
|
+
const durationMs = Date.now() - started;
|
|
205
|
+
const sizeBytes = stream.bytesWritten;
|
|
206
|
+
|
|
207
|
+
// Get stream info to retrieve expiresAt
|
|
208
|
+
let expiresAt: string | undefined;
|
|
209
|
+
try {
|
|
210
|
+
const info = await storage.get(stream.id);
|
|
211
|
+
expiresAt = info.expiresAt;
|
|
212
|
+
} catch {
|
|
213
|
+
// expiresAt is optional, ignore errors
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!options.json) {
|
|
217
|
+
const sourceLabel = args.filename === '-' ? 'stdin' : basename(args.filename);
|
|
218
|
+
console.log(`Namespace: ${tui.bold(args.namespace)}`);
|
|
219
|
+
console.log(`ID: ${stream.id}`);
|
|
220
|
+
console.log(`Size: ${tui.formatBytes(sizeBytes)}`);
|
|
221
|
+
console.log(`URL: ${tui.link(stream.url)}`);
|
|
222
|
+
if (expiresAt) {
|
|
223
|
+
console.log(`Expires: ${expiresAt}`);
|
|
224
|
+
}
|
|
225
|
+
if (metadata && Object.keys(metadata).length > 0) {
|
|
226
|
+
console.log(`Metadata:`);
|
|
227
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
228
|
+
console.log(` ${key}: ${value}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (opts.compress) {
|
|
232
|
+
console.log(`Compressed: yes`);
|
|
233
|
+
}
|
|
234
|
+
tui.success(`created stream from ${sourceLabel} in ${durationMs.toFixed(1)}ms`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
id: stream.id,
|
|
239
|
+
namespace: args.namespace,
|
|
240
|
+
url: stream.url,
|
|
241
|
+
sizeBytes,
|
|
242
|
+
metadata: metadata ?? {},
|
|
243
|
+
expiresAt,
|
|
244
|
+
};
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
export default createSubcommand;
|
|
@@ -13,7 +13,8 @@ export const deleteSubcommand = createCommand({
|
|
|
13
13
|
description: 'Delete a stream by ID (soft delete)',
|
|
14
14
|
tags: ['destructive', 'deletes-resource', 'slow', 'requires-auth'],
|
|
15
15
|
idempotent: true,
|
|
16
|
-
requires: { auth: true,
|
|
16
|
+
requires: { auth: true, region: true },
|
|
17
|
+
optional: { project: true },
|
|
17
18
|
examples: [
|
|
18
19
|
{ command: getCommand('stream delete stream-id-123'), description: 'Delete a stream' },
|
|
19
20
|
{
|
|
@@ -16,7 +16,8 @@ export const getSubcommand = createCommand({
|
|
|
16
16
|
name: 'get',
|
|
17
17
|
description: 'Get detailed information about a specific stream',
|
|
18
18
|
tags: ['read-only', 'slow', 'requires-auth'],
|
|
19
|
-
requires: { auth: true,
|
|
19
|
+
requires: { auth: true, region: true },
|
|
20
|
+
optional: { project: true },
|
|
20
21
|
idempotent: true,
|
|
21
22
|
examples: [
|
|
22
23
|
{ command: getCommand('stream get stream-id-123'), description: 'Get stream details' },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createCommand } from '../../../types';
|
|
2
|
+
import createSubcommand from './create';
|
|
2
3
|
import listSubcommand from './list';
|
|
3
4
|
import getSubcommand from './get';
|
|
4
5
|
import deleteSubcommand from './delete';
|
|
@@ -10,10 +11,18 @@ export const streamCommand = createCommand({
|
|
|
10
11
|
description: 'Manage durable streams',
|
|
11
12
|
tags: ['slow', 'requires-auth'],
|
|
12
13
|
examples: [
|
|
14
|
+
{
|
|
15
|
+
command: getCommand('cloud stream create memory-share ./notes.md'),
|
|
16
|
+
description: 'Create stream from file',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
command: `cat data.json | ${getCommand('cloud stream create memory-share -')}`,
|
|
20
|
+
description: 'Create stream from stdin',
|
|
21
|
+
},
|
|
13
22
|
{ command: getCommand('cloud stream list'), description: 'List all streams' },
|
|
14
23
|
{ command: getCommand('cloud stream get <id>'), description: 'Get stream details' },
|
|
15
24
|
],
|
|
16
|
-
subcommands: [listSubcommand, getSubcommand, deleteSubcommand],
|
|
25
|
+
subcommands: [createSubcommand, listSubcommand, getSubcommand, deleteSubcommand],
|
|
17
26
|
});
|
|
18
27
|
|
|
19
28
|
export default streamCommand;
|
|
@@ -22,7 +22,8 @@ export const listSubcommand = createCommand({
|
|
|
22
22
|
aliases: ['ls'],
|
|
23
23
|
description: 'List recent streams with optional filtering',
|
|
24
24
|
tags: ['read-only', 'slow', 'requires-auth'],
|
|
25
|
-
requires: { auth: true,
|
|
25
|
+
requires: { auth: true, region: true },
|
|
26
|
+
optional: { project: true },
|
|
26
27
|
idempotent: true,
|
|
27
28
|
examples: [
|
|
28
29
|
{ command: getCommand('cloud stream list'), description: 'List all streams' },
|
|
@@ -1,35 +1,62 @@
|
|
|
1
|
-
import { StreamStorageService, Logger } from '@agentuity/core';
|
|
1
|
+
import { StreamStorageService, type Logger } from '@agentuity/core';
|
|
2
2
|
import { createServerFetchAdapter, getServiceUrls } from '@agentuity/server';
|
|
3
3
|
import { loadProjectSDKKey } from '../../../config';
|
|
4
|
-
import {
|
|
5
|
-
import type { Config } from '../../../types';
|
|
4
|
+
import type { AuthData, Config, GlobalOptions, ProjectConfig } from '../../../types';
|
|
6
5
|
import * as tui from '../../../tui';
|
|
7
6
|
|
|
8
7
|
export async function createStorageAdapter(ctx: {
|
|
9
8
|
logger: Logger;
|
|
10
9
|
projectDir: string;
|
|
10
|
+
auth: AuthData;
|
|
11
|
+
region: string;
|
|
12
|
+
project?: ProjectConfig;
|
|
11
13
|
config: Config | null;
|
|
12
|
-
|
|
14
|
+
options: GlobalOptions;
|
|
13
15
|
}) {
|
|
16
|
+
// Try to get SDK key from project context first (preferred for project-based auth)
|
|
14
17
|
const sdkKey = await loadProjectSDKKey(ctx.logger, ctx.projectDir);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
|
|
19
|
+
let authToken: string;
|
|
20
|
+
let queryParams: Record<string, string> | undefined;
|
|
21
|
+
|
|
22
|
+
if (sdkKey) {
|
|
23
|
+
// Use SDK key auth (project context available)
|
|
24
|
+
authToken = sdkKey;
|
|
25
|
+
ctx.logger.trace('using SDK key auth for stream');
|
|
26
|
+
} else {
|
|
27
|
+
// Use CLI key auth with orgId query param
|
|
28
|
+
// Pulse server expects orgId as query param for CLI tokens (ck_*)
|
|
29
|
+
// IMPORTANT: For CLI key auth, prefer user's org ID over project's org ID
|
|
30
|
+
// because the CLI key is validated against the user's orgs, not the project's org
|
|
31
|
+
const orgId =
|
|
32
|
+
ctx.options.orgId ??
|
|
33
|
+
process.env.AGENTUITY_CLOUD_ORG_ID ??
|
|
34
|
+
ctx.config?.preferences?.orgId ??
|
|
35
|
+
ctx.project?.orgId;
|
|
36
|
+
|
|
37
|
+
if (!orgId) {
|
|
38
|
+
tui.fatal(
|
|
39
|
+
'Organization ID is required. Either run from a project directory, use --org-id flag, or set AGENTUITY_CLOUD_ORG_ID environment variable.'
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
authToken = ctx.auth.apiKey;
|
|
44
|
+
queryParams = { orgId };
|
|
45
|
+
ctx.logger.trace('using CLI key auth with orgId query param for stream');
|
|
20
46
|
}
|
|
21
47
|
|
|
48
|
+
const baseUrl = getServiceUrls(ctx.region).stream;
|
|
49
|
+
|
|
22
50
|
const adapter = createServerFetchAdapter(
|
|
23
51
|
{
|
|
24
52
|
headers: {
|
|
25
|
-
Authorization: `Bearer ${
|
|
53
|
+
Authorization: `Bearer ${authToken}`,
|
|
26
54
|
},
|
|
55
|
+
queryParams,
|
|
27
56
|
},
|
|
28
57
|
ctx.logger
|
|
29
58
|
);
|
|
30
59
|
|
|
31
|
-
const baseUrl = getServiceUrls(ctx.project.region).stream;
|
|
32
|
-
|
|
33
60
|
ctx.logger.trace('using stream url: %s', baseUrl);
|
|
34
61
|
|
|
35
62
|
return new StreamStorageService(baseUrl, adapter);
|
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/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
|
|