@agentuity/cli 0.1.26 → 0.1.28

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.
@@ -11,6 +11,11 @@ import type { LogLevel } from '../../types';
11
11
  import { existsSync, mkdirSync } from 'node:fs';
12
12
  import JSON5 from 'json5';
13
13
  import { formatSchemaCode } from './format-schema';
14
+ import {
15
+ computeApiMountPath,
16
+ joinMountAndRoute,
17
+ extractRelativeApiPath,
18
+ } from './vite/api-mount-path';
14
19
 
15
20
  const logger = createLogger((process.env.AGENTUITY_LOG_LEVEL || 'info') as LogLevel);
16
21
 
@@ -1492,26 +1497,18 @@ export async function parseRoute(
1492
1497
 
1493
1498
  const rel = relative(rootDir, filename);
1494
1499
 
1495
- // For src/api/index.ts, we don't want to add the folder name since it's the root API router
1496
- const isRootApi = filename.includes('src/api/index.ts');
1497
-
1498
- // For nested routes, use the full path from src/api/ instead of just the immediate parent
1499
- // e.g., src/api/v1/users/route.ts -> routeName = "v1/users"
1500
- // src/api/auth/route.ts -> routeName = "auth"
1501
- // src/api/test.ts -> routeName = "" (file directly in src/api/)
1502
- let routeName = '';
1503
- if (!isRootApi) {
1504
- const apiMatch = filename.match(/src\/api\/(.+?)\/[^/]+\.ts$/);
1505
- if (apiMatch) {
1506
- // File in subdirectory: src/api/auth/route.ts -> "auth"
1507
- routeName = apiMatch[1];
1508
- }
1509
- // For files directly in src/api/ (e.g., test.ts), routeName stays empty
1510
- // This prevents double /api prefix since these files often define full paths
1511
- }
1500
+ // Compute the API mount path using the shared helper
1501
+ // This ensures consistency between route type generation (here) and runtime mounting (entry-generator.ts)
1502
+ // Examples:
1503
+ // src/api/index.ts -> basePath = '/api'
1504
+ // src/api/sessions.ts -> basePath = '/api/sessions'
1505
+ // src/api/auth/route.ts -> basePath = '/api/auth'
1506
+ // src/api/users/profile/route.ts -> basePath = '/api/users/profile'
1507
+ const srcDir = join(rootDir, 'src');
1508
+ const relativeApiPath = extractRelativeApiPath(filename, srcDir);
1509
+ const basePath = computeApiMountPath(relativeApiPath);
1512
1510
 
1513
1511
  const routes: RouteDefinition = [];
1514
- const routePrefix = '/api';
1515
1512
 
1516
1513
  try {
1517
1514
  for (const body of ast.body) {
@@ -1593,9 +1590,7 @@ export async function parseRoute(
1593
1590
 
1594
1591
  // Create a route entry for each method
1595
1592
  for (const httpMethod of methods) {
1596
- const thepath = `${routePrefix}/${routeName}/${pathSuffix}`
1597
- .replaceAll(/\/{2,}/g, '/')
1598
- .replaceAll(/\/$/g, '');
1593
+ const thepath = joinMountAndRoute(basePath, pathSuffix);
1599
1594
  const id = generateRouteId(
1600
1595
  projectId,
1601
1596
  deploymentId,
@@ -1692,9 +1687,7 @@ export async function parseRoute(
1692
1687
 
1693
1688
  // Create a route entry for each supported method
1694
1689
  for (const httpMethod of SUPPORTED_HTTP_METHODS) {
1695
- const thepath = `${routePrefix}/${routeName}/${pathSuffix}`
1696
- .replaceAll(/\/{2,}/g, '/')
1697
- .replaceAll(/\/$/g, '');
1690
+ const thepath = joinMountAndRoute(basePath, pathSuffix);
1698
1691
  const id = generateRouteId(
1699
1692
  projectId,
1700
1693
  deploymentId,
@@ -1888,9 +1881,7 @@ export async function parseRoute(
1888
1881
  });
1889
1882
  }
1890
1883
  }
1891
- const thepath = `${routePrefix}/${routeName}/${suffix}`
1892
- .replaceAll(/\/{2,}/g, '/')
1893
- .replaceAll(/\/$/g, '');
1884
+ const thepath = joinMountAndRoute(basePath, suffix);
1894
1885
  const id = generateRouteId(
1895
1886
  projectId,
1896
1887
  deploymentId,
@@ -7,6 +7,7 @@ import { join } from 'node:path';
7
7
  import type { Logger, WorkbenchConfig, AnalyticsConfig } from '../../types';
8
8
  import { discoverRoutes } from './vite/route-discovery';
9
9
  import { generateWebAnalyticsFile } from './webanalytics-generator';
10
+ import { computeApiMountPath } from './vite/api-mount-path';
10
11
 
11
12
  interface GenerateEntryOptions {
12
13
  rootDir: string;
@@ -107,21 +108,14 @@ export async function generateEntryFile(options: GenerateEntryOptions): Promise<
107
108
  let routeIndex = 0;
108
109
 
109
110
  for (const routeFile of sortedRouteFiles) {
111
+ // Normalize path separators for cross-platform compatibility (Windows uses backslashes)
112
+ const normalizedRouteFile = routeFile.replace(/\\/g, '/');
110
113
  // Convert src/api/auth/route.ts -> auth/route
111
- const relativePath = routeFile.replace(/^src\/api\//, '').replace(/\.tsx?$/, '');
112
-
113
- // Determine the mount path
114
- // src/api/index.ts -> /api
115
- // src/api/auth/route.ts -> /api/auth
116
- // src/api/users/profile/route.ts -> /api/users/profile
117
- let mountPath = '/api';
118
- if (relativePath !== 'index') {
119
- // Remove 'route' or 'index' from the end
120
- const cleanPath = relativePath.replace(/\/(route|index)$/, '');
121
- if (cleanPath) {
122
- mountPath = `/api/${cleanPath}`;
123
- }
124
- }
114
+ const relativePath = normalizedRouteFile.replace(/^src\/api\//, '').replace(/\.tsx?$/, '');
115
+
116
+ // Determine the mount path using the shared helper
117
+ // This ensures consistency with route type generation in ast.ts
118
+ const mountPath = computeApiMountPath(relativePath);
125
119
 
126
120
  const importName = `router_${routeIndex++}`;
127
121
  routeImportsAndMounts.push(
@@ -0,0 +1,87 @@
1
+ /**
2
+ * API Mount Path Utilities
3
+ *
4
+ * Shared helpers for computing API route mount paths from file paths.
5
+ * Used by both entry-generator.ts (runtime mounting) and ast.ts (type generation)
6
+ * to ensure consistent path calculation.
7
+ */
8
+
9
+ /**
10
+ * Compute the API mount path from a route file's relative path.
11
+ * This is the path used in app.route(mountPath, router).
12
+ *
13
+ * The mount path is based on the DIRECTORY containing the file, not the filename.
14
+ * Files directly in src/api/ (regardless of their name) mount at /api.
15
+ * Files in subdirectories mount at /api/{subdirectory}.
16
+ *
17
+ * @param relativePath - Path relative to src/api/ without extension
18
+ * e.g., 'index', 'sessions', 'auth/route', 'users/profile/route'
19
+ * @returns Mount path like '/api', '/api/auth', '/api/users/profile'
20
+ *
21
+ * @example
22
+ * computeApiMountPath('index') // '/api' (file in src/api/)
23
+ * computeApiMountPath('sessions') // '/api' (file in src/api/)
24
+ * computeApiMountPath('route') // '/api' (file in src/api/)
25
+ * computeApiMountPath('auth/route') // '/api/auth' (file in src/api/auth/)
26
+ * computeApiMountPath('auth/index') // '/api/auth' (file in src/api/auth/)
27
+ * computeApiMountPath('users/profile/route') // '/api/users/profile' (file in src/api/users/profile/)
28
+ */
29
+ export function computeApiMountPath(relativePath: string): string {
30
+ // Extract the directory path (everything before the last /)
31
+ const lastSlashIndex = relativePath.lastIndexOf('/');
32
+ if (lastSlashIndex === -1) {
33
+ // File is directly in src/api/ (e.g., 'index', 'sessions', 'route')
34
+ return '/api';
35
+ }
36
+
37
+ // File is in a subdirectory (e.g., 'auth/route' -> 'auth')
38
+ const dirPath = relativePath.substring(0, lastSlashIndex);
39
+ return `/api/${dirPath}`;
40
+ }
41
+
42
+ /**
43
+ * Join a mount base path with a route's local path.
44
+ * Handles normalization of slashes and empty/root paths.
45
+ *
46
+ * @param base - The mount base path (e.g., '/api/sessions')
47
+ * @param route - The local route path (e.g., '/', '/users', ':id')
48
+ * @returns The combined full path
49
+ *
50
+ * @example
51
+ * joinMountAndRoute('/api/sessions', '/') // '/api/sessions'
52
+ * joinMountAndRoute('/api/sessions', '/users') // '/api/sessions/users'
53
+ * joinMountAndRoute('/api', '/health') // '/api/health'
54
+ * joinMountAndRoute('/api/users', ':id') // '/api/users/:id'
55
+ */
56
+ export function joinMountAndRoute(base: string, route: string): string {
57
+ if (!route || route === '/') {
58
+ return base;
59
+ }
60
+ const normalized = route.startsWith('/') ? route : `/${route}`;
61
+ return `${base}${normalized}`.replace(/\/{2,}/g, '/').replace(/\/$/, '');
62
+ }
63
+
64
+ import { join, relative } from 'node:path';
65
+
66
+ /**
67
+ * Extract the relative path from a full file path to src/api/.
68
+ * Normalizes path separators for cross-platform compatibility.
69
+ *
70
+ * @param filename - Full path to the route file
71
+ * @param srcDir - Path to the src directory
72
+ * @returns Path relative to src/api/ without extension
73
+ *
74
+ * @example
75
+ * // Given srcDir = '/project/src'
76
+ * extractRelativeApiPath('/project/src/api/sessions.ts', '/project/src')
77
+ * // Returns: 'sessions'
78
+ *
79
+ * extractRelativeApiPath('/project/src/api/auth/route.ts', '/project/src')
80
+ * // Returns: 'auth/route'
81
+ */
82
+ export function extractRelativeApiPath(filename: string, srcDir: string): string {
83
+ const apiDir = join(srcDir, 'api');
84
+ return relative(apiDir, filename)
85
+ .replace(/\\/g, '/') // Normalize Windows paths
86
+ .replace(/\.tsx?$/, ''); // Remove extension
87
+ }
@@ -13,6 +13,7 @@ import {
13
13
  getDefaultConfigDir,
14
14
  loadProjectSDKKey,
15
15
  updateProjectConfig,
16
+ getGlobalCatalystAPIClient,
16
17
  } from '../../config';
17
18
  import { getProjectGithubStatus } from '../git/api';
18
19
  import { runGitLink } from '../git/link';
@@ -341,8 +342,10 @@ export const deploySubcommand = createSubcommand({
341
342
  return null;
342
343
  }
343
344
  logger.debug('Checking %d packages for malware', packages.length);
345
+ // Use Catalyst client directly for malware check (security routes are on Catalyst)
346
+ const catalystClient = await getGlobalCatalystAPIClient(logger, auth, config?.name);
344
347
  const result = await projectDeploymentMalwareCheck(
345
- apiClient,
348
+ catalystClient,
346
349
  deployment!.id,
347
350
  packages
348
351
  );
@@ -1,4 +1,4 @@
1
- import type { Config, Logger, CommandDefinition } from './types';
1
+ import type { Config, Logger, CommandDefinition, SubcommandDefinition } from './types';
2
2
  import { isRunningFromExecutable, fetchLatestVersion } from './cmd/upgrade';
3
3
  import { getVersion, getCompareUrl, getReleaseUrl, toTag } from './version';
4
4
  import * as tui from './tui';
@@ -7,6 +7,9 @@ import { $ } from 'bun';
7
7
 
8
8
  const ONE_HOUR_MS = 60 * 60 * 1000;
9
9
 
10
+ // Tags that indicate a command should skip the upgrade prompt
11
+ const SKIP_UPGRADE_TAGS = ['read-only', 'fast'];
12
+
10
13
  /**
11
14
  * Check if we should skip the version check based on environment and config
12
15
  */
@@ -20,6 +23,7 @@ function shouldSkipCheck(
20
23
  skipVersionCheck?: boolean;
21
24
  },
22
25
  commandDef: CommandDefinition | undefined,
26
+ subcommandDef: SubcommandDefinition | undefined,
23
27
  args: string[]
24
28
  ): boolean {
25
29
  // Skip if running via bun/bunx (not installed executable)
@@ -64,6 +68,21 @@ function shouldSkipCheck(
64
68
  return true;
65
69
  }
66
70
 
71
+ // Skip if subcommand explicitly opts out of upgrade check
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
+ if (subcommandDef && (subcommandDef as any).skipUpgradeCheck === true) {
74
+ return true;
75
+ }
76
+
77
+ // Skip if command or subcommand has tags indicating it's read-only or fast
78
+ // These commands shouldn't be interrupted with upgrade prompts
79
+ const commandTags = commandDef?.tags ?? [];
80
+ const subcommandTags = subcommandDef?.tags ?? [];
81
+ const allTags = [...commandTags, ...subcommandTags];
82
+ if (allTags.some((tag) => SKIP_UPGRADE_TAGS.includes(tag))) {
83
+ return true;
84
+ }
85
+
67
86
  // Skip for help commands
68
87
  const helpFlags = ['--help', '-h', 'help'];
69
88
  if (args.some((arg) => helpFlags.includes(arg))) {
@@ -187,10 +206,11 @@ export async function checkForUpdates(
187
206
  skipVersionCheck?: boolean;
188
207
  },
189
208
  commandDef: CommandDefinition | undefined,
209
+ subcommandDef: SubcommandDefinition | undefined,
190
210
  args: string[]
191
211
  ): Promise<void> {
192
212
  // Determine if we should skip the check
193
- if (shouldSkipCheck(config, options, commandDef, args)) {
213
+ if (shouldSkipCheck(config, options, commandDef, subcommandDef, args)) {
194
214
  logger.trace('Skipping version check (disabled or not applicable)');
195
215
  return;
196
216
  }