@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.
Files changed (78) hide show
  1. package/dist/auth.d.ts +2 -2
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +7 -5
  4. package/dist/auth.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +24 -12
  7. package/dist/cli.js.map +1 -1
  8. package/dist/cmd/build/entry-generator.d.ts.map +1 -1
  9. package/dist/cmd/build/entry-generator.js +26 -17
  10. package/dist/cmd/build/entry-generator.js.map +1 -1
  11. package/dist/cmd/build/vite/public-asset-path-plugin.d.ts +17 -20
  12. package/dist/cmd/build/vite/public-asset-path-plugin.d.ts.map +1 -1
  13. package/dist/cmd/build/vite/public-asset-path-plugin.js +62 -43
  14. package/dist/cmd/build/vite/public-asset-path-plugin.js.map +1 -1
  15. package/dist/cmd/build/vite/vite-asset-server-config.d.ts.map +1 -1
  16. package/dist/cmd/build/vite/vite-asset-server-config.js +3 -1
  17. package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
  18. package/dist/cmd/build/vite/vite-builder.js +1 -1
  19. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  20. package/dist/cmd/canary/index.js +1 -1
  21. package/dist/cmd/canary/index.js.map +1 -1
  22. package/dist/cmd/cloud/env/org-util.d.ts +2 -1
  23. package/dist/cmd/cloud/env/org-util.d.ts.map +1 -1
  24. package/dist/cmd/cloud/env/org-util.js +4 -2
  25. package/dist/cmd/cloud/env/org-util.js.map +1 -1
  26. package/dist/cmd/cloud/stream/create.d.ts +3 -0
  27. package/dist/cmd/cloud/stream/create.d.ts.map +1 -0
  28. package/dist/cmd/cloud/stream/create.js +227 -0
  29. package/dist/cmd/cloud/stream/create.js.map +1 -0
  30. package/dist/cmd/cloud/stream/delete.d.ts.map +1 -1
  31. package/dist/cmd/cloud/stream/delete.js +2 -1
  32. package/dist/cmd/cloud/stream/delete.js.map +1 -1
  33. package/dist/cmd/cloud/stream/get.d.ts.map +1 -1
  34. package/dist/cmd/cloud/stream/get.js +2 -1
  35. package/dist/cmd/cloud/stream/get.js.map +1 -1
  36. package/dist/cmd/cloud/stream/index.d.ts.map +1 -1
  37. package/dist/cmd/cloud/stream/index.js +10 -1
  38. package/dist/cmd/cloud/stream/index.js.map +1 -1
  39. package/dist/cmd/cloud/stream/list.d.ts.map +1 -1
  40. package/dist/cmd/cloud/stream/list.js +2 -1
  41. package/dist/cmd/cloud/stream/list.js.map +1 -1
  42. package/dist/cmd/cloud/stream/util.d.ts +6 -5
  43. package/dist/cmd/cloud/stream/util.d.ts.map +1 -1
  44. package/dist/cmd/cloud/stream/util.js +26 -5
  45. package/dist/cmd/cloud/stream/util.js.map +1 -1
  46. package/dist/cmd/upgrade/index.d.ts.map +1 -1
  47. package/dist/cmd/upgrade/index.js +23 -0
  48. package/dist/cmd/upgrade/index.js.map +1 -1
  49. package/dist/cmd/upgrade/npm-availability.d.ts +44 -0
  50. package/dist/cmd/upgrade/npm-availability.d.ts.map +1 -0
  51. package/dist/cmd/upgrade/npm-availability.js +73 -0
  52. package/dist/cmd/upgrade/npm-availability.js.map +1 -0
  53. package/dist/tui.d.ts +9 -1
  54. package/dist/tui.d.ts.map +1 -1
  55. package/dist/tui.js +39 -14
  56. package/dist/tui.js.map +1 -1
  57. package/dist/version-check.d.ts.map +1 -1
  58. package/dist/version-check.js +13 -2
  59. package/dist/version-check.js.map +1 -1
  60. package/package.json +6 -6
  61. package/src/auth.ts +9 -5
  62. package/src/cli.ts +44 -12
  63. package/src/cmd/build/entry-generator.ts +26 -17
  64. package/src/cmd/build/vite/public-asset-path-plugin.ts +73 -51
  65. package/src/cmd/build/vite/vite-asset-server-config.ts +3 -1
  66. package/src/cmd/build/vite/vite-builder.ts +1 -1
  67. package/src/cmd/canary/index.ts +1 -1
  68. package/src/cmd/cloud/env/org-util.ts +5 -2
  69. package/src/cmd/cloud/stream/create.ts +248 -0
  70. package/src/cmd/cloud/stream/delete.ts +2 -1
  71. package/src/cmd/cloud/stream/get.ts +2 -1
  72. package/src/cmd/cloud/stream/index.ts +10 -1
  73. package/src/cmd/cloud/stream/list.ts +2 -1
  74. package/src/cmd/cloud/stream/util.ts +39 -12
  75. package/src/cmd/upgrade/index.ts +25 -0
  76. package/src/cmd/upgrade/npm-availability.ts +105 -0
  77. package/src/tui.ts +42 -14
  78. 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, project: 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, project: 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, project: 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 { ErrorCode } from '../../../errors';
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
- project: { region: string };
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
- if (!sdkKey) {
16
- tui.fatal(
17
- `Couldn't find the AGENTUITY_SDK_KEY in ${ctx.projectDir} .env file`,
18
- ErrorCode.CONFIG_NOT_FOUND
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 ${sdkKey}`,
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);
@@ -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
- // Use saved preference if available (regardless of TTY mode)
1791
- // This allows consistent behavior without prompting on every command
1792
- if (initial) {
1793
- const initialOrg = orgs.find((o) => o.id === initial);
1794
- if (initialOrg) {
1795
- return initialOrg.id;
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 with multiple orgs, auto-select first org
1803
- // This allows scripts and CI/CD to work without explicit org selection
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 or use --org-id to specify a different org.`
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 with no saved preference - prompt user
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
 
@@ -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