@agentuity/cli 0.0.85 → 0.0.87

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 (106) hide show
  1. package/bin/cli.ts +9 -0
  2. package/dist/bun-path.d.ts +9 -0
  3. package/dist/bun-path.d.ts.map +1 -0
  4. package/dist/bun-path.js +24 -0
  5. package/dist/bun-path.js.map +1 -0
  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 +1 -1
  13. package/dist/cmd/build/bundler.d.ts.map +1 -1
  14. package/dist/cmd/build/bundler.js +99 -81
  15. package/dist/cmd/build/bundler.js.map +1 -1
  16. package/dist/cmd/build/patch/_util.js +6 -6
  17. package/dist/cmd/build/patch/_util.js.map +1 -1
  18. package/dist/cmd/build/patch/llm.js +1 -1
  19. package/dist/cmd/build/patch/llm.js.map +1 -1
  20. package/dist/cmd/build/plugin.d.ts.map +1 -1
  21. package/dist/cmd/build/plugin.js +21 -14
  22. package/dist/cmd/build/plugin.js.map +1 -1
  23. package/dist/cmd/build/route-discovery.d.ts +8 -4
  24. package/dist/cmd/build/route-discovery.d.ts.map +1 -1
  25. package/dist/cmd/build/route-discovery.js +10 -5
  26. package/dist/cmd/build/route-discovery.js.map +1 -1
  27. package/dist/cmd/cloud/scp/download.js +3 -3
  28. package/dist/cmd/cloud/scp/download.js.map +1 -1
  29. package/dist/cmd/cloud/scp/upload.js +3 -3
  30. package/dist/cmd/cloud/scp/upload.js.map +1 -1
  31. package/dist/cmd/cloud/ssh.js +3 -3
  32. package/dist/cmd/cloud/ssh.js.map +1 -1
  33. package/dist/cmd/dev/index.d.ts.map +1 -1
  34. package/dist/cmd/dev/index.js +11 -1
  35. package/dist/cmd/dev/index.js.map +1 -1
  36. package/dist/cmd/index.d.ts.map +1 -1
  37. package/dist/cmd/index.js +7 -0
  38. package/dist/cmd/index.js.map +1 -1
  39. package/dist/cmd/profile/create.d.ts.map +1 -1
  40. package/dist/cmd/profile/create.js +1 -0
  41. package/dist/cmd/profile/create.js.map +1 -1
  42. package/dist/cmd/project/download.d.ts.map +1 -1
  43. package/dist/cmd/project/download.js +5 -15
  44. package/dist/cmd/project/download.js.map +1 -1
  45. package/dist/cmd/upgrade/index.d.ts +20 -0
  46. package/dist/cmd/upgrade/index.d.ts.map +1 -0
  47. package/dist/cmd/upgrade/index.js +307 -0
  48. package/dist/cmd/upgrade/index.js.map +1 -0
  49. package/dist/cmd/version/index.d.ts.map +1 -1
  50. package/dist/cmd/version/index.js +1 -0
  51. package/dist/cmd/version/index.js.map +1 -1
  52. package/dist/config.d.ts +1 -1
  53. package/dist/config.d.ts.map +1 -1
  54. package/dist/config.js +12 -94
  55. package/dist/config.js.map +1 -1
  56. package/dist/download.d.ts.map +1 -1
  57. package/dist/download.js +1 -6
  58. package/dist/download.js.map +1 -1
  59. package/dist/git-helper.d.ts +22 -0
  60. package/dist/git-helper.d.ts.map +1 -0
  61. package/dist/git-helper.js +71 -0
  62. package/dist/git-helper.js.map +1 -0
  63. package/dist/index.d.ts +2 -0
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +2 -0
  66. package/dist/index.js.map +1 -1
  67. package/dist/tui.d.ts.map +1 -1
  68. package/dist/tui.js +16 -0
  69. package/dist/tui.js.map +1 -1
  70. package/dist/types.d.ts +7 -0
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/types.js.map +1 -1
  73. package/dist/utils/dependency-checker.d.ts +20 -0
  74. package/dist/utils/dependency-checker.d.ts.map +1 -0
  75. package/dist/utils/dependency-checker.js +161 -0
  76. package/dist/utils/dependency-checker.js.map +1 -0
  77. package/dist/version-check.d.ts +13 -0
  78. package/dist/version-check.d.ts.map +1 -0
  79. package/dist/version-check.js +177 -0
  80. package/dist/version-check.js.map +1 -0
  81. package/package.json +3 -3
  82. package/src/bun-path.ts +26 -0
  83. package/src/cmd/ai/index.ts +1 -0
  84. package/src/cmd/build/ast.ts +7 -0
  85. package/src/cmd/build/bundler.ts +115 -94
  86. package/src/cmd/build/patch/_util.ts +6 -6
  87. package/src/cmd/build/patch/llm.ts +1 -1
  88. package/src/cmd/build/plugin.ts +23 -16
  89. package/src/cmd/build/route-discovery.ts +10 -5
  90. package/src/cmd/cloud/scp/download.ts +3 -3
  91. package/src/cmd/cloud/scp/upload.ts +3 -3
  92. package/src/cmd/cloud/ssh.ts +3 -3
  93. package/src/cmd/dev/index.ts +17 -1
  94. package/src/cmd/index.ts +8 -0
  95. package/src/cmd/profile/create.ts +1 -0
  96. package/src/cmd/project/download.ts +6 -14
  97. package/src/cmd/upgrade/index.ts +365 -0
  98. package/src/cmd/version/index.ts +1 -0
  99. package/src/config.ts +12 -121
  100. package/src/download.ts +1 -7
  101. package/src/git-helper.ts +74 -0
  102. package/src/index.ts +2 -0
  103. package/src/tui.ts +19 -0
  104. package/src/types.ts +7 -0
  105. package/src/utils/dependency-checker.ts +207 -0
  106. package/src/version-check.ts +234 -0
@@ -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' },
package/src/config.ts CHANGED
@@ -407,81 +407,6 @@ function getPlaceholderValue(schema: z.ZodTypeAny): string {
407
407
  }
408
408
  }
409
409
 
410
- function extractDefaultValue(schema: z.ZodTypeAny): unknown {
411
- let unwrapped = schema;
412
-
413
- // Unwrap optional layers
414
- while (unwrapped instanceof z.ZodOptional) {
415
- unwrapped = (unwrapped._def as unknown as { innerType: z.ZodTypeAny }).innerType;
416
- }
417
-
418
- // Check if it's a ZodDefault (has defaultValue in def or _def)
419
- const checkDef = (obj: unknown): unknown => {
420
- if (typeof obj !== 'object' || obj === null) return undefined;
421
- const anyObj = obj as Record<string, unknown>;
422
-
423
- // Check `def` property first (used in some Zod versions)
424
- if ('def' in anyObj && typeof anyObj.def === 'object' && anyObj.def !== null) {
425
- const def = anyObj.def as Record<string, unknown>;
426
- if (def.type === 'default' && 'defaultValue' in def) {
427
- const val = def.defaultValue;
428
- return typeof val === 'function' ? (val as () => unknown)() : val;
429
- }
430
- }
431
-
432
- // Check `_def` property (standard Zod property)
433
- if ('_def' in anyObj && typeof anyObj._def === 'object' && anyObj._def !== null) {
434
- const def = anyObj._def as Record<string, unknown>;
435
- if (def.type === 'default' && 'defaultValue' in def) {
436
- const val = def.defaultValue;
437
- return typeof val === 'function' ? (val as () => unknown)() : val;
438
- }
439
- }
440
-
441
- return undefined;
442
- };
443
-
444
- return checkDef(unwrapped);
445
- }
446
-
447
- function getValueWithDefaults(schema: z.ZodTypeAny, providedValue: unknown): unknown {
448
- // If value is explicitly provided, use it
449
- if (providedValue !== undefined) {
450
- return providedValue;
451
- }
452
-
453
- // Try to extract default value
454
- const defaultValue = extractDefaultValue(schema);
455
- if (defaultValue !== undefined) {
456
- return defaultValue;
457
- }
458
-
459
- // For optional fields without defaults, check if it's an object
460
- let unwrapped = schema;
461
- if (schema instanceof z.ZodOptional) {
462
- unwrapped = (schema._def as unknown as { innerType: z.ZodTypeAny }).innerType;
463
- }
464
-
465
- // If it's an object schema, recursively populate defaults
466
- if (unwrapped instanceof z.ZodObject) {
467
- const shape = unwrapped.shape;
468
- const result: Record<string, unknown> = {};
469
- let hasAnyDefaults = false;
470
-
471
- for (const [key, fieldSchema] of Object.entries(shape)) {
472
- const fieldValue = getValueWithDefaults(fieldSchema as z.ZodTypeAny, undefined);
473
- if (fieldValue !== undefined) {
474
- result[key] = fieldValue;
475
- hasAnyDefaults = true;
476
- }
477
- }
478
-
479
- return hasAnyDefaults ? result : undefined;
480
- }
481
-
482
- return undefined;
483
- }
484
-
485
410
  export function generateYAMLTemplate(name: string): string {
486
411
  const lines: string[] = [];
487
412
 
@@ -552,39 +477,6 @@ class ProjectConfigNotFoundExpection extends Error {
552
477
 
553
478
  type ProjectConfig = z.infer<typeof ProjectSchema>;
554
479
 
555
- function generateJSON5WithComments(
556
- jsonSchema: string,
557
- schema: z.ZodObject<z.ZodRawShape>,
558
- data: Record<string, unknown>
559
- ): string {
560
- const lines: string[] = ['{'];
561
- const shape = schema.shape;
562
- const keys = Object.keys(shape);
563
-
564
- lines.push(` "$schema": "${jsonSchema}",`);
565
-
566
- for (let i = 0; i < keys.length; i++) {
567
- const key = keys[i];
568
- const fieldSchema = shape[key] as z.ZodTypeAny;
569
- const description = getSchemaDescription(fieldSchema);
570
- const providedValue = data[key];
571
-
572
- if (description) {
573
- lines.push(` // ${description}`);
574
- }
575
-
576
- // Get value with defaults applied
577
- const valueWithDefaults = getValueWithDefaults(fieldSchema, providedValue);
578
- const safeValue = valueWithDefaults === undefined ? null : valueWithDefaults;
579
- const jsonValue = JSON.stringify(safeValue, null, 2).replace(/\n/g, '\n ');
580
- const comma = i < keys.length - 1 ? ',' : '';
581
- lines.push(` ${JSON.stringify(key)}: ${jsonValue}${comma}`);
582
- }
583
-
584
- lines.push('}');
585
- return lines.join('\n');
586
- }
587
-
588
480
  export async function loadProjectConfig(
589
481
  dir: string,
590
482
  config?: Config | null
@@ -636,12 +528,11 @@ export async function createProjectConfig(dir: string, config: InitialProjectCon
636
528
 
637
529
  // generate the project config
638
530
  const configPath = join(dir, 'agentuity.json');
639
- const json5Content = generateJSON5WithComments(
640
- 'https://agentuity.dev/schema/cli/v1/agentuity.json',
641
- ProjectSchema,
642
- sanitizedConfig
643
- );
644
- await Bun.write(configPath, json5Content + '\n');
531
+ const configData = {
532
+ $schema: 'https://agentuity.dev/schema/cli/v1/agentuity.json',
533
+ ...sanitizedConfig,
534
+ };
535
+ await Bun.write(configPath, JSON.stringify(configData, null, 2) + '\n');
645
536
 
646
537
  // generate the .env file with initial secret
647
538
  const envPath = join(dir, '.env');
@@ -656,9 +547,6 @@ export async function createProjectConfig(dir: string, config: InitialProjectCon
656
547
  mkdirSync(vscodeDir);
657
548
 
658
549
  const settings = {
659
- 'files.associations': {
660
- 'agentuity.json': 'jsonc',
661
- },
662
550
  'search.exclude': {
663
551
  '**/.git/**': true,
664
552
  '**/node_modules/**': true,
@@ -743,12 +631,15 @@ export function getCatalystAPIClient(logger: Logger, auth: AuthData, region: str
743
631
  return new ServerAPIClient(catalystUrl, logger, auth.apiKey);
744
632
  }
745
633
 
746
- export function getIONHost(config: Config | null) {
747
- if (config?.name === 'local') {
634
+ export function getIONHost(config: Config | null, region: string) {
635
+ if (config?.overrides?.ion_url) {
636
+ const url = new URL(config.overrides.ion_url);
637
+ return url.hostname;
638
+ }
639
+ if (config?.name === 'local' || region === 'local') {
748
640
  return 'ion.agentuity.io';
749
641
  }
750
- const url = new URL(config?.overrides?.ion_url ?? 'https://ion.agentuity.cloud');
751
- return url.hostname;
642
+ return `ion-${region}.agentuity.cloud`;
752
643
  }
753
644
 
754
645
  export function getStreamURL(region: string, config: Config | null) {
package/src/download.ts CHANGED
@@ -18,13 +18,7 @@ export async function downloadWithProgress(
18
18
  ): Promise<NodeJS.ReadableStream> {
19
19
  const { url, headers = {}, onProgress } = options;
20
20
 
21
- // Add GITHUB_TOKEN if available and not already set
22
- const requestHeaders = { ...headers };
23
- if (process.env.GITHUB_TOKEN && !requestHeaders['Authorization']) {
24
- requestHeaders['Authorization'] = `Bearer ${process.env.GITHUB_TOKEN}`;
25
- }
26
-
27
- const response = await fetch(url, { headers: requestHeaders });
21
+ const response = await fetch(url, { headers });
28
22
  if (!response.ok) {
29
23
  throw new APIError({
30
24
  url,
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Git helper utilities for detecting and using git safely.
3
+ *
4
+ * On macOS, git may be a stub that triggers Xcode Command Line Tools installation popup.
5
+ * This helper detects the real git binary and provides safe wrappers.
6
+ */
7
+
8
+ /**
9
+ * Check if git is available and is the real git binary (not the macOS stub).
10
+ *
11
+ * On macOS without Xcode CLT installed, /usr/bin/git exists but it's a stub that
12
+ * triggers a popup asking to install developer tools. We detect this by checking
13
+ * if Xcode Command Line Tools are installed using `xcode-select -p`.
14
+ *
15
+ * @returns true if git is available and functional, false otherwise
16
+ */
17
+ export async function isGitAvailable(): Promise<boolean> {
18
+ const gitPath = Bun.which('git');
19
+ if (!gitPath) {
20
+ return false;
21
+ }
22
+
23
+ // On macOS, check if Xcode Command Line Tools are installed
24
+ // xcode-select -p returns 0 if tools are installed, non-zero otherwise
25
+ if (process.platform === 'darwin') {
26
+ try {
27
+ const result = Bun.spawnSync(['xcode-select', '-p'], {
28
+ stdout: 'pipe',
29
+ stderr: 'pipe',
30
+ });
31
+
32
+ // If xcode-select -p fails, CLT are not installed, git is just a stub
33
+ if (result.exitCode !== 0) {
34
+ return false;
35
+ }
36
+ } catch {
37
+ // xcode-select not found or error - assume git is not available
38
+ return false;
39
+ }
40
+ }
41
+
42
+ // On other platforms, just verify git works
43
+ try {
44
+ const result = Bun.spawnSync(['git', '--version'], {
45
+ stdout: 'pipe',
46
+ stderr: 'pipe',
47
+ });
48
+ return result.exitCode === 0;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Get the default branch name from git config, or 'main' as fallback.
56
+ * Returns null if git is not available.
57
+ */
58
+ export async function getDefaultBranch(): Promise<string | null> {
59
+ if (!(await isGitAvailable())) {
60
+ return null;
61
+ }
62
+
63
+ try {
64
+ const result = Bun.spawnSync(['git', 'config', '--global', 'init.defaultBranch']);
65
+ if (result.exitCode === 0) {
66
+ const branch = result.stdout.toString().trim();
67
+ return branch || 'main';
68
+ }
69
+ } catch {
70
+ // Ignore errors
71
+ }
72
+
73
+ return 'main';
74
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { createCLI, registerCommands } from './cli';
2
2
  export { validateRuntime, isBun } from './runtime';
3
+ export { ensureBunOnPath } from './bun-path';
4
+ export { isGitAvailable, getDefaultBranch } from './git-helper';
3
5
  export {
4
6
  generateCLISchema,
5
7
  type CLISchema,
package/src/tui.ts CHANGED
@@ -15,6 +15,25 @@ import { type APIClient as APIClientType } from './api';
15
15
  import { getExitCode } from './errors';
16
16
  import { maskSecret } from './env-util';
17
17
 
18
+ // Install global exit handler to always restore terminal cursor
19
+ // This ensures cursor is restored even when process.exit() is called directly
20
+ let exitHandlerInstalled = false;
21
+ function ensureCursorRestoration(): void {
22
+ if (exitHandlerInstalled) return;
23
+ exitHandlerInstalled = true;
24
+
25
+ const restoreCursor = () => {
26
+ // Restore cursor visibility
27
+ process.stderr.write('\x1B[?25h');
28
+ };
29
+
30
+ // Handle process exit
31
+ process.on('exit', restoreCursor);
32
+ }
33
+
34
+ // Install handler immediately when module loads
35
+ ensureCursorRestoration();
36
+
18
37
  // Re-export maskSecret for convenience
19
38
  export { maskSecret };
20
39
 
package/src/types.ts CHANGED
@@ -89,6 +89,7 @@ export interface GlobalOptions {
89
89
  explain?: boolean;
90
90
  dryRun?: boolean;
91
91
  validate?: boolean;
92
+ skipVersionCheck?: boolean;
92
93
  }
93
94
 
94
95
  export interface PaginationInfo {
@@ -282,6 +283,8 @@ export function createCommand<
282
283
  banner?: true;
283
284
  aliases?: string[];
284
285
  hidden?: boolean;
286
+ executable?: boolean;
287
+ skipUpgradeCheck?: boolean;
285
288
  requires?: R;
286
289
  optional?: O;
287
290
  examples?: CommandExample[];
@@ -318,6 +321,8 @@ type CommandDefBase =
318
321
  description: string;
319
322
  aliases?: string[];
320
323
  banner?: boolean;
324
+ executable?: boolean;
325
+ skipUpgradeCheck?: boolean;
321
326
  examples?: CommandExample[];
322
327
  idempotent?: boolean;
323
328
  prerequisites?: string[];
@@ -332,6 +337,8 @@ type CommandDefBase =
332
337
  description: string;
333
338
  aliases?: string[];
334
339
  banner?: boolean;
340
+ executable?: boolean;
341
+ skipUpgradeCheck?: boolean;
335
342
  examples?: CommandExample[];
336
343
  idempotent?: boolean;
337
344
  prerequisites?: string[];