imposters 0.1.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 (426) hide show
  1. package/LICENSE +21 -0
  2. package/Program/package.json +6 -0
  3. package/README.md +365 -0
  4. package/api/AdminApi/package.json +6 -0
  5. package/api/ApiErrors/package.json +6 -0
  6. package/api/ApiSchemas/package.json +6 -0
  7. package/api/Conversions/package.json +6 -0
  8. package/api/ImpostersGroup/package.json +6 -0
  9. package/api/ImpostersHandlers/package.json +6 -0
  10. package/api/SystemGroup/package.json +6 -0
  11. package/api/SystemHandlers/package.json +6 -0
  12. package/bin/imposters +47 -0
  13. package/cli/Commands/package.json +6 -0
  14. package/cli/ConfigLoader/package.json +6 -0
  15. package/client/HandlerHttpClient/package.json +6 -0
  16. package/client/ImpostersClient/package.json +6 -0
  17. package/client/testing/package.json +6 -0
  18. package/dist/cjs/Program.js +4 -0
  19. package/dist/cjs/Program.js.map +1 -0
  20. package/dist/cjs/api/AdminApi.js +11 -0
  21. package/dist/cjs/api/AdminApi.js.map +1 -0
  22. package/dist/cjs/api/ApiErrors.js +30 -0
  23. package/dist/cjs/api/ApiErrors.js.map +1 -0
  24. package/dist/cjs/api/ApiSchemas.js +36 -0
  25. package/dist/cjs/api/ApiSchemas.js.map +1 -0
  26. package/dist/cjs/api/Conversions.js +41 -0
  27. package/dist/cjs/api/Conversions.js.map +1 -0
  28. package/dist/cjs/api/ImpostersGroup.js +37 -0
  29. package/dist/cjs/api/ImpostersGroup.js.map +1 -0
  30. package/dist/cjs/api/ImpostersHandlers.js +361 -0
  31. package/dist/cjs/api/ImpostersHandlers.js.map +1 -0
  32. package/dist/cjs/api/SystemGroup.js +12 -0
  33. package/dist/cjs/api/SystemGroup.js.map +1 -0
  34. package/dist/cjs/api/SystemHandlers.js +74 -0
  35. package/dist/cjs/api/SystemHandlers.js.map +1 -0
  36. package/dist/cjs/cli/Commands.js +104 -0
  37. package/dist/cjs/cli/Commands.js.map +1 -0
  38. package/dist/cjs/cli/ConfigLoader.js +34 -0
  39. package/dist/cjs/cli/ConfigLoader.js.map +1 -0
  40. package/dist/cjs/client/HandlerHttpClient.js +50 -0
  41. package/dist/cjs/client/HandlerHttpClient.js.map +1 -0
  42. package/dist/cjs/client/ImpostersClient.js +20 -0
  43. package/dist/cjs/client/ImpostersClient.js.map +1 -0
  44. package/dist/cjs/client/index.js +57 -0
  45. package/dist/cjs/client/index.js.map +1 -0
  46. package/dist/cjs/client/testing.js +94 -0
  47. package/dist/cjs/client/testing.js.map +1 -0
  48. package/dist/cjs/domain/imposter.js +125 -0
  49. package/dist/cjs/domain/imposter.js.map +1 -0
  50. package/dist/cjs/domain/route.js +185 -0
  51. package/dist/cjs/domain/route.js.map +1 -0
  52. package/dist/cjs/index.js +106 -0
  53. package/dist/cjs/index.js.map +1 -0
  54. package/dist/cjs/layers/ApiLayer.js +18 -0
  55. package/dist/cjs/layers/ApiLayer.js.map +1 -0
  56. package/dist/cjs/layers/MainLayer.js +27 -0
  57. package/dist/cjs/layers/MainLayer.js.map +1 -0
  58. package/dist/cjs/matching/ExpressionEvaluator.js +103 -0
  59. package/dist/cjs/matching/ExpressionEvaluator.js.map +1 -0
  60. package/dist/cjs/matching/RequestMatcher.js +145 -0
  61. package/dist/cjs/matching/RequestMatcher.js.map +1 -0
  62. package/dist/cjs/matching/ResponseGenerator.js +80 -0
  63. package/dist/cjs/matching/ResponseGenerator.js.map +1 -0
  64. package/dist/cjs/matching/TemplateEngine.js +55 -0
  65. package/dist/cjs/matching/TemplateEngine.js.map +1 -0
  66. package/dist/cjs/repositories/ImposterRepository.js +118 -0
  67. package/dist/cjs/repositories/ImposterRepository.js.map +1 -0
  68. package/dist/cjs/schemas/ConfigFileSchema.js +44 -0
  69. package/dist/cjs/schemas/ConfigFileSchema.js.map +1 -0
  70. package/dist/cjs/schemas/ImposterSchema.js +202 -0
  71. package/dist/cjs/schemas/ImposterSchema.js.map +1 -0
  72. package/dist/cjs/schemas/RequestLogSchema.js +51 -0
  73. package/dist/cjs/schemas/RequestLogSchema.js.map +1 -0
  74. package/dist/cjs/schemas/StubSchema.js +84 -0
  75. package/dist/cjs/schemas/StubSchema.js.map +1 -0
  76. package/dist/cjs/schemas/common.js +67 -0
  77. package/dist/cjs/schemas/common.js.map +1 -0
  78. package/dist/cjs/server/AdminServer.js +36 -0
  79. package/dist/cjs/server/AdminServer.js.map +1 -0
  80. package/dist/cjs/server/BunServer.js +13 -0
  81. package/dist/cjs/server/BunServer.js.map +1 -0
  82. package/dist/cjs/server/FiberManager.js +21 -0
  83. package/dist/cjs/server/FiberManager.js.map +1 -0
  84. package/dist/cjs/server/ImposterServer.js +234 -0
  85. package/dist/cjs/server/ImposterServer.js.map +1 -0
  86. package/dist/cjs/services/AppConfig.js +18 -0
  87. package/dist/cjs/services/AppConfig.js.map +1 -0
  88. package/dist/cjs/services/MetricsService.js +113 -0
  89. package/dist/cjs/services/MetricsService.js.map +1 -0
  90. package/dist/cjs/services/PortAllocator.js +50 -0
  91. package/dist/cjs/services/PortAllocator.js.map +1 -0
  92. package/dist/cjs/services/ProxyService.js +109 -0
  93. package/dist/cjs/services/ProxyService.js.map +1 -0
  94. package/dist/cjs/services/RequestLogger.js +60 -0
  95. package/dist/cjs/services/RequestLogger.js.map +1 -0
  96. package/dist/cjs/services/Uuid.js +10 -0
  97. package/dist/cjs/services/Uuid.js.map +1 -0
  98. package/dist/cjs/services/UuidLive.js +16 -0
  99. package/dist/cjs/services/UuidLive.js.map +1 -0
  100. package/dist/cjs/ui/UiRouter.js +242 -0
  101. package/dist/cjs/ui/UiRouter.js.map +1 -0
  102. package/dist/cjs/ui/admin/AdminLayout.js +36 -0
  103. package/dist/cjs/ui/admin/AdminLayout.js.map +1 -0
  104. package/dist/cjs/ui/admin/AdminUiRouter.js +155 -0
  105. package/dist/cjs/ui/admin/AdminUiRouter.js.map +1 -0
  106. package/dist/cjs/ui/admin/pages/AdminDashboard.js +55 -0
  107. package/dist/cjs/ui/admin/pages/AdminDashboard.js.map +1 -0
  108. package/dist/cjs/ui/admin/partials.js +64 -0
  109. package/dist/cjs/ui/admin/partials.js.map +1 -0
  110. package/dist/cjs/ui/html.js +42 -0
  111. package/dist/cjs/ui/html.js.map +1 -0
  112. package/dist/cjs/ui/layout.js +39 -0
  113. package/dist/cjs/ui/layout.js.map +1 -0
  114. package/dist/cjs/ui/pages/dashboard.js +51 -0
  115. package/dist/cjs/ui/pages/dashboard.js.map +1 -0
  116. package/dist/cjs/ui/pages/request-detail.js +119 -0
  117. package/dist/cjs/ui/pages/request-detail.js.map +1 -0
  118. package/dist/cjs/ui/pages/requests.js +120 -0
  119. package/dist/cjs/ui/pages/requests.js.map +1 -0
  120. package/dist/cjs/ui/pages/stubs.js +46 -0
  121. package/dist/cjs/ui/pages/stubs.js.map +1 -0
  122. package/dist/cjs/ui/partials.js +104 -0
  123. package/dist/cjs/ui/partials.js.map +1 -0
  124. package/dist/dts/Program.d.ts +2 -0
  125. package/dist/dts/Program.d.ts.map +1 -0
  126. package/dist/dts/api/AdminApi.d.ts +490 -0
  127. package/dist/dts/api/AdminApi.d.ts.map +1 -0
  128. package/dist/dts/api/ApiErrors.d.ts +26 -0
  129. package/dist/dts/api/ApiErrors.d.ts.map +1 -0
  130. package/dist/dts/api/ApiSchemas.d.ts +36 -0
  131. package/dist/dts/api/ApiSchemas.d.ts.map +1 -0
  132. package/dist/dts/api/Conversions.d.ts +7 -0
  133. package/dist/dts/api/Conversions.d.ts.map +1 -0
  134. package/dist/dts/api/ImpostersGroup.d.ts +448 -0
  135. package/dist/dts/api/ImpostersGroup.d.ts.map +1 -0
  136. package/dist/dts/api/ImpostersHandlers.d.ts +9 -0
  137. package/dist/dts/api/ImpostersHandlers.d.ts.map +1 -0
  138. package/dist/dts/api/SystemGroup.d.ts +46 -0
  139. package/dist/dts/api/SystemGroup.d.ts.map +1 -0
  140. package/dist/dts/api/SystemHandlers.d.ts +4 -0
  141. package/dist/dts/api/SystemHandlers.d.ts.map +1 -0
  142. package/dist/dts/cli/Commands.d.ts +4 -0
  143. package/dist/dts/cli/Commands.d.ts.map +1 -0
  144. package/dist/dts/cli/ConfigLoader.d.ts +13 -0
  145. package/dist/dts/cli/ConfigLoader.d.ts.map +1 -0
  146. package/dist/dts/client/HandlerHttpClient.d.ts +5 -0
  147. package/dist/dts/client/HandlerHttpClient.d.ts.map +1 -0
  148. package/dist/dts/client/ImpostersClient.d.ts +1868 -0
  149. package/dist/dts/client/ImpostersClient.d.ts.map +1 -0
  150. package/dist/dts/client/index.d.ts +6 -0
  151. package/dist/dts/client/index.d.ts.map +1 -0
  152. package/dist/dts/client/testing.d.ts +35 -0
  153. package/dist/dts/client/testing.d.ts.map +1 -0
  154. package/dist/dts/domain/imposter.d.ts +123 -0
  155. package/dist/dts/domain/imposter.d.ts.map +1 -0
  156. package/dist/dts/domain/route.d.ts +128 -0
  157. package/dist/dts/domain/route.d.ts.map +1 -0
  158. package/dist/dts/index.d.ts +60 -0
  159. package/dist/dts/index.d.ts.map +1 -0
  160. package/dist/dts/layers/ApiLayer.d.ts +3 -0
  161. package/dist/dts/layers/ApiLayer.d.ts.map +1 -0
  162. package/dist/dts/layers/MainLayer.d.ts +3 -0
  163. package/dist/dts/layers/MainLayer.d.ts.map +1 -0
  164. package/dist/dts/matching/ExpressionEvaluator.d.ts +11 -0
  165. package/dist/dts/matching/ExpressionEvaluator.d.ts.map +1 -0
  166. package/dist/dts/matching/RequestMatcher.d.ts +13 -0
  167. package/dist/dts/matching/RequestMatcher.d.ts.map +1 -0
  168. package/dist/dts/matching/ResponseGenerator.d.ts +9 -0
  169. package/dist/dts/matching/ResponseGenerator.d.ts.map +1 -0
  170. package/dist/dts/matching/TemplateEngine.d.ts +4 -0
  171. package/dist/dts/matching/TemplateEngine.d.ts.map +1 -0
  172. package/dist/dts/repositories/ImposterRepository.d.ts +33 -0
  173. package/dist/dts/repositories/ImposterRepository.d.ts.map +1 -0
  174. package/dist/dts/schemas/ConfigFileSchema.d.ts +142 -0
  175. package/dist/dts/schemas/ConfigFileSchema.d.ts.map +1 -0
  176. package/dist/dts/schemas/ImposterSchema.d.ts +368 -0
  177. package/dist/dts/schemas/ImposterSchema.d.ts.map +1 -0
  178. package/dist/dts/schemas/RequestLogSchema.d.ts +36 -0
  179. package/dist/dts/schemas/RequestLogSchema.d.ts.map +1 -0
  180. package/dist/dts/schemas/StubSchema.d.ts +112 -0
  181. package/dist/dts/schemas/StubSchema.d.ts.map +1 -0
  182. package/dist/dts/schemas/common.d.ts +56 -0
  183. package/dist/dts/schemas/common.d.ts.map +1 -0
  184. package/dist/dts/server/AdminServer.d.ts +11 -0
  185. package/dist/dts/server/AdminServer.d.ts.map +1 -0
  186. package/dist/dts/server/BunServer.d.ts +17 -0
  187. package/dist/dts/server/BunServer.d.ts.map +1 -0
  188. package/dist/dts/server/FiberManager.d.ts +12 -0
  189. package/dist/dts/server/FiberManager.d.ts.map +1 -0
  190. package/dist/dts/server/ImposterServer.d.ts +29 -0
  191. package/dist/dts/server/ImposterServer.d.ts.map +1 -0
  192. package/dist/dts/services/AppConfig.d.ts +14 -0
  193. package/dist/dts/services/AppConfig.d.ts.map +1 -0
  194. package/dist/dts/services/MetricsService.d.ts +26 -0
  195. package/dist/dts/services/MetricsService.d.ts.map +1 -0
  196. package/dist/dts/services/PortAllocator.d.ts +29 -0
  197. package/dist/dts/services/PortAllocator.d.ts.map +1 -0
  198. package/dist/dts/services/ProxyService.d.ts +24 -0
  199. package/dist/dts/services/ProxyService.d.ts.map +1 -0
  200. package/dist/dts/services/RequestLogger.d.ts +23 -0
  201. package/dist/dts/services/RequestLogger.d.ts.map +1 -0
  202. package/dist/dts/services/Uuid.d.ts +9 -0
  203. package/dist/dts/services/Uuid.d.ts.map +1 -0
  204. package/dist/dts/services/UuidLive.d.ts +4 -0
  205. package/dist/dts/services/UuidLive.d.ts.map +1 -0
  206. package/dist/dts/ui/UiRouter.d.ts +15 -0
  207. package/dist/dts/ui/UiRouter.d.ts.map +1 -0
  208. package/dist/dts/ui/admin/AdminLayout.d.ts +7 -0
  209. package/dist/dts/ui/admin/AdminLayout.d.ts.map +1 -0
  210. package/dist/dts/ui/admin/AdminUiRouter.d.ts +6 -0
  211. package/dist/dts/ui/admin/AdminUiRouter.d.ts.map +1 -0
  212. package/dist/dts/ui/admin/pages/AdminDashboard.d.ts +7 -0
  213. package/dist/dts/ui/admin/pages/AdminDashboard.d.ts.map +1 -0
  214. package/dist/dts/ui/admin/partials.d.ts +14 -0
  215. package/dist/dts/ui/admin/partials.d.ts.map +1 -0
  216. package/dist/dts/ui/html.d.ts +12 -0
  217. package/dist/dts/ui/html.d.ts.map +1 -0
  218. package/dist/dts/ui/layout.d.ts +9 -0
  219. package/dist/dts/ui/layout.d.ts.map +1 -0
  220. package/dist/dts/ui/pages/dashboard.d.ts +10 -0
  221. package/dist/dts/ui/pages/dashboard.d.ts.map +1 -0
  222. package/dist/dts/ui/pages/request-detail.d.ts +11 -0
  223. package/dist/dts/ui/pages/request-detail.d.ts.map +1 -0
  224. package/dist/dts/ui/pages/requests.d.ts +15 -0
  225. package/dist/dts/ui/pages/requests.d.ts.map +1 -0
  226. package/dist/dts/ui/pages/stubs.d.ts +8 -0
  227. package/dist/dts/ui/pages/stubs.d.ts.map +1 -0
  228. package/dist/dts/ui/partials.d.ts +13 -0
  229. package/dist/dts/ui/partials.d.ts.map +1 -0
  230. package/dist/esm/Program.js +2 -0
  231. package/dist/esm/Program.js.map +1 -0
  232. package/dist/esm/api/AdminApi.js +5 -0
  233. package/dist/esm/api/AdminApi.js.map +1 -0
  234. package/dist/esm/api/ApiErrors.js +20 -0
  235. package/dist/esm/api/ApiErrors.js.map +1 -0
  236. package/dist/esm/api/ApiSchemas.js +29 -0
  237. package/dist/esm/api/ApiSchemas.js.map +1 -0
  238. package/dist/esm/api/Conversions.js +32 -0
  239. package/dist/esm/api/Conversions.js.map +1 -0
  240. package/dist/esm/api/ImpostersGroup.js +30 -0
  241. package/dist/esm/api/ImpostersGroup.js.map +1 -0
  242. package/dist/esm/api/ImpostersHandlers.js +354 -0
  243. package/dist/esm/api/ImpostersHandlers.js.map +1 -0
  244. package/dist/esm/api/SystemGroup.js +6 -0
  245. package/dist/esm/api/SystemGroup.js.map +1 -0
  246. package/dist/esm/api/SystemHandlers.js +67 -0
  247. package/dist/esm/api/SystemHandlers.js.map +1 -0
  248. package/dist/esm/cli/Commands.js +98 -0
  249. package/dist/esm/cli/Commands.js.map +1 -0
  250. package/dist/esm/cli/ConfigLoader.js +25 -0
  251. package/dist/esm/cli/ConfigLoader.js.map +1 -0
  252. package/dist/esm/client/HandlerHttpClient.js +42 -0
  253. package/dist/esm/client/HandlerHttpClient.js.map +1 -0
  254. package/dist/esm/client/ImpostersClient.js +10 -0
  255. package/dist/esm/client/ImpostersClient.js.map +1 -0
  256. package/dist/esm/client/index.js +4 -0
  257. package/dist/esm/client/index.js.map +1 -0
  258. package/dist/esm/client/testing.js +86 -0
  259. package/dist/esm/client/testing.js.map +1 -0
  260. package/dist/esm/domain/imposter.js +103 -0
  261. package/dist/esm/domain/imposter.js.map +1 -0
  262. package/dist/esm/domain/route.js +164 -0
  263. package/dist/esm/domain/route.js.map +1 -0
  264. package/dist/esm/index.js +60 -0
  265. package/dist/esm/index.js.map +1 -0
  266. package/dist/esm/layers/ApiLayer.js +11 -0
  267. package/dist/esm/layers/ApiLayer.js.map +1 -0
  268. package/dist/esm/layers/MainLayer.js +20 -0
  269. package/dist/esm/layers/MainLayer.js.map +1 -0
  270. package/dist/esm/matching/ExpressionEvaluator.js +94 -0
  271. package/dist/esm/matching/ExpressionEvaluator.js.map +1 -0
  272. package/dist/esm/matching/RequestMatcher.js +135 -0
  273. package/dist/esm/matching/RequestMatcher.js.map +1 -0
  274. package/dist/esm/matching/ResponseGenerator.js +71 -0
  275. package/dist/esm/matching/ResponseGenerator.js.map +1 -0
  276. package/dist/esm/matching/TemplateEngine.js +47 -0
  277. package/dist/esm/matching/TemplateEngine.js.map +1 -0
  278. package/dist/esm/package.json +4 -0
  279. package/dist/esm/repositories/ImposterRepository.js +110 -0
  280. package/dist/esm/repositories/ImposterRepository.js.map +1 -0
  281. package/dist/esm/schemas/ConfigFileSchema.js +37 -0
  282. package/dist/esm/schemas/ConfigFileSchema.js.map +1 -0
  283. package/dist/esm/schemas/ImposterSchema.js +195 -0
  284. package/dist/esm/schemas/ImposterSchema.js.map +1 -0
  285. package/dist/esm/schemas/RequestLogSchema.js +44 -0
  286. package/dist/esm/schemas/RequestLogSchema.js.map +1 -0
  287. package/dist/esm/schemas/StubSchema.js +77 -0
  288. package/dist/esm/schemas/StubSchema.js.map +1 -0
  289. package/dist/esm/schemas/common.js +59 -0
  290. package/dist/esm/schemas/common.js.map +1 -0
  291. package/dist/esm/server/AdminServer.js +27 -0
  292. package/dist/esm/server/AdminServer.js.map +1 -0
  293. package/dist/esm/server/BunServer.js +6 -0
  294. package/dist/esm/server/BunServer.js.map +1 -0
  295. package/dist/esm/server/FiberManager.js +14 -0
  296. package/dist/esm/server/FiberManager.js.map +1 -0
  297. package/dist/esm/server/ImposterServer.js +225 -0
  298. package/dist/esm/server/ImposterServer.js.map +1 -0
  299. package/dist/esm/services/AppConfig.js +11 -0
  300. package/dist/esm/services/AppConfig.js.map +1 -0
  301. package/dist/esm/services/MetricsService.js +105 -0
  302. package/dist/esm/services/MetricsService.js.map +1 -0
  303. package/dist/esm/services/PortAllocator.js +41 -0
  304. package/dist/esm/services/PortAllocator.js.map +1 -0
  305. package/dist/esm/services/ProxyService.js +101 -0
  306. package/dist/esm/services/ProxyService.js.map +1 -0
  307. package/dist/esm/services/RequestLogger.js +53 -0
  308. package/dist/esm/services/RequestLogger.js.map +1 -0
  309. package/dist/esm/services/Uuid.js +3 -0
  310. package/dist/esm/services/Uuid.js.map +1 -0
  311. package/dist/esm/services/UuidLive.js +9 -0
  312. package/dist/esm/services/UuidLive.js.map +1 -0
  313. package/dist/esm/ui/UiRouter.js +235 -0
  314. package/dist/esm/ui/UiRouter.js.map +1 -0
  315. package/dist/esm/ui/admin/AdminLayout.js +29 -0
  316. package/dist/esm/ui/admin/AdminLayout.js.map +1 -0
  317. package/dist/esm/ui/admin/AdminUiRouter.js +148 -0
  318. package/dist/esm/ui/admin/AdminUiRouter.js.map +1 -0
  319. package/dist/esm/ui/admin/pages/AdminDashboard.js +48 -0
  320. package/dist/esm/ui/admin/pages/AdminDashboard.js.map +1 -0
  321. package/dist/esm/ui/admin/partials.js +54 -0
  322. package/dist/esm/ui/admin/partials.js.map +1 -0
  323. package/dist/esm/ui/html.js +32 -0
  324. package/dist/esm/ui/html.js.map +1 -0
  325. package/dist/esm/ui/layout.js +32 -0
  326. package/dist/esm/ui/layout.js.map +1 -0
  327. package/dist/esm/ui/pages/dashboard.js +44 -0
  328. package/dist/esm/ui/pages/dashboard.js.map +1 -0
  329. package/dist/esm/ui/pages/request-detail.js +112 -0
  330. package/dist/esm/ui/pages/request-detail.js.map +1 -0
  331. package/dist/esm/ui/pages/requests.js +112 -0
  332. package/dist/esm/ui/pages/requests.js.map +1 -0
  333. package/dist/esm/ui/pages/stubs.js +39 -0
  334. package/dist/esm/ui/pages/stubs.js.map +1 -0
  335. package/dist/esm/ui/partials.js +91 -0
  336. package/dist/esm/ui/partials.js.map +1 -0
  337. package/domain/imposter/package.json +6 -0
  338. package/domain/route/package.json +6 -0
  339. package/layers/ApiLayer/package.json +6 -0
  340. package/layers/MainLayer/package.json +6 -0
  341. package/matching/ExpressionEvaluator/package.json +6 -0
  342. package/matching/RequestMatcher/package.json +6 -0
  343. package/matching/ResponseGenerator/package.json +6 -0
  344. package/matching/TemplateEngine/package.json +6 -0
  345. package/package.json +435 -0
  346. package/repositories/ImposterRepository/package.json +6 -0
  347. package/schemas/ConfigFileSchema/package.json +6 -0
  348. package/schemas/ImposterSchema/package.json +6 -0
  349. package/schemas/RequestLogSchema/package.json +6 -0
  350. package/schemas/StubSchema/package.json +6 -0
  351. package/schemas/common/package.json +6 -0
  352. package/server/AdminServer/package.json +6 -0
  353. package/server/BunServer/package.json +6 -0
  354. package/server/FiberManager/package.json +6 -0
  355. package/server/ImposterServer/package.json +6 -0
  356. package/services/AppConfig/package.json +6 -0
  357. package/services/MetricsService/package.json +6 -0
  358. package/services/PortAllocator/package.json +6 -0
  359. package/services/ProxyService/package.json +6 -0
  360. package/services/RequestLogger/package.json +6 -0
  361. package/services/Uuid/package.json +6 -0
  362. package/services/UuidLive/package.json +6 -0
  363. package/src/Program.ts +1 -0
  364. package/src/api/AdminApi.ts +7 -0
  365. package/src/api/ApiErrors.ts +20 -0
  366. package/src/api/ApiSchemas.ts +36 -0
  367. package/src/api/Conversions.ts +34 -0
  368. package/src/api/ImpostersGroup.ts +103 -0
  369. package/src/api/ImpostersHandlers.ts +387 -0
  370. package/src/api/SystemGroup.ts +12 -0
  371. package/src/api/SystemHandlers.ts +76 -0
  372. package/src/cli/Commands.ts +119 -0
  373. package/src/cli/ConfigLoader.ts +41 -0
  374. package/src/client/HandlerHttpClient.ts +50 -0
  375. package/src/client/ImpostersClient.ts +21 -0
  376. package/src/client/index.ts +9 -0
  377. package/src/client/testing.ts +105 -0
  378. package/src/domain/imposter.ts +186 -0
  379. package/src/domain/route.ts +255 -0
  380. package/src/index.ts +153 -0
  381. package/src/layers/ApiLayer.ts +21 -0
  382. package/src/layers/MainLayer.ts +43 -0
  383. package/src/matching/ExpressionEvaluator.ts +102 -0
  384. package/src/matching/RequestMatcher.ts +162 -0
  385. package/src/matching/ResponseGenerator.ts +86 -0
  386. package/src/matching/TemplateEngine.ts +54 -0
  387. package/src/repositories/ImposterRepository.ts +145 -0
  388. package/src/schemas/ConfigFileSchema.ts +32 -0
  389. package/src/schemas/ImposterSchema.ts +232 -0
  390. package/src/schemas/RequestLogSchema.ts +38 -0
  391. package/src/schemas/StubSchema.ts +90 -0
  392. package/src/schemas/common.ts +95 -0
  393. package/src/server/AdminServer.ts +22 -0
  394. package/src/server/BunServer.ts +19 -0
  395. package/src/server/FiberManager.ts +25 -0
  396. package/src/server/ImposterServer.ts +244 -0
  397. package/src/services/AppConfig.ts +22 -0
  398. package/src/services/MetricsService.ts +157 -0
  399. package/src/services/PortAllocator.ts +68 -0
  400. package/src/services/ProxyService.ts +139 -0
  401. package/src/services/RequestLogger.ts +87 -0
  402. package/src/services/Uuid.ts +9 -0
  403. package/src/services/UuidLive.ts +9 -0
  404. package/src/types/bun.d.ts +6 -0
  405. package/src/ui/UiRouter.ts +278 -0
  406. package/src/ui/admin/AdminLayout.ts +36 -0
  407. package/src/ui/admin/AdminUiRouter.ts +170 -0
  408. package/src/ui/admin/pages/AdminDashboard.ts +54 -0
  409. package/src/ui/admin/partials.ts +83 -0
  410. package/src/ui/html.ts +37 -0
  411. package/src/ui/layout.ts +44 -0
  412. package/src/ui/pages/dashboard.ts +64 -0
  413. package/src/ui/pages/request-detail.ts +142 -0
  414. package/src/ui/pages/requests.ts +141 -0
  415. package/src/ui/pages/stubs.ts +52 -0
  416. package/src/ui/partials.ts +133 -0
  417. package/ui/UiRouter/package.json +6 -0
  418. package/ui/admin/AdminLayout/package.json +6 -0
  419. package/ui/admin/AdminUiRouter/package.json +6 -0
  420. package/ui/admin/pages/AdminDashboard/package.json +6 -0
  421. package/ui/admin/partials/package.json +6 -0
  422. package/ui/html/package.json +6 -0
  423. package/ui/layout/package.json +6 -0
  424. package/ui/pages/dashboard/package.json +6 -0
  425. package/ui/pages/requests/package.json +6 -0
  426. package/ui/pages/stubs/package.json +6 -0
@@ -0,0 +1,87 @@
1
+ import type { Queue, Scope } from "effect"
2
+ import { Context, Effect, HashMap, Layer, PubSub, Ref } from "effect"
3
+ import type { RequestLogEntry } from "../schemas/RequestLogSchema"
4
+
5
+ const MAX_ENTRIES = 100
6
+
7
+ export interface RequestLoggerShape {
8
+ readonly log: (entry: RequestLogEntry) => Effect.Effect<void>
9
+ readonly getEntries: (
10
+ imposterId: string,
11
+ opts?: { limit?: number; method?: string; path?: string; status?: number }
12
+ ) => Effect.Effect<ReadonlyArray<RequestLogEntry>>
13
+ readonly getCount: (imposterId: string) => Effect.Effect<number>
14
+ readonly clear: (imposterId: string) => Effect.Effect<void>
15
+ readonly subscribe: Effect.Effect<Queue.Dequeue<RequestLogEntry>, never, Scope.Scope>
16
+ readonly getEntryById: (imposterId: string, entryId: string) => Effect.Effect<RequestLogEntry | null>
17
+ readonly removeImposter: (imposterId: string) => Effect.Effect<void>
18
+ }
19
+
20
+ export class RequestLogger extends Context.Tag("RequestLogger")<RequestLogger, RequestLoggerShape>() {}
21
+
22
+ export const RequestLoggerLive = Layer.scoped(
23
+ RequestLogger,
24
+ Effect.gen(function*() {
25
+ const storeRef = yield* Ref.make(HashMap.empty<string, Array<RequestLogEntry>>())
26
+ const pubsub = yield* PubSub.sliding<RequestLogEntry>(256)
27
+
28
+ const log = (entry: RequestLogEntry): Effect.Effect<void> =>
29
+ Effect.gen(function*() {
30
+ yield* Ref.update(storeRef, (store) => {
31
+ const existing = HashMap.get(store, entry.imposterId)
32
+ const entries = existing._tag === "Some" ? existing.value : []
33
+ const updated = [...entries, entry].slice(-MAX_ENTRIES)
34
+ return HashMap.set(store, entry.imposterId, updated)
35
+ })
36
+ yield* PubSub.publish(pubsub, entry)
37
+ })
38
+
39
+ const getEntries = (
40
+ imposterId: string,
41
+ opts?: { limit?: number; method?: string; path?: string; status?: number }
42
+ ): Effect.Effect<ReadonlyArray<RequestLogEntry>> =>
43
+ Ref.get(storeRef).pipe(
44
+ Effect.map((store) => {
45
+ const existing = HashMap.get(store, imposterId)
46
+ let entries = existing._tag === "Some" ? existing.value : []
47
+ if (opts?.method !== undefined) {
48
+ entries = entries.filter((e) => e.request.method.toUpperCase() === opts.method!.toUpperCase())
49
+ }
50
+ if (opts?.path !== undefined) {
51
+ entries = entries.filter((e) => e.request.path === opts.path)
52
+ }
53
+ if (opts?.status !== undefined) {
54
+ entries = entries.filter((e) => e.response.status === opts.status)
55
+ }
56
+ const limit = opts?.limit ?? 50
57
+ return entries.slice(-limit)
58
+ })
59
+ )
60
+
61
+ const getCount = (imposterId: string): Effect.Effect<number> =>
62
+ Ref.get(storeRef).pipe(
63
+ Effect.map((store) => {
64
+ const existing = HashMap.get(store, imposterId)
65
+ return existing._tag === "Some" ? existing.value.length : 0
66
+ })
67
+ )
68
+
69
+ const clear = (imposterId: string): Effect.Effect<void> =>
70
+ Ref.update(storeRef, (store) => HashMap.set(store, imposterId, []))
71
+
72
+ const subscribe: Effect.Effect<Queue.Dequeue<RequestLogEntry>, never, Scope.Scope> = PubSub.subscribe(pubsub)
73
+
74
+ const getEntryById = (imposterId: string, entryId: string): Effect.Effect<RequestLogEntry | null> =>
75
+ Ref.get(storeRef).pipe(
76
+ Effect.map((store) => {
77
+ const existing = HashMap.get(store, imposterId)
78
+ if (existing._tag === "None") return null
79
+ return existing.value.find((e) => e.id === entryId) ?? null
80
+ })
81
+ )
82
+
83
+ const removeImposter = (imposterId: string): Effect.Effect<void> => Ref.update(storeRef, HashMap.remove(imposterId))
84
+
85
+ return { log, getEntries, getCount, clear, subscribe, getEntryById, removeImposter } satisfies RequestLoggerShape
86
+ })
87
+ )
@@ -0,0 +1,9 @@
1
+ import { Context, type Effect } from "effect"
2
+
3
+ export class Uuid extends Context.Tag("UuidService")<
4
+ Uuid,
5
+ {
6
+ readonly generate: Effect.Effect<string>
7
+ readonly generateShort: Effect.Effect<string>
8
+ }
9
+ >() {}
@@ -0,0 +1,9 @@
1
+ import * as Effect from "effect/Effect"
2
+ import * as Layer from "effect/Layer"
3
+ import { v4 } from "uuid"
4
+ import { Uuid } from "./Uuid"
5
+
6
+ export const UuidLive = Layer.succeed(Uuid, {
7
+ generate: Effect.sync(() => v4()),
8
+ generateShort: Effect.sync(() => v4().substring(0, 8))
9
+ })
@@ -0,0 +1,6 @@
1
+ declare const Bun: {
2
+ serve(options: {
3
+ readonly port: number
4
+ readonly fetch: (request: Request) => Promise<Response>
5
+ }): { readonly port: number; stop(closeActive: boolean): void }
6
+ }
@@ -0,0 +1,278 @@
1
+ import { Effect, Ref } from "effect"
2
+ import type { ImposterConfig } from "../domain/imposter"
3
+ import type { ImposterRepositoryShape } from "../repositories/ImposterRepository"
4
+ import { NonEmptyString } from "../schemas/common"
5
+ import type { Stub } from "../schemas/StubSchema"
6
+ import type { RequestLoggerShape } from "../services/RequestLogger"
7
+ import { dashboardPage } from "./pages/dashboard"
8
+ import { requestDetailPage } from "./pages/request-detail"
9
+ import { requestsPage, testResultPartial } from "./pages/requests"
10
+ import { stubsPage } from "./pages/stubs"
11
+ import { errorPartial, requestTablePartial, stubListPartial } from "./partials"
12
+
13
+ export interface UiDeps {
14
+ readonly id: string
15
+ readonly config: ImposterConfig
16
+ readonly stubsRef: Ref.Ref<ReadonlyArray<Stub>>
17
+ readonly repo: ImposterRepositoryShape
18
+ readonly requestLogger: RequestLoggerShape
19
+ readonly runPromise: <A>(effect: Effect.Effect<A>) => Promise<A>
20
+ }
21
+
22
+ const htmlResponse = (body: string, status = 200): Response =>
23
+ new Response(body, {
24
+ status,
25
+ headers: { "content-type": "text/html; charset=utf-8" }
26
+ })
27
+
28
+ const parseStubIdFromPath = (path: string): string | null => {
29
+ const match = path.match(/^\/stubs\/(.+)$/)
30
+ return match ? match[1]! : null
31
+ }
32
+
33
+ export const makeUiRouter = (deps: UiDeps) => async (request: Request): Promise<Response | null> => {
34
+ const url = new URL(request.url)
35
+ if (!url.pathname.startsWith("/_admin")) return null
36
+
37
+ const path = url.pathname.slice("/_admin".length) || "/"
38
+ const method = request.method.toUpperCase()
39
+
40
+ // GET / — dashboard
41
+ if (method === "GET" && (path === "/" || path === "")) {
42
+ return deps.runPromise(
43
+ Effect.gen(function*() {
44
+ const stubs = yield* Ref.get(deps.stubsRef)
45
+ const requestCount = yield* deps.requestLogger.getCount(deps.id)
46
+ const recentRequests = yield* deps.requestLogger.getEntries(deps.id, { limit: 10 })
47
+ return htmlResponse(
48
+ dashboardPage({
49
+ config: deps.config,
50
+ stubCount: stubs.length,
51
+ requestCount,
52
+ recentRequests: recentRequests.slice().reverse()
53
+ }).value
54
+ )
55
+ })
56
+ )
57
+ }
58
+
59
+ // GET /stubs — stubs page
60
+ if (method === "GET" && path === "/stubs") {
61
+ return deps.runPromise(
62
+ Effect.gen(function*() {
63
+ const stubs = yield* Ref.get(deps.stubsRef)
64
+ return htmlResponse(stubsPage({ config: deps.config, stubs }).value)
65
+ })
66
+ )
67
+ }
68
+
69
+ // POST /stubs — add stub
70
+ if (method === "POST" && path === "/stubs") {
71
+ try {
72
+ const formData = await request.formData()
73
+ const predicatesRaw = formData.get("predicates") as string | null
74
+ const responsesRaw = formData.get("responses") as string | null
75
+ const responseMode = (formData.get("responseMode") as string | null) || "sequential"
76
+
77
+ if (!responsesRaw) {
78
+ return htmlResponse(errorPartial("Responses field is required.").value, 200)
79
+ }
80
+
81
+ let predicates: unknown
82
+ let responses: unknown
83
+ try {
84
+ predicates = JSON.parse(predicatesRaw || "[]")
85
+ responses = JSON.parse(responsesRaw)
86
+ } catch {
87
+ return htmlResponse(errorPartial("Invalid JSON in predicates or responses.").value, 200)
88
+ }
89
+
90
+ if (!Array.isArray(responses) || responses.length === 0) {
91
+ return htmlResponse(errorPartial("Responses must be a non-empty array.").value, 200)
92
+ }
93
+
94
+ const stubId = NonEmptyString.make(crypto.randomUUID().slice(0, 8))
95
+ const stub: Stub = {
96
+ id: stubId,
97
+ predicates: predicates as Stub["predicates"],
98
+ responses: responses as unknown as Stub["responses"],
99
+ responseMode: responseMode as Stub["responseMode"]
100
+ }
101
+
102
+ return await deps.runPromise(
103
+ Effect.gen(function*() {
104
+ yield* deps.repo.addStub(deps.id, stub).pipe(
105
+ Effect.catchAll(() => Effect.void)
106
+ )
107
+ const updated = yield* deps.repo.getStubs(deps.id).pipe(
108
+ Effect.catchAll(() => Effect.succeed([] as ReadonlyArray<Stub>))
109
+ )
110
+ yield* Ref.set(deps.stubsRef, updated)
111
+ return htmlResponse(stubListPartial(updated).value)
112
+ })
113
+ )
114
+ } catch {
115
+ return htmlResponse(errorPartial("Failed to parse form data.").value, 200)
116
+ }
117
+ }
118
+
119
+ // DELETE /stubs/:id — delete stub
120
+ const deleteStubId = method === "DELETE" ? parseStubIdFromPath(path) : null
121
+ if (deleteStubId !== null) {
122
+ return deps.runPromise(
123
+ Effect.gen(function*() {
124
+ yield* deps.repo.removeStub(deps.id, deleteStubId).pipe(
125
+ Effect.catchAll(() => Effect.void)
126
+ )
127
+ const updated = yield* deps.repo.getStubs(deps.id).pipe(
128
+ Effect.catchAll(() => Effect.succeed([] as ReadonlyArray<Stub>))
129
+ )
130
+ yield* Ref.set(deps.stubsRef, updated)
131
+ return htmlResponse(stubListPartial(updated).value)
132
+ })
133
+ )
134
+ }
135
+
136
+ // PUT /stubs/:id — update stub
137
+ const putStubId = method === "PUT" ? parseStubIdFromPath(path) : null
138
+ if (putStubId !== null) {
139
+ try {
140
+ const formData = await request.formData()
141
+ const predicatesRaw = formData.get("predicates") as string | null
142
+ const responsesRaw = formData.get("responses") as string | null
143
+ const responseMode = formData.get("responseMode") as string | null
144
+
145
+ return await deps.runPromise(
146
+ Effect.gen(function*() {
147
+ yield* deps.repo.updateStub(deps.id, putStubId, (existing) => ({
148
+ ...existing,
149
+ ...(predicatesRaw ? { predicates: JSON.parse(predicatesRaw) } : {}),
150
+ ...(responsesRaw ? { responses: JSON.parse(responsesRaw) } : {}),
151
+ ...(responseMode ? { responseMode: responseMode as Stub["responseMode"] } : {})
152
+ })).pipe(Effect.catchAll(() => Effect.void))
153
+ const updated = yield* deps.repo.getStubs(deps.id).pipe(
154
+ Effect.catchAll(() => Effect.succeed([] as ReadonlyArray<Stub>))
155
+ )
156
+ yield* Ref.set(deps.stubsRef, updated)
157
+ return htmlResponse(stubListPartial(updated).value)
158
+ })
159
+ )
160
+ } catch {
161
+ return htmlResponse(errorPartial("Failed to parse form data.").value, 200)
162
+ }
163
+ }
164
+
165
+ // GET /requests — full page
166
+ if (method === "GET" && path === "/requests") {
167
+ return deps.runPromise(
168
+ Effect.gen(function*() {
169
+ const entries = yield* deps.requestLogger.getEntries(deps.id, { limit: 100 })
170
+ return htmlResponse(requestsPage({ config: deps.config, entries }).value)
171
+ })
172
+ )
173
+ }
174
+
175
+ // GET /requests/list — HTMX partial (filtered table body)
176
+ if (method === "GET" && path === "/requests/list") {
177
+ const params = url.searchParams
178
+ return deps.runPromise(
179
+ Effect.gen(function*() {
180
+ const opts: { limit?: number; method?: string; path?: string; status?: number } = { limit: 100 }
181
+ const methodFilter = params.get("method")
182
+ if (methodFilter) opts.method = methodFilter
183
+ const pathFilter = params.get("path")
184
+ if (pathFilter) opts.path = pathFilter
185
+ const statusFilter = params.get("status")
186
+ if (statusFilter) opts.status = Number(statusFilter)
187
+ const entries = yield* deps.requestLogger.getEntries(deps.id, opts)
188
+ return htmlResponse(requestTablePartial(entries.slice().reverse()).value)
189
+ })
190
+ )
191
+ }
192
+
193
+ // POST /requests/test — send test request to own port
194
+ if (method === "POST" && path === "/requests/test") {
195
+ try {
196
+ const formData = await request.formData()
197
+ const testMethod = (formData.get("method") as string) || "GET"
198
+ const testPath = (formData.get("path") as string) || "/"
199
+ const testBody = formData.get("body") as string | null
200
+ const testContentType = (formData.get("contentType") as string) || "application/json"
201
+ const testHeadersRaw = (formData.get("headers") as string) || ""
202
+
203
+ const headers: Record<string, string> = {}
204
+ if (testContentType && testBody) {
205
+ headers["content-type"] = testContentType
206
+ }
207
+ for (const line of testHeadersRaw.split("\n")) {
208
+ const colonIdx = line.indexOf(":")
209
+ if (colonIdx > 0) {
210
+ headers[line.slice(0, colonIdx).trim()] = line.slice(colonIdx + 1).trim()
211
+ }
212
+ }
213
+
214
+ const startTime = Date.now()
215
+ const testResp = await fetch(`http://localhost:${deps.config.port}${testPath}`, {
216
+ method: testMethod,
217
+ headers,
218
+ ...(testBody && testMethod !== "GET" && testMethod !== "HEAD" ? { body: testBody } : {})
219
+ })
220
+ const duration = Date.now() - startTime
221
+
222
+ const respBody = await testResp.text()
223
+ const respHeaders: Record<string, string> = {}
224
+ testResp.headers.forEach((val, key) => {
225
+ respHeaders[key] = val
226
+ })
227
+
228
+ return htmlResponse(
229
+ testResultPartial({ status: testResp.status, headers: respHeaders, body: respBody, duration }).value
230
+ )
231
+ } catch (err) {
232
+ return htmlResponse(
233
+ `<div class="bg-red-50 border border-red-200 text-red-700 rounded p-3">Request failed: ${String(err)}</div>`
234
+ )
235
+ }
236
+ }
237
+
238
+ // DELETE /requests — clear log
239
+ if (method === "DELETE" && path === "/requests") {
240
+ return deps.runPromise(
241
+ Effect.gen(function*() {
242
+ yield* deps.requestLogger.clear(deps.id)
243
+ return htmlResponse(requestTablePartial([]).value)
244
+ })
245
+ )
246
+ }
247
+
248
+ // GET /requests/:id — detail page
249
+ if (method === "GET" && path.startsWith("/requests/")) {
250
+ const entryId = path.slice("/requests/".length)
251
+ if (entryId && !entryId.includes("/")) {
252
+ return deps.runPromise(
253
+ Effect.gen(function*() {
254
+ const entry = yield* deps.requestLogger.getEntryById(deps.id, entryId)
255
+ if (entry === null) {
256
+ return htmlResponse("<h1>Request not found</h1>", 404)
257
+ }
258
+ // Try to find matched stub
259
+ let matchedStub: Stub | null = null
260
+ if (entry.response.matchedStubId) {
261
+ try {
262
+ const stubs = yield* deps.repo.getStubs(deps.id).pipe(
263
+ Effect.catchAll(() => Effect.succeed([] as ReadonlyArray<Stub>))
264
+ )
265
+ matchedStub = stubs.find((s) => s.id === entry.response.matchedStubId) ?? null
266
+ } catch {
267
+ // ignore
268
+ }
269
+ }
270
+ return htmlResponse(requestDetailPage({ config: deps.config, entry, matchedStub }).value)
271
+ })
272
+ )
273
+ }
274
+ }
275
+
276
+ // Fallback: 404 within /_admin
277
+ return htmlResponse("<h1>Not Found</h1>", 404)
278
+ }
@@ -0,0 +1,36 @@
1
+ import { html, raw } from "../html"
2
+ import type { SafeHtml } from "../html"
3
+
4
+ export interface AdminLayoutOpts {
5
+ readonly title: string
6
+ readonly activeTab?: "dashboard"
7
+ }
8
+
9
+ export const adminLayout = (opts: AdminLayoutOpts, content: SafeHtml): SafeHtml =>
10
+ html`<!DOCTYPE html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="utf-8">
14
+ <meta name="viewport" content="width=device-width, initial-scale=1">
15
+ <title>${opts.title}</title>
16
+ <script src="https://cdn.tailwindcss.com"></script>
17
+ <script src="https://unpkg.com/htmx.org@2.0.4"></script>
18
+ </head>
19
+ <body class="bg-gray-50 min-h-screen">
20
+ <nav class="bg-gray-800 text-white shadow-md">
21
+ <div class="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between">
22
+ <div class="flex items-center gap-3">
23
+ <span class="text-lg font-bold">Imposters</span>
24
+ <span class="text-gray-400 text-sm">Admin</span>
25
+ </div>
26
+ <div class="flex gap-3 items-center">
27
+ <a href="/_ui" class="px-3 py-1.5 bg-gray-700 text-white rounded text-sm font-medium hover:bg-gray-600">Dashboard</a>
28
+ <a href="/docs" class="text-gray-400 hover:text-white text-sm">API Docs</a>
29
+ </div>
30
+ </div>
31
+ </nav>
32
+ <main class="max-w-6xl mx-auto px-4 py-6">
33
+ ${raw(content.value)}
34
+ </main>
35
+ </body>
36
+ </html>`
@@ -0,0 +1,170 @@
1
+ import { adminDashboardPage } from "./pages/AdminDashboard"
2
+ import type { AdminImposterData } from "./partials"
3
+ import { adminErrorPartial, imposterListPartial, imposterRowPartial } from "./partials"
4
+
5
+ export interface AdminUiDeps {
6
+ readonly apiHandler: (request: Request) => Promise<Response>
7
+ readonly adminPort: number
8
+ }
9
+
10
+ const htmlResponse = (body: string, status = 200): Response =>
11
+ new Response(body, {
12
+ status,
13
+ headers: { "content-type": "text/html; charset=utf-8" }
14
+ })
15
+
16
+ const toAdminData = (imp: any): AdminImposterData => ({
17
+ id: imp.id,
18
+ name: imp.name ?? `imposter-${imp.id}`,
19
+ port: imp.port,
20
+ status: imp.status,
21
+ protocol: imp.protocol ?? "HTTP",
22
+ stubCount: imp.stubs?.length ?? imp.endpointCount ?? imp.stubCount ?? 0
23
+ })
24
+
25
+ const fetchImposters = async (apiHandler: (r: Request) => Promise<Response>): Promise<Array<AdminImposterData>> => {
26
+ const resp = await apiHandler(new Request("http://localhost/imposters?limit=50", { method: "GET" }))
27
+ if (!resp.ok) return []
28
+ const data = await resp.json()
29
+ const items = Array.isArray(data) ? data : (data.imposters ?? data.items ?? [])
30
+ return items.map(toAdminData)
31
+ }
32
+
33
+ const fetchImposter = async (
34
+ apiHandler: (r: Request) => Promise<Response>,
35
+ id: string
36
+ ): Promise<AdminImposterData | null> => {
37
+ const resp = await apiHandler(new Request(`http://localhost/imposters/${id}`, { method: "GET" }))
38
+ if (!resp.ok) return null
39
+ const imp = await resp.json()
40
+ return toAdminData(imp)
41
+ }
42
+
43
+ export const makeAdminUiRouter = (deps: AdminUiDeps) => async (request: Request): Promise<Response | null> => {
44
+ const url = new URL(request.url)
45
+ if (!url.pathname.startsWith("/_ui")) return null
46
+
47
+ const path = url.pathname.slice("/_ui".length) || "/"
48
+ const method = request.method.toUpperCase()
49
+
50
+ // GET / — dashboard
51
+ if (method === "GET" && (path === "/" || path === "")) {
52
+ const imposters = await fetchImposters(deps.apiHandler)
53
+ return htmlResponse(adminDashboardPage({ imposters }).value)
54
+ }
55
+
56
+ // GET /imposters — HTMX partial (imposter list)
57
+ if (method === "GET" && path === "/imposters") {
58
+ const imposters = await fetchImposters(deps.apiHandler)
59
+ return htmlResponse(imposterListPartial(imposters).value)
60
+ }
61
+
62
+ // POST /imposters — create imposter from form
63
+ if (method === "POST" && path === "/imposters") {
64
+ try {
65
+ const formData = await request.formData()
66
+ const name = formData.get("name") as string | null
67
+ const portStr = formData.get("port") as string | null
68
+ const autoStart = formData.get("autoStart") === "on"
69
+
70
+ const payload: Record<string, unknown> = {
71
+ protocol: "HTTP"
72
+ }
73
+ if (name && name.trim()) payload.name = name.trim()
74
+ if (portStr && portStr.trim()) payload.port = Number(portStr.trim())
75
+
76
+ const createResp = await deps.apiHandler(
77
+ new Request("http://localhost/imposters", {
78
+ method: "POST",
79
+ headers: { "content-type": "application/json" },
80
+ body: JSON.stringify(payload)
81
+ })
82
+ )
83
+
84
+ if (!createResp.ok) {
85
+ const errBody = await createResp.text()
86
+ const imposters = await fetchImposters(deps.apiHandler)
87
+ return htmlResponse(
88
+ adminErrorPartial(`Failed to create imposter: ${errBody}`).value +
89
+ imposterListPartial(imposters).value
90
+ )
91
+ }
92
+
93
+ const created = await createResp.json()
94
+
95
+ // Auto-start if requested
96
+ if (autoStart) {
97
+ await deps.apiHandler(
98
+ new Request(`http://localhost/imposters/${created.id}`, {
99
+ method: "PATCH",
100
+ headers: { "content-type": "application/json" },
101
+ body: JSON.stringify({ status: "running" })
102
+ })
103
+ )
104
+ }
105
+
106
+ const imposters = await fetchImposters(deps.apiHandler)
107
+ return htmlResponse(imposterListPartial(imposters).value)
108
+ } catch (err) {
109
+ const imposters = await fetchImposters(deps.apiHandler)
110
+ return htmlResponse(
111
+ adminErrorPartial(`Error: ${String(err)}`).value +
112
+ imposterListPartial(imposters).value
113
+ )
114
+ }
115
+ }
116
+
117
+ // POST /imposters/:id/start
118
+ const startMatch = path.match(/^\/imposters\/([^/]+)\/start$/)
119
+ if (method === "POST" && startMatch) {
120
+ const id = startMatch[1]!
121
+ await deps.apiHandler(
122
+ new Request(`http://localhost/imposters/${id}`, {
123
+ method: "PATCH",
124
+ headers: { "content-type": "application/json" },
125
+ body: JSON.stringify({ status: "running" })
126
+ })
127
+ )
128
+ // Small delay for the server to start
129
+ await new Promise((r) => setTimeout(r, 100))
130
+ const imp = await fetchImposter(deps.apiHandler, id)
131
+ if (!imp) {
132
+ return htmlResponse(adminErrorPartial("Imposter not found").value, 404)
133
+ }
134
+ return htmlResponse(imposterRowPartial(imp).value)
135
+ }
136
+
137
+ // POST /imposters/:id/stop
138
+ const stopMatch = path.match(/^\/imposters\/([^/]+)\/stop$/)
139
+ if (method === "POST" && stopMatch) {
140
+ const id = stopMatch[1]!
141
+ await deps.apiHandler(
142
+ new Request(`http://localhost/imposters/${id}`, {
143
+ method: "PATCH",
144
+ headers: { "content-type": "application/json" },
145
+ body: JSON.stringify({ status: "stopped" })
146
+ })
147
+ )
148
+ await new Promise((r) => setTimeout(r, 100))
149
+ const imp = await fetchImposter(deps.apiHandler, id)
150
+ if (!imp) {
151
+ return htmlResponse(adminErrorPartial("Imposter not found").value, 404)
152
+ }
153
+ return htmlResponse(imposterRowPartial(imp).value)
154
+ }
155
+
156
+ // DELETE /imposters/:id
157
+ const deleteMatch = path.match(/^\/imposters\/([^/]+)$/)
158
+ if (method === "DELETE" && deleteMatch) {
159
+ const id = deleteMatch[1]!
160
+ await deps.apiHandler(
161
+ new Request(`http://localhost/imposters/${id}?force=true`, {
162
+ method: "DELETE"
163
+ })
164
+ )
165
+ const imposters = await fetchImposters(deps.apiHandler)
166
+ return htmlResponse(imposterListPartial(imposters).value)
167
+ }
168
+
169
+ return null
170
+ }
@@ -0,0 +1,54 @@
1
+ import { html, raw } from "../../html"
2
+ import type { SafeHtml } from "../../html"
3
+ import { adminLayout } from "../AdminLayout"
4
+ import type { AdminImposterData } from "../partials"
5
+ import { createFormPartial, imposterListPartial } from "../partials"
6
+
7
+ export interface AdminDashboardData {
8
+ readonly imposters: ReadonlyArray<AdminImposterData>
9
+ }
10
+
11
+ const summaryBar = (data: AdminDashboardData): SafeHtml => {
12
+ const total = data.imposters.length
13
+ const running = data.imposters.filter((i) => i.status === "running").length
14
+ const stopped = total - running
15
+ return html`<div class="grid grid-cols-3 gap-4 mb-6">
16
+ <div class="bg-white rounded-lg shadow p-4">
17
+ <div class="text-sm text-gray-500">Total Imposters</div>
18
+ <div class="text-2xl font-bold text-gray-800">${String(total)}</div>
19
+ </div>
20
+ <div class="bg-white rounded-lg shadow p-4">
21
+ <div class="text-sm text-gray-500">Running</div>
22
+ <div class="text-2xl font-bold text-green-600">${String(running)}</div>
23
+ </div>
24
+ <div class="bg-white rounded-lg shadow p-4">
25
+ <div class="text-sm text-gray-500">Stopped</div>
26
+ <div class="text-2xl font-bold text-gray-500">${String(stopped)}</div>
27
+ </div>
28
+ </div>`
29
+ }
30
+
31
+ export const adminDashboardPage = (data: AdminDashboardData): SafeHtml => {
32
+ const content = html`
33
+ ${summaryBar(data)}
34
+ ${createFormPartial()}
35
+ <div class="bg-white rounded-lg shadow overflow-x-auto">
36
+ <table class="w-full text-left">
37
+ <thead>
38
+ <tr class="text-xs text-gray-500 uppercase border-b">
39
+ <th class="py-3 px-4">Name</th>
40
+ <th class="py-3 px-4">Port</th>
41
+ <th class="py-3 px-4">Status</th>
42
+ <th class="py-3 px-4">Protocol</th>
43
+ <th class="py-3 px-4">Stubs</th>
44
+ <th class="py-3 px-4">Actions</th>
45
+ </tr>
46
+ </thead>
47
+ <tbody id="imposter-list">
48
+ ${raw(imposterListPartial(data.imposters).value)}
49
+ </tbody>
50
+ </table>
51
+ </div>`
52
+
53
+ return adminLayout({ title: "Imposters — Admin Dashboard" }, content)
54
+ }