@agentuity/cli 1.0.59 → 2.0.0-beta.1

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 (189) hide show
  1. package/bin/cli.ts +2 -3
  2. package/dist/cmd/build/app-config-extractor.d.ts +27 -0
  3. package/dist/cmd/build/app-config-extractor.d.ts.map +1 -0
  4. package/dist/cmd/build/app-config-extractor.js +152 -0
  5. package/dist/cmd/build/app-config-extractor.js.map +1 -0
  6. package/dist/cmd/build/app-router-detector.d.ts +2 -5
  7. package/dist/cmd/build/app-router-detector.d.ts.map +1 -1
  8. package/dist/cmd/build/app-router-detector.js +130 -154
  9. package/dist/cmd/build/app-router-detector.js.map +1 -1
  10. package/dist/cmd/build/ci.d.ts.map +1 -1
  11. package/dist/cmd/build/ci.js +5 -21
  12. package/dist/cmd/build/ci.js.map +1 -1
  13. package/dist/cmd/build/ids.d.ts +11 -0
  14. package/dist/cmd/build/ids.d.ts.map +1 -0
  15. package/dist/cmd/build/ids.js +18 -0
  16. package/dist/cmd/build/ids.js.map +1 -0
  17. package/dist/cmd/build/index.d.ts.map +1 -1
  18. package/dist/cmd/build/index.js +8 -0
  19. package/dist/cmd/build/index.js.map +1 -1
  20. package/dist/cmd/build/vite/agent-discovery.d.ts +8 -4
  21. package/dist/cmd/build/vite/agent-discovery.d.ts.map +1 -1
  22. package/dist/cmd/build/vite/agent-discovery.js +166 -487
  23. package/dist/cmd/build/vite/agent-discovery.js.map +1 -1
  24. package/dist/cmd/build/vite/bun-dev-server.d.ts +43 -14
  25. package/dist/cmd/build/vite/bun-dev-server.d.ts.map +1 -1
  26. package/dist/cmd/build/vite/bun-dev-server.js +290 -129
  27. package/dist/cmd/build/vite/bun-dev-server.js.map +1 -1
  28. package/dist/cmd/build/vite/config-loader.d.ts +15 -20
  29. package/dist/cmd/build/vite/config-loader.d.ts.map +1 -1
  30. package/dist/cmd/build/vite/config-loader.js +41 -74
  31. package/dist/cmd/build/vite/config-loader.js.map +1 -1
  32. package/dist/cmd/build/vite/docs-generator.d.ts.map +1 -1
  33. package/dist/cmd/build/vite/docs-generator.js +0 -2
  34. package/dist/cmd/build/vite/docs-generator.js.map +1 -1
  35. package/dist/cmd/build/vite/index.d.ts.map +1 -1
  36. package/dist/cmd/build/vite/index.js +0 -36
  37. package/dist/cmd/build/vite/index.js.map +1 -1
  38. package/dist/cmd/build/vite/lifecycle-generator.d.ts +10 -2
  39. package/dist/cmd/build/vite/lifecycle-generator.d.ts.map +1 -1
  40. package/dist/cmd/build/vite/lifecycle-generator.js +302 -23
  41. package/dist/cmd/build/vite/lifecycle-generator.js.map +1 -1
  42. package/dist/cmd/build/vite/route-discovery.d.ts +11 -38
  43. package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -1
  44. package/dist/cmd/build/vite/route-discovery.js +97 -177
  45. package/dist/cmd/build/vite/route-discovery.js.map +1 -1
  46. package/dist/cmd/build/vite/server-bundler.js +1 -1
  47. package/dist/cmd/build/vite/server-bundler.js.map +1 -1
  48. package/dist/cmd/build/vite/static-renderer.d.ts +0 -2
  49. package/dist/cmd/build/vite/static-renderer.d.ts.map +1 -1
  50. package/dist/cmd/build/vite/static-renderer.js +19 -13
  51. package/dist/cmd/build/vite/static-renderer.js.map +1 -1
  52. package/dist/cmd/build/vite/vite-asset-server-config.d.ts +6 -3
  53. package/dist/cmd/build/vite/vite-asset-server-config.d.ts.map +1 -1
  54. package/dist/cmd/build/vite/vite-asset-server-config.js +175 -69
  55. package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
  56. package/dist/cmd/build/vite/vite-asset-server.d.ts +8 -3
  57. package/dist/cmd/build/vite/vite-asset-server.d.ts.map +1 -1
  58. package/dist/cmd/build/vite/vite-asset-server.js +14 -13
  59. package/dist/cmd/build/vite/vite-asset-server.js.map +1 -1
  60. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  61. package/dist/cmd/build/vite/vite-builder.js +42 -190
  62. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  63. package/dist/cmd/build/vite/ws-proxy.d.ts +53 -0
  64. package/dist/cmd/build/vite/ws-proxy.d.ts.map +1 -0
  65. package/dist/cmd/build/vite/ws-proxy.js +95 -0
  66. package/dist/cmd/build/vite/ws-proxy.js.map +1 -0
  67. package/dist/cmd/build/vite-bundler.d.ts.map +1 -1
  68. package/dist/cmd/build/vite-bundler.js +0 -3
  69. package/dist/cmd/build/vite-bundler.js.map +1 -1
  70. package/dist/cmd/cloud/deploy-fork.d.ts.map +1 -1
  71. package/dist/cmd/cloud/deploy-fork.js +15 -36
  72. package/dist/cmd/cloud/deploy-fork.js.map +1 -1
  73. package/dist/cmd/cloud/sandbox/exec.d.ts.map +1 -1
  74. package/dist/cmd/cloud/sandbox/exec.js +28 -86
  75. package/dist/cmd/cloud/sandbox/exec.js.map +1 -1
  76. package/dist/cmd/cloud/sandbox/run.d.ts.map +1 -1
  77. package/dist/cmd/cloud/sandbox/run.js +2 -9
  78. package/dist/cmd/cloud/sandbox/run.js.map +1 -1
  79. package/dist/cmd/cloud/sandbox/snapshot/build.js +2 -2
  80. package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
  81. package/dist/cmd/coder/hub-url.d.ts.map +1 -1
  82. package/dist/cmd/coder/hub-url.js +1 -3
  83. package/dist/cmd/coder/hub-url.js.map +1 -1
  84. package/dist/cmd/coder/start.js +6 -6
  85. package/dist/cmd/coder/start.js.map +1 -1
  86. package/dist/cmd/coder/tui-init.d.ts +2 -2
  87. package/dist/cmd/coder/tui-init.js +2 -2
  88. package/dist/cmd/coder/tui-init.js.map +1 -1
  89. package/dist/cmd/dev/file-watcher.d.ts.map +1 -1
  90. package/dist/cmd/dev/file-watcher.js +2 -8
  91. package/dist/cmd/dev/file-watcher.js.map +1 -1
  92. package/dist/cmd/dev/index.d.ts.map +1 -1
  93. package/dist/cmd/dev/index.js +432 -752
  94. package/dist/cmd/dev/index.js.map +1 -1
  95. package/dist/cmd/dev/process-manager.d.ts +104 -0
  96. package/dist/cmd/dev/process-manager.d.ts.map +1 -0
  97. package/dist/cmd/dev/process-manager.js +204 -0
  98. package/dist/cmd/dev/process-manager.js.map +1 -0
  99. package/dist/errors.d.ts +10 -24
  100. package/dist/errors.d.ts.map +1 -1
  101. package/dist/errors.js +12 -42
  102. package/dist/errors.js.map +1 -1
  103. package/dist/schema-generator.d.ts.map +1 -1
  104. package/dist/schema-generator.js +12 -2
  105. package/dist/schema-generator.js.map +1 -1
  106. package/dist/tui.d.ts.map +1 -1
  107. package/dist/tui.js +5 -19
  108. package/dist/tui.js.map +1 -1
  109. package/dist/utils/version-mismatch.d.ts +39 -0
  110. package/dist/utils/version-mismatch.d.ts.map +1 -0
  111. package/dist/utils/version-mismatch.js +161 -0
  112. package/dist/utils/version-mismatch.js.map +1 -0
  113. package/package.json +6 -6
  114. package/src/cmd/ai/prompt/agent.md +0 -1
  115. package/src/cmd/ai/prompt/api.md +0 -7
  116. package/src/cmd/ai/prompt/web.md +51 -213
  117. package/src/cmd/build/app-config-extractor.ts +186 -0
  118. package/src/cmd/build/app-router-detector.ts +152 -182
  119. package/src/cmd/build/ci.ts +5 -21
  120. package/src/cmd/build/ids.ts +19 -0
  121. package/src/cmd/build/index.ts +10 -0
  122. package/src/cmd/build/vite/agent-discovery.ts +208 -679
  123. package/src/cmd/build/vite/bun-dev-server.ts +383 -146
  124. package/src/cmd/build/vite/config-loader.ts +45 -77
  125. package/src/cmd/build/vite/docs-generator.ts +0 -2
  126. package/src/cmd/build/vite/index.ts +1 -42
  127. package/src/cmd/build/vite/lifecycle-generator.ts +345 -21
  128. package/src/cmd/build/vite/route-discovery.ts +116 -274
  129. package/src/cmd/build/vite/server-bundler.ts +1 -1
  130. package/src/cmd/build/vite/static-renderer.ts +23 -15
  131. package/src/cmd/build/vite/vite-asset-server-config.ts +200 -70
  132. package/src/cmd/build/vite/vite-asset-server.ts +25 -15
  133. package/src/cmd/build/vite/vite-builder.ts +49 -220
  134. package/src/cmd/build/vite/ws-proxy.ts +126 -0
  135. package/src/cmd/build/vite-bundler.ts +0 -4
  136. package/src/cmd/cloud/deploy-fork.ts +16 -39
  137. package/src/cmd/cloud/sandbox/exec.ts +23 -130
  138. package/src/cmd/cloud/sandbox/run.ts +2 -9
  139. package/src/cmd/cloud/sandbox/snapshot/build.ts +2 -2
  140. package/src/cmd/coder/hub-url.ts +1 -3
  141. package/src/cmd/coder/start.ts +6 -6
  142. package/src/cmd/coder/tui-init.ts +4 -4
  143. package/src/cmd/dev/file-watcher.ts +2 -9
  144. package/src/cmd/dev/index.ts +476 -859
  145. package/src/cmd/dev/process-manager.ts +261 -0
  146. package/src/errors.ts +12 -44
  147. package/src/schema-generator.ts +12 -2
  148. package/src/tui.ts +5 -18
  149. package/src/utils/version-mismatch.ts +204 -0
  150. package/dist/cmd/build/ast.d.ts +0 -78
  151. package/dist/cmd/build/ast.d.ts.map +0 -1
  152. package/dist/cmd/build/ast.js +0 -2703
  153. package/dist/cmd/build/ast.js.map +0 -1
  154. package/dist/cmd/build/entry-generator.d.ts +0 -25
  155. package/dist/cmd/build/entry-generator.d.ts.map +0 -1
  156. package/dist/cmd/build/entry-generator.js +0 -695
  157. package/dist/cmd/build/entry-generator.js.map +0 -1
  158. package/dist/cmd/build/vite/api-mount-path.d.ts +0 -61
  159. package/dist/cmd/build/vite/api-mount-path.d.ts.map +0 -1
  160. package/dist/cmd/build/vite/api-mount-path.js +0 -83
  161. package/dist/cmd/build/vite/api-mount-path.js.map +0 -1
  162. package/dist/cmd/build/vite/registry-generator.d.ts +0 -19
  163. package/dist/cmd/build/vite/registry-generator.d.ts.map +0 -1
  164. package/dist/cmd/build/vite/registry-generator.js +0 -1108
  165. package/dist/cmd/build/vite/registry-generator.js.map +0 -1
  166. package/dist/cmd/build/webanalytics-generator.d.ts +0 -16
  167. package/dist/cmd/build/webanalytics-generator.d.ts.map +0 -1
  168. package/dist/cmd/build/webanalytics-generator.js +0 -178
  169. package/dist/cmd/build/webanalytics-generator.js.map +0 -1
  170. package/dist/cmd/build/workbench.d.ts +0 -7
  171. package/dist/cmd/build/workbench.d.ts.map +0 -1
  172. package/dist/cmd/build/workbench.js +0 -55
  173. package/dist/cmd/build/workbench.js.map +0 -1
  174. package/dist/utils/route-migration.d.ts +0 -62
  175. package/dist/utils/route-migration.d.ts.map +0 -1
  176. package/dist/utils/route-migration.js +0 -630
  177. package/dist/utils/route-migration.js.map +0 -1
  178. package/dist/utils/stream-capture.d.ts +0 -9
  179. package/dist/utils/stream-capture.d.ts.map +0 -1
  180. package/dist/utils/stream-capture.js +0 -34
  181. package/dist/utils/stream-capture.js.map +0 -1
  182. package/src/cmd/build/ast.ts +0 -3529
  183. package/src/cmd/build/entry-generator.ts +0 -760
  184. package/src/cmd/build/vite/api-mount-path.ts +0 -87
  185. package/src/cmd/build/vite/registry-generator.ts +0 -1267
  186. package/src/cmd/build/webanalytics-generator.ts +0 -197
  187. package/src/cmd/build/workbench.ts +0 -58
  188. package/src/utils/route-migration.ts +0 -757
  189. package/src/utils/stream-capture.ts +0 -39
@@ -0,0 +1,186 @@
1
+ /**
2
+ * App Config Extractor
3
+ *
4
+ * Extracts analytics and workbench config from the user's createApp() call
5
+ * in app.ts. This is the v2 approach where createApp() is the single source
6
+ * of truth for runtime configuration (replacing agentuity.config.ts).
7
+ *
8
+ * Uses TypeScript's compiler API to reliably detect and extract values.
9
+ */
10
+
11
+ import ts from 'typescript';
12
+ import { join } from 'node:path';
13
+ import type { Logger } from '../../types';
14
+
15
+ /**
16
+ * Extracted runtime config from createApp() call.
17
+ */
18
+ export interface ExtractedAppConfig {
19
+ /** analytics option value: boolean, object, or undefined if not set */
20
+ analytics?: boolean | Record<string, unknown>;
21
+ /** workbench option value: boolean, string, object, or undefined if not set */
22
+ workbench?: boolean | string | Record<string, unknown>;
23
+ }
24
+
25
+ /**
26
+ * Extract analytics and workbench config from a createApp() call.
27
+ *
28
+ * Uses TypeScript AST to find `createApp({ ... })` and extract the values
29
+ * of the `analytics` and `workbench` properties.
30
+ */
31
+ function extractCreateAppConfig(sourceFile: ts.SourceFile): ExtractedAppConfig {
32
+ const result: ExtractedAppConfig = {};
33
+
34
+ // Walk the AST looking for createApp({ ... }) calls
35
+ function visit(node: ts.Node): void {
36
+ // Check for createApp(...) call
37
+ if (
38
+ ts.isCallExpression(node) &&
39
+ ts.isIdentifier(node.expression) &&
40
+ node.expression.text === 'createApp' &&
41
+ node.arguments.length > 0
42
+ ) {
43
+ const firstArg = node.arguments[0];
44
+ if (!firstArg) return;
45
+ if (ts.isObjectLiteralExpression(firstArg)) {
46
+ // Found createApp({ ... }) — extract properties
47
+ for (const prop of firstArg.properties) {
48
+ if (!ts.isPropertyAssignment(prop)) continue;
49
+
50
+ const name = ts.isIdentifier(prop.name) ? prop.name.text : undefined;
51
+ if (!name) continue;
52
+
53
+ // prop.initializer should always exist for PropertyAssignment, but check to satisfy TS
54
+ if (!('initializer' in prop)) continue;
55
+ const initializer = (prop as { initializer: ts.Expression }).initializer;
56
+
57
+ if (name === 'analytics') {
58
+ const value = extractValue(initializer);
59
+ if (value !== undefined) {
60
+ if (typeof value === 'boolean') {
61
+ result.analytics = value;
62
+ } else if (typeof value === 'object') {
63
+ result.analytics = value as Record<string, unknown>;
64
+ }
65
+ // Ignore string/number for analytics
66
+ }
67
+ } else if (name === 'workbench') {
68
+ const value = extractValue(initializer);
69
+ if (value !== undefined) {
70
+ if (typeof value === 'boolean' || typeof value === 'string') {
71
+ result.workbench = value;
72
+ } else if (typeof value === 'object') {
73
+ result.workbench = value as Record<string, unknown>;
74
+ }
75
+ // Ignore number for workbench
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ ts.forEachChild(node, visit);
83
+ }
84
+
85
+ visit(sourceFile);
86
+ return result;
87
+ }
88
+
89
+ /**
90
+ * Extract a JavaScript value from a TypeScript AST expression node.
91
+ * Handles: boolean literals, string literals, numeric literals,
92
+ * object literals (as Record), and identifier references (by name).
93
+ */
94
+ function extractValue(
95
+ node: ts.Node
96
+ ): boolean | string | number | Record<string, unknown> | undefined {
97
+ if (ts.isLiteralExpression(node)) {
98
+ // String literal or numeric literal
99
+ if (ts.isStringLiteral(node)) {
100
+ return node.text;
101
+ }
102
+ if (ts.isNumericLiteral(node)) {
103
+ return Number(node.text);
104
+ }
105
+ }
106
+
107
+ if (node.kind === ts.SyntaxKind.TrueKeyword) {
108
+ return true;
109
+ }
110
+ if (node.kind === ts.SyntaxKind.FalseKeyword) {
111
+ return false;
112
+ }
113
+
114
+ if (ts.isIdentifier(node)) {
115
+ // Return the identifier name (e.g., a variable reference)
116
+ return node.text;
117
+ }
118
+
119
+ if (ts.isObjectLiteralExpression(node)) {
120
+ const obj: Record<string, unknown> = {};
121
+ for (const prop of node.properties) {
122
+ if (!ts.isPropertyAssignment(prop)) continue;
123
+ if (!ts.isIdentifier(prop.name)) continue;
124
+
125
+ const key = prop.name.text;
126
+ obj[key] = extractValue(prop.initializer);
127
+ }
128
+ return obj;
129
+ }
130
+
131
+ return undefined;
132
+ }
133
+
134
+ /**
135
+ * Detect and extract analytics/workbench config from app.ts.
136
+ *
137
+ * This is the v2 approach: runtime config lives in createApp() only.
138
+ * The CLI reads these values at build time via AST parsing.
139
+ */
140
+ export async function extractAppConfig(
141
+ rootDir: string,
142
+ logger: Logger
143
+ ): Promise<ExtractedAppConfig> {
144
+ // Look for app.ts in root first, then src/
145
+ let appFile = join(rootDir, 'app.ts');
146
+ if (!(await Bun.file(appFile).exists())) {
147
+ appFile = join(rootDir, 'src', 'app.ts');
148
+ if (!(await Bun.file(appFile).exists())) {
149
+ logger.trace('[config-extract] No app.ts found');
150
+ return {};
151
+ }
152
+ }
153
+
154
+ try {
155
+ const source = await Bun.file(appFile).text();
156
+
157
+ // Quick bail-out before parsing
158
+ if (!source.includes('createApp')) {
159
+ logger.trace('[config-extract] No createApp call in %s', appFile);
160
+ return {};
161
+ }
162
+
163
+ // Parse with TypeScript
164
+ const sourceFile = ts.createSourceFile(
165
+ appFile,
166
+ source,
167
+ ts.ScriptTarget.Latest,
168
+ true,
169
+ ts.ScriptKind.TS
170
+ );
171
+
172
+ const config = extractCreateAppConfig(sourceFile);
173
+
174
+ if (config.analytics !== undefined) {
175
+ logger.debug('[config-extract] Found analytics in createApp(): %o', config.analytics);
176
+ }
177
+ if (config.workbench !== undefined) {
178
+ logger.debug('[config-extract] Found workbench in createApp(): %o', config.workbench);
179
+ }
180
+
181
+ return config;
182
+ } catch (error) {
183
+ logger.warn('[config-extract] Failed to parse app.ts:', error);
184
+ return {};
185
+ }
186
+ }
@@ -5,23 +5,15 @@
5
5
  * to `createApp()`. If detected, resolves the router variable(s) to their import
6
6
  * sources and mount paths.
7
7
  *
8
- * This allows the build tooling to derive route metadata from the actual code-based
9
- * route tree instead of relying on filesystem-based discovery.
8
+ * Uses TypeScript's compiler API to reliably detect the pattern, consistent with
9
+ * the lifecycle generator approach.
10
10
  */
11
11
 
12
- import * as acornLoose from 'acorn-loose';
12
+ import ts from 'typescript';
13
13
  import { join, dirname, resolve } from 'node:path';
14
- import { existsSync, statSync } from 'node:fs';
14
+ import { statSync } from 'node:fs';
15
15
  import type { Logger } from '../../types';
16
16
 
17
- interface ASTNode {
18
- type: string;
19
- start?: number;
20
- end?: number;
21
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
- [key: string]: any;
23
- }
24
-
25
17
  /**
26
18
  * A resolved mount point from `createApp({ router })`.
27
19
  */
@@ -46,7 +38,7 @@ export interface AppRouterDetection {
46
38
  * Resolve an import path to an actual file on disk.
47
39
  * Tries the path as-is, then with common extensions.
48
40
  */
49
- function resolveImportFile(fromDir: string, importPath: string): string | null {
41
+ async function resolveImportFile(fromDir: string, importPath: string): Promise<string | null> {
50
42
  if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
51
43
  return null; // Package import — can't resolve
52
44
  }
@@ -54,7 +46,8 @@ function resolveImportFile(fromDir: string, importPath: string): string | null {
54
46
  const basePath = resolve(fromDir, importPath);
55
47
  const extensions = ['.ts', '.tsx', '/index.ts', '/index.tsx'];
56
48
 
57
- if (existsSync(basePath)) {
49
+ const baseFile = Bun.file(basePath);
50
+ if (await baseFile.exists()) {
58
51
  try {
59
52
  if (statSync(basePath).isFile()) return basePath;
60
53
  } catch {
@@ -64,7 +57,7 @@ function resolveImportFile(fromDir: string, importPath: string): string | null {
64
57
 
65
58
  for (const ext of extensions) {
66
59
  const candidate = basePath + ext;
67
- if (existsSync(candidate)) {
60
+ if (await Bun.file(candidate).exists()) {
68
61
  return candidate;
69
62
  }
70
63
  }
@@ -73,91 +66,158 @@ function resolveImportFile(fromDir: string, importPath: string): string | null {
73
66
  }
74
67
 
75
68
  /**
76
- * Extract the `router` property value from a `createApp()` call's argument object.
77
- *
78
- * Handles three forms:
79
- * - `createApp({ router: myVar })` → plain Hono, default /api mount
80
- * - `createApp({ router: { path: '/v1', router: myVar } })` → single RouteMount
81
- * - `createApp({ router: [{ path: '/v1', router: v1 }, ...] })` → array of RouteMounts
69
+ * A router mount extracted from the AST before file resolution.
82
70
  */
83
- function extractRouterFromCreateApp(
84
- callNode: ASTNode
85
- ): Array<{ path: string; varName: string }> | null {
86
- // createApp() must have at least one argument (the config object)
87
- if (!callNode.arguments || callNode.arguments.length === 0) {
88
- return null;
89
- }
71
+ interface RawMount {
72
+ path: string;
73
+ varName: string;
74
+ }
90
75
 
91
- const configArg = callNode.arguments[0] as ASTNode;
92
- if (configArg.type !== 'ObjectExpression') {
76
+ /**
77
+ * Extract router mounts from a createApp() call using TypeScript's AST.
78
+ * Returns null if no router property found.
79
+ */
80
+ function extractRouterMounts(sourceFile: ts.SourceFile): RawMount[] | null {
81
+ let result: RawMount[] | null = null;
82
+
83
+ function getStringLiteral(node: ts.Expression): string | null {
84
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
85
+ return node.text;
86
+ }
93
87
  return null;
94
88
  }
95
89
 
96
- // Find the `router` property
97
- const routerProp = configArg.properties?.find(
98
- (p: ASTNode) =>
99
- p.type === 'Property' && p.key?.type === 'Identifier' && p.key?.name === 'router'
100
- );
90
+ function extractMountFromObject(obj: ts.ObjectLiteralExpression): RawMount | null {
91
+ let path: string | undefined;
92
+ let varName: string | undefined;
101
93
 
102
- if (!routerProp) {
103
- return null; // No router property — file-based routing
104
- }
94
+ for (const prop of obj.properties) {
95
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue;
96
+
97
+ if (prop.name.text === 'path') {
98
+ path = getStringLiteral(prop.initializer) ?? undefined;
99
+ }
100
+ if (prop.name.text === 'router') {
101
+ if (ts.isIdentifier(prop.initializer)) {
102
+ varName = prop.initializer.text;
103
+ }
104
+ }
105
+ }
105
106
 
106
- const routerValue = routerProp.value as ASTNode;
107
+ // Also handle shorthand: { path: '/v1', router } where router is shorthand
108
+ for (const prop of obj.properties) {
109
+ if (ts.isShorthandPropertyAssignment(prop) && prop.name.text === 'router') {
110
+ varName = prop.name.text;
111
+ }
112
+ }
107
113
 
108
- // Form 1: Plain Hono variable createApp({ router: myRouter })
109
- if (routerValue.type === 'Identifier') {
110
- return [{ path: '/api', varName: routerValue.name }];
114
+ return path && varName ? { path, varName } : null;
111
115
  }
112
116
 
113
- // Form 2: Single RouteMount → createApp({ router: { path: '/v1', router: myRouter } })
114
- if (routerValue.type === 'ObjectExpression') {
115
- const mount = extractRouteMountFromObject(routerValue);
116
- return mount ? [mount] : null;
117
+ function processRouterValue(value: ts.Expression): RawMount[] | null {
118
+ // Form 1: Identifier → createApp({ router: myRouter })
119
+ if (ts.isIdentifier(value)) {
120
+ return [{ path: '/api', varName: value.text }];
121
+ }
122
+
123
+ // Form 2: Object → createApp({ router: { path: '/v1', router: myRouter } })
124
+ if (ts.isObjectLiteralExpression(value)) {
125
+ const mount = extractMountFromObject(value);
126
+ return mount ? [mount] : null;
127
+ }
128
+
129
+ // Form 3: Array → createApp({ router: [...] })
130
+ if (ts.isArrayLiteralExpression(value)) {
131
+ const mounts: RawMount[] = [];
132
+ for (const element of value.elements) {
133
+ if (ts.isObjectLiteralExpression(element)) {
134
+ const mount = extractMountFromObject(element);
135
+ if (mount) mounts.push(mount);
136
+ }
137
+ }
138
+ return mounts.length > 0 ? mounts : null;
139
+ }
140
+
141
+ return null;
117
142
  }
118
143
 
119
- // Form 3: Array of RouteMounts → createApp({ router: [{ path: '/v1', router: v1 }, ...] })
120
- if (routerValue.type === 'ArrayExpression') {
121
- const mounts: Array<{ path: string; varName: string }> = [];
122
- for (const element of routerValue.elements || []) {
123
- if (element?.type === 'ObjectExpression') {
124
- const mount = extractRouteMountFromObject(element);
125
- if (mount) mounts.push(mount);
144
+ function visit(node: ts.Node): void {
145
+ if (result) return;
146
+
147
+ // Find createApp(...) with or without await
148
+ let callExpr: ts.CallExpression | undefined;
149
+
150
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
151
+ if (node.expression.text === 'createApp') callExpr = node;
152
+ } else if (ts.isAwaitExpression(node) && ts.isCallExpression(node.expression)) {
153
+ const call = node.expression;
154
+ if (ts.isIdentifier(call.expression) && call.expression.text === 'createApp') {
155
+ callExpr = call;
156
+ }
157
+ }
158
+
159
+ if (callExpr && callExpr.arguments.length > 0) {
160
+ const configArg = callExpr.arguments[0];
161
+ if (configArg && ts.isObjectLiteralExpression(configArg)) {
162
+ for (const prop of configArg.properties) {
163
+ // Handle: router: value
164
+ if (
165
+ ts.isPropertyAssignment(prop) &&
166
+ ts.isIdentifier(prop.name) &&
167
+ prop.name.text === 'router'
168
+ ) {
169
+ result = processRouterValue(prop.initializer);
170
+ return;
171
+ }
172
+
173
+ // Handle shorthand: createApp({ router })
174
+ if (ts.isShorthandPropertyAssignment(prop) && prop.name.text === 'router') {
175
+ result = [{ path: '/api', varName: 'router' }];
176
+ return;
177
+ }
178
+ }
126
179
  }
127
180
  }
128
- return mounts.length > 0 ? mounts : null;
181
+
182
+ ts.forEachChild(node, visit);
129
183
  }
130
184
 
131
- return null;
185
+ visit(sourceFile);
186
+ return result;
132
187
  }
133
188
 
134
189
  /**
135
- * Extract { path, router } from an object literal AST node.
190
+ * Build import map from the source file: variable name import path
136
191
  */
137
- function extractRouteMountFromObject(objNode: ASTNode): { path: string; varName: string } | null {
138
- let path: string | undefined;
139
- let varName: string | undefined;
192
+ function buildImportMap(sourceFile: ts.SourceFile): Map<string, string> {
193
+ const importMap = new Map<string, string>();
140
194
 
141
- for (const prop of objNode.properties || []) {
142
- if (prop.type !== 'Property' || prop.key?.type !== 'Identifier') continue;
195
+ for (const stmt of sourceFile.statements) {
196
+ if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
143
197
 
144
- if (prop.key.name === 'path' && prop.value?.type === 'Literal') {
145
- path = String(prop.value.value);
198
+ const importPath = stmt.moduleSpecifier.text;
199
+ const clause = stmt.importClause;
200
+ if (!clause) continue;
201
+
202
+ // Default import: import router from './api'
203
+ if (clause.name) {
204
+ importMap.set(clause.name.text, importPath);
146
205
  }
147
- if (prop.key.name === 'router' && prop.value?.type === 'Identifier') {
148
- varName = prop.value.name;
206
+
207
+ // Named imports: import { v1, v2 } from './routers'
208
+ if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
209
+ for (const spec of clause.namedBindings.elements) {
210
+ importMap.set(spec.name.text, importPath);
211
+ }
149
212
  }
150
213
  }
151
214
 
152
- return path && varName ? { path, varName } : null;
215
+ return importMap;
153
216
  }
154
217
 
155
218
  /**
156
219
  * Detect whether `src/app.ts` uses `createApp({ router })`.
157
220
  *
158
- * Parses the file with acorn-loose, finds `createApp()` calls,
159
- * and resolves router variables to their import source files.
160
- *
161
221
  * Returns `{ detected: false, mounts: [] }` when:
162
222
  * - `src/app.ts` doesn't exist
163
223
  * - `createApp()` is called without a `router` property
@@ -169,76 +229,57 @@ export async function detectExplicitRouter(
169
229
  ): Promise<AppRouterDetection> {
170
230
  const noDetection: AppRouterDetection = { detected: false, mounts: [] };
171
231
 
172
- // Look for app.ts in src/ (standard location)
173
- const appFile = join(rootDir, 'src', 'app.ts');
174
- if (!existsSync(appFile)) {
175
- // Also try root-level app.ts
176
- const rootAppFile = join(rootDir, 'app.ts');
177
- if (!existsSync(rootAppFile)) {
232
+ // Look for app.ts in src/ (standard location), then root
233
+ let appFile = join(rootDir, 'src', 'app.ts');
234
+ if (!(await Bun.file(appFile).exists())) {
235
+ appFile = join(rootDir, 'app.ts');
236
+ if (!(await Bun.file(appFile).exists())) {
178
237
  logger.trace('[router-detect] No app.ts found');
179
238
  return noDetection;
180
239
  }
181
- return detectInFile(rootAppFile, logger);
182
240
  }
183
241
 
184
- return detectInFile(appFile, logger);
185
- }
186
-
187
- async function detectInFile(appFile: string, logger: Logger): Promise<AppRouterDetection> {
188
- const noDetection: AppRouterDetection = { detected: false, mounts: [] };
189
- const appDir = dirname(appFile);
190
-
191
242
  try {
192
243
  const source = await Bun.file(appFile).text();
193
- const transpiler = new Bun.Transpiler({ loader: 'ts', target: 'bun' });
194
- const contents = transpiler.transformSync(source);
244
+ const appDir = dirname(appFile);
195
245
 
196
- // Quick check: skip AST parsing if createApp is not called with router
197
- if (!contents.includes('createApp') || !contents.includes('router')) {
246
+ // Quick bail-out before parsing
247
+ if (!source.includes('createApp') || !source.includes('router')) {
198
248
  logger.trace('[router-detect] No createApp + router pattern found in %s', appFile);
199
249
  return noDetection;
200
250
  }
201
251
 
202
- const ast = acornLoose.parse(contents, {
203
- locations: true,
204
- ecmaVersion: 'latest',
205
- sourceType: 'module',
206
- }) as ASTNode;
207
-
208
- // Build import map: variable name → import path
209
- const importMap = new Map<string, string>();
210
- for (const node of ast.body || []) {
211
- if (node.type === 'ImportDeclaration' && node.source?.value) {
212
- for (const spec of node.specifiers || []) {
213
- if (spec.local?.name) {
214
- importMap.set(spec.local.name, String(node.source.value));
215
- }
216
- }
217
- }
218
- }
219
-
220
- // Walk all statements looking for createApp() calls
221
- const routerMounts = findCreateAppRouterCalls(ast, importMap);
252
+ // Parse with TypeScript
253
+ const sourceFile = ts.createSourceFile(
254
+ appFile,
255
+ source,
256
+ ts.ScriptTarget.Latest,
257
+ true,
258
+ ts.ScriptKind.TS
259
+ );
222
260
 
223
- if (!routerMounts || routerMounts.length === 0) {
261
+ const rawMounts = extractRouterMounts(sourceFile);
262
+ if (!rawMounts || rawMounts.length === 0) {
224
263
  logger.trace('[router-detect] createApp() found but no router property');
225
264
  return noDetection;
226
265
  }
227
266
 
267
+ // Build import map to resolve variable names to file paths
268
+ const importMap = buildImportMap(sourceFile);
269
+
228
270
  // Resolve each router variable to its file
229
271
  const mounts: DetectedRouteMount[] = [];
230
- for (const { path, varName } of routerMounts) {
272
+ for (const { path, varName } of rawMounts) {
231
273
  const importPath = importMap.get(varName);
232
274
  if (!importPath) {
233
275
  logger.debug(
234
276
  '[router-detect] Router variable %s is not imported — may be defined locally',
235
277
  varName
236
278
  );
237
- // Could be defined in the same file — skip for now
238
279
  continue;
239
280
  }
240
281
 
241
- const resolvedFile = resolveImportFile(appDir, importPath);
282
+ const resolvedFile = await resolveImportFile(appDir, importPath);
242
283
  if (!resolvedFile) {
243
284
  logger.warn(
244
285
  '[router-detect] Could not resolve import %s for router variable %s',
@@ -277,74 +318,3 @@ async function detectInFile(appFile: string, logger: Logger): Promise<AppRouterD
277
318
  return noDetection;
278
319
  }
279
320
  }
280
-
281
- /**
282
- * Walk the AST looking for `createApp({ router: ... })` calls.
283
- * Handles:
284
- * - `createApp({ router })` (top-level expression)
285
- * - `const app = await createApp({ router })` (variable declaration)
286
- * - `export const app = await createApp({ router })` (exported)
287
- */
288
- function findCreateAppRouterCalls(
289
- ast: ASTNode,
290
- importMap: Map<string, string>
291
- ): Array<{ path: string; varName: string }> | null {
292
- for (const node of ast.body || []) {
293
- // Check expression statements: createApp({ router })
294
- if (node.type === 'ExpressionStatement') {
295
- const result = checkForCreateAppCall(node.expression, importMap);
296
- if (result) return result;
297
- }
298
-
299
- // Check variable declarations: const app = await createApp({ router })
300
- if (node.type === 'VariableDeclaration') {
301
- for (const decl of node.declarations || []) {
302
- if (decl.init) {
303
- const result = checkForCreateAppCall(decl.init, importMap);
304
- if (result) return result;
305
- }
306
- }
307
- }
308
-
309
- // Check exports: export const app = await createApp({ router })
310
- if (node.type === 'ExportNamedDeclaration' && node.declaration) {
311
- if (node.declaration.type === 'VariableDeclaration') {
312
- for (const decl of node.declaration.declarations || []) {
313
- if (decl.init) {
314
- const result = checkForCreateAppCall(decl.init, importMap);
315
- if (result) return result;
316
- }
317
- }
318
- }
319
- }
320
- }
321
-
322
- return null;
323
- }
324
-
325
- /**
326
- * Check if an expression node is a `createApp({ router })` call.
327
- * Unwraps `await` expressions.
328
- */
329
- function checkForCreateAppCall(
330
- expr: ASTNode,
331
- importMap: Map<string, string>
332
- ): Array<{ path: string; varName: string }> | null {
333
- if (!expr) return null;
334
-
335
- // Unwrap AwaitExpression: await createApp(...)
336
- if (expr.type === 'AwaitExpression' && expr.argument) {
337
- return checkForCreateAppCall(expr.argument, importMap);
338
- }
339
-
340
- // Check for createApp({ router })
341
- if (
342
- expr.type === 'CallExpression' &&
343
- expr.callee?.type === 'Identifier' &&
344
- expr.callee?.name === 'createApp'
345
- ) {
346
- return extractRouterFromCreateApp(expr);
347
- }
348
-
349
- return null;
350
- }