@agentuity/cli 1.0.8 → 1.0.9
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/ai/claude-code/constants.d.ts +13 -0
- package/dist/cmd/ai/claude-code/constants.d.ts.map +1 -0
- package/dist/cmd/ai/claude-code/constants.js +16 -0
- package/dist/cmd/ai/claude-code/constants.js.map +1 -0
- package/dist/cmd/ai/claude-code/index.d.ts +3 -0
- package/dist/cmd/ai/claude-code/index.d.ts.map +1 -0
- package/dist/cmd/ai/claude-code/index.js +22 -0
- package/dist/cmd/ai/claude-code/index.js.map +1 -0
- package/dist/cmd/ai/claude-code/install.d.ts +3 -0
- package/dist/cmd/ai/claude-code/install.d.ts.map +1 -0
- package/dist/cmd/ai/claude-code/install.js +133 -0
- package/dist/cmd/ai/claude-code/install.js.map +1 -0
- package/dist/cmd/ai/claude-code/uninstall.d.ts +3 -0
- package/dist/cmd/ai/claude-code/uninstall.d.ts.map +1 -0
- package/dist/cmd/ai/claude-code/uninstall.js +105 -0
- package/dist/cmd/ai/claude-code/uninstall.js.map +1 -0
- package/dist/cmd/ai/index.d.ts.map +1 -1
- package/dist/cmd/ai/index.js +6 -0
- package/dist/cmd/ai/index.js.map +1 -1
- package/dist/cmd/build/vite/server-bundler.d.ts.map +1 -1
- package/dist/cmd/build/vite/server-bundler.js +22 -1
- package/dist/cmd/build/vite/server-bundler.js.map +1 -1
- package/dist/cmd/upgrade/index.d.ts.map +1 -1
- package/dist/cmd/upgrade/index.js +24 -11
- package/dist/cmd/upgrade/index.js.map +1 -1
- package/dist/cmd/upgrade/npm-availability.d.ts +45 -12
- package/dist/cmd/upgrade/npm-availability.d.ts.map +1 -1
- package/dist/cmd/upgrade/npm-availability.js +73 -26
- package/dist/cmd/upgrade/npm-availability.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/cmd/ai/claude-code/constants.ts +26 -0
- package/src/cmd/ai/claude-code/index.ts +23 -0
- package/src/cmd/ai/claude-code/install.ts +181 -0
- package/src/cmd/ai/claude-code/uninstall.ts +122 -0
- package/src/cmd/ai/index.ts +6 -0
- package/src/cmd/build/vite/server-bundler.ts +28 -1
- package/src/cmd/upgrade/index.ts +35 -12
- package/src/cmd/upgrade/npm-availability.ts +106 -36
- package/src/version-check.ts +20 -2
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { createSubcommand, type CommandContext } from '../../../types';
|
|
3
|
+
import * as tui from '../../../tui';
|
|
4
|
+
import { getCommand } from '../../../command-prefix';
|
|
5
|
+
import {
|
|
6
|
+
type ClaudeSettings,
|
|
7
|
+
CLAUDE_SETTINGS_FILE,
|
|
8
|
+
PLUGIN_INSTALL_DIR,
|
|
9
|
+
AGENTUITY_ALLOW_PERMISSIONS,
|
|
10
|
+
AGENTUITY_DENY_PERMISSIONS,
|
|
11
|
+
} from './constants';
|
|
12
|
+
|
|
13
|
+
export const uninstallSubcommand = createSubcommand({
|
|
14
|
+
name: 'uninstall',
|
|
15
|
+
description: 'Uninstall Agentuity Coder plugin for Claude Code',
|
|
16
|
+
tags: ['fast'],
|
|
17
|
+
examples: [
|
|
18
|
+
{
|
|
19
|
+
command: getCommand('ai claude-code uninstall'),
|
|
20
|
+
description: 'Uninstall Agentuity Coder plugin for Claude Code',
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
async handler(ctx: CommandContext) {
|
|
24
|
+
const { options } = ctx;
|
|
25
|
+
const jsonMode = !!options?.json;
|
|
26
|
+
|
|
27
|
+
if (!jsonMode) {
|
|
28
|
+
tui.newline();
|
|
29
|
+
tui.output(tui.bold('Uninstalling Agentuity Coder plugin for Claude Code'));
|
|
30
|
+
tui.newline();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let removedPlugin = false;
|
|
34
|
+
let removedPermissions = false;
|
|
35
|
+
|
|
36
|
+
if (await Bun.file(`${PLUGIN_INSTALL_DIR}/package.json`).exists()) {
|
|
37
|
+
try {
|
|
38
|
+
rmSync(PLUGIN_INSTALL_DIR, { recursive: true, force: true });
|
|
39
|
+
removedPlugin = true;
|
|
40
|
+
if (!jsonMode) {
|
|
41
|
+
tui.success('Removed plugin installation directory');
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (!jsonMode) {
|
|
45
|
+
tui.warning(`Failed to remove plugin directory: ${error}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
if (!jsonMode) {
|
|
50
|
+
tui.info('Plugin installation directory not found - nothing to remove');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (await Bun.file(CLAUDE_SETTINGS_FILE).exists()) {
|
|
55
|
+
try {
|
|
56
|
+
const content = readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8');
|
|
57
|
+
const settings: ClaudeSettings = JSON.parse(content);
|
|
58
|
+
|
|
59
|
+
if (settings.permissions) {
|
|
60
|
+
const allPerms = [...AGENTUITY_ALLOW_PERMISSIONS, ...AGENTUITY_DENY_PERMISSIONS];
|
|
61
|
+
|
|
62
|
+
if (settings.permissions.allow) {
|
|
63
|
+
const originalAllowLen = settings.permissions.allow.length;
|
|
64
|
+
settings.permissions.allow = settings.permissions.allow.filter(
|
|
65
|
+
(p) => !allPerms.includes(p)
|
|
66
|
+
);
|
|
67
|
+
if (settings.permissions.allow.length < originalAllowLen) {
|
|
68
|
+
removedPermissions = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (settings.permissions.deny) {
|
|
73
|
+
const originalDenyLen = settings.permissions.deny.length;
|
|
74
|
+
settings.permissions.deny = settings.permissions.deny.filter(
|
|
75
|
+
(p) => !allPerms.includes(p)
|
|
76
|
+
);
|
|
77
|
+
if (settings.permissions.deny.length < originalDenyLen) {
|
|
78
|
+
removedPermissions = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (removedPermissions) {
|
|
83
|
+
writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2) + '\n');
|
|
84
|
+
if (!jsonMode) {
|
|
85
|
+
tui.success('Removed Agentuity permissions from Claude Code settings');
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
if (!jsonMode) {
|
|
89
|
+
tui.info('No Agentuity permissions found in Claude Code settings');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (!jsonMode) {
|
|
95
|
+
tui.warning(`Failed to update Claude Code settings: ${error}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
if (!jsonMode) {
|
|
100
|
+
tui.info('Claude Code settings file not found');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!jsonMode) {
|
|
105
|
+
tui.newline();
|
|
106
|
+
|
|
107
|
+
if (removedPlugin || removedPermissions) {
|
|
108
|
+
tui.output(tui.bold('Agentuity Coder plugin uninstalled successfully'));
|
|
109
|
+
} else {
|
|
110
|
+
tui.output(tui.bold('Agentuity Coder plugin was not installed'));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
tui.newline();
|
|
114
|
+
tui.info(`To reinstall, run: ${tui.bold(getCommand('ai claude-code install'))}`);
|
|
115
|
+
tui.newline();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { success: true, removedPlugin, removedPermissions };
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export default uninstallSubcommand;
|
package/src/cmd/ai/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import capabilitiesCommand from './capabilities';
|
|
|
3
3
|
import promptCommand from './prompt';
|
|
4
4
|
import schemaCommand from './schema';
|
|
5
5
|
import opencodeCommand from './opencode';
|
|
6
|
+
import claudeCodeCommand from './claude-code';
|
|
6
7
|
import introSubcommand from './intro';
|
|
7
8
|
import detectSubcommand from './detect';
|
|
8
9
|
import { getCommand } from '../../command-prefix';
|
|
@@ -25,6 +26,10 @@ export const command = createCommand({
|
|
|
25
26
|
command: getCommand('ai opencode install'),
|
|
26
27
|
description: 'Install Agentuity Open Code plugin',
|
|
27
28
|
},
|
|
29
|
+
{
|
|
30
|
+
command: getCommand('ai claude-code install'),
|
|
31
|
+
description: 'Install Agentuity Coder plugin for Claude Code',
|
|
32
|
+
},
|
|
28
33
|
{
|
|
29
34
|
command: getCommand('ai capabilities show'),
|
|
30
35
|
description: 'Show CLI capabilities for AI agents',
|
|
@@ -38,6 +43,7 @@ export const command = createCommand({
|
|
|
38
43
|
detectSubcommand,
|
|
39
44
|
introSubcommand,
|
|
40
45
|
opencodeCommand,
|
|
46
|
+
claudeCodeCommand,
|
|
41
47
|
capabilitiesCommand,
|
|
42
48
|
promptCommand,
|
|
43
49
|
schemaCommand,
|
|
@@ -316,10 +316,28 @@ export async function installExternalsAndBuild(options: ServerBundleOptions): Pr
|
|
|
316
316
|
},
|
|
317
317
|
};
|
|
318
318
|
|
|
319
|
+
// Detect files belonging to @agentuity/postgres or @agentuity/drizzle.
|
|
320
|
+
// Matches both published paths (node_modules/@agentuity/postgres/) and
|
|
321
|
+
// symlinked/monorepo paths (packages/postgres/dist/, packages/postgres/src/).
|
|
322
|
+
const isAgentuityPostgres = (filePath: string) =>
|
|
323
|
+
filePath.includes('/@agentuity/postgres/') ||
|
|
324
|
+
filePath.includes('\\@agentuity\\postgres\\') ||
|
|
325
|
+
filePath.includes('/packages/postgres/');
|
|
326
|
+
|
|
327
|
+
const isAgentuityDrizzle = (filePath: string) =>
|
|
328
|
+
filePath.includes('/@agentuity/drizzle/') ||
|
|
329
|
+
filePath.includes('\\@agentuity\\drizzle\\') ||
|
|
330
|
+
filePath.includes('/packages/drizzle/');
|
|
331
|
+
|
|
319
332
|
const dbRewritePlugin: BunPlugin = {
|
|
320
333
|
name: 'agentuity:db-rewrite',
|
|
321
334
|
setup(build) {
|
|
322
335
|
build.onResolve({ filter: /^drizzle-orm\/bun-sql$/ }, (args) => {
|
|
336
|
+
// Don't redirect if the importer is @agentuity/drizzle itself — that would create a cycle.
|
|
337
|
+
// Matches both published packages in node_modules and symlinked monorepo paths.
|
|
338
|
+
if (args.importer && isAgentuityDrizzle(args.importer)) {
|
|
339
|
+
return; // Let default resolution handle it
|
|
340
|
+
}
|
|
323
341
|
// Resolve to @agentuity/drizzle — the bundler will find it in node_modules
|
|
324
342
|
// and bundle it into .agentuity/app.js (NOT kept external).
|
|
325
343
|
const resolved = import.meta.resolveSync('@agentuity/drizzle', args.importer);
|
|
@@ -333,7 +351,16 @@ export async function installExternalsAndBuild(options: ServerBundleOptions): Pr
|
|
|
333
351
|
namespace: 'file',
|
|
334
352
|
},
|
|
335
353
|
async (args) => {
|
|
336
|
-
|
|
354
|
+
// Skip node_modules and the rewrite-target packages themselves.
|
|
355
|
+
// The symlink check is needed because symlinked packages (e.g. via
|
|
356
|
+
// workspace links) resolve to paths outside node_modules/ (like
|
|
357
|
+
// ../../sdk/packages/postgres/dist/) and would otherwise be rewritten,
|
|
358
|
+
// creating circular imports (postgres importing from itself).
|
|
359
|
+
if (
|
|
360
|
+
args.path.includes('/node_modules/') ||
|
|
361
|
+
isAgentuityPostgres(args.path) ||
|
|
362
|
+
isAgentuityDrizzle(args.path)
|
|
363
|
+
) {
|
|
337
364
|
return;
|
|
338
365
|
}
|
|
339
366
|
const contents = await Bun.file(args.path).text();
|
package/src/cmd/upgrade/index.ts
CHANGED
|
@@ -71,19 +71,35 @@ export async function fetchLatestVersion(): Promise<string> {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* Upgrade the CLI using bun global install
|
|
74
|
+
* Upgrade the CLI using bun global install.
|
|
75
|
+
* Retries on transient resolution errors caused by npm CDN propagation delays.
|
|
75
76
|
*/
|
|
76
|
-
async function performBunUpgrade(
|
|
77
|
+
async function performBunUpgrade(
|
|
78
|
+
version: string,
|
|
79
|
+
onRetry?: (attempt: number, delayMs: number) => void
|
|
80
|
+
): Promise<void> {
|
|
77
81
|
// Remove 'v' prefix for npm version
|
|
78
82
|
const npmVersion = version.replace(/^v/, '');
|
|
79
83
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
const { installWithRetry } = await import('./npm-availability');
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
await installWithRetry(
|
|
88
|
+
async () => {
|
|
89
|
+
// Use bun to install the specific version globally
|
|
90
|
+
// Run from tmpdir to avoid interference from any local package.json/node_modules
|
|
91
|
+
const result = await $`bun add -g @agentuity/cli@${npmVersion}`
|
|
92
|
+
.cwd(tmpdir())
|
|
93
|
+
.quiet()
|
|
94
|
+
.nothrow();
|
|
95
|
+
return { exitCode: result.exitCode, stderr: result.stderr };
|
|
96
|
+
},
|
|
97
|
+
{ onRetry }
|
|
98
|
+
);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Failed to install @agentuity/cli@${npmVersion}: ${error instanceof Error ? error.message : String(error)}`
|
|
102
|
+
);
|
|
87
103
|
}
|
|
88
104
|
}
|
|
89
105
|
|
|
@@ -194,8 +210,9 @@ export const command = createCommand({
|
|
|
194
210
|
callback: async () => {
|
|
195
211
|
const { waitForNpmAvailability } = await import('./npm-availability');
|
|
196
212
|
return await waitForNpmAvailability(latestVersion, {
|
|
197
|
-
maxAttempts:
|
|
198
|
-
initialDelayMs:
|
|
213
|
+
maxAttempts: 10,
|
|
214
|
+
initialDelayMs: 5000,
|
|
215
|
+
maxDelayMs: 15000,
|
|
199
216
|
});
|
|
200
217
|
},
|
|
201
218
|
});
|
|
@@ -244,8 +261,14 @@ export const command = createCommand({
|
|
|
244
261
|
|
|
245
262
|
// Perform the upgrade using bun
|
|
246
263
|
await tui.spinner({
|
|
264
|
+
type: 'logger',
|
|
247
265
|
message: `Installing @agentuity/cli@${normalizedLatest}...`,
|
|
248
|
-
callback: async () =>
|
|
266
|
+
callback: async (log) =>
|
|
267
|
+
await performBunUpgrade(latestVersion, (attempt, delayMs) => {
|
|
268
|
+
log(
|
|
269
|
+
`Package not yet available on CDN, retrying in ${Math.round(delayMs / 1000)}s (attempt ${attempt})...`
|
|
270
|
+
);
|
|
271
|
+
}),
|
|
249
272
|
});
|
|
250
273
|
|
|
251
274
|
// Verify the upgrade
|
|
@@ -1,62 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* npm registry availability checking utilities.
|
|
3
|
-
* Used to verify a version is available
|
|
3
|
+
* Used to verify a version is available via bun's resolver before attempting upgrade.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { $ } from 'bun';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
}
|
|
9
|
+
const PACKAGE_SPEC = '@agentuity/cli';
|
|
19
10
|
|
|
20
11
|
/**
|
|
21
|
-
* Check if a specific version of @agentuity/cli is
|
|
22
|
-
* Uses
|
|
12
|
+
* Check if a specific version of @agentuity/cli is resolvable by bun.
|
|
13
|
+
* Uses `bun info` to verify bun's own resolver/CDN can see the version,
|
|
14
|
+
* which avoids the race where npm registry returns 200 but bun's CDN
|
|
15
|
+
* has not yet propagated the version.
|
|
23
16
|
*
|
|
24
17
|
* @param version - Version to check (with or without 'v' prefix)
|
|
25
|
-
* @param options - Optional configuration
|
|
26
18
|
* @returns true if version is available, false otherwise
|
|
27
19
|
*/
|
|
28
|
-
export async function isVersionAvailableOnNpm(
|
|
29
|
-
version: string,
|
|
30
|
-
options: CheckNpmOptions = {}
|
|
31
|
-
): Promise<boolean> {
|
|
32
|
-
const { timeoutMs = EXPLICIT_CHECK_TIMEOUT_MS } = options;
|
|
20
|
+
export async function isVersionAvailableOnNpm(version: string): Promise<boolean> {
|
|
33
21
|
const normalizedVersion = version.replace(/^v/, '');
|
|
34
|
-
const url = `${NPM_REGISTRY_URL}/${encodeURIComponent(PACKAGE_NAME)}/${normalizedVersion}`;
|
|
35
|
-
|
|
36
22
|
try {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
23
|
+
const result = await $`bun info ${PACKAGE_SPEC}@${normalizedVersion} --json`
|
|
24
|
+
.cwd(tmpdir())
|
|
25
|
+
.quiet()
|
|
26
|
+
.nothrow();
|
|
27
|
+
if (result.exitCode !== 0) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const info = JSON.parse(result.stdout.toString());
|
|
31
|
+
if (info.error) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return info.version === normalizedVersion;
|
|
45
35
|
} catch {
|
|
46
|
-
// Network error or timeout - assume not available
|
|
47
36
|
return false;
|
|
48
37
|
}
|
|
49
38
|
}
|
|
50
39
|
|
|
51
40
|
/**
|
|
52
|
-
* Quick check if a version is available
|
|
53
|
-
* Used for implicit version checks (auto-upgrade flow)
|
|
41
|
+
* Quick check if a version is available via bun's resolver.
|
|
42
|
+
* Used for implicit version checks (auto-upgrade flow).
|
|
54
43
|
*
|
|
55
44
|
* @param version - Version to check (with or without 'v' prefix)
|
|
56
|
-
* @returns true if version is available, false if unavailable or
|
|
45
|
+
* @returns true if version is available, false if unavailable or error
|
|
57
46
|
*/
|
|
58
47
|
export async function isVersionAvailableOnNpmQuick(version: string): Promise<boolean> {
|
|
59
|
-
return isVersionAvailableOnNpm(version
|
|
48
|
+
return isVersionAvailableOnNpm(version);
|
|
60
49
|
}
|
|
61
50
|
|
|
62
51
|
export interface WaitForNpmOptions {
|
|
@@ -103,3 +92,84 @@ export async function waitForNpmAvailability(
|
|
|
103
92
|
|
|
104
93
|
return false;
|
|
105
94
|
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Patterns in bun's stderr that indicate a resolution/CDN propagation failure
|
|
98
|
+
* (as opposed to a permanent install error like permissions or disk space).
|
|
99
|
+
*/
|
|
100
|
+
const RESOLUTION_ERROR_PATTERNS = [/failed to resolve/i, /no version matching/i];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check whether a bun install failure is a transient resolution error
|
|
104
|
+
* caused by npm CDN propagation delays.
|
|
105
|
+
*/
|
|
106
|
+
export function isResolutionError(stderr: string): boolean {
|
|
107
|
+
return RESOLUTION_ERROR_PATTERNS.some((pattern) => pattern.test(stderr));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface InstallWithRetryOptions {
|
|
111
|
+
/** Maximum number of attempts including the first (default: 7 → 1 initial + 6 retries) */
|
|
112
|
+
maxAttempts?: number;
|
|
113
|
+
/** Initial delay in ms before the first retry (default: 5000) */
|
|
114
|
+
initialDelayMs?: number;
|
|
115
|
+
/** Maximum delay cap in ms (default: 30000) */
|
|
116
|
+
maxDelayMs?: number;
|
|
117
|
+
/** Multiplier applied to the delay after each retry (default: 2) */
|
|
118
|
+
multiplier?: number;
|
|
119
|
+
/** Callback invoked before each retry with the attempt number and upcoming delay */
|
|
120
|
+
onRetry?: (attempt: number, delayMs: number) => void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Run an install function and retry on transient resolution errors with
|
|
125
|
+
* exponential backoff. This covers the window (~2 min) where npm CDN nodes
|
|
126
|
+
* have not yet propagated a newly-published version.
|
|
127
|
+
*
|
|
128
|
+
* Total wait with defaults: 5 + 10 + 20 + 30 + 30 + 30 = 125 s ≈ 2 min
|
|
129
|
+
*
|
|
130
|
+
* @param installFn - Async function that performs the install and returns exitCode + stderr
|
|
131
|
+
* @param options - Retry configuration
|
|
132
|
+
* @returns The successful result (exitCode 0)
|
|
133
|
+
* @throws Error if all retries are exhausted or a non-resolution error occurs
|
|
134
|
+
*/
|
|
135
|
+
export async function installWithRetry(
|
|
136
|
+
installFn: () => Promise<{ exitCode: number; stderr: Buffer }>,
|
|
137
|
+
options: InstallWithRetryOptions = {}
|
|
138
|
+
): Promise<{ exitCode: number; stderr: Buffer }> {
|
|
139
|
+
const {
|
|
140
|
+
maxAttempts = 7,
|
|
141
|
+
initialDelayMs = 5000,
|
|
142
|
+
maxDelayMs = 30000,
|
|
143
|
+
multiplier = 2,
|
|
144
|
+
onRetry,
|
|
145
|
+
} = options;
|
|
146
|
+
|
|
147
|
+
let delay = initialDelayMs;
|
|
148
|
+
|
|
149
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
150
|
+
const result = await installFn();
|
|
151
|
+
|
|
152
|
+
if (result.exitCode === 0) {
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const stderr = result.stderr.toString();
|
|
157
|
+
|
|
158
|
+
// Only retry on resolution/propagation errors — bail immediately for anything else
|
|
159
|
+
if (!isResolutionError(stderr)) {
|
|
160
|
+
throw new Error(stderr);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Last attempt exhausted — throw
|
|
164
|
+
if (attempt === maxAttempts) {
|
|
165
|
+
throw new Error(stderr);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
onRetry?.(attempt, delay);
|
|
169
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
170
|
+
delay = Math.min(Math.round(delay * multiplier), maxDelayMs);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Unreachable, but satisfies TypeScript
|
|
174
|
+
throw new Error('Install failed after retries');
|
|
175
|
+
}
|
package/src/version-check.ts
CHANGED
|
@@ -172,9 +172,27 @@ async function performUpgrade(logger: Logger, targetVersion: string): Promise<vo
|
|
|
172
172
|
|
|
173
173
|
logger.info('Upgrading to version %s...', npmVersion);
|
|
174
174
|
|
|
175
|
-
// Use bun to install the specific version globally
|
|
175
|
+
// Use bun to install the specific version globally with retry for CDN propagation delays
|
|
176
176
|
// Run from tmpdir to avoid interference from any local package.json/node_modules
|
|
177
|
-
|
|
177
|
+
const { installWithRetry } = await import('./cmd/upgrade/npm-availability');
|
|
178
|
+
await installWithRetry(
|
|
179
|
+
async () => {
|
|
180
|
+
const result = await $`bun add -g @agentuity/cli@${npmVersion}`
|
|
181
|
+
.cwd(tmpdir())
|
|
182
|
+
.quiet()
|
|
183
|
+
.nothrow();
|
|
184
|
+
return { exitCode: result.exitCode, stderr: result.stderr };
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
onRetry: (attempt, delayMs) => {
|
|
188
|
+
logger.info(
|
|
189
|
+
'Package not yet available on CDN, retrying in %ds (attempt %d)...',
|
|
190
|
+
Math.round(delayMs / 1000),
|
|
191
|
+
attempt
|
|
192
|
+
);
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
);
|
|
178
196
|
|
|
179
197
|
// If we got here, the upgrade succeeded
|
|
180
198
|
// Re-run the original command with the new binary
|