@agentuity/cli 1.0.8 → 1.0.10

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 (67) hide show
  1. package/dist/cmd/ai/claude-code/constants.d.ts +13 -0
  2. package/dist/cmd/ai/claude-code/constants.d.ts.map +1 -0
  3. package/dist/cmd/ai/claude-code/constants.js +16 -0
  4. package/dist/cmd/ai/claude-code/constants.js.map +1 -0
  5. package/dist/cmd/ai/claude-code/index.d.ts +3 -0
  6. package/dist/cmd/ai/claude-code/index.d.ts.map +1 -0
  7. package/dist/cmd/ai/claude-code/index.js +22 -0
  8. package/dist/cmd/ai/claude-code/index.js.map +1 -0
  9. package/dist/cmd/ai/claude-code/install.d.ts +3 -0
  10. package/dist/cmd/ai/claude-code/install.d.ts.map +1 -0
  11. package/dist/cmd/ai/claude-code/install.js +133 -0
  12. package/dist/cmd/ai/claude-code/install.js.map +1 -0
  13. package/dist/cmd/ai/claude-code/uninstall.d.ts +3 -0
  14. package/dist/cmd/ai/claude-code/uninstall.d.ts.map +1 -0
  15. package/dist/cmd/ai/claude-code/uninstall.js +105 -0
  16. package/dist/cmd/ai/claude-code/uninstall.js.map +1 -0
  17. package/dist/cmd/ai/index.d.ts.map +1 -1
  18. package/dist/cmd/ai/index.js +6 -0
  19. package/dist/cmd/ai/index.js.map +1 -1
  20. package/dist/cmd/build/entry-generator.d.ts.map +1 -1
  21. package/dist/cmd/build/entry-generator.js +1 -8
  22. package/dist/cmd/build/entry-generator.js.map +1 -1
  23. package/dist/cmd/build/vite/config-loader.d.ts +9 -0
  24. package/dist/cmd/build/vite/config-loader.d.ts.map +1 -1
  25. package/dist/cmd/build/vite/config-loader.js +30 -0
  26. package/dist/cmd/build/vite/config-loader.js.map +1 -1
  27. package/dist/cmd/build/vite/server-bundler.d.ts.map +1 -1
  28. package/dist/cmd/build/vite/server-bundler.js +22 -1
  29. package/dist/cmd/build/vite/server-bundler.js.map +1 -1
  30. package/dist/cmd/build/vite/vite-asset-server-config.d.ts.map +1 -1
  31. package/dist/cmd/build/vite/vite-asset-server-config.js +19 -14
  32. package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
  33. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  34. package/dist/cmd/build/vite/vite-builder.js +12 -8
  35. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  36. package/dist/cmd/dev/download.d.ts.map +1 -1
  37. package/dist/cmd/dev/download.js +63 -53
  38. package/dist/cmd/dev/download.js.map +1 -1
  39. package/dist/cmd/upgrade/index.d.ts.map +1 -1
  40. package/dist/cmd/upgrade/index.js +24 -11
  41. package/dist/cmd/upgrade/index.js.map +1 -1
  42. package/dist/cmd/upgrade/npm-availability.d.ts +45 -12
  43. package/dist/cmd/upgrade/npm-availability.d.ts.map +1 -1
  44. package/dist/cmd/upgrade/npm-availability.js +73 -26
  45. package/dist/cmd/upgrade/npm-availability.js.map +1 -1
  46. package/dist/types.d.ts +4 -2
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/types.js.map +1 -1
  49. package/dist/version-check.d.ts.map +1 -1
  50. package/dist/version-check.js +13 -2
  51. package/dist/version-check.js.map +1 -1
  52. package/package.json +6 -7
  53. package/src/cmd/ai/claude-code/constants.ts +26 -0
  54. package/src/cmd/ai/claude-code/index.ts +23 -0
  55. package/src/cmd/ai/claude-code/install.ts +181 -0
  56. package/src/cmd/ai/claude-code/uninstall.ts +122 -0
  57. package/src/cmd/ai/index.ts +6 -0
  58. package/src/cmd/build/entry-generator.ts +1 -8
  59. package/src/cmd/build/vite/config-loader.ts +37 -0
  60. package/src/cmd/build/vite/server-bundler.ts +28 -1
  61. package/src/cmd/build/vite/vite-asset-server-config.ts +22 -13
  62. package/src/cmd/build/vite/vite-builder.ts +17 -8
  63. package/src/cmd/dev/download.ts +76 -78
  64. package/src/cmd/upgrade/index.ts +35 -12
  65. package/src/cmd/upgrade/npm-availability.ts +106 -36
  66. package/src/types.ts +4 -2
  67. 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;
@@ -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,
@@ -252,18 +252,11 @@ if (isDevelopment() && process.env.VITE_PORT) {
252
252
  const url = new URL(c.req.url);
253
253
  const queryString = url.search; // Includes the '?' prefix
254
254
 
255
- // Get the requested WebSocket subprotocol (Vite uses 'vite-hmr')
256
- const requestedProtocol = c.req.header('sec-websocket-protocol');
257
-
258
255
  const success = server.upgrade(c.req.raw, {
259
256
  data: { type: 'vite-hmr', queryString },
260
- // Echo back the requested subprotocol so the browser accepts the connection
261
- headers: requestedProtocol ? {
262
- 'Sec-WebSocket-Protocol': requestedProtocol,
263
- } : undefined,
264
257
  });
265
258
  if (success) {
266
- otel.logger.debug('[HMR Proxy] WebSocket upgrade successful (protocol: %s)', requestedProtocol || 'none');
259
+ otel.logger.debug('[HMR Proxy] WebSocket upgrade successful');
267
260
  return new Response(null);
268
261
  }
269
262
  otel.logger.error('[HMR Proxy] WebSocket upgrade returned false');
@@ -71,3 +71,40 @@ export function getWorkbenchConfig(
71
71
  headers: workbench.headers ?? {},
72
72
  };
73
73
  }
74
+
75
+ /**
76
+ * Known Vite framework plugin name prefixes.
77
+ * Each framework's Vite plugin registers one or more plugins whose names
78
+ * start with these prefixes. We match against these to detect whether the
79
+ * user has already configured a framework plugin in their agentuity.config.ts.
80
+ */
81
+ const FRAMEWORK_PLUGIN_PREFIXES = [
82
+ 'vite:react', // @vitejs/plugin-react (vite:react-babel, vite:react-refresh, …)
83
+ 'vite:preact', // @preact/preset-vite
84
+ 'vite-plugin-svelte', // @sveltejs/vite-plugin-svelte
85
+ 'vite:vue', // @vitejs/plugin-vue (vite:vue, vite:vue-jsx)
86
+ 'vite-plugin-solid', // vite-plugin-solid
87
+ 'solid', // vite-plugin-solid also uses plain "solid"
88
+ ];
89
+
90
+ /**
91
+ * Check if the user's plugins include any known UI-framework Vite plugin
92
+ * (React, Svelte, Vue, Solid, Preact, …).
93
+ *
94
+ * Detection is name-based: Vite plugins expose a `name` property and every
95
+ * major framework plugin uses a predictable prefix. This avoids dynamically
96
+ * importing every possible framework just to compare names.
97
+ */
98
+ export function hasFrameworkPlugin(userPlugins: import('vite').PluginOption[]): boolean {
99
+ const flat = (userPlugins as unknown[]).flat(Infinity).filter(Boolean);
100
+ return flat.some(
101
+ (p: unknown) =>
102
+ p &&
103
+ typeof p === 'object' &&
104
+ 'name' in p &&
105
+ typeof (p as { name: unknown }).name === 'string' &&
106
+ FRAMEWORK_PLUGIN_PREFIXES.some((prefix) =>
107
+ ((p as { name: string }).name).startsWith(prefix)
108
+ )
109
+ );
110
+ }
@@ -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
- if (args.path.includes('/node_modules/')) {
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();
@@ -128,24 +128,33 @@ export async function generateAssetServerConfig(
128
128
  'process.env.NODE_ENV': JSON.stringify('development'),
129
129
  },
130
130
 
131
- // Plugins: User plugins first (e.g., Tailwind), then React and browser env
131
+ // Plugins: User plugins first (includes framework plugin like React/Svelte/Vue), then browser env
132
132
  // Try project's node_modules first, fall back to CLI's bundled version
133
133
  plugins: await (async () => {
134
- const projectRequire = createRequire(join(rootDir, 'package.json'));
135
- let reactPluginPath = '@vitejs/plugin-react';
136
- try {
137
- reactPluginPath = projectRequire.resolve('@vitejs/plugin-react');
138
- } catch {
139
- // Project doesn't have @vitejs/plugin-react, use CLI's bundled version
140
- }
141
- const reactPlugin = (await import(reactPluginPath)).default();
142
134
  const { browserEnvPlugin } = await import('./browser-env-plugin');
143
135
  const { publicAssetPathPlugin } = await import('./public-asset-path-plugin');
136
+ const { hasFrameworkPlugin } = await import('./config-loader');
137
+
138
+ // Auto-add React plugin if no framework plugin is present (backwards compatibility)
139
+ const resolvedUserPlugins = [...userPlugins];
140
+ if (resolvedUserPlugins.length === 0 || !hasFrameworkPlugin(resolvedUserPlugins)) {
141
+ logger.debug(
142
+ 'No framework plugin found in agentuity.config.ts plugins, adding React automatically for dev server'
143
+ );
144
+ const projectRequire = createRequire(join(rootDir, 'package.json'));
145
+ let reactPluginPath = '@vitejs/plugin-react';
146
+ try {
147
+ reactPluginPath = projectRequire.resolve('@vitejs/plugin-react');
148
+ } catch {
149
+ // Project doesn't have @vitejs/plugin-react, use CLI's bundled version
150
+ }
151
+ const reactModule = await import(reactPluginPath);
152
+ resolvedUserPlugins.unshift(reactModule.default());
153
+ }
154
+
144
155
  return [
145
- // User-defined plugins from agentuity.config.ts (e.g., Tailwind CSS)
146
- ...userPlugins,
147
- // React plugin for JSX/TSX transformation and Fast Refresh
148
- reactPlugin,
156
+ // User-defined plugins from agentuity.config.ts (framework plugin + extras)
157
+ ...resolvedUserPlugins,
149
158
  // Browser env plugin to map process.env to import.meta.env
150
159
  browserEnvPlugin(),
151
160
  // Warn about incorrect public asset paths in dev mode
@@ -180,8 +180,24 @@ export async function runViteBuild(options: ViteBuildOptions): Promise<void> {
180
180
 
181
181
  // Load custom user plugins from agentuity.config.ts if it exists
182
182
  const clientOutDir = join(rootDir, '.agentuity/client');
183
+ const { loadAgentuityConfig, hasFrameworkPlugin } = await import('./config-loader');
184
+ const userConfig = await loadAgentuityConfig(rootDir, logger);
185
+ const userPlugins = userConfig?.plugins || [];
186
+
187
+ // Auto-add React plugin if no framework plugin is present (backwards compatibility)
188
+ if (userPlugins.length === 0 || !hasFrameworkPlugin(userPlugins)) {
189
+ logger.debug(
190
+ 'No framework plugin found in agentuity.config.ts plugins, adding React automatically'
191
+ );
192
+ userPlugins.unshift(react());
193
+ }
194
+
195
+ if (userPlugins.length > 0) {
196
+ logger.debug('Loaded %d custom plugin(s) from agentuity.config.ts', userPlugins.length);
197
+ }
198
+
183
199
  const plugins = [
184
- react(),
200
+ ...userPlugins,
185
201
  browserEnvPlugin(),
186
202
  // Fix incorrect public asset paths and rewrite to CDN URLs
187
203
  publicAssetPathPlugin({ cdnBaseUrl }),
@@ -189,13 +205,6 @@ export async function runViteBuild(options: ViteBuildOptions): Promise<void> {
189
205
  // Emit analytics beacon as hashed CDN asset (prod builds only)
190
206
  beaconPlugin({ enabled: analyticsEnabled && !dev }),
191
207
  ];
192
- const { loadAgentuityConfig } = await import('./config-loader');
193
- const userConfig = await loadAgentuityConfig(rootDir, logger);
194
- const userPlugins = userConfig?.plugins || [];
195
- plugins.push(...userPlugins);
196
- if (userPlugins.length > 0) {
197
- logger.debug('Loaded %d custom plugin(s) from agentuity.config.ts', userPlugins.length);
198
- }
199
208
 
200
209
  // Merge custom define values from user config
201
210
  const userDefine = userConfig?.define || {};
@@ -1,118 +1,116 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { existsSync, createReadStream, mkdirSync, rmSync } from 'node:fs';
2
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir, platform } from 'node:os';
4
4
  import { join, dirname } from 'node:path';
5
5
  import * as tar from 'tar';
6
- import { downloadRelease } from '@terascope/fetch-github-release';
6
+ import { StructuredError } from '@agentuity/core';
7
7
  import { spinner } from '../../tui';
8
8
 
9
- const user = 'agentuity';
10
- const repo = 'gravity';
11
-
12
- function filterRelease(release: { prerelease: boolean }) {
13
- // Filter out prereleases.
14
- return release.prerelease === false;
15
- }
16
-
17
- function filterAsset(asset: { name: string }): boolean {
18
- // Filter out the release matching our os and architecture
19
- let arch: string = process.arch;
20
- if (arch === 'x64') {
21
- arch = 'x86_64';
22
- }
23
- return asset.name.includes(arch) && asset.name.includes(platform());
24
- }
25
-
26
9
  interface GravityClient {
27
10
  filename: string;
28
11
  version: string;
29
12
  }
30
13
 
14
+ const GravityVersionError = StructuredError('GravityVersionError')<{
15
+ status: number;
16
+ statusText: string;
17
+ }>();
18
+ const GravityDownloadError = StructuredError('GravityDownloadError')<{
19
+ status: number;
20
+ statusText: string;
21
+ }>();
22
+ const GravityExtractionError = StructuredError('GravityExtractionError')<{
23
+ path: string;
24
+ }>();
25
+
26
+ function getBaseURL(): string {
27
+ return process.env.AGENTUITY_SH_URL || 'https://agentuity.sh';
28
+ }
29
+
31
30
  /**
32
31
  *
33
32
  * @returns full path to the downloaded file
34
33
  */
35
34
  export async function download(gravityDir: string): Promise<GravityClient> {
36
- const outputdir = join(tmpdir(), randomUUID());
35
+ const baseURL = getBaseURL();
37
36
 
38
- const res = (await spinner({
37
+ // Step 1: Get the latest version from agentuity.sh
38
+ const tag = (await spinner({
39
39
  message: 'Checking Agentuity Gravity',
40
40
  callback: async () => {
41
- return downloadRelease(
42
- user,
43
- repo,
44
- outputdir,
45
- filterRelease,
46
- filterAsset,
47
- false,
48
- true,
49
- true,
50
- ''
51
- );
41
+ const resp = await fetch(`${baseURL}/release/gravity/version`, {
42
+ signal: AbortSignal.timeout(10_000),
43
+ });
44
+ if (!resp.ok) {
45
+ throw new GravityVersionError({
46
+ status: resp.status,
47
+ statusText: resp.statusText,
48
+ });
49
+ }
50
+ const text = (await resp.text()).trim();
51
+ return text.startsWith('v') ? text : `v${text}`;
52
52
  },
53
53
  clearOnSuccess: true,
54
- })) as { release: string; assetFileNames: string[] };
54
+ })) as string;
55
55
 
56
- const versionTok = res.release.split('@');
57
- const version = versionTok[1] ?? 'unknown';
56
+ const version = tag.startsWith('v') ? tag.slice(1) : tag;
58
57
  const releaseFilename = join(gravityDir, version, 'gravity');
59
- const mustDownload = !existsSync(releaseFilename);
60
58
 
61
- if (!mustDownload) {
59
+ // Step 2: Check if already downloaded
60
+ if (await Bun.file(releaseFilename).exists()) {
62
61
  return { filename: releaseFilename, version };
63
62
  }
64
63
 
65
- const downloadedFile = await spinner({
66
- message: `Downloading Gravity ${version}`,
67
- callback: async () => {
68
- const res = (await downloadRelease(
69
- user,
70
- repo,
71
- outputdir,
72
- filterRelease,
73
- filterAsset,
74
- false,
75
- true,
76
- false,
77
- ''
78
- )) as string[];
79
- const file = res[0];
80
- if (!file) {
81
- throw new Error('No file downloaded from release');
82
- }
83
- return file;
84
- },
85
- clearOnSuccess: true,
86
- });
64
+ // Step 3: Download the binary from agentuity.sh
65
+ const os = platform();
66
+ let arch: string = process.arch;
67
+ if (arch === 'x64') {
68
+ arch = 'x86_64';
69
+ }
87
70
 
88
- if (downloadedFile.endsWith('.tar.gz')) {
71
+ const tmpFile = join(tmpdir(), `${randomUUID()}.tar.gz`);
72
+
73
+ try {
89
74
  await spinner({
90
- message: 'Extracting release',
75
+ message: `Downloading Gravity ${version}`,
91
76
  callback: async () => {
92
- return new Promise<void>((resolve, reject) => {
93
- const input = createReadStream(downloadedFile);
94
- const downloadDir = dirname(releaseFilename);
95
- if (!existsSync(downloadDir)) {
96
- mkdirSync(downloadDir, { recursive: true });
97
- }
98
- input.on('finish', resolve);
99
- input.on('end', resolve);
100
- input.on('error', reject);
101
- input.pipe(tar.x({ C: downloadDir, chmod: true }));
77
+ const resp = await fetch(`${baseURL}/release/gravity/${tag}/${os}/${arch}`, {
78
+ signal: AbortSignal.timeout(60_000),
102
79
  });
80
+ if (!resp.ok) {
81
+ throw new GravityDownloadError({
82
+ status: resp.status,
83
+ statusText: resp.statusText,
84
+ });
85
+ }
86
+ const buffer = await resp.arrayBuffer();
87
+ writeFileSync(tmpFile, Buffer.from(buffer));
103
88
  },
104
89
  clearOnSuccess: true,
105
90
  });
106
- } else {
107
- // TODO:
108
- }
109
91
 
110
- if (existsSync(outputdir)) {
111
- rmSync(outputdir, { recursive: true });
92
+ // Step 4: Extract the tarball
93
+ await spinner({
94
+ message: 'Extracting release',
95
+ callback: async () => {
96
+ const downloadDir = dirname(releaseFilename);
97
+ if (!(await Bun.file(downloadDir).exists())) {
98
+ mkdirSync(downloadDir, { recursive: true });
99
+ }
100
+ await tar.x({ file: tmpFile, cwd: downloadDir, chmod: true });
101
+ },
102
+ clearOnSuccess: true,
103
+ });
104
+ } finally {
105
+ // Clean up temp file regardless of success or failure
106
+ if (await Bun.file(tmpFile).exists()) {
107
+ rmSync(tmpFile);
108
+ }
112
109
  }
113
110
 
114
- if (!existsSync(releaseFilename)) {
115
- throw new Error(`Failed to extract gravity binary to ${releaseFilename}`);
111
+ // Step 5: Verify the binary was extracted
112
+ if (!(await Bun.file(releaseFilename).exists())) {
113
+ throw new GravityExtractionError({ path: releaseFilename });
116
114
  }
117
115
 
118
116
  return { filename: releaseFilename, version };
@@ -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(version: string): Promise<void> {
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
- // Use bun to install the specific version globally
81
- // Run from tmpdir to avoid interference from any local package.json/node_modules
82
- const result = await $`bun add -g @agentuity/cli@${npmVersion}`.cwd(tmpdir()).quiet().nothrow();
83
-
84
- if (result.exitCode !== 0) {
85
- const stderr = result.stderr.toString();
86
- throw new Error(`Failed to install @agentuity/cli@${npmVersion}: ${stderr}`);
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: 6,
198
- initialDelayMs: 2000,
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 () => await performBunUpgrade(latestVersion),
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