@shortlink-org/portolan 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 (826) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +387 -0
  3. package/catalog/enum_test.go +46 -0
  4. package/catalog/model.go +932 -0
  5. package/catalog/roundtrip_test.go +185 -0
  6. package/catalog/via_test.go +38 -0
  7. package/cli/portolan.mjs +368 -0
  8. package/go.mod +14 -0
  9. package/go.sum +14 -0
  10. package/index.html +13 -0
  11. package/internal/commands/cargo.go +154 -0
  12. package/internal/commands/commands.go +143 -0
  13. package/internal/commands/commands_test.go +323 -0
  14. package/internal/commands/gradle.go +77 -0
  15. package/internal/commands/justfile.go +82 -0
  16. package/internal/commands/makefile.go +106 -0
  17. package/internal/commands/maven.go +93 -0
  18. package/internal/commands/packagejson.go +159 -0
  19. package/internal/commands/pyproject.go +169 -0
  20. package/internal/commands/taskfile.go +101 -0
  21. package/internal/commands/testdata/estate/.cargo/config.toml +7 -0
  22. package/internal/commands/testdata/estate/Makefile +33 -0
  23. package/internal/commands/testdata/estate/Taskfile.yml +21 -0
  24. package/internal/commands/testdata/estate/build.gradle.kts +21 -0
  25. package/internal/commands/testdata/estate/justfile +20 -0
  26. package/internal/commands/testdata/estate/package.json +13 -0
  27. package/internal/commands/testdata/estate/pom.xml +29 -0
  28. package/internal/commands/testdata/estate/pyproject.toml +27 -0
  29. package/internal/commands/testdata/estate/xtask/src/main.rs +27 -0
  30. package/internal/commands/testdata/golden/commands.json +305 -0
  31. package/internal/gohttp/analyze.go +2500 -0
  32. package/internal/gohttp/endpoints.go +1067 -0
  33. package/internal/gohttp/roots.go +320 -0
  34. package/internal/gohttp/typed.go +143 -0
  35. package/internal/goscan/constants.go +85 -0
  36. package/internal/goscan/goscan_test.go +227 -0
  37. package/internal/goscan/names.go +52 -0
  38. package/internal/goscan/parse_test.go +11 -0
  39. package/internal/goscan/source.go +36 -0
  40. package/internal/goscan/tree.go +155 -0
  41. package/internal/goscan/types.go +99 -0
  42. package/internal/wsdl/ids.go +127 -0
  43. package/internal/wsdl/ids_test.go +21 -0
  44. package/internal/wsdl/model.go +62 -0
  45. package/internal/wsdl/parse.go +920 -0
  46. package/internal/wsdl/parse_test.go +133 -0
  47. package/package.json +117 -0
  48. package/plugin/describe.go +107 -0
  49. package/plugin/describe_test.go +114 -0
  50. package/plugin/protocol.go +117 -0
  51. package/plugin/schematest/schematest.go +126 -0
  52. package/plugins/README.md +1160 -0
  53. package/plugins/extract-adr/describe.go +19 -0
  54. package/plugins/extract-adr/describe_test.go +11 -0
  55. package/plugins/extract-adr/extract.go +153 -0
  56. package/plugins/extract-adr/extract_test.go +350 -0
  57. package/plugins/extract-adr/history.go +99 -0
  58. package/plugins/extract-adr/main.go +65 -0
  59. package/plugins/extract-adr/options.schema.json +26 -0
  60. package/plugins/extract-adr/parse.go +642 -0
  61. package/plugins/extract-adr/parse_test.go +405 -0
  62. package/plugins/extract-asyncapi/describe.go +19 -0
  63. package/plugins/extract-asyncapi/describe_test.go +11 -0
  64. package/plugins/extract-asyncapi/extract.go +315 -0
  65. package/plugins/extract-asyncapi/extract_test.go +197 -0
  66. package/plugins/extract-asyncapi/main.go +48 -0
  67. package/plugins/extract-asyncapi/options.schema.json +26 -0
  68. package/plugins/extract-asyncapi/spec.go +151 -0
  69. package/plugins/extract-celery/README.md +169 -0
  70. package/plugins/extract-celery/extract.py +346 -0
  71. package/plugins/extract-celery/extract_test.py +192 -0
  72. package/plugins/extract-celery/main.py +66 -0
  73. package/plugins/extract-celery/options.py +31 -0
  74. package/plugins/extract-celery/options.schema.json +31 -0
  75. package/plugins/extract-celery/producers.py +156 -0
  76. package/plugins/extract-celery/routes_test.py +106 -0
  77. package/plugins/extract-commands/describe.go +19 -0
  78. package/plugins/extract-commands/describe_test.go +11 -0
  79. package/plugins/extract-commands/extract.go +79 -0
  80. package/plugins/extract-commands/extract_test.go +92 -0
  81. package/plugins/extract-commands/main.go +55 -0
  82. package/plugins/extract-commands/options.schema.json +20 -0
  83. package/plugins/extract-csr/avro.go +258 -0
  84. package/plugins/extract-csr/describe.go +19 -0
  85. package/plugins/extract-csr/describe_test.go +11 -0
  86. package/plugins/extract-csr/extract.go +338 -0
  87. package/plugins/extract-csr/extract_test.go +374 -0
  88. package/plugins/extract-csr/jsonschema.go +343 -0
  89. package/plugins/extract-csr/lock.go +25 -0
  90. package/plugins/extract-csr/main.go +84 -0
  91. package/plugins/extract-csr/options.schema.json +43 -0
  92. package/plugins/extract-csr/subject.go +102 -0
  93. package/plugins/extract-django/README.md +386 -0
  94. package/plugins/extract-django/apps.py +100 -0
  95. package/plugins/extract-django/auth.py +302 -0
  96. package/plugins/extract-django/auth_test.py +188 -0
  97. package/plugins/extract-django/clients.py +121 -0
  98. package/plugins/extract-django/contracts.py +458 -0
  99. package/plugins/extract-django/database.py +119 -0
  100. package/plugins/extract-django/domain.py +318 -0
  101. package/plugins/extract-django/events.py +86 -0
  102. package/plugins/extract-django/extract.py +601 -0
  103. package/plugins/extract-django/extract_test.py +397 -0
  104. package/plugins/extract-django/flows.py +1278 -0
  105. package/plugins/extract-django/flows_test.py +338 -0
  106. package/plugins/extract-django/ids.py +42 -0
  107. package/plugins/extract-django/ids_test.py +30 -0
  108. package/plugins/extract-django/lifecycle.py +222 -0
  109. package/plugins/extract-django/lifecycle_test.py +76 -0
  110. package/plugins/extract-django/main.py +66 -0
  111. package/plugins/extract-django/openapi.py +229 -0
  112. package/plugins/extract-django/openapi_test.py +60 -0
  113. package/plugins/extract-django/operations.py +96 -0
  114. package/plugins/extract-django/options.py +61 -0
  115. package/plugins/extract-django/options.schema.json +126 -0
  116. package/plugins/extract-django/routing.py +214 -0
  117. package/plugins/extract-django/routing_test.py +198 -0
  118. package/plugins/extract-django/serializers.py +558 -0
  119. package/plugins/extract-django/store.py +434 -0
  120. package/plugins/extract-django/transport.py +242 -0
  121. package/plugins/extract-flows/describe.go +19 -0
  122. package/plugins/extract-flows/describe_test.go +11 -0
  123. package/plugins/extract-flows/extract.go +91 -0
  124. package/plugins/extract-flows/main.go +45 -0
  125. package/plugins/extract-flows/options.schema.json +16 -0
  126. package/plugins/extract-flows/parse.go +593 -0
  127. package/plugins/extract-flows/parse_test.go +204 -0
  128. package/plugins/extract-glossary/describe.go +19 -0
  129. package/plugins/extract-glossary/describe_test.go +11 -0
  130. package/plugins/extract-glossary/extract.go +115 -0
  131. package/plugins/extract-glossary/extract_test.go +220 -0
  132. package/plugins/extract-glossary/main.go +59 -0
  133. package/plugins/extract-glossary/options.schema.json +21 -0
  134. package/plugins/extract-glossary/parse.go +214 -0
  135. package/plugins/extract-glossary/parse_test.go +203 -0
  136. package/plugins/extract-go/aggregate.go +214 -0
  137. package/plugins/extract-go/client.go +409 -0
  138. package/plugins/extract-go/client_test.go +266 -0
  139. package/plugins/extract-go/describe.go +19 -0
  140. package/plugins/extract-go/describe_test.go +11 -0
  141. package/plugins/extract-go/enum.go +195 -0
  142. package/plugins/extract-go/enum_test.go +82 -0
  143. package/plugins/extract-go/event.go +99 -0
  144. package/plugins/extract-go/extract.go +191 -0
  145. package/plugins/extract-go/extract_test.go +261 -0
  146. package/plugins/extract-go/flow.go +1441 -0
  147. package/plugins/extract-go/flow_test.go +609 -0
  148. package/plugins/extract-go/httpclient.go +174 -0
  149. package/plugins/extract-go/httpclient_test.go +296 -0
  150. package/plugins/extract-go/ids.go +92 -0
  151. package/plugins/extract-go/layout.go +225 -0
  152. package/plugins/extract-go/layout_test.go +125 -0
  153. package/plugins/extract-go/lifecycle.go +324 -0
  154. package/plugins/extract-go/lifecycle_test.go +108 -0
  155. package/plugins/extract-go/main.go +79 -0
  156. package/plugins/extract-go/operation.go +157 -0
  157. package/plugins/extract-go/options.schema.json +66 -0
  158. package/plugins/extract-go/source.go +316 -0
  159. package/plugins/extract-go/transport.go +321 -0
  160. package/plugins/extract-go/transport_test.go +145 -0
  161. package/plugins/extract-go/wiring.go +434 -0
  162. package/plugins/extract-go-nats/describe.go +19 -0
  163. package/plugins/extract-go-nats/describe_test.go +11 -0
  164. package/plugins/extract-go-nats/extract.go +177 -0
  165. package/plugins/extract-go-nats/extract_test.go +389 -0
  166. package/plugins/extract-go-nats/index.go +379 -0
  167. package/plugins/extract-go-nats/main.go +42 -0
  168. package/plugins/extract-go-nats/options.schema.json +21 -0
  169. package/plugins/extract-go-nats/resolve.go +161 -0
  170. package/plugins/extract-go-nats/sites.go +224 -0
  171. package/plugins/extract-graphql/describe.go +19 -0
  172. package/plugins/extract-graphql/describe_test.go +11 -0
  173. package/plugins/extract-graphql/extract.go +433 -0
  174. package/plugins/extract-graphql/extract_test.go +256 -0
  175. package/plugins/extract-graphql/ids.go +49 -0
  176. package/plugins/extract-graphql/lex.go +237 -0
  177. package/plugins/extract-graphql/main.go +51 -0
  178. package/plugins/extract-graphql/options.schema.json +31 -0
  179. package/plugins/extract-graphql/parse.go +621 -0
  180. package/plugins/extract-graphql/parse_test.go +122 -0
  181. package/plugins/extract-http-clients/describe.go +19 -0
  182. package/plugins/extract-http-clients/describe_test.go +11 -0
  183. package/plugins/extract-http-clients/extract.go +705 -0
  184. package/plugins/extract-http-clients/extract_test.go +1263 -0
  185. package/plugins/extract-http-clients/main.go +42 -0
  186. package/plugins/extract-http-clients/options.schema.json +33 -0
  187. package/plugins/extract-java/README.md +190 -0
  188. package/plugins/extract-java/build/org/portolan/extract/Catalog.class +0 -0
  189. package/plugins/extract-java/build/org/portolan/extract/Clients$1.class +0 -0
  190. package/plugins/extract-java/build/org/portolan/extract/Clients$2.class +0 -0
  191. package/plugins/extract-java/build/org/portolan/extract/Clients$Call.class +0 -0
  192. package/plugins/extract-java/build/org/portolan/extract/Clients$Client.class +0 -0
  193. package/plugins/extract-java/build/org/portolan/extract/Clients$Route.class +0 -0
  194. package/plugins/extract-java/build/org/portolan/extract/Clients.class +0 -0
  195. package/plugins/extract-java/build/org/portolan/extract/Domain$Aggregate.class +0 -0
  196. package/plugins/extract-java/build/org/portolan/extract/Domain.class +0 -0
  197. package/plugins/extract-java/build/org/portolan/extract/Events$Found.class +0 -0
  198. package/plugins/extract-java/build/org/portolan/extract/Events.class +0 -0
  199. package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
  200. package/plugins/extract-java/build/org/portolan/extract/Flows$1.class +0 -0
  201. package/plugins/extract-java/build/org/portolan/extract/Flows$Binding.class +0 -0
  202. package/plugins/extract-java/build/org/portolan/extract/Flows$Draft.class +0 -0
  203. package/plugins/extract-java/build/org/portolan/extract/Flows$Frame.class +0 -0
  204. package/plugins/extract-java/build/org/portolan/extract/Flows$Options.class +0 -0
  205. package/plugins/extract-java/build/org/portolan/extract/Flows.class +0 -0
  206. package/plugins/extract-java/build/org/portolan/extract/Ids.class +0 -0
  207. package/plugins/extract-java/build/org/portolan/extract/Json$Reader.class +0 -0
  208. package/plugins/extract-java/build/org/portolan/extract/Json.class +0 -0
  209. package/plugins/extract-java/build/org/portolan/extract/Lifecycle$1.class +0 -0
  210. package/plugins/extract-java/build/org/portolan/extract/Lifecycle$Move.class +0 -0
  211. package/plugins/extract-java/build/org/portolan/extract/Lifecycle.class +0 -0
  212. package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
  213. package/plugins/extract-java/build/org/portolan/extract/OpenApi$Operation.class +0 -0
  214. package/plugins/extract-java/build/org/portolan/extract/OpenApi$Spec.class +0 -0
  215. package/plugins/extract-java/build/org/portolan/extract/OpenApi.class +0 -0
  216. package/plugins/extract-java/build/org/portolan/extract/Operations$1.class +0 -0
  217. package/plugins/extract-java/build/org/portolan/extract/Operations$UseCase.class +0 -0
  218. package/plugins/extract-java/build/org/portolan/extract/Operations.class +0 -0
  219. package/plugins/extract-java/build/org/portolan/extract/Proto$Rpc.class +0 -0
  220. package/plugins/extract-java/build/org/portolan/extract/Proto$Service.class +0 -0
  221. package/plugins/extract-java/build/org/portolan/extract/Proto.class +0 -0
  222. package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
  223. package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
  224. package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
  225. package/plugins/extract-java/build/org/portolan/extract/Protocol.class +0 -0
  226. package/plugins/extract-java/build/org/portolan/extract/Source$Project.class +0 -0
  227. package/plugins/extract-java/build/org/portolan/extract/Source$Unit.class +0 -0
  228. package/plugins/extract-java/build/org/portolan/extract/Source.class +0 -0
  229. package/plugins/extract-java/build/org/portolan/extract/Transport$Endpoint.class +0 -0
  230. package/plugins/extract-java/build/org/portolan/extract/Transport.class +0 -0
  231. package/plugins/extract-java/build/org/portolan/extract/Yaml$Line.class +0 -0
  232. package/plugins/extract-java/build/org/portolan/extract/Yaml$Reader.class +0 -0
  233. package/plugins/extract-java/build/org/portolan/extract/Yaml.class +0 -0
  234. package/plugins/extract-java/options.schema.json +70 -0
  235. package/plugins/extract-openapi/describe.go +19 -0
  236. package/plugins/extract-openapi/describe_test.go +11 -0
  237. package/plugins/extract-openapi/discover.go +243 -0
  238. package/plugins/extract-openapi/extract.go +526 -0
  239. package/plugins/extract-openapi/extract_test.go +545 -0
  240. package/plugins/extract-openapi/main.go +75 -0
  241. package/plugins/extract-openapi/options.schema.json +62 -0
  242. package/plugins/extract-openapi/spec.go +350 -0
  243. package/plugins/extract-project/README.md +22 -0
  244. package/plugins/extract-project/describe.go +19 -0
  245. package/plugins/extract-project/describe_test.go +11 -0
  246. package/plugins/extract-project/extract.go +221 -0
  247. package/plugins/extract-project/extract_test.go +109 -0
  248. package/plugins/extract-project/main.go +41 -0
  249. package/plugins/extract-project/options.schema.json +48 -0
  250. package/plugins/extract-proto/ast.go +125 -0
  251. package/plugins/extract-proto/consumes.go +77 -0
  252. package/plugins/extract-proto/describe.go +19 -0
  253. package/plugins/extract-proto/extract.go +293 -0
  254. package/plugins/extract-proto/extract_test.go +459 -0
  255. package/plugins/extract-proto/ids.go +89 -0
  256. package/plugins/extract-proto/ids_test.go +57 -0
  257. package/plugins/extract-proto/lex.go +285 -0
  258. package/plugins/extract-proto/main.go +100 -0
  259. package/plugins/extract-proto/module.go +120 -0
  260. package/plugins/extract-proto/options.schema.json +51 -0
  261. package/plugins/extract-proto/parse.go +720 -0
  262. package/plugins/extract-proto/parse_test.go +307 -0
  263. package/plugins/extract-proto/provides.go +236 -0
  264. package/plugins/extract-proto/resolve.go +222 -0
  265. package/plugins/extract-proto/resolve_test.go +119 -0
  266. package/plugins/extract-python-kafka/README.md +36 -0
  267. package/plugins/extract-python-kafka/extract.py +211 -0
  268. package/plugins/extract-python-kafka/extract_test.py +68 -0
  269. package/plugins/extract-python-kafka/main.py +59 -0
  270. package/plugins/extract-python-kafka/options.py +29 -0
  271. package/plugins/extract-python-kafka/options.schema.json +31 -0
  272. package/plugins/extract-redis/README.md +11 -0
  273. package/plugins/extract-redis/describe.go +19 -0
  274. package/plugins/extract-redis/describe_test.go +11 -0
  275. package/plugins/extract-redis/extract.go +183 -0
  276. package/plugins/extract-redis/extract_test.go +168 -0
  277. package/plugins/extract-redis/keyspaces.go +469 -0
  278. package/plugins/extract-redis/main.go +44 -0
  279. package/plugins/extract-redis/options.schema.json +30 -0
  280. package/plugins/extract-river/describe.go +19 -0
  281. package/plugins/extract-river/describe_test.go +11 -0
  282. package/plugins/extract-river/extract.go +507 -0
  283. package/plugins/extract-river/extract_test.go +132 -0
  284. package/plugins/extract-river/main.go +42 -0
  285. package/plugins/extract-river/options.schema.json +21 -0
  286. package/plugins/extract-rust/Cargo.lock +133 -0
  287. package/plugins/extract-rust/Cargo.toml +23 -0
  288. package/plugins/extract-rust/README.md +204 -0
  289. package/plugins/extract-rust/options.schema.json +65 -0
  290. package/plugins/extract-rust/rustfmt.toml +1 -0
  291. package/plugins/extract-sql/ddl.go +893 -0
  292. package/plugins/extract-sql/ddl_test.go +401 -0
  293. package/plugins/extract-sql/describe.go +19 -0
  294. package/plugins/extract-sql/describe_test.go +11 -0
  295. package/plugins/extract-sql/layout.go +221 -0
  296. package/plugins/extract-sql/layout_test.go +55 -0
  297. package/plugins/extract-sql/lineage.go +107 -0
  298. package/plugins/extract-sql/main.go +141 -0
  299. package/plugins/extract-sql/maps.go +564 -0
  300. package/plugins/extract-sql/maps_java.go +117 -0
  301. package/plugins/extract-sql/maps_rust.go +333 -0
  302. package/plugins/extract-sql/maps_rust_test.go +70 -0
  303. package/plugins/extract-sql/maps_test.go +204 -0
  304. package/plugins/extract-sql/maps_ts.go +398 -0
  305. package/plugins/extract-sql/maps_ts_test.go +136 -0
  306. package/plugins/extract-sql/options.schema.json +53 -0
  307. package/plugins/extract-sql/projection.go +70 -0
  308. package/plugins/extract-sql/projection_test.go +50 -0
  309. package/plugins/extract-sql/store.go +420 -0
  310. package/plugins/extract-sql/store_test.go +233 -0
  311. package/plugins/extract-sql/view.go +295 -0
  312. package/plugins/extract-ts/README.md +306 -0
  313. package/plugins/extract-ts/ast.test.ts +178 -0
  314. package/plugins/extract-ts/ast.ts +648 -0
  315. package/plugins/extract-ts/clients.ts +185 -0
  316. package/plugins/extract-ts/domain.ts +164 -0
  317. package/plugins/extract-ts/extract.test.ts +208 -0
  318. package/plugins/extract-ts/extract.ts +200 -0
  319. package/plugins/extract-ts/flows.ts +783 -0
  320. package/plugins/extract-ts/graphql.test.ts +73 -0
  321. package/plugins/extract-ts/graphql.ts +181 -0
  322. package/plugins/extract-ts/ids.test.ts +21 -0
  323. package/plugins/extract-ts/ids.ts +52 -0
  324. package/plugins/extract-ts/lifecycle.ts +138 -0
  325. package/plugins/extract-ts/main.ts +48 -0
  326. package/plugins/extract-ts/openapi.test.ts +64 -0
  327. package/plugins/extract-ts/openapi.ts +99 -0
  328. package/plugins/extract-ts/operations.ts +93 -0
  329. package/plugins/extract-ts/options.schema.json +96 -0
  330. package/plugins/extract-ts/source.test.ts +174 -0
  331. package/plugins/extract-ts/source.ts +465 -0
  332. package/plugins/extract-ts/transport.ts +147 -0
  333. package/plugins/extract-ts/tsconfig.json +13 -0
  334. package/plugins/extract-ts/wiring.ts +190 -0
  335. package/plugins/extract-watermill/describe.go +19 -0
  336. package/plugins/extract-watermill/describe_test.go +11 -0
  337. package/plugins/extract-watermill/extract.go +1228 -0
  338. package/plugins/extract-watermill/extract_test.go +234 -0
  339. package/plugins/extract-watermill/main.go +41 -0
  340. package/plugins/extract-watermill/options.schema.json +21 -0
  341. package/plugins/extract-wsdl/describe.go +19 -0
  342. package/plugins/extract-wsdl/describe_test.go +11 -0
  343. package/plugins/extract-wsdl/extract.go +148 -0
  344. package/plugins/extract-wsdl/extract_test.go +45 -0
  345. package/plugins/extract-wsdl/main.go +53 -0
  346. package/plugins/extract-wsdl/options.schema.json +59 -0
  347. package/plugins/fetch-bsr/auth.go +114 -0
  348. package/plugins/fetch-bsr/bsr.go +240 -0
  349. package/plugins/fetch-bsr/cache.go +66 -0
  350. package/plugins/fetch-bsr/describe.go +19 -0
  351. package/plugins/fetch-bsr/fetch.go +192 -0
  352. package/plugins/fetch-bsr/fetch_test.go +465 -0
  353. package/plugins/fetch-bsr/lock.go +71 -0
  354. package/plugins/fetch-bsr/main.go +117 -0
  355. package/plugins/fetch-bsr/options.schema.json +42 -0
  356. package/plugins/fetch-csr/auth.go +95 -0
  357. package/plugins/fetch-csr/cache.go +69 -0
  358. package/plugins/fetch-csr/describe.go +19 -0
  359. package/plugins/fetch-csr/describe_test.go +11 -0
  360. package/plugins/fetch-csr/fetch.go +254 -0
  361. package/plugins/fetch-csr/fetch_test.go +494 -0
  362. package/plugins/fetch-csr/lock.go +95 -0
  363. package/plugins/fetch-csr/main.go +122 -0
  364. package/plugins/fetch-csr/options.schema.json +42 -0
  365. package/plugins/fetch-csr/registry.go +209 -0
  366. package/plugins/fetch-git/cache.go +65 -0
  367. package/plugins/fetch-git/describe.go +19 -0
  368. package/plugins/fetch-git/describe_test.go +11 -0
  369. package/plugins/fetch-git/fetch.go +181 -0
  370. package/plugins/fetch-git/fetch_test.go +332 -0
  371. package/plugins/fetch-git/git.go +169 -0
  372. package/plugins/fetch-git/lock.go +72 -0
  373. package/plugins/fetch-git/main.go +127 -0
  374. package/plugins/fetch-git/offline.go +39 -0
  375. package/plugins/fetch-git/options.schema.json +42 -0
  376. package/plugins/fetch-git/pin.go +90 -0
  377. package/plugins/fetch-git/pin_test.go +74 -0
  378. package/plugins/gen-backstage/describe.go +17 -0
  379. package/plugins/gen-backstage/main.go +22 -0
  380. package/plugins/gen-backstage/options.schema.json +30 -0
  381. package/plugins/gen-backstage/plugin.go +473 -0
  382. package/plugins/gen-backstage/plugin_test.go +145 -0
  383. package/plugins/gen-backstage.wasm +0 -0
  384. package/plugins/gen-markdown/adr.go +162 -0
  385. package/plugins/gen-markdown/aggregate.go +362 -0
  386. package/plugins/gen-markdown/canonical.go +93 -0
  387. package/plugins/gen-markdown/context.go +90 -0
  388. package/plugins/gen-markdown/coverage_test.go +89 -0
  389. package/plugins/gen-markdown/describe.go +19 -0
  390. package/plugins/gen-markdown/describe_test.go +11 -0
  391. package/plugins/gen-markdown/external.go +71 -0
  392. package/plugins/gen-markdown/flow.go +277 -0
  393. package/plugins/gen-markdown/glossary.go +76 -0
  394. package/plugins/gen-markdown/glossary_test.go +148 -0
  395. package/plugins/gen-markdown/llms.go +302 -0
  396. package/plugins/gen-markdown/main.go +22 -0
  397. package/plugins/gen-markdown/markdown.go +254 -0
  398. package/plugins/gen-markdown/markdown_test.go +100 -0
  399. package/plugins/gen-markdown/module.go +107 -0
  400. package/plugins/gen-markdown/options.schema.json +16 -0
  401. package/plugins/gen-markdown/plugin.go +39 -0
  402. package/plugins/gen-markdown/quality_test.go +176 -0
  403. package/plugins/gen-markdown/redis_test.go +38 -0
  404. package/plugins/gen-markdown/render.go +344 -0
  405. package/plugins/gen-markdown/render_test.go +186 -0
  406. package/plugins/gen-markdown/service.go +456 -0
  407. package/plugins/gen-markdown/source.go +177 -0
  408. package/plugins/gen-markdown/store.go +201 -0
  409. package/plugins/gen-markdown.wasm +0 -0
  410. package/plugins/gen-mermaid/describe.go +17 -0
  411. package/plugins/gen-mermaid/main.go +22 -0
  412. package/plugins/gen-mermaid/options.schema.json +10 -0
  413. package/plugins/gen-mermaid/plugin.go +104 -0
  414. package/plugins/gen-mermaid/plugin_test.go +33 -0
  415. package/plugins/gen-mermaid.wasm +0 -0
  416. package/plugins/openapi/ids.go +261 -0
  417. package/plugins/openapi/ids_test.go +98 -0
  418. package/plugins/pyplugin/catalog.py +209 -0
  419. package/plugins/pyplugin/celery_conf.py +436 -0
  420. package/plugins/pyplugin/celery_tasks.py +76 -0
  421. package/plugins/pyplugin/kafka.py +570 -0
  422. package/plugins/pyplugin/names.py +38 -0
  423. package/plugins/pyplugin/protocol.py +79 -0
  424. package/plugins/pyplugin/source.py +285 -0
  425. package/plugins/verify-codeowners/describe.go +19 -0
  426. package/plugins/verify-codeowners/describe_test.go +11 -0
  427. package/plugins/verify-codeowners/main.go +75 -0
  428. package/plugins/verify-codeowners/match.go +85 -0
  429. package/plugins/verify-codeowners/match_test.go +47 -0
  430. package/plugins/verify-codeowners/options.schema.json +15 -0
  431. package/plugins/verify-codeowners/owners.go +164 -0
  432. package/plugins/verify-codeowners/owners_test.go +225 -0
  433. package/plugins/verify-codeowners/parse.go +90 -0
  434. package/plugins/verify-codeowners/parse_test.go +62 -0
  435. package/plugins/verify-otel/describe.go +19 -0
  436. package/plugins/verify-otel/describe_test.go +11 -0
  437. package/plugins/verify-otel/main.go +53 -0
  438. package/plugins/verify-otel/match.go +336 -0
  439. package/plugins/verify-otel/options.schema.json +30 -0
  440. package/plugins/verify-otel/otlp.go +200 -0
  441. package/plugins/verify-otel/verify.go +734 -0
  442. package/plugins/verify-otel/verify_test.go +460 -0
  443. package/portolan.json +1073 -0
  444. package/public/favicon.svg +12 -0
  445. package/schema/portolan.schema.json +2558 -0
  446. package/scripts/README.md +53 -0
  447. package/scripts/build-report.mjs +60 -0
  448. package/scripts/build-report.test.mjs +44 -0
  449. package/scripts/builtin-plugins.mjs +49 -0
  450. package/scripts/catalog-sources.mjs +67 -0
  451. package/scripts/diff.mjs +315 -0
  452. package/scripts/diff.test.mjs +151 -0
  453. package/scripts/forge-comment.mjs +144 -0
  454. package/scripts/forge-comment.test.mjs +151 -0
  455. package/scripts/forge-release.mjs +138 -0
  456. package/scripts/forge-release.test.mjs +106 -0
  457. package/scripts/forge.mjs +61 -0
  458. package/scripts/gen-likec4.mjs +854 -0
  459. package/scripts/gen-likec4.test.mjs +258 -0
  460. package/scripts/gen.mjs +603 -0
  461. package/scripts/generated-docs.test.mjs +66 -0
  462. package/scripts/local-api.mjs +1192 -0
  463. package/scripts/local-api.test.mjs +387 -0
  464. package/scripts/manifest.mjs +214 -0
  465. package/scripts/manifest.test.mjs +148 -0
  466. package/scripts/output-path.mjs +79 -0
  467. package/scripts/output-path.test.mjs +45 -0
  468. package/scripts/package-smoke.mjs +37 -0
  469. package/scripts/plugin-host.mjs +419 -0
  470. package/scripts/plugin-host.test.mjs +109 -0
  471. package/scripts/plugin-wasm-worker.mjs +34 -0
  472. package/scripts/run-builtin.mjs +81 -0
  473. package/scripts/schema.mjs +357 -0
  474. package/scripts/site-docs.mjs +295 -0
  475. package/scripts/site-docs.test.mjs +181 -0
  476. package/scripts/vendor-lock.mjs +58 -0
  477. package/scripts/vendor-lock.test.mjs +69 -0
  478. package/src/README.md +38 -0
  479. package/src/app/App.tsx +448 -0
  480. package/src/app/BranchPicker.test.ts +46 -0
  481. package/src/app/BranchPicker.tsx +153 -0
  482. package/src/app/Breadcrumbs.test.ts +38 -0
  483. package/src/app/Breadcrumbs.tsx +223 -0
  484. package/src/app/BuildStamp.tsx +146 -0
  485. package/src/app/CommandPalette.tsx +292 -0
  486. package/src/app/HashScroll.tsx +45 -0
  487. package/src/app/Sidebar.tsx +1742 -0
  488. package/src/app/TopBar.tsx +393 -0
  489. package/src/app/density.tsx +64 -0
  490. package/src/app/forge-access.test.ts +27 -0
  491. package/src/app/forge-access.tsx +80 -0
  492. package/src/app/panels.tsx +141 -0
  493. package/src/app/pins.tsx +242 -0
  494. package/src/app/query-client.ts +24 -0
  495. package/src/app/responsive.ts +55 -0
  496. package/src/app/shortcuts.test.ts +39 -0
  497. package/src/app/shortcuts.tsx +282 -0
  498. package/src/app/theme.tsx +51 -0
  499. package/src/app/title.test.ts +14 -0
  500. package/src/app/title.ts +30 -0
  501. package/src/app/toast.tsx +59 -0
  502. package/src/app/tree-keys.test.ts +80 -0
  503. package/src/app/tree-keys.ts +87 -0
  504. package/src/app/ui-store.ts +52 -0
  505. package/src/catalog-error.test.ts +105 -0
  506. package/src/catalog-profile.test.ts +59 -0
  507. package/src/catalog-profile.ts +101 -0
  508. package/src/catalog-stores.test.ts +431 -0
  509. package/src/catalog.test.ts +1215 -0
  510. package/src/catalog.ts +3125 -0
  511. package/src/chat/Cards.tsx +408 -0
  512. package/src/chat/ChatPanel.tsx +79 -0
  513. package/src/chat/ChatSettings.tsx +219 -0
  514. package/src/chat/Composer.tsx +99 -0
  515. package/src/chat/Conversation.tsx +281 -0
  516. package/src/chat/Header.tsx +46 -0
  517. package/src/chat/Message.tsx +49 -0
  518. package/src/chat/Notice.tsx +195 -0
  519. package/src/chat/Starter.tsx +67 -0
  520. package/src/chat/Steps.tsx +45 -0
  521. package/src/chat/Waiting.tsx +87 -0
  522. package/src/chat/flags.test.ts +59 -0
  523. package/src/chat/flags.ts +155 -0
  524. package/src/chat/linkify.test.ts +30 -0
  525. package/src/chat/linkify.ts +45 -0
  526. package/src/chat/prefs.ts +56 -0
  527. package/src/chat/prompt.test.ts +43 -0
  528. package/src/chat/prompt.ts +157 -0
  529. package/src/chat/store.ts +35 -0
  530. package/src/chat/tools.ts +58 -0
  531. package/src/chat/transport.ts +70 -0
  532. package/src/components/AdrRow.tsx +29 -0
  533. package/src/components/AnchorLink.tsx +67 -0
  534. package/src/components/ApiReference.tsx +134 -0
  535. package/src/components/AsyncApiReference.tsx +120 -0
  536. package/src/components/CatalogStamp.tsx +155 -0
  537. package/src/components/ChannelRows.tsx +132 -0
  538. package/src/components/CommandRows.tsx +139 -0
  539. package/src/components/DiagramSkeleton.tsx +71 -0
  540. package/src/components/EditorLink.tsx +85 -0
  541. package/src/components/FieldTree.tsx +305 -0
  542. package/src/components/FlowTrigger.tsx +34 -0
  543. package/src/components/Ident.tsx +89 -0
  544. package/src/components/Integrations.tsx +243 -0
  545. package/src/components/LifecycleDiagram.tsx +110 -0
  546. package/src/components/MachineDocs.tsx +42 -0
  547. package/src/components/Markdown.tsx +173 -0
  548. package/src/components/MarkdownTable.tsx +206 -0
  549. package/src/components/Mermaid.tsx +211 -0
  550. package/src/components/MethodRows.tsx +309 -0
  551. package/src/components/Overlay.tsx +125 -0
  552. package/src/components/PageHeader.tsx +189 -0
  553. package/src/components/RowActions.tsx +97 -0
  554. package/src/components/SchemaDocument.tsx +89 -0
  555. package/src/components/Select.tsx +126 -0
  556. package/src/components/ShapeRows.tsx +118 -0
  557. package/src/components/SourceDoc.tsx +82 -0
  558. package/src/components/SourcePreview.tsx +441 -0
  559. package/src/components/TabRow.tsx +182 -0
  560. package/src/components/TechIcon.tsx +34 -0
  561. package/src/components/Toc.tsx +132 -0
  562. package/src/components/WhatLinksHere.tsx +273 -0
  563. package/src/components/WsdlReference.tsx +84 -0
  564. package/src/components/ddd-icons.tsx +107 -0
  565. package/src/components/kind.tsx +157 -0
  566. package/src/components/logo.tsx +61 -0
  567. package/src/components/primitives.tsx +264 -0
  568. package/src/data.test.ts +18 -0
  569. package/src/data.ts +202 -0
  570. package/src/enrich.test.ts +1150 -0
  571. package/src/enrich.ts +672 -0
  572. package/src/er/ErCanvas.tsx +533 -0
  573. package/src/er/RedisSchema.tsx +74 -0
  574. package/src/er/RelationCard.tsx +267 -0
  575. package/src/er/StoreHeader.tsx +155 -0
  576. package/src/er/TableNode.tsx +84 -0
  577. package/src/er/ViewNode.tsx +83 -0
  578. package/src/er/layout.test.ts +115 -0
  579. package/src/er/layout.ts +77 -0
  580. package/src/er/lineage.test.ts +140 -0
  581. package/src/er/lineage.ts +107 -0
  582. package/src/er/markers.tsx +79 -0
  583. package/src/er/spec.test.ts +344 -0
  584. package/src/er/spec.ts +426 -0
  585. package/src/flow/ChainList.tsx +162 -0
  586. package/src/flow/FlowTable.tsx +123 -0
  587. package/src/flow/FlowToolbar.tsx +372 -0
  588. package/src/flow/StepDetail.tsx +380 -0
  589. package/src/flow/StepRail.tsx +428 -0
  590. package/src/flow/answers.test.ts +50 -0
  591. package/src/flow/answers.ts +58 -0
  592. package/src/flow/chain.test.ts +383 -0
  593. package/src/flow/chain.ts +353 -0
  594. package/src/flow/chapters.test.ts +185 -0
  595. package/src/flow/chapters.ts +225 -0
  596. package/src/flow/continues.test.ts +102 -0
  597. package/src/flow/continues.ts +65 -0
  598. package/src/flow/cross-context.test.ts +97 -0
  599. package/src/flow/cross-context.ts +35 -0
  600. package/src/flow/mermaid.test.ts +22 -0
  601. package/src/flow/mermaid.ts +141 -0
  602. package/src/flow/outline.test.ts +212 -0
  603. package/src/flow/outline.ts +170 -0
  604. package/src/flow/paths.test.ts +129 -0
  605. package/src/flow/paths.ts +147 -0
  606. package/src/flow/prefs.test.ts +80 -0
  607. package/src/flow/prefs.ts +104 -0
  608. package/src/graph/DependencyGraph.tsx +283 -0
  609. package/src/graph/DependencyNodes.tsx +162 -0
  610. package/src/graph/EdgeLabel.tsx +61 -0
  611. package/src/graph/FocusedEventGraph.tsx +211 -0
  612. package/src/graph/GraphToolbar.tsx +187 -0
  613. package/src/graph/RoutedEdge.tsx +80 -0
  614. package/src/graph/dependency-layout.test.ts +360 -0
  615. package/src/graph/dependency-layout.ts +444 -0
  616. package/src/graph/elk.test.ts +70 -0
  617. package/src/graph/elk.ts +287 -0
  618. package/src/graph/nodes.tsx +97 -0
  619. package/src/graph/theme.ts +81 -0
  620. package/src/graph/useElkFlow.ts +125 -0
  621. package/src/index.css +1547 -0
  622. package/src/language/InLanguage.tsx +40 -0
  623. package/src/language/TermCard.tsx +132 -0
  624. package/src/language/cards.test.ts +114 -0
  625. package/src/language/cards.ts +111 -0
  626. package/src/lib/admonition.test.ts +49 -0
  627. package/src/lib/admonition.ts +48 -0
  628. package/src/lib/adr.test.ts +131 -0
  629. package/src/lib/adr.ts +68 -0
  630. package/src/lib/api.test.ts +122 -0
  631. package/src/lib/api.ts +135 -0
  632. package/src/lib/backlinks.test.ts +330 -0
  633. package/src/lib/backlinks.ts +567 -0
  634. package/src/lib/branch-compare.ts +16 -0
  635. package/src/lib/build-info.test.ts +76 -0
  636. package/src/lib/build-info.ts +80 -0
  637. package/src/lib/catalog-diff.test.ts +275 -0
  638. package/src/lib/catalog-diff.ts +655 -0
  639. package/src/lib/clipboard.ts +43 -0
  640. package/src/lib/comparison-memory.test.ts +40 -0
  641. package/src/lib/comparison-memory.ts +39 -0
  642. package/src/lib/context-color.ts +27 -0
  643. package/src/lib/context-map.test.ts +148 -0
  644. package/src/lib/context-map.ts +399 -0
  645. package/src/lib/data-model.test.ts +245 -0
  646. package/src/lib/data-model.ts +399 -0
  647. package/src/lib/data-problems.test.ts +178 -0
  648. package/src/lib/data-problems.ts +314 -0
  649. package/src/lib/derive.test.ts +251 -0
  650. package/src/lib/derive.ts +515 -0
  651. package/src/lib/editor-link.test.ts +125 -0
  652. package/src/lib/editor-link.ts +96 -0
  653. package/src/lib/editor-prefs.ts +27 -0
  654. package/src/lib/event-graph.test.ts +237 -0
  655. package/src/lib/event-graph.ts +327 -0
  656. package/src/lib/export-canvas.ts +37 -0
  657. package/src/lib/export-file.test.ts +30 -0
  658. package/src/lib/export-file.ts +20 -0
  659. package/src/lib/flow-tree.test.ts +177 -0
  660. package/src/lib/flow-tree.ts +160 -0
  661. package/src/lib/forge-refs.test.ts +50 -0
  662. package/src/lib/forge-refs.ts +60 -0
  663. package/src/lib/format.test.ts +35 -0
  664. package/src/lib/format.ts +64 -0
  665. package/src/lib/github-catalog.test.ts +301 -0
  666. package/src/lib/github-catalog.ts +610 -0
  667. package/src/lib/integrations.test.ts +119 -0
  668. package/src/lib/integrations.ts +133 -0
  669. package/src/lib/kinds.test.ts +91 -0
  670. package/src/lib/kinds.ts +204 -0
  671. package/src/lib/lifecycle.test.ts +93 -0
  672. package/src/lib/lifecycle.ts +165 -0
  673. package/src/lib/local-api.ts +194 -0
  674. package/src/lib/motion-features.ts +11 -0
  675. package/src/lib/motion.test.ts +120 -0
  676. package/src/lib/motion.tsx +269 -0
  677. package/src/lib/palette-recent.test.ts +80 -0
  678. package/src/lib/palette-recent.ts +90 -0
  679. package/src/lib/palette.test.ts +365 -0
  680. package/src/lib/palette.ts +593 -0
  681. package/src/lib/pins.test.ts +139 -0
  682. package/src/lib/pins.ts +130 -0
  683. package/src/lib/problems.test.ts +112 -0
  684. package/src/lib/project-preview.ts +5 -0
  685. package/src/lib/proto-problems.test.ts +121 -0
  686. package/src/lib/proto-problems.ts +65 -0
  687. package/src/lib/queries.test.ts +113 -0
  688. package/src/lib/queries.ts +131 -0
  689. package/src/lib/registry.test.ts +219 -0
  690. package/src/lib/registry.ts +208 -0
  691. package/src/lib/scenarios.ts +547 -0
  692. package/src/lib/setup-info.test.ts +199 -0
  693. package/src/lib/setup-info.ts +415 -0
  694. package/src/lib/shape.test.ts +289 -0
  695. package/src/lib/shape.ts +454 -0
  696. package/src/lib/sidebar-prefs.test.ts +32 -0
  697. package/src/lib/sidebar-prefs.ts +75 -0
  698. package/src/lib/source-code.test.ts +159 -0
  699. package/src/lib/source-code.ts +286 -0
  700. package/src/lib/source-doc.test.ts +131 -0
  701. package/src/lib/source-doc.ts +103 -0
  702. package/src/lib/source-highlight.test.ts +29 -0
  703. package/src/lib/source-highlight.ts +24 -0
  704. package/src/lib/source-link.test.ts +153 -0
  705. package/src/lib/source-link.ts +201 -0
  706. package/src/lib/spec-files.test.ts +24 -0
  707. package/src/lib/spec-files.ts +23 -0
  708. package/src/lib/table-directive.test.ts +139 -0
  709. package/src/lib/table-directive.ts +106 -0
  710. package/src/lib/tech.test.ts +120 -0
  711. package/src/lib/tech.ts +421 -0
  712. package/src/lib/terms.test.ts +67 -0
  713. package/src/lib/terms.ts +179 -0
  714. package/src/lib/wire-problems.test.ts +263 -0
  715. package/src/lib/wire-problems.ts +281 -0
  716. package/src/likec4/C4View.tsx +55 -0
  717. package/src/likec4/CanvasBridge.tsx +187 -0
  718. package/src/likec4/FlowView.tsx +235 -0
  719. package/src/likec4/InteractiveView.tsx +144 -0
  720. package/src/likec4/canvas-viewport.test.ts +149 -0
  721. package/src/likec4/canvas-viewport.ts +135 -0
  722. package/src/likec4/flow-edges.test.ts +51 -0
  723. package/src/likec4/flow-edges.ts +89 -0
  724. package/src/likec4/frame-theme.test.ts +76 -0
  725. package/src/likec4/frame-theme.ts +101 -0
  726. package/src/likec4/highlight-css.test.ts +49 -0
  727. package/src/likec4/highlight-css.ts +52 -0
  728. package/src/likec4/ids.test.ts +151 -0
  729. package/src/likec4/ids.ts +115 -0
  730. package/src/likec4/levels.tsx +70 -0
  731. package/src/likec4/mapping.test.ts +66 -0
  732. package/src/likec4/mapping.ts +54 -0
  733. package/src/likec4/reserved.json +141 -0
  734. package/src/likec4/view-index.ts +78 -0
  735. package/src/main.tsx +13 -0
  736. package/src/map/ContextMapGraph.tsx +274 -0
  737. package/src/map/ContextNode.tsx +70 -0
  738. package/src/map/FloatingEdge.tsx +86 -0
  739. package/src/merge.test.ts +1038 -0
  740. package/src/merge.ts +1004 -0
  741. package/src/pages/AdrDetail.tsx +242 -0
  742. package/src/pages/AdrIndex.tsx +114 -0
  743. package/src/pages/AggregatePage.tsx +727 -0
  744. package/src/pages/BlockPage.tsx +478 -0
  745. package/src/pages/CatalogFailure.tsx +111 -0
  746. package/src/pages/Changes.tsx +313 -0
  747. package/src/pages/ContextMap.tsx +534 -0
  748. package/src/pages/ContextPage.tsx +346 -0
  749. package/src/pages/EnumPage.tsx +282 -0
  750. package/src/pages/EventPage.tsx +686 -0
  751. package/src/pages/ExternalPage.tsx +204 -0
  752. package/src/pages/FlowDetail.tsx +663 -0
  753. package/src/pages/FlowIndex.tsx +303 -0
  754. package/src/pages/GraphPage.tsx +217 -0
  755. package/src/pages/Language.tsx +257 -0
  756. package/src/pages/ModulePage.tsx +366 -0
  757. package/src/pages/NotFound.tsx +54 -0
  758. package/src/pages/Overview.tsx +344 -0
  759. package/src/pages/Problems.tsx +360 -0
  760. package/src/pages/RegistryIndex.tsx +118 -0
  761. package/src/pages/ServicePage.tsx +746 -0
  762. package/src/pages/Settings.tsx +975 -0
  763. package/src/pages/StorePage.tsx +114 -0
  764. package/src/peek/PeekCard.test.ts +35 -0
  765. package/src/peek/PeekCard.tsx +118 -0
  766. package/src/peek/PeekLayer.tsx +147 -0
  767. package/src/peek/model.test.ts +156 -0
  768. package/src/peek/model.ts +253 -0
  769. package/src/routes.test.ts +222 -0
  770. package/src/routes.ts +449 -0
  771. package/src/selection/DetailPanel.tsx +1407 -0
  772. package/src/selection/hash.test.ts +73 -0
  773. package/src/selection/hash.ts +58 -0
  774. package/src/selection/model.test.ts +199 -0
  775. package/src/selection/model.ts +473 -0
  776. package/src/selection/pages.test.ts +115 -0
  777. package/src/selection/pages.ts +268 -0
  778. package/src/selection/store.test.ts +62 -0
  779. package/src/selection/store.ts +46 -0
  780. package/src/selection/sync.tsx +99 -0
  781. package/src/table/ColumnsMenu.tsx +72 -0
  782. package/src/table/DataTable.tsx +780 -0
  783. package/src/table/Facets.tsx +54 -0
  784. package/src/table/GroupMenu.tsx +69 -0
  785. package/src/table/Toolbar.tsx +190 -0
  786. package/src/table/cells.tsx +117 -0
  787. package/src/table/compare.test.ts +117 -0
  788. package/src/table/compare.ts +196 -0
  789. package/src/table/export.test.ts +88 -0
  790. package/src/table/export.ts +66 -0
  791. package/src/table/facet-groups.test.ts +74 -0
  792. package/src/table/facet-groups.ts +56 -0
  793. package/src/table/grouping.test.ts +84 -0
  794. package/src/table/grouping.ts +82 -0
  795. package/src/table/infer.test.ts +57 -0
  796. package/src/table/infer.ts +50 -0
  797. package/src/table/persist.test.ts +76 -0
  798. package/src/table/persist.ts +112 -0
  799. package/src/table/sort-url.test.ts +75 -0
  800. package/src/table/sort-url.ts +52 -0
  801. package/src/table/types.ts +80 -0
  802. package/src/table/useTableState.ts +220 -0
  803. package/src/test-catalog.ts +7 -0
  804. package/src/testing/estate/data__catalog.json +2847 -0
  805. package/src/testing/estate/data__flows.json +1724 -0
  806. package/src/testing/estate/examples__auth__portolan__api.json +212 -0
  807. package/src/testing/estate/examples__auth__portolan__domain.json +1204 -0
  808. package/src/testing/estate/examples__auth__portolan__glossary.json +226 -0
  809. package/src/testing/estate/examples__auth__portolan__observed.json +757 -0
  810. package/src/testing/estate/examples__auth__portolan__stores.json +195 -0
  811. package/src/testing/estate/examples__shop__cart__portolan__api.json +234 -0
  812. package/src/testing/estate/examples__shop__cart__portolan__commands.json +86 -0
  813. package/src/testing/estate/examples__shop__cart__portolan__domain.json +966 -0
  814. package/src/testing/estate/examples__shop__cart__portolan__observed.json +498 -0
  815. package/src/testing/estate/examples__shop__cart__portolan__stores.json +208 -0
  816. package/src/testing/estate/examples__shop__oms__portolan__glossary.json +98 -0
  817. package/src/testing/estate/examples__shop__oms__portolan__proto.json +186 -0
  818. package/src/testing/estate.ts +49 -0
  819. package/src/trail/Trail.tsx +81 -0
  820. package/src/trail/model.test.ts +157 -0
  821. package/src/trail/model.ts +189 -0
  822. package/src/trail/record.tsx +32 -0
  823. package/src/trail/store.test.ts +66 -0
  824. package/src/trail/store.ts +102 -0
  825. package/tsconfig.json +35 -0
  826. package/vite.config.ts +171 -0
@@ -0,0 +1,1192 @@
1
+ import { execFileSync, spawn } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import {
4
+ cpSync,
5
+ lstatSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readFileSync,
9
+ readdirSync,
10
+ realpathSync,
11
+ renameSync,
12
+ rmSync,
13
+ statSync,
14
+ symlinkSync,
15
+ writeFileSync,
16
+ } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { createServer as createNetServer } from "node:net";
19
+ import { basename, dirname, join, posix, relative, resolve, sep } from "node:path";
20
+
21
+ import { publicSetupFrom } from "../src/lib/setup-info.ts";
22
+ import { loadManifest } from "./manifest.mjs";
23
+ import { builtinPluginNames } from "./builtin-plugins.mjs";
24
+
25
+ export const LOCAL_API_PREFIX = "/__portolan";
26
+ export const GENERATOR_EVENT_PREFIX = "::portolan-event::";
27
+
28
+ const SKIP = new Set([".git", ".portolan", "build", "dist", "node_modules", "target", "vendor"]);
29
+ const MAX_FILES = 12_000;
30
+ const MAX_COMPONENTS = 100;
31
+ const MAX_SOURCE_BYTES = 1024 * 1024;
32
+ const jobs = new Map();
33
+ const repositoryCredentials = new Map();
34
+ const SNAPSHOT_SKIP = new Set([".git", ".portolan", "dist", "node_modules", "target"]);
35
+ const PROJECT_PREVIEW_TTL_MS = 15 * 60 * 1000;
36
+
37
+ function safeRoot(workspace, input) {
38
+ if (typeof input !== "string" || !input.trim()) throw new Error("Project path is required.");
39
+ const clean = input.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, "");
40
+ if (clean.startsWith("/") || clean.split("/").includes("..")) throw new Error("Project path must stay inside this repository.");
41
+ const workspaceReal = realpathSync(workspace);
42
+ const targetReal = realpathSync(resolve(workspaceReal, clean));
43
+ if (targetReal !== workspaceReal && !targetReal.startsWith(`${workspaceReal}${sep}`)) {
44
+ throw new Error("Project path resolves outside this repository.");
45
+ }
46
+ const root = relative(workspaceReal, targetReal).replaceAll(sep, "/") || ".";
47
+ return { root, absolute: targetReal };
48
+ }
49
+
50
+ /** A UTF-8 source file inside the served workspace, never outside it. */
51
+ export function readLocalSource(workspace, input) {
52
+ if (typeof input !== "string" || !input.trim()) throw new Error("Source path is required.");
53
+ const clean = input.trim().replaceAll("\\", "/").replace(/^\.\//, "");
54
+ if (clean.includes("\0") || clean.startsWith("/") || clean.split("/").includes("..")) {
55
+ throw new Error("Source path must stay inside this repository.");
56
+ }
57
+ const workspaceReal = realpathSync(workspace);
58
+ const targetReal = realpathSync(resolve(workspaceReal, clean));
59
+ if (targetReal !== workspaceReal && !targetReal.startsWith(`${workspaceReal}${sep}`)) {
60
+ throw new Error("Source path resolves outside this repository.");
61
+ }
62
+ const stat = statSync(targetReal);
63
+ if (!stat.isFile()) throw new Error("Source path is not a file.");
64
+ if (stat.size > MAX_SOURCE_BYTES) throw new Error("Source file is larger than the 1 MB preview limit.");
65
+ const bytes = readFileSync(targetReal);
66
+ if (bytes.includes(0)) throw new Error("Source file is binary.");
67
+ let content;
68
+ try {
69
+ content = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
70
+ } catch {
71
+ throw new Error("Source file is not UTF-8 text.");
72
+ }
73
+ return {
74
+ path: relative(workspaceReal, targetReal).replaceAll(sep, "/"),
75
+ content,
76
+ };
77
+ }
78
+
79
+ function walk(root) {
80
+ const files = new Set();
81
+ const pending = [{ absolute: root, relative: "" }];
82
+ while (pending.length && files.size < MAX_FILES) {
83
+ const current = pending.pop();
84
+ for (const entry of readdirSync(current.absolute, { withFileTypes: true })) {
85
+ if (SKIP.has(entry.name) || (entry.isDirectory() && entry.name.startsWith("."))) continue;
86
+ const name = current.relative ? `${current.relative}/${entry.name}` : entry.name;
87
+ const absolute = join(current.absolute, entry.name);
88
+ const stat = lstatSync(absolute);
89
+ if (stat.isSymbolicLink()) continue;
90
+ if (stat.isDirectory()) pending.push({ absolute, relative: name });
91
+ else if (stat.isFile()) files.add(name);
92
+ if (files.size >= MAX_FILES) break;
93
+ }
94
+ }
95
+ return files;
96
+ }
97
+
98
+ function slug(value) {
99
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
100
+ }
101
+
102
+ function matches(files, pattern) {
103
+ return [...files].filter((name) => pattern.test(name)).sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b));
104
+ }
105
+
106
+ function compactDirectories(paths) {
107
+ const directories = [...new Set(paths.map((name) => posix.dirname(name)))].sort((a, b) => a.length - b.length);
108
+ return directories.filter((dir, index) => !directories.some((parent, other) => other < index && (dir === parent || dir.startsWith(`${parent}/`))));
109
+ }
110
+
111
+ const COMPONENT_MARKERS = new Map([
112
+ ["go.mod", "Go"],
113
+ ["package.json", "Node.js"],
114
+ ["Cargo.toml", "Rust"],
115
+ ["pom.xml", "Java"],
116
+ ["build.gradle", "Java"],
117
+ ["build.gradle.kts", "Kotlin"],
118
+ ["manage.py", "Django"],
119
+ ]);
120
+
121
+ function componentCandidates(files) {
122
+ const roots = new Map();
123
+ for (const name of files) {
124
+ const marker = posix.basename(name);
125
+ const technology = COMPONENT_MARKERS.get(marker);
126
+ if (!technology) continue;
127
+ const path = posix.dirname(name);
128
+ const key = path === "." ? "." : path;
129
+ if (key.split("/").some((segment) => ["fixture", "fixtures", "test", "tests", "testdata"].includes(segment.toLowerCase()))) continue;
130
+ const found = roots.get(key) ?? { path: key, markers: new Set(), technologies: new Set() };
131
+ found.markers.add(marker);
132
+ found.technologies.add(technology);
133
+ roots.set(key, found);
134
+ }
135
+ return [...roots.values()]
136
+ .sort((a, b) => a.path === "." ? -1 : b.path === "." ? 1 : a.path.localeCompare(b.path))
137
+ .slice(0, MAX_COMPONENTS)
138
+ .map((candidate) => {
139
+ const base = candidate.path === "." ? "repository root" : posix.basename(candidate.path);
140
+ return {
141
+ path: candidate.path,
142
+ name: base.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" ") || base,
143
+ markers: [...candidate.markers].sort(),
144
+ technologies: [...candidate.technologies].sort(),
145
+ };
146
+ });
147
+ }
148
+
149
+ function detected(plugin, candidates, options = {}, label = candidates[0], ambiguous = false, selected = true) {
150
+ if (!candidates.length) return null;
151
+ return {
152
+ plugin,
153
+ confidence: ambiguous && candidates.length > 1 ? "medium" : "high",
154
+ evidence: label,
155
+ candidates,
156
+ options,
157
+ selected,
158
+ };
159
+ }
160
+
161
+ function compatibleAdrs(root, candidates) {
162
+ return candidates.filter((name) => {
163
+ let source = "";
164
+ try { source = readFileSync(join(root, name), "utf8"); } catch { return false; }
165
+ return /^#\s+[^\n]+\.\d{4}\s+[—-]/m.test(source) && /^-\s+\*\*Status:\*\*/mi.test(source) && /^-\s+\*\*Date:\*\*/mi.test(source);
166
+ });
167
+ }
168
+
169
+ function goDomainEvidence(root, files) {
170
+ const layout = /^internal\/(?:domain\/([^/]+)|([^/]+)\/domain)\/[^/]+\.go$/i;
171
+ const candidates = matches(files, layout);
172
+ for (const name of candidates) {
173
+ const match = layout.exec(name);
174
+ if (!match) continue;
175
+ const aggregate = match[1] ?? match[2];
176
+ const packageName = aggregate.replace(/[^a-zA-Z0-9_]/g, "_");
177
+ const rootName = aggregate
178
+ .split(/[^a-zA-Z0-9]+|_/)
179
+ .filter(Boolean)
180
+ .map((part) => part[0]?.toUpperCase() + part.slice(1))
181
+ .join("");
182
+ let source = "";
183
+ try { source = readFileSync(join(root, name), "utf8"); } catch { continue; }
184
+ const packagePattern = new RegExp(`\\bpackage\\s+${packageName}\\b`);
185
+ const rootPattern = new RegExp(`\\btype\\s+${rootName}\\s+struct\\s*\\{`);
186
+ if (packagePattern.test(source) && rootPattern.test(source)) return name;
187
+ }
188
+ return "";
189
+ }
190
+
191
+ function laidOutDomainEvidence(root, files, language) {
192
+ const extension = language === "typescript" ? "ts" : language === "rust" ? "rs" : "java";
193
+ const prefix = language === "java" ? /(?:^|\/)domain\/([^/]+)\/[^/]+\.java$/i : /^src\/domain\/([^/]+)\/[^/]+\.(?:ts|rs)$/i;
194
+ for (const name of matches(files, prefix)) {
195
+ if (!name.endsWith(`.${extension}`)) continue;
196
+ const match = prefix.exec(name);
197
+ if (!match) continue;
198
+ const rootName = match[1].split(/[^a-zA-Z0-9]+|_/).filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1)).join("");
199
+ let source = "";
200
+ try { source = readFileSync(join(root, name), "utf8"); } catch { continue; }
201
+ const claim = language === "typescript"
202
+ ? new RegExp(`\\b(?:export\\s+)?class\\s+${rootName}\\b`)
203
+ : language === "rust"
204
+ ? new RegExp(`\\bpub\\s+struct\\s+${rootName}\\b`)
205
+ : /@AggregateRoot\b/.test(source) || new RegExp(`\\bclass\\s+${rootName}\\b`).test(source);
206
+ if (claim instanceof RegExp ? claim.test(source) : claim) return name;
207
+ }
208
+ return "";
209
+ }
210
+
211
+ function goHTTPClientEvidence(root, files) {
212
+ for (const name of matches(files, /\.go$/).filter((name) => !name.endsWith("_test.go"))) {
213
+ let source = "";
214
+ try { source = readFileSync(join(root, name), "utf8"); } catch { continue; }
215
+ if (
216
+ /\bhttp\.(?:NewRequest(?:WithContext)?|Get|Post|PostForm|Head)\s*\(/.test(source)
217
+ || /ClientWithResponses(?:Interface)?\b/.test(source)
218
+ || /github\.com\/hooklift\/gowsdl\/soap/.test(source)
219
+ ) return name;
220
+ }
221
+ return "";
222
+ }
223
+
224
+ function goSOAPClientEvidence(root, files) {
225
+ for (const name of matches(files, /\.go$/).filter((name) => !name.endsWith("_test.go"))) {
226
+ let source = "";
227
+ try { source = readFileSync(join(root, name), "utf8"); } catch { continue; }
228
+ if (
229
+ /github\.com\/hooklift\/gowsdl\/soap/.test(source)
230
+ || /\bCallContext\s*\([^,]+,\s*[^,]*(?:soap)?action/i.test(source)
231
+ || /(?:Header\.)?(?:Add|Set)\s*\(\s*["'](?:SOAPAction|Content-Type)["']/i.test(source)
232
+ ) return name;
233
+ }
234
+ return "";
235
+ }
236
+
237
+ function goRedisEvidence(root, files) {
238
+ const redisImport = /github\.com\/(?:redis\/go-redis(?:\/v\d+)?|go-redis\/redis(?:\/v\d+)?|redis\/rueidis|gomodule\/redigo\/redis)(?=\")/g;
239
+ for (const name of matches(files, /\.go$/).filter((name) => !name.endsWith("_test.go"))) {
240
+ let source = "";
241
+ try { source = readFileSync(join(root, name), "utf8"); } catch { continue; }
242
+ const imports = [...source.matchAll(redisImport)].map((match) => match[0]);
243
+ if (!imports.length) continue;
244
+
245
+ const aliases = new Set();
246
+ for (const importPath of imports) {
247
+ const escaped = importPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
248
+ const declaration = new RegExp(`(?:^|\\n)\\s*(?:import\\s+)?(?:([A-Za-z_][A-Za-z0-9_]*)\\s+)?\"${escaped}\"`, "m").exec(source);
249
+ const alias = declaration?.[1];
250
+ if (alias && alias !== "_" && alias !== ".") aliases.add(alias);
251
+ else aliases.add(importPath.includes("rueidis") ? "rueidis" : "redis");
252
+ }
253
+ if ([...aliases].some((alias) => new RegExp(`\\b${alias}\\.(?:NewClient|NewClusterClient|NewFailoverClient|NewFailoverClusterClient|Dial|DialURL)\\s*\\(`).test(source))) {
254
+ return name;
255
+ }
256
+ }
257
+ return "";
258
+ }
259
+
260
+ function detectionsFor(root, files) {
261
+ let goMod = "";
262
+ if (files.has("go.mod")) {
263
+ try { goMod = readFileSync(join(root, "go.mod"), "utf8"); } catch {}
264
+ }
265
+ const openapi = matches(files, /(^|\/)(openapi|swagger)[^/]*\.(ya?ml|json)$/i);
266
+ const wsdls = matches(files, /\.wsdl$/i);
267
+ const asyncapi = matches(files, /(^|\/)asyncapi[^/]*\.(ya?ml|json)$/i);
268
+ const graphql = matches(files, /\.graphqls?$/i);
269
+ const protos = matches(files, /\.proto$/i);
270
+ const sql = matches(files, /(^|\/)(migrations?|repository)(\/|.*\/).*\.sql$/i);
271
+ const adrs = matches(files, /(^|\/)(docs\/adr|adr)\/.*\.md$/i);
272
+ const supportedAdrs = compatibleAdrs(root, adrs);
273
+ const glossaries = matches(files, /(^|\/)glossary\.md$/i);
274
+ // The app module is the one file a Celery project always has; the tasks
275
+ // and the calls that enqueue them are found from there.
276
+ const celery = matches(files, /(^|\/)celery\.py$/);
277
+ const sqlRoots = [...new Set(sql.map((name) => {
278
+ const segments = name.split("/");
279
+ const repository = segments.findIndex((part) => /^(repository|repositories)$/i.test(part));
280
+ return repository >= 0 ? segments.slice(0, repository + 1).join("/") : "";
281
+ }).filter(Boolean))];
282
+ const featureSql = sqlRoots.some((name) => /^internal\/[^/]+\/infrastructure\/repository$/i.test(name));
283
+ const sqlRoot = sqlRoots.length === 1 && !featureSql ? sqlRoots[0] : "";
284
+ const graphqlDirs = compactDirectories(graphql);
285
+ const protoDirs = compactDirectories(protos);
286
+ const projectMarkers = ["go.mod", "package.json", "Cargo.toml", "pom.xml", "build.gradle", "build.gradle.kts", "manage.py", "Dockerfile", "README.md"].filter((name) => files.has(name));
287
+ const projectEvidence = projectMarkers.length ? projectMarkers : [[...files].sort()[0]].filter(Boolean);
288
+ const goDomain = files.has("go.mod") ? goDomainEvidence(root, files) : "";
289
+ const goHTTPClient = files.has("go.mod") ? goHTTPClientEvidence(root, files) : "";
290
+ const goSOAPClient = files.has("go.mod") ? goSOAPClientEvidence(root, files) : "";
291
+ const goRedis = files.has("go.mod") ? goRedisEvidence(root, files) : "";
292
+ const tsDomain = files.has("package.json") ? laidOutDomainEvidence(root, files, "typescript") : "";
293
+ const rustDomain = files.has("Cargo.toml") ? laidOutDomainEvidence(root, files, "rust") : "";
294
+ const javaDomain = ["pom.xml", "build.gradle", "build.gradle.kts"].some((name) => files.has(name)) ? laidOutDomainEvidence(root, files, "java") : "";
295
+ return [
296
+ detected("project", projectEvidence, {}, projectEvidence.join(", ")),
297
+ detected("go-domain", goDomain ? [goDomain] : [], {}, goDomain),
298
+ detected("ts-domain", tsDomain ? [tsDomain] : [], {}, tsDomain),
299
+ detected("rust-domain", rustDomain ? [rustDomain] : [], {}, rustDomain),
300
+ detected("java-domain", javaDomain ? [javaDomain] : [], {}, javaDomain),
301
+ detected("django-domain", files.has("manage.py") && matches(files, /(^|\/)models(?:\/[^/]+)?\.py$/i).length ? ["manage.py"] : []),
302
+ detected("celery", celery, {}, celery[0]),
303
+ detected("openapi", openapi, openapi[0] ? { spec: openapi[0] } : {}, openapi[0], true),
304
+ detected(
305
+ "wsdl",
306
+ wsdls,
307
+ {
308
+ ...(wsdls.length === 1 ? { spec: wsdls[0] } : {}),
309
+ ...(goSOAPClient ? { mode: "external" } : {}),
310
+ },
311
+ wsdls.length === 1 ? wsdls[0] : `${wsdls.length} WSDL documents`,
312
+ true,
313
+ ),
314
+ detected("http-clients", goHTTPClient ? [goHTTPClient] : [], {}, goHTTPClient),
315
+ detected("redis", goRedis ? [goRedis] : [], {}, goRedis),
316
+ detected("river", goMod.includes("github.com/riverqueue/river") ? ["go.mod"] : [], {}, "go.mod · github.com/riverqueue/river"),
317
+ detected("watermill", goMod.includes("github.com/ThreeDotsLabs/watermill") ? ["go.mod"] : [], {}, "go.mod · github.com/ThreeDotsLabs/watermill"),
318
+ detected("asyncapi", asyncapi, asyncapi[0] ? { spec: asyncapi[0] } : {}, asyncapi[0], true),
319
+ detected("graphql", graphql, graphqlDirs[0] ? { schema: graphqlDirs.length === 1 ? graphqlDirs[0] : graphql[0] } : {}, graphqlDirs.length === 1 ? graphqlDirs[0] : graphql[0]),
320
+ detected("proto", protos, protoDirs.length ? { paths: protoDirs } : {}, protoDirs.join(", ")),
321
+ detected("sql", sql, sqlRoot ? { repositories: sqlRoot } : {}, sqlRoot || (sqlRoots.length > 1 ? `${sqlRoots.length} repository packages` : sql[0])),
322
+ detected(
323
+ "adr",
324
+ adrs,
325
+ supportedAdrs[0] ? { files: [`${posix.dirname(supportedAdrs[0])}/*.md`] } : {},
326
+ supportedAdrs[0] ? `${posix.dirname(supportedAdrs[0])}/*.md` : `${posix.dirname(adrs[0] ?? "docs/adr/x.md")}/*.md (format not recognized)`,
327
+ !supportedAdrs.length,
328
+ supportedAdrs.length > 0,
329
+ ),
330
+ detected("glossary", glossaries, glossaries.length ? { files: glossaries } : {}, glossaries.join(", ")),
331
+ ].filter(Boolean);
332
+ }
333
+
334
+ export function discoverProject(workspace, input) {
335
+ const { root, absolute } = safeRoot(workspace, input);
336
+ const files = walk(absolute);
337
+ const detections = detectionsFor(absolute, files);
338
+ const components = componentCandidates(files);
339
+ const id = slug(basename(absolute)) || "service";
340
+ return {
341
+ root,
342
+ filesScanned: files.size,
343
+ truncated: files.size >= MAX_FILES,
344
+ components,
345
+ componentsTruncated: components.length >= MAX_COMPONENTS,
346
+ defaults: { id, name: id.split("-").map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" "), group: id, component: id, context: id, service: id },
347
+ detections,
348
+ };
349
+ }
350
+
351
+ function repositoryParts(repository) {
352
+ const value = String(repository ?? "").trim();
353
+ if (!value || value.startsWith("-") || /[\r\n\0]/.test(value)) throw new Error("Repository URL is required.");
354
+ let path;
355
+ let host;
356
+ let fetchUrl = value;
357
+ let transport = "https";
358
+ if (value.startsWith("git@")) {
359
+ const match = /^git@([^:/\s]+):(.+)$/.exec(value);
360
+ if (!match) throw new Error("Use a valid SSH repository URL.");
361
+ [, host, path] = match;
362
+ transport = "ssh";
363
+ }
364
+ else if (value.includes("://")) {
365
+ const url = new URL(value);
366
+ const embeddedCredentials = url.protocol === "https:" ? Boolean(url.username || url.password) : Boolean(url.password);
367
+ if (!["https:", "ssh:"].includes(url.protocol) || embeddedCredentials) throw new Error("Use an HTTPS or SSH repository URL without embedded credentials.");
368
+ host = url.hostname;
369
+ path = url.pathname;
370
+ transport = url.protocol === "ssh:" ? "ssh" : "https";
371
+ } else {
372
+ if (!/^[a-z0-9.-]+\/[a-z0-9._/-]+$/i.test(value)) throw new Error("Repository must name a host, owner and repository.");
373
+ host = value.slice(0, value.indexOf("/"));
374
+ path = value.slice(value.indexOf("/") + 1);
375
+ fetchUrl = `https://${value}`;
376
+ }
377
+ const segments = path.replace(/^\//, "").replace(/\.git$/, "").split("/").filter(Boolean);
378
+ if (segments.length < 2) throw new Error("Repository must name an owner and repository.");
379
+ const normalizedHost = host.toLowerCase();
380
+ const provider = normalizedHost === "github.com" || normalizedHost.endsWith(".github.com")
381
+ ? "GitHub"
382
+ : normalizedHost === "gitlab.com" || normalizedHost.endsWith(".gitlab.com")
383
+ ? "GitLab"
384
+ : "Git server";
385
+ return { value, fetchUrl, host, provider, transport, owner: segments.at(-2), name: segments.at(-1), web: `${host}/${segments.at(-2)}/${segments.at(-1)}` };
386
+ }
387
+
388
+ class LocalApiError extends Error {
389
+ constructor(message, { code = "request_failed", status = 400, retryable = false, provider, host, credentialSupported = false, credentialPresent = false } = {}) {
390
+ super(message);
391
+ this.name = "LocalApiError";
392
+ this.code = code;
393
+ this.status = status;
394
+ this.retryable = retryable;
395
+ this.provider = provider;
396
+ this.host = host;
397
+ this.credentialSupported = credentialSupported;
398
+ this.credentialPresent = credentialPresent;
399
+ }
400
+ }
401
+
402
+ /** Turn unstable git stderr and timeout shapes into a small, safe API contract. */
403
+ export function classifyRepositoryFailure(cause, provider = "Git server", phase = "inspect the repository") {
404
+ const text = [cause?.message, cause?.stderr, cause?.stdout].filter(Boolean).map(String).join("\n");
405
+ const lower = text.toLowerCase();
406
+ const timedOut = cause?.code === "ETIMEDOUT" || /timed? out|timeout|signal sigterm/.test(lower) && cause?.status == null;
407
+ if (timedOut) {
408
+ return new LocalApiError(`${provider} did not respond in time while Portolan tried to ${phase}. Check your network or VPN, then retry.`, {
409
+ code: "repository_timeout", status: 504, retryable: true, provider,
410
+ });
411
+ }
412
+ if (/returned error:\s*403|http[^\n]*403|status(?: code)?\s*403/.test(lower) || /permission denied|access denied|not authorized/.test(lower)) {
413
+ return new LocalApiError(`${provider} refused access to this repository. Check repository permissions and organization or SSO authorization, then retry.`, {
414
+ code: "repository_forbidden", status: 403, retryable: true, provider,
415
+ });
416
+ }
417
+ if (/returned error:\s*401|http[^\n]*401|status(?: code)?\s*401|authentication failed|could not read username|terminal prompts disabled|invalid credentials/.test(lower)) {
418
+ return new LocalApiError(`${provider} requires authentication for this repository. Authenticate Git with a read-only credential, then retry.`, {
419
+ code: "repository_auth_required", status: 401, retryable: true, provider,
420
+ });
421
+ }
422
+ return new LocalApiError(`Could not ${phase}. Check the repository URL, ref and network access, then retry.`, {
423
+ code: "repository_unavailable", status: 400, retryable: true, provider,
424
+ });
425
+ }
426
+
427
+ function credentialUsername(provider) {
428
+ return provider === "GitHub" ? "x-access-token" : provider === "GitLab" ? "oauth2" : "git";
429
+ }
430
+
431
+ function environmentCredential(repo) {
432
+ const token = repo.provider === "GitHub"
433
+ ? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN
434
+ : repo.provider === "GitLab"
435
+ ? process.env.GITLAB_TOKEN
436
+ : undefined;
437
+ return token ? { token, username: credentialUsername(repo.provider), provider: repo.provider } : null;
438
+ }
439
+
440
+ function credentialFor(repo) {
441
+ return repositoryCredentials.get(repo.host.toLowerCase()) ?? environmentCredential(repo);
442
+ }
443
+
444
+ export function storeRepositoryCredential(request) {
445
+ const repo = repositoryParts(request.repository);
446
+ if (repo.transport !== "https" || !["GitHub", "GitLab"].includes(repo.provider)) {
447
+ throw new LocalApiError("Access tokens are supported for HTTPS GitHub and GitLab repositories.", { code: "credential_unsupported", status: 400 });
448
+ }
449
+ const token = String(request.token ?? "").trim();
450
+ if (!token || token.length > 8_192 || /[\r\n\0]/.test(token)) {
451
+ throw new LocalApiError("Enter a valid access token.", { code: "credential_invalid", status: 400 });
452
+ }
453
+ repositoryCredentials.set(repo.host.toLowerCase(), { token, username: credentialUsername(repo.provider), provider: repo.provider });
454
+ return { host: repo.host.toLowerCase(), provider: repo.provider, scope: "session", stored: true };
455
+ }
456
+
457
+ export function forgetRepositoryCredential(request) {
458
+ const repo = repositoryParts(request.repository);
459
+ repositoryCredentials.delete(repo.host.toLowerCase());
460
+ return { host: repo.host.toLowerCase(), provider: repo.provider, scope: "session", stored: false };
461
+ }
462
+
463
+ function gitConfigEnvironment(env) {
464
+ const parsed = Number.parseInt(String(env.GIT_CONFIG_COUNT ?? "0"), 10);
465
+ const count = Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
466
+ return { ...env, GIT_CONFIG_COUNT: String(count + 1), [`GIT_CONFIG_KEY_${count}`]: "credential.helper", [`GIT_CONFIG_VALUE_${count}`]: "" };
467
+ }
468
+
469
+ function gitAuthEnvironment(repositories = []) {
470
+ const credentials = {};
471
+ if (repositories.length) {
472
+ for (const repo of repositories) {
473
+ const credential = credentialFor(repo);
474
+ if (repo.transport === "https" && credential) credentials[repo.host.toLowerCase()] = credential;
475
+ }
476
+ } else {
477
+ for (const [host, credential] of repositoryCredentials) credentials[host] = credential;
478
+ if (!credentials["github.com"] && (process.env.GH_TOKEN || process.env.GITHUB_TOKEN)) {
479
+ credentials["github.com"] = { token: process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN, username: "x-access-token", provider: "GitHub" };
480
+ }
481
+ if (!credentials["gitlab.com"] && process.env.GITLAB_TOKEN) {
482
+ credentials["gitlab.com"] = { token: process.env.GITLAB_TOKEN, username: "oauth2", provider: "GitLab" };
483
+ }
484
+ }
485
+ const entries = Object.entries(credentials);
486
+ if (!entries.length) return { env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }, dispose() {} };
487
+ const holder = mkdtempSync(join(tmpdir(), "portolan-git-auth-"));
488
+ const helper = join(holder, process.platform === "win32" ? "askpass.cmd" : "askpass.cjs");
489
+ const script = process.platform === "win32"
490
+ ? `@echo off\r\n"${process.execPath}" "${join(holder, "askpass.mjs")}" %*\r\n`
491
+ : `#!${process.execPath}\nconst process = require("node:process");\nconst credentials = JSON.parse(process.env.PORTOLAN_GIT_CREDENTIALS_JSON || "{}");\nconst prompt = String(process.argv[2] || "");\nconst authority = /https?:\\/\\/([^/'\"]+)/i.exec(prompt)?.[1];\nconst host = authority?.split("@").pop()?.replace(/:\\d+$/, "").toLowerCase();\nconst values = Object.values(credentials);\nconst credential = credentials[host] || (values.length === 1 ? values[0] : null);\nif (credential) process.stdout.write(String(/username/i.test(prompt) ? credential.username : credential.token));\n`;
492
+ if (process.platform === "win32") {
493
+ writeFileSync(join(holder, "askpass.mjs"), `import process from "node:process";\nconst credentials = JSON.parse(process.env.PORTOLAN_GIT_CREDENTIALS_JSON || "{}");\nconst prompt = String(process.argv[2] || "");\nconst authority = /https?:\\/\\/([^/'\"]+)/i.exec(prompt)?.[1];\nconst host = authority?.split("@").pop()?.replace(/:\\d+$/, "").toLowerCase();\nconst values = Object.values(credentials);\nconst credential = credentials[host] || (values.length === 1 ? values[0] : null);\nif (credential) process.stdout.write(String(/username/i.test(prompt) ? credential.username : credential.token));\n`);
494
+ }
495
+ writeFileSync(helper, script, { mode: 0o700 });
496
+ const env = gitConfigEnvironment({
497
+ ...process.env,
498
+ GIT_TERMINAL_PROMPT: "0",
499
+ GIT_ASKPASS: helper,
500
+ GIT_ASKPASS_REQUIRE: "force",
501
+ PORTOLAN_GIT_CREDENTIALS_JSON: JSON.stringify(credentials),
502
+ });
503
+ return { env, dispose() { rmSync(holder, { recursive: true, force: true }); } };
504
+ }
505
+
506
+ function repositoryFailure(cause, repo, phase) {
507
+ const failure = classifyRepositoryFailure(cause, repo.provider, phase);
508
+ failure.host = repo.host.toLowerCase();
509
+ failure.credentialSupported = repo.transport === "https" && ["GitHub", "GitLab"].includes(repo.provider);
510
+ failure.credentialPresent = repositoryCredentials.has(repo.host.toLowerCase());
511
+ return failure;
512
+ }
513
+
514
+ function remoteCommit(repo, ref) {
515
+ if (ref.startsWith("-") || /[\s\r\n\0]/.test(ref)) throw new Error("Branch, tag or commit is not valid.");
516
+ if (/^[0-9a-f]{40}$/i.test(ref)) return ref.toLowerCase();
517
+ const query = ref.trim() || "HEAD";
518
+ const auth = gitAuthEnvironment([repo]);
519
+ let output;
520
+ try {
521
+ output = execFileSync("git", ["ls-remote", "--quiet", repo.fetchUrl, query, `${query}^{}`], {
522
+ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 30_000, maxBuffer: 1024 * 1024, env: auth.env,
523
+ });
524
+ } finally { auth.dispose(); }
525
+ const lines = output.trim().split("\n").filter(Boolean);
526
+ const peeled = lines.find((line) => line.trimEnd().endsWith("^{}"));
527
+ const commit = (peeled ?? lines[0] ?? "").trim().split(/\s+/)[0];
528
+ if (!/^[0-9a-f]{40}$/i.test(commit)) throw new Error(`Repository has no ref \"${query}\".`);
529
+ return commit.toLowerCase();
530
+ }
531
+
532
+ export function resolveRepositoryCommit(repository, ref) {
533
+ const repo = repositoryParts(repository);
534
+ try { return remoteCommit(repo, String(ref ?? "").trim()); }
535
+ catch (cause) { throw repositoryFailure(cause, repo, "resolve the requested ref"); }
536
+ }
537
+
538
+ function inspectionKey(repository, commit, sourcePath = "") {
539
+ return createHash("sha256").update(`${repository}\0${commit}\0${sourcePath}`).digest("hex").slice(0, 16);
540
+ }
541
+
542
+ function cleanSourcePath(sourcePath) {
543
+ const clean = String(sourcePath ?? "").replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, "");
544
+ if (clean.startsWith("/") || clean.startsWith("-") || clean.split("/").includes("..")) throw new Error("Repository path must be relative.");
545
+ return clean;
546
+ }
547
+
548
+ export function inspectionRoot(repository, commit, sourcePath = "") {
549
+ const clean = cleanSourcePath(sourcePath);
550
+ return [".portolan", "inspect", inspectionKey(repository, commit, clean), clean].filter(Boolean).join("/");
551
+ }
552
+
553
+ export function prepareRepository(workspace, request) {
554
+ const repo = repositoryParts(request.repository);
555
+ const ref = String(request.ref ?? "").trim();
556
+ const sourcePath = cleanSourcePath(request.sourcePath);
557
+ const commit = resolveRepositoryCommit(repo.value, ref);
558
+ const root = inspectionRoot(repo.value, commit, sourcePath);
559
+ const checkout = join(workspace, ".portolan", "inspect", inspectionKey(repo.value, commit, sourcePath));
560
+ if (!lstatExists(checkout)) {
561
+ mkdirSync(dirname(checkout), { recursive: true });
562
+ const staging = mkdtempSync(join(dirname(checkout), ".checkout-"));
563
+ try {
564
+ const options = { cwd: staging, stdio: "pipe", timeout: 60_000, maxBuffer: 4 * 1024 * 1024, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } };
565
+ execFileSync("git", ["init", "--quiet"], options);
566
+ const auth = gitAuthEnvironment([repo]);
567
+ try { execFileSync("git", ["fetch", "--quiet", "--depth", "1", repo.fetchUrl, commit], { ...options, env: auth.env }); }
568
+ finally { auth.dispose(); }
569
+ if (sourcePath) execFileSync("git", ["sparse-checkout", "set", "--no-cone", sourcePath], options);
570
+ execFileSync("git", ["checkout", "--quiet", "--detach", "FETCH_HEAD"], options);
571
+ renameSync(staging, checkout);
572
+ } catch (cause) {
573
+ throw repositoryFailure(cause, repo, "download the repository");
574
+ } finally {
575
+ if (lstatExists(staging)) rmSync(staging, { recursive: true, force: true });
576
+ }
577
+ }
578
+ if (sourcePath && !lstatExists(join(checkout, sourcePath))) throw new Error(`Repository path \"${sourcePath}\" does not exist at ${commit.slice(0, 7)}.`);
579
+ const discovery = discoverProject(workspace, root);
580
+ return { repository: repo.value, ref, commit, sourcePath, checkoutRoot: root, discovery };
581
+ }
582
+
583
+ function lstatExists(path) {
584
+ try { lstatSync(path); return true; } catch { return false; }
585
+ }
586
+
587
+ function pluginOptions(plugin, project, detectedOptions = {}) {
588
+ const group = project.group ?? project.context;
589
+ const component = project.component ?? project.service;
590
+ const common = { context: group, service: component };
591
+ if (plugin === "project") {
592
+ return {
593
+ group,
594
+ component,
595
+ ...detectedOptions,
596
+ componentName: project.name,
597
+ ...(project.groupKind ? { groupKind: project.groupKind } : {}),
598
+ ...(project.componentKind ? { componentKind: project.componentKind } : {}),
599
+ ...(project.repository ? { repo: repositoryParts(project.repository).web } : {}),
600
+ out: "project.json",
601
+ };
602
+ }
603
+ if (["go-domain", "ts-domain", "rust-domain", "java-domain", "django-domain"].includes(plugin)) {
604
+ return { ...common, ...(project.repository ? { repo: repositoryParts(project.repository).web } : {}), ...detectedOptions, serviceName: project.name, out: "domain.json" };
605
+ }
606
+ if (plugin === "sql") return { ...common, store: "pg", ...detectedOptions, out: "stores.json" };
607
+ if (plugin === "redis") return { ...common, store: "redis", ...detectedOptions, out: "redis.json" };
608
+ if (plugin === "openapi") return { ...common, ...detectedOptions, out: "api.json" };
609
+ if (plugin === "wsdl") return { ...common, ...detectedOptions, out: "wsdl.json" };
610
+ if (plugin === "http-clients") return { ...common, ...detectedOptions, out: "http-clients.json" };
611
+ if (plugin === "river") return { ...common, ...detectedOptions, out: "river.json" };
612
+ if (plugin === "watermill") return { ...common, ...detectedOptions, out: "watermill.json" };
613
+ if (plugin === "asyncapi") return { ...common, ...detectedOptions, out: "bus.json" };
614
+ if (plugin === "celery") return { ...common, ...detectedOptions, out: "celery.json" };
615
+ if (plugin === "graphql") return { ...common, ...detectedOptions, out: "graphql.json" };
616
+ if (plugin === "proto") return { ...common, ...detectedOptions, out: "proto.json" };
617
+ if (plugin === "glossary") return { context: group, ...detectedOptions, out: "glossary.json" };
618
+ if (plugin === "adr") return { ...detectedOptions, out: "adr.json" };
619
+ return {};
620
+ }
621
+
622
+ export function planProject(workspace, manifest, request) {
623
+ const external = request.source === "external";
624
+ const repo = external ? repositoryParts(request.repository) : null;
625
+ const sourcePath = external ? cleanSourcePath(request.sourcePath) : "";
626
+ if (external && !/^[0-9a-f]{40}$/i.test(String(request.commit ?? ""))) throw new Error("Inspect the repository to resolve an immutable commit first.");
627
+ const inspectedRoot = external ? inspectionRoot(repo.value, String(request.commit), sourcePath) : request.root;
628
+ const discovery = discoverProject(workspace, inspectedRoot);
629
+ const declared = new Set([
630
+ ...builtinPluginNames(),
631
+ ...(manifest.plugins ?? []).map((plugin) => plugin.name),
632
+ ]);
633
+ const detected = new Set(discovery.detections.map((item) => item.plugin));
634
+ const requested = Array.isArray(request.plugins) ? request.plugins : [];
635
+ const plugins = [...new Set(requested)].filter((plugin) => detected.has(plugin) && declared.has(plugin));
636
+ if (plugins.length === 0) throw new Error("Select at least one detected plugin that is declared in portolan.json.");
637
+ const id = slug(String(request.id ?? ""));
638
+ if (!id) throw new Error("Project id must contain letters or numbers.");
639
+ if ((manifest.projects ?? []).some((project) => project.id === id)) throw new Error(`Project id \"${id}\" already exists.`);
640
+ const finalRoot = external
641
+ ? ["vendor", "repos", repo.owner, repo.name, sourcePath].filter(Boolean).join("/")
642
+ : discovery.root;
643
+ if ((manifest.projects ?? []).some((project) => project.root === finalRoot)) throw new Error(`Project path \"${finalRoot}\" already exists.`);
644
+ const project = {
645
+ id,
646
+ name: String(request.name ?? "").trim() || discovery.defaults.name,
647
+ root: finalRoot,
648
+ ...(String(request.group ?? request.context ?? "").trim() ? { group: slug(String(request.group ?? request.context)) } : {}),
649
+ ...(String(request.component ?? request.service ?? "").trim() ? { component: slug(String(request.component ?? request.service)) } : {}),
650
+ ...(String(request.groupKind ?? "").trim() ? { groupKind: String(request.groupKind).trim() } : {}),
651
+ ...(String(request.componentKind ?? "").trim() ? { componentKind: String(request.componentKind).trim() } : {}),
652
+ ...(String(request.repository ?? "").trim() ? { repository: String(request.repository).trim() } : {}),
653
+ };
654
+ const out = `${finalRoot}/portolan`;
655
+ const detectionByPlugin = new Map(discovery.detections.map((item) => [item.plugin, item]));
656
+ const hasDomainModel = plugins.some((plugin) => ["go-domain", "ts-domain", "rust-domain", "java-domain", "django-domain"].includes(plugin));
657
+ const steps = plugins.map((plugin) => ({
658
+ plugin,
659
+ in: finalRoot,
660
+ out,
661
+ options: pluginOptions(
662
+ plugin,
663
+ project,
664
+ plugin === "project"
665
+ ? { groupKind: hasDomainModel ? "bounded-context" : "system", ...(hasDomainModel ? { componentKind: "service" } : {}) }
666
+ : detectionByPlugin.get(plugin)?.options,
667
+ ),
668
+ }));
669
+ const source = external ? "vendor/repos/**/portolan/*.json" : `${out}/*.json`;
670
+ const fetch = external ? { repo: repo.value, commit: String(request.commit), paths: sourcePath ? [sourcePath] : [] } : null;
671
+ return { project, plugins, steps, source, discovery, fetch };
672
+ }
673
+
674
+ function manifestWithProject(manifest, plan, { isolated = false } = {}) {
675
+ const fetchIndex = (manifest.extract ?? []).findIndex((step) => step.plugin === "git");
676
+ const extract = isolated ? [] : [...(manifest.extract ?? [])];
677
+ if (plan.fetch) {
678
+ if (!builtinPluginNames().has("git") && !manifest.plugins?.some((plugin) => plugin.name === "git")) throw new Error("The built-in git fetcher is not available.");
679
+ if (!isolated && fetchIndex >= 0) {
680
+ const fetchStep = extract[fetchIndex];
681
+ extract[fetchIndex] = { ...fetchStep, options: { ...fetchStep.options, repos: [...(fetchStep.options?.repos ?? []), plan.fetch] } };
682
+ } else {
683
+ extract.unshift({ plugin: "git", in: "vendor", out: "vendor/repos", options: { cache: "vendor/repos", repos: [plan.fetch] } });
684
+ }
685
+ }
686
+ extract.push(...plan.steps);
687
+ return {
688
+ ...manifest,
689
+ projects: isolated ? [plan.project] : [...(manifest.projects ?? []), plan.project],
690
+ sources: isolated
691
+ ? [...(plan.fetch ? ["vendor/repos/*/*/git.repo.json"] : []), plan.source]
692
+ : [...new Set([...(manifest.sources ?? []), ...(plan.fetch ? ["vendor/repos/*/*/git.repo.json"] : []), plan.source])],
693
+ extract,
694
+ ...(isolated ? { verify: [], generate: [] } : {}),
695
+ };
696
+ }
697
+
698
+ function writeManifest(path, manifest) {
699
+ const staging = mkdtempSync(join(dirname(path), ".portolan-manifest-"));
700
+ const temp = join(staging, "portolan.json");
701
+ try {
702
+ writeFileSync(temp, `${JSON.stringify(manifest, null, 2)}\n`, { flag: "wx" });
703
+ const validation = loadManifest(temp);
704
+ if (validation.problems.length) throw new Error(validation.problems.join("\n"));
705
+ renameSync(temp, path);
706
+ } finally {
707
+ rmSync(staging, { recursive: true, force: true });
708
+ }
709
+ }
710
+
711
+ export function writeProject(workspace, request) {
712
+ const manifestPath = join(workspace, "portolan.json");
713
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
714
+ const plan = planProject(workspace, manifest, request);
715
+ writeManifest(manifestPath, manifestWithProject(manifest, plan));
716
+ return plan;
717
+ }
718
+
719
+ function setup(workspace) {
720
+ const manifestText = readFileSync(join(workspace, "portolan.json"), "utf8");
721
+ let report;
722
+ try { report = JSON.parse(readFileSync(join(workspace, ".portolan/build-report.json"), "utf8")); } catch {}
723
+ return publicSetupFrom(JSON.parse(manifestText), report, createHash("sha256").update(manifestText).digest("hex"));
724
+ }
725
+
726
+ function send(res, status, value) {
727
+ res.statusCode = status;
728
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
729
+ res.end(`${JSON.stringify(value)}\n`);
730
+ }
731
+
732
+ async function body(req) {
733
+ let text = "";
734
+ for await (const chunk of req) {
735
+ text += chunk;
736
+ if (text.length > 64 * 1024) throw new Error("Request body is too large.");
737
+ }
738
+ return text ? JSON.parse(text) : {};
739
+ }
740
+
741
+ function localRequest(req) {
742
+ const address = req.socket.remoteAddress ?? "";
743
+ const localAddress = address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
744
+ const authority = String(req.headers.host ?? "");
745
+ const host = authority.startsWith("[") ? authority.slice(1, authority.indexOf("]")) : authority.split(":")[0];
746
+ const localHost = ["localhost", "127.0.0.1", "::1"].includes(host);
747
+ let localOrigin = true;
748
+ if (req.headers.origin) {
749
+ try { localOrigin = ["localhost", "127.0.0.1", "::1"].includes(new URL(req.headers.origin).hostname); }
750
+ catch { localOrigin = false; }
751
+ }
752
+ return localAddress && localHost && localOrigin;
753
+ }
754
+
755
+ function emit(job, event) {
756
+ const enriched = { at: new Date().toISOString(), ...event };
757
+ job.events.push(enriched);
758
+ for (const response of job.subscribers) response.write(`data: ${JSON.stringify(enriched)}\n\n`);
759
+ }
760
+
761
+ function feed(job, stream, chunk) {
762
+ job.buffers[stream] += String(chunk);
763
+ const lines = job.buffers[stream].split(/\r?\n/);
764
+ job.buffers[stream] = lines.pop() ?? "";
765
+ for (const line of lines) {
766
+ if (!line) continue;
767
+ if (line.startsWith(GENERATOR_EVENT_PREFIX)) {
768
+ try { emit(job, JSON.parse(line.slice(GENERATOR_EVENT_PREFIX.length))); } catch { emit(job, { type: "log", stream, message: line }); }
769
+ } else emit(job, { type: "log", stream, message: line });
770
+ }
771
+ }
772
+
773
+ function workspaceFingerprint(workspace) {
774
+ const hash = createHash("sha256");
775
+ try {
776
+ const options = { cwd: workspace, encoding: "buffer", maxBuffer: 128 * 1024 * 1024 };
777
+ hash.update(execFileSync("git", ["diff", "--binary", "HEAD", "--", "."], options));
778
+ const untracked = execFileSync("git", ["ls-files", "--others", "--exclude-standard", "-z"], options)
779
+ .toString().split("\0").filter(Boolean).sort();
780
+ for (const name of untracked) {
781
+ hash.update(name); hash.update("\0");
782
+ try { hash.update(readFileSync(join(workspace, name))); } catch {}
783
+ }
784
+ } catch {
785
+ const pending = [workspace];
786
+ const files = [];
787
+ while (pending.length) {
788
+ const dir = pending.pop();
789
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
790
+ if (SNAPSHOT_SKIP.has(entry.name) || entry.name === "build" && relative(workspace, dir).startsWith("plugins")) continue;
791
+ const path = join(dir, entry.name);
792
+ if (entry.isDirectory()) pending.push(path);
793
+ else if (entry.isFile()) files.push(path);
794
+ }
795
+ }
796
+ for (const path of files.sort()) { hash.update(relative(workspace, path)); hash.update("\0"); hash.update(readFileSync(path)); }
797
+ }
798
+ return hash.digest("hex");
799
+ }
800
+
801
+ function snapshotWorkspace(workspace) {
802
+ const holder = mkdtempSync(join(tmpdir(), "portolan-preview-"));
803
+ const snapshot = join(holder, "workspace");
804
+ cpSync(workspace, snapshot, {
805
+ recursive: true,
806
+ filter(source) {
807
+ const name = relative(workspace, source).replaceAll(sep, "/");
808
+ if (!name) return true;
809
+ return !name.split("/").some((segment) => SNAPSHOT_SKIP.has(segment) || segment === "build" && name.startsWith("plugins/"));
810
+ },
811
+ });
812
+ symlinkSync(join(workspace, "node_modules"), join(snapshot, "node_modules"), "dir");
813
+ const gitMetadata = join(workspace, ".git");
814
+ if (lstatExists(gitMetadata)) symlinkSync(gitMetadata, join(snapshot, ".git"), lstatSync(gitMetadata).isDirectory() ? "dir" : "file");
815
+ return { holder, snapshot };
816
+ }
817
+
818
+ function fileDiff(workspace, snapshot, change) {
819
+ const before = join(workspace, change.path);
820
+ const after = join(snapshot, change.path);
821
+ const left = lstatExists(before) ? before : "/dev/null";
822
+ const right = lstatExists(after) ? after : "/dev/null";
823
+ if ((lstatExists(before) && statSync(before).size > 512_000) || (lstatExists(after) && statSync(after).size > 512_000)) {
824
+ return { path: change.path, status: change.kind, diff: "Binary or large file changed; textual diff omitted." };
825
+ }
826
+ let diff = "";
827
+ try {
828
+ diff = execFileSync("git", ["diff", "--no-index", "--no-ext-diff", "--unified=3", "--", left, right], { encoding: "utf8", maxBuffer: 2 * 1024 * 1024 });
829
+ } catch (cause) {
830
+ if (cause?.status !== 1) throw cause;
831
+ diff = String(cause.stdout ?? "");
832
+ }
833
+ const portable = diff
834
+ .replaceAll(`a${before}`, `a/${change.path}`)
835
+ .replaceAll(`b${after}`, `b/${change.path}`)
836
+ .replaceAll(before, change.path)
837
+ .replaceAll(after, change.path);
838
+ return { path: change.path, status: change.kind, diff: portable.slice(0, 200_000) };
839
+ }
840
+
841
+ export function diffGeneratedFiles(workspace, snapshot, events) {
842
+ const changes = new Map();
843
+ for (const event of events) {
844
+ if (event.type !== "step-finished") continue;
845
+ for (const change of event.changes ?? []) changes.set(change.path, change);
846
+ }
847
+ const all = [...changes.values()];
848
+ let remaining = 2 * 1024 * 1024;
849
+ let contentTruncated = false;
850
+ const shown = all.slice(0, 100).map((change) => {
851
+ const file = fileDiff(workspace, snapshot, change);
852
+ if (file.diff.length > remaining) { file.diff = `${file.diff.slice(0, Math.max(0, remaining))}\n… diff truncated`; contentTruncated = true; }
853
+ remaining = Math.max(0, remaining - file.diff.length);
854
+ return file;
855
+ });
856
+ return { files: shown, totalFiles: all.length, truncated: all.length > shown.length || contentTruncated };
857
+ }
858
+
859
+ const TRIAL_FACTS = [
860
+ ["contexts", "contexts"],
861
+ ["services", "components"],
862
+ ["contracts", "API contracts"],
863
+ ["apiOperations", "API operations"],
864
+ ["integrations", "integrations"],
865
+ ["aggregates", "aggregates"],
866
+ ["entities", "entities"],
867
+ ["domainOperations", "domain operations"],
868
+ ["events", "domain events"],
869
+ ["channels", "message channels"],
870
+ ["messages", "messages"],
871
+ ["stores", "data stores"],
872
+ ["tables", "tables"],
873
+ ["keyPatterns", "key patterns"],
874
+ ["flows", "flows"],
875
+ ["adrs", "ADRs"],
876
+ ["terms", "glossary terms"],
877
+ ];
878
+
879
+ /** Summarise the catalog facts written by a project's selected extractors. */
880
+ export function summarizeProjectTrial(snapshot, plan, events) {
881
+ const stepOutputs = new Set(plan.steps.map((step) => step.out));
882
+ const steps = events
883
+ .filter((event) => event.type === "step-finished" && event.phase === "extract" && stepOutputs.has(event.output) && plan.plugins.includes(event.plugin))
884
+ .map((event) => ({
885
+ plugin: event.plugin,
886
+ status: event.status,
887
+ durationMs: event.durationMs,
888
+ fileCount: event.fileCount,
889
+ changedCount: event.changedCount,
890
+ warnings: event.warnings ?? [],
891
+ ...(event.message ? { message: event.message } : {}),
892
+ }));
893
+ const facts = new Map(TRIAL_FACTS.map(([key]) => [key, new Set()]));
894
+ const add = (key, id) => { if (id !== undefined && id !== null && String(id)) facts.get(key)?.add(String(id)); };
895
+ const visitService = (service, contextId = "") => {
896
+ const serviceId = service.id ?? `${contextId}/${service.slug ?? service.name ?? "service"}`;
897
+ add("services", serviceId);
898
+ for (const contract of service.provides ?? []) {
899
+ const contractId = `${serviceId}/${contract.id ?? contract.name ?? "contract"}`;
900
+ add("contracts", contractId);
901
+ for (const method of contract.methods ?? []) add("apiOperations", `${contractId}/${method.name ?? method.id}`);
902
+ }
903
+ for (const integration of service.consumes ?? []) add("integrations", `${serviceId}/${integration.id ?? integration.peer ?? JSON.stringify(integration)}`);
904
+ for (const aggregate of service.aggregates ?? []) {
905
+ add("aggregates", aggregate.id ?? `${serviceId}/${aggregate.slug ?? aggregate.name}`);
906
+ for (const entity of aggregate.entities ?? []) add("entities", entity.id ?? `${serviceId}/${entity.name}`);
907
+ for (const event of aggregate.events ?? []) add("events", event.id ?? `${serviceId}/${event.name}`);
908
+ for (const operation of aggregate.operations ?? []) add("domainOperations", `${serviceId}/${aggregate.id ?? "aggregate"}/${operation.name ?? operation.id}`);
909
+ }
910
+ for (const channel of service.channels ?? []) {
911
+ const channelId = `${serviceId}/${channel.address ?? channel.name ?? "channel"}`;
912
+ add("channels", channelId);
913
+ for (const message of channel.messages ?? []) add("messages", `${channelId}/${message.name ?? message.id}`);
914
+ }
915
+ };
916
+ const files = [...new Set(events
917
+ .filter((event) => event.type === "step-finished" && event.phase === "extract" && stepOutputs.has(event.output) && plan.plugins.includes(event.plugin))
918
+ .flatMap((event) => event.files ?? []))];
919
+ for (const name of files) {
920
+ let fragment;
921
+ try { fragment = JSON.parse(readFileSync(join(snapshot, name), "utf8")); } catch { continue; }
922
+ for (const context of fragment.contexts ?? []) {
923
+ const contextId = context.id ?? context.slug ?? context.name;
924
+ add("contexts", contextId);
925
+ for (const service of context.services ?? []) visitService(service, contextId);
926
+ }
927
+ for (const store of fragment.stores ?? []) {
928
+ const storeId = store.id ?? store.slug ?? store.name;
929
+ add("stores", storeId);
930
+ for (const table of store.tables ?? []) add("tables", `${storeId}/${table.id ?? table.name}`);
931
+ for (const pattern of store.keyspaces ?? store.keyPatterns ?? store.keys ?? []) add("keyPatterns", `${storeId}/${pattern.id ?? pattern.pattern ?? pattern.name ?? JSON.stringify(pattern)}`);
932
+ }
933
+ for (const flow of fragment.flows ?? []) add("flows", flow.id ?? flow.slug ?? flow.name);
934
+ for (const adr of fragment.adrs ?? []) add("adrs", adr.id ?? adr.slug ?? adr.title);
935
+ for (const term of fragment.terms ?? []) add("terms", term.id ?? term.slug ?? term.name);
936
+ }
937
+ const warnings = steps.flatMap((step) => step.warnings.map((message) => ({ plugin: step.plugin, message })));
938
+ return {
939
+ steps,
940
+ facts: TRIAL_FACTS.map(([key, label]) => ({ key, label, count: facts.get(key)?.size ?? 0 })).filter((fact) => fact.count > 0),
941
+ warnings,
942
+ generatedFiles: files.length,
943
+ };
944
+ }
945
+
946
+ function prepareProjectTrial(workspace, request) {
947
+ const manifest = JSON.parse(readFileSync(join(workspace, "portolan.json"), "utf8"));
948
+ const plan = planProject(workspace, manifest, request);
949
+ const fingerprint = workspaceFingerprint(workspace);
950
+ const snapshot = snapshotWorkspace(workspace);
951
+ if (workspaceFingerprint(workspace) !== fingerprint) {
952
+ rmSync(snapshot.holder, { recursive: true, force: true });
953
+ throw new Error("Files changed while the trial workspace was being created. Try again.");
954
+ }
955
+ writeManifest(join(snapshot.snapshot, "portolan.json"), manifestWithProject(manifest, plan, { isolated: true }));
956
+ return { ...snapshot, fingerprint, plan };
957
+ }
958
+
959
+ function freeLocalPort() {
960
+ return new Promise((resolvePort, reject) => {
961
+ const server = createNetServer();
962
+ server.unref();
963
+ server.once("error", reject);
964
+ server.listen(0, "127.0.0.1", () => {
965
+ const address = server.address();
966
+ const port = typeof address === "object" && address ? address.port : 0;
967
+ server.close((error) => error ? reject(error) : resolvePort(port));
968
+ });
969
+ });
970
+ }
971
+
972
+ async function waitForPreview(url, child) {
973
+ for (let attempt = 0; attempt < 80; attempt += 1) {
974
+ if (child.previewError) throw child.previewError;
975
+ if (child.exitCode !== null) throw new Error("The preview server stopped before it became ready.");
976
+ try {
977
+ const response = await fetch(url, { signal: AbortSignal.timeout(500) });
978
+ if (response.ok) return;
979
+ } catch {}
980
+ await new Promise((done) => setTimeout(done, 100));
981
+ }
982
+ throw new Error("The preview server did not become ready in time.");
983
+ }
984
+
985
+ function disposeProjectTrial(job) {
986
+ if (job.previewTimer) { clearTimeout(job.previewTimer); job.previewTimer = null; }
987
+ if (job.previewChild?.exitCode === null) job.previewChild.kill("SIGTERM");
988
+ job.previewChild = null;
989
+ job.previewUrl = null;
990
+ if (job.snapshotHolder && lstatExists(job.snapshotHolder)) rmSync(job.snapshotHolder, { recursive: true, force: true });
991
+ job.snapshotHolder = null;
992
+ job.runRoot = null;
993
+ }
994
+
995
+ async function startProjectPreview(job) {
996
+ const port = await freeLocalPort();
997
+ const origin = `http://127.0.0.1:${port}`;
998
+ const cli = process.env.PORTOLAN_CLI;
999
+ const command = cli ? process.execPath : join(job.runRoot, "node_modules/vite/bin/vite.js");
1000
+ const args = cli
1001
+ ? [cli, "dev", "--cwd", job.runRoot, "--host", "127.0.0.1", "--port", String(port)]
1002
+ : ["--host", "127.0.0.1", "--port", String(port), "--strictPort"];
1003
+ const child = spawn(command, args, {
1004
+ cwd: job.runRoot,
1005
+ env: { ...process.env, PORTOLAN_PROJECT_PREVIEW: "1", ...(cli ? { PORTOLAN_CLI: cli } : {}) },
1006
+ stdio: "ignore",
1007
+ });
1008
+ child.once("error", (error) => { child.previewError = error; });
1009
+ job.previewChild = child;
1010
+ await waitForPreview(origin, child);
1011
+ const context = job.projectPlan.project.group ?? job.projectPlan.discovery.defaults.group;
1012
+ const component = job.projectPlan.project.component ?? job.projectPlan.discovery.defaults.component;
1013
+ job.previewUrl = `${origin}/c/${encodeURIComponent(context)}/${encodeURIComponent(component)}`;
1014
+ job.previewTimer = setTimeout(() => disposeProjectTrial(job), PROJECT_PREVIEW_TTL_MS);
1015
+ job.previewTimer.unref();
1016
+ return job.previewUrl;
1017
+ }
1018
+
1019
+ function startJob(workspace, mode, approvedPreview, preparedTrial) {
1020
+ const id = randomUUID();
1021
+ const preview = mode === "preview" || mode === "project-preview";
1022
+ const fingerprint = preparedTrial?.fingerprint ?? (preview ? workspaceFingerprint(workspace) : approvedPreview?.fingerprint);
1023
+ const snapshot = preparedTrial ?? (preview ? snapshotWorkspace(workspace) : null);
1024
+ if (preview && !preparedTrial && workspaceFingerprint(workspace) !== fingerprint) {
1025
+ rmSync(snapshot.holder, { recursive: true, force: true });
1026
+ throw new Error("Files changed while the preview workspace was being created. Try again.");
1027
+ }
1028
+ const generatedAt = preview ? new Date().toISOString() : approvedPreview?.generatedAt;
1029
+ const gitAuth = gitAuthEnvironment();
1030
+ const job = { id, mode, status: "running", events: [], subscribers: new Set(), buffers: { stdout: "", stderr: "" }, child: null, gitAuth, runRoot: snapshot?.snapshot ?? workspace, snapshotHolder: snapshot?.holder ?? null, fingerprint, generatedAt, projectPlan: preparedTrial?.plan ?? null, projectRequest: preparedTrial ? structuredClone(preparedTrial.request) : null };
1031
+ jobs.set(id, job);
1032
+ const cli = process.env.PORTOLAN_CLI;
1033
+ const command = cli ? process.execPath : process.platform === "win32" ? "npm.cmd" : "npm";
1034
+ const args = cli
1035
+ ? [cli, mode === "check" ? "check" : "generate", "--cwd", job.runRoot]
1036
+ : ["run", mode === "check" ? "gen:check" : "gen"];
1037
+ let child;
1038
+ try {
1039
+ child = spawn(command, args, {
1040
+ cwd: job.runRoot,
1041
+ env: { ...gitAuth.env, PORTOLAN_EVENTS: "1", ...(generatedAt ? { PORTOLAN_GENERATED_AT: generatedAt } : {}) },
1042
+ stdio: ["ignore", "pipe", "pipe"],
1043
+ detached: process.platform !== "win32",
1044
+ });
1045
+ } catch (cause) {
1046
+ gitAuth.dispose();
1047
+ jobs.delete(id);
1048
+ throw cause;
1049
+ }
1050
+ job.child = child;
1051
+ emit(job, { type: "run-started", runId: id, mode });
1052
+ child.stdout.on("data", (chunk) => feed(job, "stdout", chunk));
1053
+ child.stderr.on("data", (chunk) => feed(job, "stderr", chunk));
1054
+ child.on("error", (error) => emit(job, { type: "run-finished", status: "failed", message: error.message }));
1055
+ child.on("close", async (code, signal) => {
1056
+ job.gitAuth.dispose();
1057
+ job.gitAuth = null;
1058
+ for (const stream of ["stdout", "stderr"]) if (job.buffers[stream]) emit(job, { type: "log", stream, message: job.buffers[stream] });
1059
+ job.status = signal ? "cancelled" : code === 0 ? "ok" : "failed";
1060
+ if (preview && job.status !== "cancelled") {
1061
+ try { job.preview = diffGeneratedFiles(workspace, job.runRoot, job.events); emit(job, { type: "preview-ready", ...job.preview }); }
1062
+ catch (cause) { job.status = "failed"; emit(job, { type: "log", stream: "stderr", message: `Could not build preview: ${cause instanceof Error ? cause.message : String(cause)}` }); }
1063
+ }
1064
+ if (mode === "project-preview" && job.status === "ok") {
1065
+ try {
1066
+ job.trial = summarizeProjectTrial(job.runRoot, job.projectPlan, job.events);
1067
+ try { job.trial.previewUrl = await startProjectPreview(job); }
1068
+ catch (cause) { job.trial.previewError = cause instanceof Error ? cause.message : String(cause); }
1069
+ emit(job, { type: "project-trial-ready", plan: job.projectPlan, ...job.trial });
1070
+ }
1071
+ catch (cause) { job.status = "failed"; emit(job, { type: "log", stream: "stderr", message: `Could not summarise project trial: ${cause instanceof Error ? cause.message : String(cause)}` }); }
1072
+ }
1073
+ emit(job, { type: "process-finished", status: job.status, code, signal });
1074
+ for (const response of job.subscribers) response.end();
1075
+ job.subscribers.clear();
1076
+ if (job.snapshotHolder && !(mode === "project-preview" && job.status === "ok" && job.previewUrl)) disposeProjectTrial(job);
1077
+ });
1078
+ return job;
1079
+ }
1080
+
1081
+ function startProjectTrial(workspace, request) {
1082
+ const prepared = prepareProjectTrial(workspace, request);
1083
+ prepared.request = request;
1084
+ try { return startJob(workspace, "project-preview", null, prepared); }
1085
+ catch (cause) { rmSync(prepared.holder, { recursive: true, force: true }); throw cause; }
1086
+ }
1087
+
1088
+ export function localApiPlugin(workspace = process.cwd()) {
1089
+ return {
1090
+ name: "portolan-local-api",
1091
+ apply: "serve",
1092
+ configureServer(server) {
1093
+ server.middlewares.use(async (req, res, next) => {
1094
+ const url = new URL(req.url ?? "/", "http://localhost");
1095
+ if (!url.pathname.startsWith(LOCAL_API_PREFIX)) return next();
1096
+ if (!localRequest(req)) return send(res, 403, { error: "The local API is available only through localhost." });
1097
+ try {
1098
+ if (req.method === "GET" && url.pathname === `${LOCAL_API_PREFIX}/status`) {
1099
+ const active = [...jobs.values()].find((job) => job.status === "running");
1100
+ return send(res, 200, { local: true, workspace: realpathSync(workspace), setup: setup(workspace), activeRun: active ? { id: active.id, mode: active.mode } : null });
1101
+ }
1102
+ const eventMatch = url.pathname.match(/^\/__portolan\/runs\/([^/]+)\/events$/);
1103
+ if (req.method === "GET" && eventMatch) {
1104
+ const job = jobs.get(eventMatch[1]);
1105
+ if (!job) return send(res, 404, { error: "Generation run not found." });
1106
+ res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
1107
+ for (const event of job.events) res.write(`data: ${JSON.stringify(event)}\n\n`);
1108
+ if (job.status === "running") job.subscribers.add(res); else res.end();
1109
+ req.on("close", () => job.subscribers.delete(res));
1110
+ return;
1111
+ }
1112
+ if (req.method !== "POST" || req.headers["content-type"]?.split(";")[0] !== "application/json" || req.headers["x-portolan-local"] !== "1") {
1113
+ return send(res, 405, { error: "Use a local JSON request." });
1114
+ }
1115
+ const input = await body(req);
1116
+ if (url.pathname === `${LOCAL_API_PREFIX}/source`) return send(res, 200, readLocalSource(workspace, input.path));
1117
+ if (url.pathname === `${LOCAL_API_PREFIX}/repositories/credentials`) return send(res, 201, storeRepositoryCredential(input));
1118
+ if (url.pathname === `${LOCAL_API_PREFIX}/repositories/credentials/forget`) return send(res, 200, forgetRepositoryCredential(input));
1119
+ if (url.pathname === `${LOCAL_API_PREFIX}/repositories/prepare`) return send(res, 200, prepareRepository(workspace, input));
1120
+ if (url.pathname === `${LOCAL_API_PREFIX}/discover`) return send(res, 200, discoverProject(workspace, input.path));
1121
+ if (url.pathname === `${LOCAL_API_PREFIX}/projects/trials`) {
1122
+ if ([...jobs.values()].some((job) => job.status === "running")) return send(res, 409, { error: "A generator run is already active." });
1123
+ const job = startProjectTrial(workspace, input);
1124
+ return send(res, 202, { runId: job.id, mode: job.mode, plan: job.projectPlan });
1125
+ }
1126
+ const applyTrialMatch = url.pathname.match(/^\/__portolan\/projects\/trials\/([^/]+)\/apply$/);
1127
+ if (applyTrialMatch) {
1128
+ if ([...jobs.values()].some((job) => job.status === "running")) return send(res, 409, { error: "A generator run is already active." });
1129
+ const trial = jobs.get(applyTrialMatch[1]);
1130
+ if (!trial?.projectRequest || !trial.trial || trial.status !== "ok") return send(res, 409, { error: "Run a successful project trial before adding it." });
1131
+ if (trial.applied) return send(res, 409, { error: "This project trial was already applied." });
1132
+ if (workspaceFingerprint(workspace) !== trial.fingerprint) return send(res, 409, { error: "Files changed after this project trial. Run it again." });
1133
+ disposeProjectTrial(trial);
1134
+ const result = writeProject(workspace, trial.projectRequest);
1135
+ trial.applied = true;
1136
+ const generation = input.generate ? startJob(workspace, "write", trial) : null;
1137
+ return send(res, 201, { ...result, setup: setup(workspace), run: generation ? { runId: generation.id, mode: generation.mode } : null });
1138
+ }
1139
+ const disposeTrialMatch = url.pathname.match(/^\/__portolan\/projects\/trials\/([^/]+)\/dispose$/);
1140
+ if (disposeTrialMatch) {
1141
+ const trial = jobs.get(disposeTrialMatch[1]);
1142
+ if (!trial?.projectRequest) return send(res, 404, { error: "Project trial not found." });
1143
+ if (trial.status === "running") return send(res, 409, { error: "Cancel the running project trial first." });
1144
+ disposeProjectTrial(trial);
1145
+ return send(res, 200, { runId: trial.id, status: "disposed" });
1146
+ }
1147
+ if (url.pathname === `${LOCAL_API_PREFIX}/projects/preview`) {
1148
+ const manifest = JSON.parse(readFileSync(join(workspace, "portolan.json"), "utf8"));
1149
+ return send(res, 200, planProject(workspace, manifest, input));
1150
+ }
1151
+ if (url.pathname === `${LOCAL_API_PREFIX}/projects`) {
1152
+ const result = writeProject(workspace, input);
1153
+ return send(res, 201, { ...result, setup: setup(workspace) });
1154
+ }
1155
+ if (url.pathname === `${LOCAL_API_PREFIX}/runs`) {
1156
+ if ([...jobs.values()].some((job) => job.status === "running")) return send(res, 409, { error: "A generator run is already active." });
1157
+ const mode = ["check", "preview"].includes(input.mode) ? input.mode : "write";
1158
+ let approvedPreview = null;
1159
+ if (mode === "write") {
1160
+ approvedPreview = jobs.get(input.previewRunId);
1161
+ if (!approvedPreview?.preview || approvedPreview.status !== "ok") return send(res, 409, { error: "Preview the generated diff before applying it." });
1162
+ if (workspaceFingerprint(workspace) !== approvedPreview.fingerprint) return send(res, 409, { error: "Files changed after this preview. Run the preview again." });
1163
+ }
1164
+ const job = startJob(workspace, mode, approvedPreview);
1165
+ return send(res, 202, { runId: job.id, mode });
1166
+ }
1167
+ const cancelMatch = url.pathname.match(/^\/__portolan\/runs\/([^/]+)\/cancel$/);
1168
+ if (cancelMatch) {
1169
+ const job = jobs.get(cancelMatch[1]);
1170
+ if (!job || job.status !== "running") return send(res, 404, { error: "Active generation run not found." });
1171
+ if (process.platform !== "win32" && job.child.pid) process.kill(-job.child.pid, "SIGTERM");
1172
+ else job.child.kill("SIGTERM");
1173
+ return send(res, 202, { runId: job.id, status: "cancelling" });
1174
+ }
1175
+ return send(res, 404, { error: "Local API route not found." });
1176
+ } catch (error) {
1177
+ return send(res, error instanceof LocalApiError ? error.status : 400, {
1178
+ error: error instanceof Error ? error.message : String(error),
1179
+ ...(error instanceof LocalApiError ? {
1180
+ code: error.code,
1181
+ retryable: error.retryable,
1182
+ provider: error.provider,
1183
+ host: error.host,
1184
+ credentialSupported: error.credentialSupported,
1185
+ credentialPresent: error.credentialPresent,
1186
+ } : {}),
1187
+ });
1188
+ }
1189
+ });
1190
+ },
1191
+ };
1192
+ }