@agentuity/cli 0.0.86 → 0.0.88

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 (119) hide show
  1. package/bin/cli.ts +7 -0
  2. package/dist/bun-path.d.ts.map +1 -1
  3. package/dist/bun-path.js +1 -3
  4. package/dist/bun-path.js.map +1 -1
  5. package/dist/cli.js +3 -3
  6. package/dist/cmd/ai/index.d.ts.map +1 -1
  7. package/dist/cmd/ai/index.js +1 -0
  8. package/dist/cmd/ai/index.js.map +1 -1
  9. package/dist/cmd/build/ast.d.ts.map +1 -1
  10. package/dist/cmd/build/ast.js +5 -0
  11. package/dist/cmd/build/ast.js.map +1 -1
  12. package/dist/cmd/build/bundler.d.ts.map +1 -1
  13. package/dist/cmd/build/bundler.js +152 -7
  14. package/dist/cmd/build/bundler.js.map +1 -1
  15. package/dist/cmd/build/config-loader.d.ts +16 -0
  16. package/dist/cmd/build/config-loader.d.ts.map +1 -0
  17. package/dist/cmd/build/config-loader.js +165 -0
  18. package/dist/cmd/build/config-loader.js.map +1 -0
  19. package/dist/cmd/build/patch/_util.js +6 -6
  20. package/dist/cmd/build/patch/_util.js.map +1 -1
  21. package/dist/cmd/build/patch/llm.js +1 -1
  22. package/dist/cmd/build/patch/llm.js.map +1 -1
  23. package/dist/cmd/build/plugin.d.ts.map +1 -1
  24. package/dist/cmd/build/plugin.js +36 -15
  25. package/dist/cmd/build/plugin.js.map +1 -1
  26. package/dist/cmd/build/route-discovery.d.ts +8 -4
  27. package/dist/cmd/build/route-discovery.d.ts.map +1 -1
  28. package/dist/cmd/build/route-discovery.js +10 -5
  29. package/dist/cmd/build/route-discovery.js.map +1 -1
  30. package/dist/cmd/build/workbench.d.ts +1 -0
  31. package/dist/cmd/build/workbench.d.ts.map +1 -1
  32. package/dist/cmd/build/workbench.js +8 -1
  33. package/dist/cmd/build/workbench.js.map +1 -1
  34. package/dist/cmd/cloud/index.d.ts.map +1 -1
  35. package/dist/cmd/cloud/index.js +2 -0
  36. package/dist/cmd/cloud/index.js.map +1 -1
  37. package/dist/cmd/cloud/redis/get.d.ts +2 -0
  38. package/dist/cmd/cloud/redis/get.d.ts.map +1 -0
  39. package/dist/cmd/cloud/redis/get.js +62 -0
  40. package/dist/cmd/cloud/redis/get.js.map +1 -0
  41. package/dist/cmd/cloud/redis/index.d.ts +2 -0
  42. package/dist/cmd/cloud/redis/index.d.ts.map +1 -0
  43. package/dist/cmd/cloud/redis/index.js +13 -0
  44. package/dist/cmd/cloud/redis/index.js.map +1 -0
  45. package/dist/cmd/cloud/scp/download.js +3 -3
  46. package/dist/cmd/cloud/scp/download.js.map +1 -1
  47. package/dist/cmd/cloud/scp/upload.js +3 -3
  48. package/dist/cmd/cloud/scp/upload.js.map +1 -1
  49. package/dist/cmd/cloud/ssh.js +3 -3
  50. package/dist/cmd/cloud/ssh.js.map +1 -1
  51. package/dist/cmd/dev/index.d.ts.map +1 -1
  52. package/dist/cmd/dev/index.js +5 -0
  53. package/dist/cmd/dev/index.js.map +1 -1
  54. package/dist/cmd/index.d.ts.map +1 -1
  55. package/dist/cmd/index.js +7 -0
  56. package/dist/cmd/index.js.map +1 -1
  57. package/dist/cmd/profile/create.d.ts.map +1 -1
  58. package/dist/cmd/profile/create.js +1 -0
  59. package/dist/cmd/profile/create.js.map +1 -1
  60. package/dist/cmd/upgrade/index.d.ts +20 -0
  61. package/dist/cmd/upgrade/index.d.ts.map +1 -0
  62. package/dist/cmd/upgrade/index.js +307 -0
  63. package/dist/cmd/upgrade/index.js.map +1 -0
  64. package/dist/cmd/version/index.d.ts.map +1 -1
  65. package/dist/cmd/version/index.js +1 -0
  66. package/dist/cmd/version/index.js.map +1 -1
  67. package/dist/config.d.ts +1 -1
  68. package/dist/config.d.ts.map +1 -1
  69. package/dist/config.js +12 -94
  70. package/dist/config.js.map +1 -1
  71. package/dist/index.d.ts +1 -1
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/tui.d.ts +5 -0
  75. package/dist/tui.d.ts.map +1 -1
  76. package/dist/tui.js +23 -0
  77. package/dist/tui.js.map +1 -1
  78. package/dist/types.d.ts +73 -0
  79. package/dist/types.d.ts.map +1 -1
  80. package/dist/types.js.map +1 -1
  81. package/dist/utils/dependency-checker.d.ts +20 -0
  82. package/dist/utils/dependency-checker.d.ts.map +1 -0
  83. package/dist/utils/dependency-checker.js +161 -0
  84. package/dist/utils/dependency-checker.js.map +1 -0
  85. package/dist/version-check.d.ts +13 -0
  86. package/dist/version-check.d.ts.map +1 -0
  87. package/dist/version-check.js +177 -0
  88. package/dist/version-check.js.map +1 -0
  89. package/package.json +6 -4
  90. package/src/bun-path.ts +1 -3
  91. package/src/cli.ts +3 -3
  92. package/src/cmd/ai/index.ts +1 -0
  93. package/src/cmd/build/ast.ts +7 -0
  94. package/src/cmd/build/bundler.ts +181 -8
  95. package/src/cmd/build/config-loader.ts +200 -0
  96. package/src/cmd/build/patch/_util.ts +6 -6
  97. package/src/cmd/build/patch/llm.ts +1 -1
  98. package/src/cmd/build/plugin.ts +40 -17
  99. package/src/cmd/build/route-discovery.ts +10 -5
  100. package/src/cmd/build/workbench.ts +9 -1
  101. package/src/cmd/cloud/index.ts +2 -0
  102. package/src/cmd/cloud/redis/get.ts +72 -0
  103. package/src/cmd/cloud/redis/index.ts +13 -0
  104. package/src/cmd/cloud/scp/download.ts +3 -3
  105. package/src/cmd/cloud/scp/upload.ts +3 -3
  106. package/src/cmd/cloud/ssh.ts +3 -3
  107. package/src/cmd/dev/index.ts +11 -0
  108. package/src/cmd/index.ts +8 -0
  109. package/src/cmd/profile/create.ts +1 -0
  110. package/src/cmd/project/download.ts +1 -1
  111. package/src/cmd/upgrade/index.ts +365 -0
  112. package/src/cmd/version/index.ts +1 -0
  113. package/src/config.ts +12 -121
  114. package/src/git-helper.ts +4 -4
  115. package/src/index.ts +4 -0
  116. package/src/tui.ts +27 -0
  117. package/src/types.ts +80 -0
  118. package/src/utils/dependency-checker.ts +207 -0
  119. package/src/version-check.ts +234 -0
@@ -1,6 +1,7 @@
1
1
  import { createCommand } from '../../types';
2
2
  import { deploySubcommand } from './deploy';
3
3
  import { dbCommand } from './db';
4
+ import { redisCommand } from './redis';
4
5
  import { storageCommand } from './storage';
5
6
  import { sessionCommand } from './session';
6
7
  import { threadCommand } from './thread';
@@ -34,6 +35,7 @@ export const command = createCommand({
34
35
  secretCommand,
35
36
  deploySubcommand,
36
37
  dbCommand,
38
+ redisCommand,
37
39
  storageCommand,
38
40
  sessionCommand,
39
41
  threadCommand,
@@ -0,0 +1,72 @@
1
+ import { z } from 'zod';
2
+ import { listResources } from '@agentuity/server';
3
+ import { createSubcommand } from '../../../types';
4
+ import * as tui from '../../../tui';
5
+ import { getCatalystAPIClient } from '../../../config';
6
+ import { getCommand } from '../../../command-prefix';
7
+
8
+ const RedisGetResponseSchema = z.object({
9
+ url: z.string().optional().describe('Redis connection URL'),
10
+ });
11
+
12
+ export const showSubcommand = createSubcommand({
13
+ name: 'show',
14
+ aliases: ['info'],
15
+ description: 'Show Redis connection URL',
16
+ tags: ['read-only', 'fast', 'requires-auth'],
17
+ requires: { auth: true, org: true, region: true },
18
+ idempotent: true,
19
+ examples: [
20
+ { command: getCommand('cloud redis show'), description: 'Show Redis connection URL' },
21
+ {
22
+ command: getCommand('cloud redis show --show-credentials'),
23
+ description: 'Show Redis URL with credentials visible',
24
+ },
25
+ {
26
+ command: getCommand('--json cloud redis show'),
27
+ description: 'Show Redis URL as JSON',
28
+ },
29
+ ],
30
+ schema: {
31
+ options: z.object({
32
+ showCredentials: z
33
+ .boolean()
34
+ .optional()
35
+ .describe(
36
+ 'Show credentials in plain text (default: masked in terminal, unmasked in JSON)'
37
+ ),
38
+ }),
39
+ response: RedisGetResponseSchema,
40
+ },
41
+
42
+ async handler(ctx) {
43
+ const { logger, opts, options, orgId, region, auth } = ctx;
44
+
45
+ const catalystClient = getCatalystAPIClient(logger, auth, region);
46
+
47
+ const resources = await tui.spinner({
48
+ message: `Fetching Redis for ${orgId} in ${region}`,
49
+ clearOnSuccess: true,
50
+ callback: async () => {
51
+ return listResources(catalystClient, orgId, region);
52
+ },
53
+ });
54
+
55
+ if (!resources.redis) {
56
+ tui.info('No Redis provisioned for this organization');
57
+ return { url: undefined };
58
+ }
59
+
60
+ const shouldShowCredentials = opts.showCredentials === true;
61
+ const shouldMask = !options.json && !shouldShowCredentials;
62
+
63
+ if (!options.json) {
64
+ const displayUrl = shouldMask ? tui.maskSecret(resources.redis.url) : resources.redis.url;
65
+ tui.output(tui.bold('Redis URL: ') + displayUrl);
66
+ }
67
+
68
+ return {
69
+ url: resources.redis.url,
70
+ };
71
+ },
72
+ });
@@ -0,0 +1,13 @@
1
+ import { createCommand } from '../../../types';
2
+ import { showSubcommand } from './get';
3
+ import { getCommand } from '../../../command-prefix';
4
+
5
+ export const redisCommand = createCommand({
6
+ name: 'redis',
7
+ description: 'Manage Redis resources',
8
+ tags: ['slow', 'requires-auth'],
9
+ examples: [
10
+ { command: getCommand('cloud redis show'), description: 'Show Redis connection URL' },
11
+ ],
12
+ subcommands: [showSubcommand],
13
+ });
@@ -36,7 +36,7 @@ export const downloadCommand = createSubcommand({
36
36
  description: 'Download multiple files',
37
37
  },
38
38
  ],
39
- requires: { apiClient: true, auth: true },
39
+ requires: { apiClient: true, auth: true, region: true },
40
40
  optional: { project: true },
41
41
  prerequisites: ['cloud deploy'],
42
42
  schema: {
@@ -51,7 +51,7 @@ export const downloadCommand = createSubcommand({
51
51
  },
52
52
 
53
53
  async handler(ctx) {
54
- const { apiClient, args, opts, project, projectDir, config } = ctx;
54
+ const { apiClient, args, opts, project, projectDir, config, region } = ctx;
55
55
 
56
56
  let identifier = opts?.identifier ?? project?.projectId;
57
57
 
@@ -59,7 +59,7 @@ export const downloadCommand = createSubcommand({
59
59
  identifier = await tui.showProjectList(apiClient, true);
60
60
  }
61
61
 
62
- const hostname = getIONHost(config);
62
+ const hostname = getIONHost(config, region);
63
63
  const destination = args.destination ?? projectDir;
64
64
 
65
65
  try {
@@ -39,7 +39,7 @@ export const uploadCommand = createSubcommand({
39
39
  description: 'Upload multiple files',
40
40
  },
41
41
  ],
42
- requires: { apiClient: true, auth: true },
42
+ requires: { apiClient: true, auth: true, region: true },
43
43
  schema: {
44
44
  args,
45
45
  options,
@@ -54,7 +54,7 @@ export const uploadCommand = createSubcommand({
54
54
  prerequisites: ['cloud deploy'],
55
55
 
56
56
  async handler(ctx) {
57
- const { apiClient, args, opts, project, projectDir, config } = ctx;
57
+ const { apiClient, args, opts, project, projectDir, config, region } = ctx;
58
58
 
59
59
  let identifier = opts?.identifier ?? project?.projectId;
60
60
 
@@ -62,7 +62,7 @@ export const uploadCommand = createSubcommand({
62
62
  identifier = await tui.showProjectList(apiClient, true);
63
63
  }
64
64
 
65
- const hostname = getIONHost(config);
65
+ const hostname = getIONHost(config, region);
66
66
  const destination = args.destination ?? '.';
67
67
 
68
68
  try {
@@ -35,13 +35,13 @@ export const sshSubcommand = createSubcommand({
35
35
  },
36
36
  ],
37
37
  toplevel: true,
38
- requires: { auth: true, apiClient: true },
38
+ requires: { auth: true, apiClient: true, region: true },
39
39
  optional: { project: true },
40
40
  prerequisites: ['cloud deploy'],
41
41
  schema: { args, options },
42
42
 
43
43
  async handler(ctx) {
44
- const { apiClient, project, projectDir, args, config, opts } = ctx;
44
+ const { apiClient, project, projectDir, args, config, opts, region } = ctx;
45
45
 
46
46
  let projectId = project?.projectId;
47
47
  let identifier = args?.identifier;
@@ -56,7 +56,7 @@ export const sshSubcommand = createSubcommand({
56
56
  projectId = await tui.showProjectList(apiClient, true);
57
57
  }
58
58
 
59
- const hostname = getIONHost(config);
59
+ const hostname = getIONHost(config, region);
60
60
 
61
61
  const cmd = ['ssh', `${identifier ?? projectId}@${hostname}`, command].filter(
62
62
  Boolean
@@ -938,6 +938,17 @@ export const command = createCommand({
938
938
  return;
939
939
  }
940
940
 
941
+ // Ignore .git folder
942
+ if (changedFile && (changedFile === '.git' || changedFile.startsWith('.git/'))) {
943
+ logger.trace(
944
+ 'File change ignored (.git folder): %s (event: %s, file: %s)',
945
+ watchDir,
946
+ eventType,
947
+ changedFile
948
+ );
949
+ return;
950
+ }
951
+
941
952
  // Ignore changes in .agentuity directory (build output)
942
953
  // Check both relative path and normalized absolute path
943
954
  const isInAgentuityDir =
package/src/cmd/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { CommandDefinition } from '../types';
2
+ import { isRunningFromExecutable } from './upgrade';
2
3
 
3
4
  // Use dynamic imports for bundler compatibility while maintaining lazy loading
4
5
  export async function discoverCommands(): Promise<CommandDefinition[]> {
@@ -12,12 +13,19 @@ export async function discoverCommands(): Promise<CommandDefinition[]> {
12
13
  import('./profile').then((m) => m.command),
13
14
  import('./project').then((m) => m.command),
14
15
  import('./repl').then((m) => m.command),
16
+ import('./upgrade').then((m) => m.command),
15
17
  import('./version').then((m) => m.command),
16
18
  ]);
17
19
 
18
20
  const commands: CommandDefinition[] = [];
21
+ const isExecutable = isRunningFromExecutable();
19
22
 
20
23
  for (const cmd of commandModules) {
24
+ // Skip commands that require running from an executable when not in one
25
+ if (cmd.executable && !isExecutable) {
26
+ continue;
27
+ }
28
+
21
29
  commands.push(cmd);
22
30
 
23
31
  // Auto-create hidden top-level aliases for subcommands with toplevel: true
@@ -73,6 +73,7 @@ export const createCommand = createSubcommand({
73
73
  if (name === 'local') {
74
74
  // if we're creating a local profile, go ahead and fill it out for the dev to make it easier to get started
75
75
  const localConfig = (await loadConfig(filename)) as Config;
76
+ localConfig.name = name;
76
77
  localConfig.overrides = {
77
78
  api_url: 'https://api.agentuity.io',
78
79
  app_url: 'https://app.agentuity.io',
@@ -231,7 +231,7 @@ export async function setupProject(options: SetupOptions): Promise<void> {
231
231
  // Check for real git (not macOS stub that triggers Xcode CLT popup)
232
232
  const { isGitAvailable, getDefaultBranch } = await import('../../git-helper');
233
233
  const gitAvailable = await isGitAvailable();
234
-
234
+
235
235
  if (gitAvailable) {
236
236
  // Get default branch from git config, fallback to 'main'
237
237
  const defaultBranch = (await getDefaultBranch()) || 'main';
@@ -0,0 +1,365 @@
1
+ import { createCommand } from '../../types';
2
+ import { getVersion } from '../../version';
3
+ import { getCommand } from '../../command-prefix';
4
+ import { z } from 'zod';
5
+ import { ErrorCode, createError, exitWithError } from '../../errors';
6
+ import * as tui from '../../tui';
7
+ import { downloadWithProgress } from '../../download';
8
+ import { $ } from 'bun';
9
+ import { join } from 'node:path';
10
+ import { tmpdir } from 'node:os';
11
+ import { randomUUID } from 'node:crypto';
12
+
13
+ const UpgradeOptionsSchema = z.object({
14
+ force: z.boolean().optional().describe('Force upgrade even if version is the same'),
15
+ });
16
+
17
+ const UpgradeResponseSchema = z.object({
18
+ upgraded: z.boolean().describe('Whether an upgrade was performed'),
19
+ from: z.string().describe('Version before upgrade'),
20
+ to: z.string().describe('Version after upgrade'),
21
+ message: z.string().describe('Status message'),
22
+ });
23
+
24
+ /**
25
+ * Check if running from a compiled executable (not via bun/bunx)
26
+ * @internal Exported for testing
27
+ */
28
+ export function isRunningFromExecutable(): boolean {
29
+ const scriptPath = process.argv[1] || '';
30
+
31
+ // Check if running from compiled binary (uses Bun's virtual filesystem)
32
+ // When compiled with `bun build --compile`, the path is in the virtual /$bunfs/root/ directory
33
+ const isCompiledBinary = process.argv[0] === 'bun' && scriptPath.startsWith('/$bunfs/root/');
34
+
35
+ if (isCompiledBinary) {
36
+ return true;
37
+ }
38
+
39
+ // If running via bun/bunx (from node_modules or .ts files), it's not an executable
40
+ if (Bun.main.includes('/node_modules/') || Bun.main.includes('.ts')) {
41
+ return false;
42
+ }
43
+
44
+ // Check if in a bin directory but not in node_modules (globally installed)
45
+ const normalized = Bun.main;
46
+ const isGlobal =
47
+ normalized.includes('/bin/') &&
48
+ !normalized.includes('/node_modules/') &&
49
+ !normalized.includes('/packages/cli/bin');
50
+
51
+ return isGlobal;
52
+ }
53
+
54
+ /**
55
+ * Get the OS and architecture for downloading the binary
56
+ * @internal Exported for testing
57
+ */
58
+ export function getPlatformInfo(): { os: string; arch: string } {
59
+ const platform = process.platform;
60
+ const arch = process.arch;
61
+
62
+ let os: string;
63
+ let archStr: string;
64
+
65
+ switch (platform) {
66
+ case 'darwin':
67
+ os = 'darwin';
68
+ break;
69
+ case 'linux':
70
+ os = 'linux';
71
+ break;
72
+ default:
73
+ throw new Error(`Unsupported platform: ${platform}`);
74
+ }
75
+
76
+ switch (arch) {
77
+ case 'x64':
78
+ archStr = 'x64';
79
+ break;
80
+ case 'arm64':
81
+ archStr = 'arm64';
82
+ break;
83
+ default:
84
+ throw new Error(`Unsupported architecture: ${arch}`);
85
+ }
86
+
87
+ return { os, arch: archStr };
88
+ }
89
+
90
+ /**
91
+ * Fetch the latest version from the API
92
+ * @internal Exported for testing
93
+ */
94
+ export async function fetchLatestVersion(): Promise<string> {
95
+ const response = await fetch('https://agentuity.sh/release/sdk/version', {
96
+ signal: AbortSignal.timeout(10000), // 10 second timeout
97
+ });
98
+ if (!response.ok) {
99
+ throw new Error(`Failed to fetch version: ${response.statusText}`);
100
+ }
101
+
102
+ const version = await response.text();
103
+ const trimmedVersion = version.trim();
104
+
105
+ // Validate version format
106
+ if (
107
+ !/^v?[0-9]+\.[0-9]+\.[0-9]+/.test(trimmedVersion) ||
108
+ trimmedVersion.includes('message') ||
109
+ trimmedVersion.includes('error') ||
110
+ trimmedVersion.includes('<html>')
111
+ ) {
112
+ throw new Error(`Invalid version format received: ${trimmedVersion}`);
113
+ }
114
+
115
+ // Ensure version has 'v' prefix
116
+ return trimmedVersion.startsWith('v') ? trimmedVersion : `v${trimmedVersion}`;
117
+ }
118
+
119
+ /**
120
+ * Download the binary for the specified version
121
+ */
122
+ async function downloadBinary(
123
+ version: string,
124
+ platform: { os: string; arch: string }
125
+ ): Promise<string> {
126
+ const { os, arch } = platform;
127
+ const url = `https://agentuity.sh/release/sdk/${version}/${os}/${arch}`;
128
+
129
+ const tmpDir = tmpdir();
130
+ const tmpFile = join(tmpDir, `agentuity-${randomUUID()}`);
131
+ const gzFile = `${tmpFile}.gz`;
132
+
133
+ const stream = await downloadWithProgress({
134
+ url,
135
+ message: `Downloading version ${version}...`,
136
+ });
137
+
138
+ // Write to temp file
139
+ const writer = Bun.file(gzFile).writer();
140
+ for await (const chunk of stream) {
141
+ writer.write(chunk);
142
+ }
143
+ await writer.end();
144
+
145
+ // Verify file was downloaded
146
+ if (!(await Bun.file(gzFile).exists())) {
147
+ throw new Error('Download failed - file not created');
148
+ }
149
+
150
+ // Decompress using gunzip
151
+ try {
152
+ await $`gunzip ${gzFile}`.quiet();
153
+ } catch (error) {
154
+ if (await Bun.file(gzFile).exists()) {
155
+ await $`rm ${gzFile}`.quiet();
156
+ }
157
+ throw new Error(
158
+ `Decompression failed: ${error instanceof Error ? error.message : 'Unknown error'}`
159
+ );
160
+ }
161
+
162
+ // Verify decompressed file exists
163
+ if (!(await Bun.file(tmpFile).exists())) {
164
+ throw new Error('Decompression failed - file not found');
165
+ }
166
+
167
+ // Verify it's a valid binary
168
+ const fileType = await $`file ${tmpFile}`.text();
169
+ if (!fileType.match(/(executable|ELF|Mach-O|PE32)/i)) {
170
+ throw new Error('Downloaded file is not a valid executable');
171
+ }
172
+
173
+ // Make executable
174
+ await $`chmod 755 ${tmpFile}`.quiet();
175
+
176
+ return tmpFile;
177
+ }
178
+
179
+ /**
180
+ * Validate the downloaded binary by running version command
181
+ */
182
+ async function validateBinary(binaryPath: string, expectedVersion: string): Promise<void> {
183
+ try {
184
+ const result = await $`${binaryPath} version`.text();
185
+ const actualVersion = result.trim();
186
+
187
+ // Normalize versions for comparison (remove 'v' prefix)
188
+ const normalizedExpected = expectedVersion.replace(/^v/, '');
189
+ const normalizedActual = actualVersion.replace(/^v/, '');
190
+
191
+ if (normalizedActual !== normalizedExpected) {
192
+ throw new Error(`Version mismatch: expected ${expectedVersion}, got ${actualVersion}`);
193
+ }
194
+ } catch (error) {
195
+ if (error instanceof Error) {
196
+ throw new Error(`Binary validation failed: ${error.message}`);
197
+ }
198
+ throw new Error('Binary validation failed');
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Replace the current binary with the new one
204
+ * Uses platform-specific safe replacement strategies
205
+ */
206
+ async function replaceBinary(newBinaryPath: string, currentBinaryPath: string): Promise<void> {
207
+ const platform = process.platform;
208
+
209
+ if (platform === 'darwin' || platform === 'linux') {
210
+ // Unix: Use atomic move via temp file
211
+ const backupPath = `${currentBinaryPath}.backup`;
212
+ const tempPath = `${currentBinaryPath}.new`;
213
+
214
+ try {
215
+ // Copy new binary to temp location next to current binary
216
+ await $`cp ${newBinaryPath} ${tempPath}`.quiet();
217
+ await $`chmod 755 ${tempPath}`.quiet();
218
+
219
+ // Backup current binary
220
+ if (await Bun.file(currentBinaryPath).exists()) {
221
+ await $`cp ${currentBinaryPath} ${backupPath}`.quiet();
222
+ }
223
+
224
+ // Atomic rename
225
+ await $`mv ${tempPath} ${currentBinaryPath}`.quiet();
226
+
227
+ // Clean up backup after successful replacement
228
+ if (await Bun.file(backupPath).exists()) {
229
+ await $`rm ${backupPath}`.quiet();
230
+ }
231
+ } catch (error) {
232
+ // Try to restore backup if replacement failed
233
+ if (await Bun.file(backupPath).exists()) {
234
+ await $`mv ${backupPath} ${currentBinaryPath}`.quiet();
235
+ }
236
+ // Clean up temp file if it exists
237
+ if (await Bun.file(tempPath).exists()) {
238
+ await $`rm ${tempPath}`.quiet();
239
+ }
240
+ throw error;
241
+ }
242
+ } else {
243
+ throw new Error(`Unsupported platform for binary replacement: ${platform}`);
244
+ }
245
+ }
246
+
247
+ export const command = createCommand({
248
+ name: 'upgrade',
249
+ description: 'Upgrade the CLI to the latest version',
250
+ executable: true,
251
+ skipUpgradeCheck: true,
252
+ tags: ['update'],
253
+ examples: [
254
+ {
255
+ command: getCommand('upgrade'),
256
+ description: 'Check for updates and prompt to upgrade',
257
+ },
258
+ {
259
+ command: getCommand('upgrade --force'),
260
+ description: 'Force upgrade even if already on latest version',
261
+ },
262
+ ],
263
+ schema: {
264
+ options: UpgradeOptionsSchema,
265
+ response: UpgradeResponseSchema,
266
+ },
267
+
268
+ async handler(ctx) {
269
+ const { logger, options } = ctx;
270
+ const { force } = ctx.opts;
271
+
272
+ const currentVersion = getVersion();
273
+ // Use process.execPath to get the actual file path (Bun.main is virtual for compiled binaries)
274
+ const currentBinaryPath = process.execPath;
275
+
276
+ try {
277
+ // Fetch latest version
278
+ const latestVersion = await tui.spinner({
279
+ message: 'Checking for updates...',
280
+ clearOnSuccess: true,
281
+ callback: async () => await fetchLatestVersion(),
282
+ });
283
+
284
+ // Compare versions
285
+ const normalizedCurrent = currentVersion.replace(/^v/, '');
286
+ const normalizedLatest = latestVersion.replace(/^v/, '');
287
+
288
+ if (normalizedCurrent === normalizedLatest && !force) {
289
+ const message = `Already on latest version ${currentVersion}`;
290
+ tui.success(message);
291
+ return {
292
+ upgraded: false,
293
+ from: currentVersion,
294
+ to: latestVersion,
295
+ message,
296
+ };
297
+ }
298
+
299
+ // Confirm upgrade
300
+ if (!force) {
301
+ tui.info(`Current version: ${tui.muted(currentVersion)}`);
302
+ tui.info(`Latest version: ${tui.bold(latestVersion)}`);
303
+ tui.info('');
304
+
305
+ const shouldUpgrade = await tui.confirm('Do you want to upgrade?', true);
306
+
307
+ if (!shouldUpgrade) {
308
+ const message = 'Upgrade cancelled';
309
+ tui.info(message);
310
+ return {
311
+ upgraded: false,
312
+ from: currentVersion,
313
+ to: latestVersion,
314
+ message,
315
+ };
316
+ }
317
+ }
318
+
319
+ // Get platform info
320
+ const platform = getPlatformInfo();
321
+
322
+ // Download binary
323
+ const tmpBinaryPath = await tui.spinner({
324
+ type: 'progress',
325
+ message: 'Downloading...',
326
+ callback: async () => await downloadBinary(latestVersion, platform),
327
+ });
328
+
329
+ // Validate binary
330
+ await tui.spinner({
331
+ message: 'Validating binary...',
332
+ callback: async () => await validateBinary(tmpBinaryPath, latestVersion),
333
+ });
334
+
335
+ // Replace binary
336
+ await tui.spinner({
337
+ message: 'Installing...',
338
+ callback: async () => await replaceBinary(tmpBinaryPath, currentBinaryPath),
339
+ });
340
+
341
+ // Clean up temp file
342
+ if (await Bun.file(tmpBinaryPath).exists()) {
343
+ await $`rm ${tmpBinaryPath}`.quiet();
344
+ }
345
+
346
+ const message = `Successfully upgraded from ${currentVersion} to ${latestVersion}`;
347
+ tui.success(message);
348
+
349
+ return {
350
+ upgraded: true,
351
+ from: currentVersion,
352
+ to: latestVersion,
353
+ message,
354
+ };
355
+ } catch (error) {
356
+ exitWithError(
357
+ createError(ErrorCode.INTERNAL_ERROR, 'Upgrade failed', {
358
+ error: error instanceof Error ? error.message : 'Unknown error',
359
+ }),
360
+ logger,
361
+ options.errorFormat
362
+ );
363
+ }
364
+ },
365
+ });
@@ -10,6 +10,7 @@ const VersionResponseSchema = z.string().describe('CLI version number');
10
10
  export const command = createCommand({
11
11
  name: 'version',
12
12
  description: 'Display version information',
13
+ skipUpgradeCheck: true,
13
14
  tags: ['read-only', 'fast'],
14
15
  examples: [
15
16
  { command: getCommand('version'), description: 'Show the CLI semantic version' },