@ronkovic/aad 0.3.0

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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/bin/aad.js +2 -0
  4. package/package.json +78 -0
  5. package/src/__tests__/e2e/pipeline-e2e.test.ts +279 -0
  6. package/src/__tests__/e2e/resume-e2e.test.ts +200 -0
  7. package/src/__tests__/integration/cli-smoke.test.ts +175 -0
  8. package/src/__tests__/integration/pipeline.test.ts +346 -0
  9. package/src/bun-imports.d.ts +14 -0
  10. package/src/main.ts +52 -0
  11. package/src/modules/claude-provider/__tests__/claude-cli.adapter.test.ts +277 -0
  12. package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +127 -0
  13. package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +347 -0
  14. package/src/modules/claude-provider/__tests__/effort-strategy.test.ts +212 -0
  15. package/src/modules/claude-provider/__tests__/provider-registry.test.ts +251 -0
  16. package/src/modules/claude-provider/__tests__/retry.test.ts +201 -0
  17. package/src/modules/claude-provider/claude-cli.adapter.ts +156 -0
  18. package/src/modules/claude-provider/claude-provider.port.ts +35 -0
  19. package/src/modules/claude-provider/claude-sdk.adapter.ts +217 -0
  20. package/src/modules/claude-provider/effort-strategy.ts +94 -0
  21. package/src/modules/claude-provider/index.ts +32 -0
  22. package/src/modules/claude-provider/provider-registry.ts +92 -0
  23. package/src/modules/claude-provider/retry.ts +81 -0
  24. package/src/modules/cli/__tests__/app.test.ts +160 -0
  25. package/src/modules/cli/__tests__/cleanup.test.ts +111 -0
  26. package/src/modules/cli/__tests__/commands.test.ts +186 -0
  27. package/src/modules/cli/__tests__/output.test.ts +329 -0
  28. package/src/modules/cli/__tests__/resume.test.ts +324 -0
  29. package/src/modules/cli/__tests__/run.test.ts +168 -0
  30. package/src/modules/cli/__tests__/shutdown.test.ts +168 -0
  31. package/src/modules/cli/__tests__/status.test.ts +144 -0
  32. package/src/modules/cli/app.ts +241 -0
  33. package/src/modules/cli/commands/cleanup.ts +120 -0
  34. package/src/modules/cli/commands/resume.ts +156 -0
  35. package/src/modules/cli/commands/run.ts +322 -0
  36. package/src/modules/cli/commands/status.ts +101 -0
  37. package/src/modules/cli/index.ts +29 -0
  38. package/src/modules/cli/output.ts +256 -0
  39. package/src/modules/cli/shutdown.ts +122 -0
  40. package/src/modules/dashboard/__tests__/api-routes.test.ts +204 -0
  41. package/src/modules/dashboard/__tests__/file-watcher.test.ts +34 -0
  42. package/src/modules/dashboard/__tests__/server.test.ts +120 -0
  43. package/src/modules/dashboard/__tests__/sse-broadcaster.test.ts +163 -0
  44. package/src/modules/dashboard/__tests__/sse-routes.test.ts +58 -0
  45. package/src/modules/dashboard/__tests__/state-aggregator.test.ts +330 -0
  46. package/src/modules/dashboard/index.ts +8 -0
  47. package/src/modules/dashboard/routes/api.ts +84 -0
  48. package/src/modules/dashboard/routes/sse.ts +37 -0
  49. package/src/modules/dashboard/server.ts +111 -0
  50. package/src/modules/dashboard/services/file-watcher.ts +36 -0
  51. package/src/modules/dashboard/services/sse-broadcaster.ts +81 -0
  52. package/src/modules/dashboard/services/state-aggregator.ts +132 -0
  53. package/src/modules/dashboard/ui/dashboard.html +405 -0
  54. package/src/modules/git-workspace/__tests__/branch-manager.test.ts +335 -0
  55. package/src/modules/git-workspace/__tests__/git-exec.test.ts +91 -0
  56. package/src/modules/git-workspace/__tests__/memory-sync.test.ts +273 -0
  57. package/src/modules/git-workspace/__tests__/merge-service.test.ts +286 -0
  58. package/src/modules/git-workspace/__tests__/settings-merge.test.ts +163 -0
  59. package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +247 -0
  60. package/src/modules/git-workspace/branch-manager.ts +191 -0
  61. package/src/modules/git-workspace/git-exec.ts +124 -0
  62. package/src/modules/git-workspace/index.ts +17 -0
  63. package/src/modules/git-workspace/memory-sync.ts +89 -0
  64. package/src/modules/git-workspace/merge-service.ts +156 -0
  65. package/src/modules/git-workspace/settings-merge.ts +95 -0
  66. package/src/modules/git-workspace/worktree-manager.ts +199 -0
  67. package/src/modules/logging/__tests__/log-store.test.ts +242 -0
  68. package/src/modules/logging/__tests__/logger.test.ts +81 -0
  69. package/src/modules/logging/__tests__/sse-transport.test.ts +93 -0
  70. package/src/modules/logging/index.ts +7 -0
  71. package/src/modules/logging/log-store.ts +80 -0
  72. package/src/modules/logging/logger.ts +55 -0
  73. package/src/modules/logging/transports/sse-transport.ts +28 -0
  74. package/src/modules/multi-repo/__tests__/multi-repo-planner.test.ts +93 -0
  75. package/src/modules/multi-repo/__tests__/repo-context.test.ts +79 -0
  76. package/src/modules/multi-repo/index.ts +12 -0
  77. package/src/modules/multi-repo/multi-repo-planner.ts +112 -0
  78. package/src/modules/multi-repo/repo-context.ts +71 -0
  79. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +10 -0
  80. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +10 -0
  81. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +13 -0
  82. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +10 -0
  83. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +10 -0
  84. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +5 -0
  85. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +5 -0
  86. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +10 -0
  87. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +10 -0
  88. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +13 -0
  89. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +10 -0
  90. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +10 -0
  91. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +5 -0
  92. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +5 -0
  93. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +10 -0
  94. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +10 -0
  95. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +13 -0
  96. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +10 -0
  97. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +10 -0
  98. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +5 -0
  99. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +5 -0
  100. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +10 -0
  101. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +10 -0
  102. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +13 -0
  103. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +10 -0
  104. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +10 -0
  105. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +5 -0
  106. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +5 -0
  107. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +10 -0
  108. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +10 -0
  109. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +13 -0
  110. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +10 -0
  111. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +10 -0
  112. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +5 -0
  113. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +5 -0
  114. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +10 -0
  115. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +10 -0
  116. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +13 -0
  117. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +10 -0
  118. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +10 -0
  119. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +5 -0
  120. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +5 -0
  121. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +10 -0
  122. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +10 -0
  123. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +13 -0
  124. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +10 -0
  125. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +10 -0
  126. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +5 -0
  127. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +5 -0
  128. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +10 -0
  129. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +10 -0
  130. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +13 -0
  131. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +10 -0
  132. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +10 -0
  133. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +5 -0
  134. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +5 -0
  135. package/src/modules/persistence/__tests__/file-lock.test.ts +141 -0
  136. package/src/modules/persistence/__tests__/index.test.ts +38 -0
  137. package/src/modules/persistence/__tests__/stores.test.ts +594 -0
  138. package/src/modules/persistence/file-lock.ts +158 -0
  139. package/src/modules/persistence/fs-run-store.ts +73 -0
  140. package/src/modules/persistence/fs-task-store.ts +152 -0
  141. package/src/modules/persistence/fs-worker-store.ts +116 -0
  142. package/src/modules/persistence/in-memory-stores.ts +98 -0
  143. package/src/modules/persistence/index.ts +60 -0
  144. package/src/modules/persistence/stores.port.ts +60 -0
  145. package/src/modules/planning/__tests__/file-conflict-validator.test.ts +256 -0
  146. package/src/modules/planning/__tests__/planning-service.test.ts +366 -0
  147. package/src/modules/planning/__tests__/project-detection.test.ts +707 -0
  148. package/src/modules/planning/file-conflict-validator.ts +135 -0
  149. package/src/modules/planning/index.ts +40 -0
  150. package/src/modules/planning/planning.service.ts +262 -0
  151. package/src/modules/planning/project-detection.ts +525 -0
  152. package/src/modules/plugin/__tests__/plugin-loader.test.ts +83 -0
  153. package/src/modules/plugin/__tests__/plugin-manager.test.ts +187 -0
  154. package/src/modules/plugin/index.ts +3 -0
  155. package/src/modules/plugin/plugin-loader.ts +46 -0
  156. package/src/modules/plugin/plugin-manager.ts +90 -0
  157. package/src/modules/plugin/plugin.types.ts +37 -0
  158. package/src/modules/process-manager/__tests__/process-manager.test.ts +210 -0
  159. package/src/modules/process-manager/__tests__/worker.test.ts +89 -0
  160. package/src/modules/process-manager/index.ts +5 -0
  161. package/src/modules/process-manager/process-manager.ts +193 -0
  162. package/src/modules/process-manager/worker.ts +106 -0
  163. package/src/modules/task-execution/__tests__/default-spawner.test.ts +154 -0
  164. package/src/modules/task-execution/__tests__/executor.test.ts +760 -0
  165. package/src/modules/task-execution/__tests__/implementer-green.test.ts +286 -0
  166. package/src/modules/task-execution/__tests__/merge-phase.test.ts +368 -0
  167. package/src/modules/task-execution/__tests__/reviewer.test.ts +302 -0
  168. package/src/modules/task-execution/__tests__/tester-red.test.ts +281 -0
  169. package/src/modules/task-execution/__tests__/tester-verify.test.ts +313 -0
  170. package/src/modules/task-execution/executor.ts +303 -0
  171. package/src/modules/task-execution/index.ts +45 -0
  172. package/src/modules/task-execution/phases/default-spawner.ts +49 -0
  173. package/src/modules/task-execution/phases/implementer-green.ts +100 -0
  174. package/src/modules/task-execution/phases/merge.ts +122 -0
  175. package/src/modules/task-execution/phases/reviewer.ts +160 -0
  176. package/src/modules/task-execution/phases/tester-red.ts +100 -0
  177. package/src/modules/task-execution/phases/tester-verify.ts +120 -0
  178. package/src/modules/task-queue/__tests__/dependency-resolver.test.ts +456 -0
  179. package/src/modules/task-queue/__tests__/dispatcher.test.ts +824 -0
  180. package/src/modules/task-queue/__tests__/task-plan.test.ts +122 -0
  181. package/src/modules/task-queue/__tests__/task.test.ts +130 -0
  182. package/src/modules/task-queue/dependency-resolver.ts +171 -0
  183. package/src/modules/task-queue/dispatcher.ts +372 -0
  184. package/src/modules/task-queue/index.ts +16 -0
  185. package/src/modules/task-queue/task-plan.ts +40 -0
  186. package/src/modules/task-queue/task.ts +67 -0
  187. package/src/shared/__tests__/config.test.ts +204 -0
  188. package/src/shared/__tests__/errors.test.ts +285 -0
  189. package/src/shared/__tests__/events.test.ts +496 -0
  190. package/src/shared/__tests__/types.test.ts +360 -0
  191. package/src/shared/config.ts +133 -0
  192. package/src/shared/errors.ts +128 -0
  193. package/src/shared/events.ts +171 -0
  194. package/src/shared/types.ts +143 -0
  195. package/tsconfig.json +30 -0
@@ -0,0 +1,525 @@
1
+ // Project Detection - port from .aad/scripts/lib/project-detection.sh
2
+ // Detects project type, package manager, framework, test framework, ORM, and architecture patterns
3
+
4
+ import path from "node:path";
5
+
6
+ // Type Definitions
7
+ export type ProjectType =
8
+ | "go"
9
+ | "go-workspace"
10
+ | "rust"
11
+ | "python"
12
+ | "nodejs"
13
+ | "nextjs"
14
+ | "express"
15
+ | "react"
16
+ | "terraform"
17
+ | "unknown";
18
+
19
+ export type PackageManager =
20
+ | "uv"
21
+ | "poetry"
22
+ | "pipenv"
23
+ | "pip"
24
+ | "pnpm"
25
+ | "yarn"
26
+ | "npm"
27
+ | "bun"
28
+ | "cargo"
29
+ | "go"
30
+ | "unknown";
31
+
32
+ export type TestFramework =
33
+ | "pytest"
34
+ | "unittest"
35
+ | "vitest"
36
+ | "jest"
37
+ | "bun:test"
38
+ | "mocha"
39
+ | "go-test"
40
+ | "cargo-test"
41
+ | "terraform-validate"
42
+ | "unknown";
43
+
44
+ export type Framework =
45
+ | "nextjs"
46
+ | "express"
47
+ | "react"
48
+ | "fastapi"
49
+ | "django"
50
+ | "flask"
51
+ | "gin"
52
+ | "echo"
53
+ | "actix"
54
+ | "unknown";
55
+
56
+ export type OrmType =
57
+ | "prisma"
58
+ | "drizzle"
59
+ | "typeorm"
60
+ | "sequelize"
61
+ | "sqlalchemy"
62
+ | "tortoise"
63
+ | "gorm"
64
+ | "sqlx"
65
+ | "sqlc"
66
+ | "diesel"
67
+ | "sea-orm"
68
+ | "unknown";
69
+
70
+ export type ArchitecturePattern =
71
+ | "hexagonal"
72
+ | "clean-architecture"
73
+ | "ddd-layered"
74
+ | "go-standard-layout"
75
+ | "react-standard"
76
+ | "custom"
77
+ | "unknown";
78
+
79
+ export interface WorkspaceInfo {
80
+ path: string;
81
+ projectType: ProjectType;
82
+ packageManager: PackageManager;
83
+ testFramework: TestFramework;
84
+ framework?: Framework;
85
+ orm?: OrmType;
86
+ architecture?: ArchitecturePattern;
87
+ }
88
+
89
+ export interface ProjectAnalysis {
90
+ projectType: ProjectType;
91
+ packageManager: PackageManager;
92
+ isMonorepo: boolean;
93
+ workspaces: WorkspaceInfo[];
94
+ framework?: Framework;
95
+ orm?: OrmType;
96
+ architecture?: ArchitecturePattern;
97
+ }
98
+
99
+ // FileChecker interface for dependency injection (testability)
100
+ export interface FileChecker {
101
+ exists(path: string): Promise<boolean>;
102
+ readText(path: string): Promise<string>;
103
+ glob(pattern: string, cwd: string): Promise<string[]>;
104
+ }
105
+
106
+ // Bun-based FileChecker implementation
107
+ export function createBunFileChecker(): FileChecker {
108
+ return {
109
+ async exists(filePath: string): Promise<boolean> {
110
+ return await Bun.file(filePath).exists();
111
+ },
112
+ async readText(filePath: string): Promise<string> {
113
+ try {
114
+ return await Bun.file(filePath).text();
115
+ } catch {
116
+ return "";
117
+ }
118
+ },
119
+ async glob(pattern: string, cwd: string): Promise<string[]> {
120
+ const globber = new Bun.Glob(pattern);
121
+ const matches: string[] = [];
122
+ for await (const file of globber.scan({
123
+ cwd,
124
+ absolute: false,
125
+ onlyFiles: true,
126
+ })) {
127
+ // Exclude node_modules, .git, and .aad directories
128
+ if (
129
+ !file.includes("node_modules/") &&
130
+ !file.includes(".git/") &&
131
+ !file.includes(".aad/")
132
+ ) {
133
+ matches.push(file);
134
+ }
135
+ }
136
+ return matches;
137
+ },
138
+ };
139
+ }
140
+
141
+ // ============================================================
142
+ // Project Type Detection
143
+ // ============================================================
144
+
145
+ export async function detectProjectType(
146
+ projectRoot: string,
147
+ checker: FileChecker
148
+ ): Promise<ProjectType> {
149
+ // Go
150
+ if (await checker.exists(path.join(projectRoot, "go.work"))) {
151
+ return "go-workspace";
152
+ }
153
+ if (await checker.exists(path.join(projectRoot, "go.mod"))) {
154
+ return "go";
155
+ }
156
+
157
+ // Rust
158
+ if (await checker.exists(path.join(projectRoot, "Cargo.toml"))) {
159
+ return "rust";
160
+ }
161
+
162
+ // Python
163
+ if (await checker.exists(path.join(projectRoot, "pyproject.toml"))) {
164
+ return "python";
165
+ }
166
+ if (await checker.exists(path.join(projectRoot, "requirements.txt"))) {
167
+ return "python";
168
+ }
169
+
170
+ // Node.js/TypeScript
171
+ const packageJsonPath = path.join(projectRoot, "package.json");
172
+ if (await checker.exists(packageJsonPath)) {
173
+ const content = await checker.readText(packageJsonPath);
174
+ if (content.includes('"next"')) return "nextjs";
175
+ if (content.includes('"express"')) return "express";
176
+ if (content.includes('"react"')) return "react";
177
+ return "nodejs";
178
+ }
179
+
180
+ // Terraform
181
+ const tfFiles = await checker.glob("*.tf", projectRoot);
182
+ if (tfFiles.length > 0) {
183
+ return "terraform";
184
+ }
185
+
186
+ return "unknown";
187
+ }
188
+
189
+ // ============================================================
190
+ // Package Manager Detection
191
+ // ============================================================
192
+
193
+ export async function detectPackageManager(
194
+ projectRoot: string,
195
+ checker: FileChecker
196
+ ): Promise<PackageManager> {
197
+ // Python
198
+ if (await checker.exists(path.join(projectRoot, "uv.lock"))) return "uv";
199
+ if (await checker.exists(path.join(projectRoot, "poetry.lock"))) return "poetry";
200
+ if (await checker.exists(path.join(projectRoot, "Pipfile.lock"))) return "pipenv";
201
+ if (await checker.exists(path.join(projectRoot, "requirements.txt"))) return "pip";
202
+
203
+ // Node.js
204
+ if (await checker.exists(path.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
205
+ if (await checker.exists(path.join(projectRoot, "yarn.lock"))) return "yarn";
206
+ if (await checker.exists(path.join(projectRoot, "package-lock.json"))) return "npm";
207
+ if (await checker.exists(path.join(projectRoot, "bun.lockb"))) return "bun";
208
+
209
+ // Rust/Go
210
+ if (await checker.exists(path.join(projectRoot, "Cargo.lock"))) return "cargo";
211
+ if (await checker.exists(path.join(projectRoot, "go.sum"))) return "go";
212
+
213
+ return "unknown";
214
+ }
215
+
216
+ // ============================================================
217
+ // Framework Detection
218
+ // ============================================================
219
+
220
+ export async function detectFramework(
221
+ projectRoot: string,
222
+ projectType: ProjectType,
223
+ checker: FileChecker
224
+ ): Promise<Framework> {
225
+ switch (projectType) {
226
+ case "python": {
227
+ const pyprojectPath = path.join(projectRoot, "pyproject.toml");
228
+ if (await checker.exists(pyprojectPath)) {
229
+ const content = await checker.readText(pyprojectPath);
230
+ if (content.includes("fastapi")) return "fastapi";
231
+ if (content.includes("django")) return "django";
232
+ if (content.includes("flask")) return "flask";
233
+ }
234
+ break;
235
+ }
236
+ case "rust": {
237
+ const cargoPath = path.join(projectRoot, "Cargo.toml");
238
+ if (await checker.exists(cargoPath)) {
239
+ const content = await checker.readText(cargoPath);
240
+ if (content.includes("actix")) return "actix";
241
+ }
242
+ break;
243
+ }
244
+ case "go":
245
+ case "go-workspace":
246
+ return "unknown"; // Go typically doesn't use heavy frameworks
247
+ case "nextjs":
248
+ return "nextjs";
249
+ case "express":
250
+ return "express";
251
+ case "react":
252
+ return "react";
253
+ }
254
+ return "unknown";
255
+ }
256
+
257
+ // ============================================================
258
+ // Test Framework Detection
259
+ // ============================================================
260
+
261
+ export async function detectTestFramework(
262
+ projectRoot: string,
263
+ projectType: ProjectType,
264
+ checker: FileChecker
265
+ ): Promise<TestFramework> {
266
+ switch (projectType) {
267
+ case "go":
268
+ case "go-workspace":
269
+ return "go-test";
270
+
271
+ case "rust":
272
+ return "cargo-test";
273
+
274
+ case "python": {
275
+ const pyprojectPath = path.join(projectRoot, "pyproject.toml");
276
+ if (await checker.exists(pyprojectPath)) {
277
+ const content = await checker.readText(pyprojectPath);
278
+ if (content.includes("pytest")) return "pytest";
279
+ }
280
+ return "unittest";
281
+ }
282
+
283
+ case "nextjs":
284
+ case "react":
285
+ case "express":
286
+ case "nodejs": {
287
+ const packageJsonPath = path.join(projectRoot, "package.json");
288
+ if (await checker.exists(packageJsonPath)) {
289
+ const content = await checker.readText(packageJsonPath);
290
+ if (content.includes('"bun-types"') || content.includes('"@types/bun"')) {
291
+ return "bun:test";
292
+ }
293
+ if (content.includes('"vitest"')) return "vitest";
294
+ if (content.includes('"jest"')) return "jest";
295
+ if (content.includes('"mocha"')) return "mocha";
296
+ }
297
+ return "unknown";
298
+ }
299
+
300
+ case "terraform":
301
+ return "terraform-validate";
302
+
303
+ default:
304
+ return "unknown";
305
+ }
306
+ }
307
+
308
+ // ============================================================
309
+ // ORM Detection
310
+ // ============================================================
311
+
312
+ export async function detectOrm(
313
+ projectRoot: string,
314
+ projectType: ProjectType,
315
+ checker: FileChecker
316
+ ): Promise<OrmType> {
317
+ switch (projectType) {
318
+ case "go":
319
+ case "go-workspace": {
320
+ if (await checker.exists(path.join(projectRoot, "sqlc.yaml"))) return "sqlc";
321
+ const goModPath = path.join(projectRoot, "go.mod");
322
+ if (await checker.exists(goModPath)) {
323
+ const content = await checker.readText(goModPath);
324
+ if (content.includes("gorm.io/gorm")) return "gorm";
325
+ if (content.includes("jmoiron/sqlx")) return "sqlx";
326
+ }
327
+ break;
328
+ }
329
+
330
+ case "python": {
331
+ const pyprojectPath = path.join(projectRoot, "pyproject.toml");
332
+ if (await checker.exists(pyprojectPath)) {
333
+ const content = await checker.readText(pyprojectPath);
334
+ if (content.includes("sqlalchemy")) return "sqlalchemy";
335
+ if (content.includes("tortoise")) return "tortoise";
336
+ }
337
+ break;
338
+ }
339
+
340
+ case "rust": {
341
+ const cargoPath = path.join(projectRoot, "Cargo.toml");
342
+ if (await checker.exists(cargoPath)) {
343
+ const content = await checker.readText(cargoPath);
344
+ if (content.includes("sqlx")) return "sqlx";
345
+ if (content.includes("diesel")) return "diesel";
346
+ if (content.includes("sea-orm")) return "sea-orm";
347
+ }
348
+ break;
349
+ }
350
+
351
+ case "nextjs":
352
+ case "react":
353
+ case "express":
354
+ case "nodejs": {
355
+ const packageJsonPath = path.join(projectRoot, "package.json");
356
+ if (await checker.exists(packageJsonPath)) {
357
+ const content = await checker.readText(packageJsonPath);
358
+ if (content.includes('"prisma"')) return "prisma";
359
+ if (content.includes('"drizzle-orm"')) return "drizzle";
360
+ if (content.includes('"typeorm"')) return "typeorm";
361
+ if (content.includes('"sequelize"')) return "sequelize";
362
+ }
363
+ break;
364
+ }
365
+ }
366
+ return "unknown";
367
+ }
368
+
369
+ // ============================================================
370
+ // Architecture Pattern Detection
371
+ // ============================================================
372
+
373
+ export async function detectArchitecturePattern(
374
+ projectRoot: string,
375
+ checker: FileChecker
376
+ ): Promise<ArchitecturePattern> {
377
+ // Go standard layout
378
+ if (
379
+ (await checker.exists(path.join(projectRoot, "internal"))) &&
380
+ (await checker.exists(path.join(projectRoot, "cmd")))
381
+ ) {
382
+ return "go-standard-layout";
383
+ }
384
+
385
+ // Clean Architecture
386
+ if (
387
+ (await checker.exists(path.join(projectRoot, "usecases"))) &&
388
+ (await checker.exists(path.join(projectRoot, "gateways")))
389
+ ) {
390
+ return "clean-architecture";
391
+ }
392
+
393
+ // DDD Layered
394
+ if (
395
+ (await checker.exists(path.join(projectRoot, "domain"))) &&
396
+ (await checker.exists(path.join(projectRoot, "infrastructure")))
397
+ ) {
398
+ return "ddd-layered";
399
+ }
400
+
401
+ // React standard
402
+ if (await checker.exists(path.join(projectRoot, "src/components"))) {
403
+ return "react-standard";
404
+ }
405
+
406
+ return "custom";
407
+ }
408
+
409
+ // ============================================================
410
+ // Monorepo Detection
411
+ // ============================================================
412
+
413
+ export async function isMonorepo(
414
+ projectRoot: string,
415
+ checker: FileChecker
416
+ ): Promise<boolean> {
417
+ // Go workspace
418
+ if (await checker.exists(path.join(projectRoot, "go.work"))) {
419
+ return true;
420
+ }
421
+
422
+ // npm/yarn/pnpm workspaces
423
+ const packageJsonPath = path.join(projectRoot, "package.json");
424
+ if (await checker.exists(packageJsonPath)) {
425
+ const content = await checker.readText(packageJsonPath);
426
+ if (content.includes('"workspaces"')) {
427
+ return true;
428
+ }
429
+ }
430
+
431
+ // Multiple language files in subdirectories
432
+ const patterns = ["**/go.mod", "**/package.json", "**/Cargo.toml", "**/pyproject.toml"];
433
+ let totalCount = 0;
434
+
435
+ for (const pattern of patterns) {
436
+ const matches = await checker.glob(pattern, projectRoot);
437
+ totalCount += matches.length;
438
+ }
439
+
440
+ return totalCount > 1;
441
+ }
442
+
443
+ // ============================================================
444
+ // Workspace Detection
445
+ // ============================================================
446
+
447
+ export async function detectWorkspaces(
448
+ projectRoot: string,
449
+ checker: FileChecker
450
+ ): Promise<string[]> {
451
+ if (!(await isMonorepo(projectRoot, checker))) {
452
+ // Single project: return root only
453
+ return [projectRoot];
454
+ }
455
+
456
+ const workspaces = new Set<string>();
457
+ workspaces.add(projectRoot);
458
+
459
+ // Scan for language-specific project files
460
+ const patterns = [
461
+ { pattern: "**/go.mod", maxDepth: 3 },
462
+ { pattern: "**/package.json", maxDepth: 3 },
463
+ { pattern: "**/Cargo.toml", maxDepth: 3 },
464
+ { pattern: "**/pyproject.toml", maxDepth: 3 },
465
+ ];
466
+
467
+ for (const { pattern } of patterns) {
468
+ const matches = await checker.glob(pattern, projectRoot);
469
+ for (const match of matches) {
470
+ const dir = path.dirname(path.join(projectRoot, match));
471
+ workspaces.add(dir);
472
+ }
473
+ }
474
+
475
+ return Array.from(workspaces).sort();
476
+ }
477
+
478
+ // ============================================================
479
+ // Full Project Analysis
480
+ // ============================================================
481
+
482
+ export async function analyzeProject(
483
+ projectRoot: string,
484
+ checker: FileChecker = createBunFileChecker()
485
+ ): Promise<ProjectAnalysis> {
486
+ const projectType = await detectProjectType(projectRoot, checker);
487
+ const packageManager = await detectPackageManager(projectRoot, checker);
488
+ const mono = await isMonorepo(projectRoot, checker);
489
+ const workspacePaths = await detectWorkspaces(projectRoot, checker);
490
+
491
+ const framework = await detectFramework(projectRoot, projectType, checker);
492
+ const orm = await detectOrm(projectRoot, projectType, checker);
493
+ const architecture = await detectArchitecturePattern(projectRoot, checker);
494
+
495
+ const workspaces: WorkspaceInfo[] = await Promise.all(
496
+ workspacePaths.map(async (wsPath) => {
497
+ const wsType = await detectProjectType(wsPath, checker);
498
+ const wsPkgMgr = await detectPackageManager(wsPath, checker);
499
+ const wsTestFw = await detectTestFramework(wsPath, wsType, checker);
500
+ const wsFw = await detectFramework(wsPath, wsType, checker);
501
+ const wsOrm = await detectOrm(wsPath, wsType, checker);
502
+ const wsArch = await detectArchitecturePattern(wsPath, checker);
503
+
504
+ return {
505
+ path: wsPath,
506
+ projectType: wsType,
507
+ packageManager: wsPkgMgr,
508
+ testFramework: wsTestFw,
509
+ framework: wsFw !== "unknown" ? wsFw : undefined,
510
+ orm: wsOrm !== "unknown" ? wsOrm : undefined,
511
+ architecture: wsArch !== "custom" ? wsArch : undefined,
512
+ };
513
+ })
514
+ );
515
+
516
+ return {
517
+ projectType,
518
+ packageManager,
519
+ isMonorepo: mono,
520
+ workspaces,
521
+ framework: framework !== "unknown" ? framework : undefined,
522
+ orm: orm !== "unknown" ? orm : undefined,
523
+ architecture: architecture !== "custom" ? architecture : undefined,
524
+ };
525
+ }
@@ -0,0 +1,83 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { loadPlugin } from "../plugin-loader";
3
+ import { join } from "node:path";
4
+ import { writeFileSync, mkdirSync, rmSync } from "node:fs";
5
+
6
+ const fixturesDir = join(import.meta.dir, "__fixtures__");
7
+
8
+ function setupFixtures() {
9
+ mkdirSync(fixturesDir, { recursive: true });
10
+
11
+ // Valid plugin
12
+ writeFileSync(
13
+ join(fixturesDir, "valid-plugin.ts"),
14
+ `export default {
15
+ name: "valid-plugin",
16
+ version: "1.0.0",
17
+ activate() {},
18
+ deactivate() {},
19
+ };`
20
+ );
21
+
22
+ // Invalid plugin (missing activate)
23
+ writeFileSync(
24
+ join(fixturesDir, "invalid-plugin.ts"),
25
+ `export default {
26
+ name: "invalid",
27
+ version: "1.0.0",
28
+ };`
29
+ );
30
+
31
+ // Invalid plugin (missing name)
32
+ writeFileSync(
33
+ join(fixturesDir, "no-name-plugin.ts"),
34
+ `export default {
35
+ version: "1.0.0",
36
+ activate() {},
37
+ };`
38
+ );
39
+ }
40
+
41
+ function cleanupFixtures() {
42
+ try {
43
+ rmSync(fixturesDir, { recursive: true, force: true });
44
+ } catch {
45
+ // ignore
46
+ }
47
+ }
48
+
49
+ describe("loadPlugin", () => {
50
+ test("loads a valid plugin from absolute path", async () => {
51
+ setupFixtures();
52
+ try {
53
+ const plugin = await loadPlugin(join(fixturesDir, "valid-plugin.ts"));
54
+ expect(plugin.name).toBe("valid-plugin");
55
+ expect(plugin.version).toBe("1.0.0");
56
+ expect(typeof plugin.activate).toBe("function");
57
+ } finally {
58
+ cleanupFixtures();
59
+ }
60
+ });
61
+
62
+ test("rejects invalid plugin (missing activate)", async () => {
63
+ setupFixtures();
64
+ try {
65
+ await expect(loadPlugin(join(fixturesDir, "invalid-plugin.ts"))).rejects.toThrow("Invalid plugin");
66
+ } finally {
67
+ cleanupFixtures();
68
+ }
69
+ });
70
+
71
+ test("rejects invalid plugin (missing name)", async () => {
72
+ setupFixtures();
73
+ try {
74
+ await expect(loadPlugin(join(fixturesDir, "no-name-plugin.ts"))).rejects.toThrow("Invalid plugin");
75
+ } finally {
76
+ cleanupFixtures();
77
+ }
78
+ });
79
+
80
+ test("rejects non-existent path", async () => {
81
+ await expect(loadPlugin("/nonexistent/path/plugin.ts")).rejects.toThrow("Failed to load plugin");
82
+ });
83
+ });