From 4a0a6de25adf9fe6aa24dfa773ec4e200bbc5bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Br=C3=A4mer?= <22003767+robinbraemer@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:49:37 +0200 Subject: [PATCH 1/4] fix(cli): percent-encode literal colons in :action-suffixed paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every :action-suffixed public operation (clusters.resume, machines.suspend, installs.previewHostnames.bindFloating, and every other REST-RPC-style route) failed client-side with "Missing path parameter: resume" before any request was built. Root cause: OpenAPI paths like /clusters/{id}:resume rewrite to Effect's Express-style template /clusters/:id:resume. Effect's HttpApiClient.compilePath (node_modules/effect/src/unstable/httpapi/ HttpApiClient.ts) matches every occurrence of :word in a compiled path as a substitutable parameter, with no way to tell a literal colon from a parameter marker — it independently matched both :id and :resume, then threw because no "resume" value was ever supplied. Fix lives in the generator, not generated CLI code, per this repo's "add support to the OpenAPI producer or generator; never hide a contract gap with a manual endpoint implementation" rule. Patched @effect/openapi-generator's toHttpApiPath (both dist and src, via `bun patch`) to percent-encode literal colons in the raw OpenAPI path before rewriting {param} to :param, so only the colons we just introduced remain unescaped. Effect's compilePath then treats the %3A-encoded action suffix as inert literal text instead of a second path parameter, reconstructing the exact original path on request. Rationale: a colon substituted through Effect's params regex can never survive as a literal ':' in the output — compilePath's replacer always emits `${slash}${encodeURIComponent(value)}`, which strips the matched colon and encodeURIComponent-escapes whatever value it's given. The only way to get an un-mangled literal colon into the compiled URL is for it to never match the params regex in the first place, which requires it not be a bare `:` followed by a word character anywhere in the template. Rejected: supplying the action name as a synthetic second path param (e.g. `{ id, resume: "resume" }`) — traced through compilePath's actual replace logic and confirmed by direct testing that this drops the separating colon entirely (`/clusters/clu_xxxresume`, not `/clusters/clu_xxx:resume`), so it cannot reproduce the real request. Bypassing Effect's generic client and hand-building requests for these routes — the CLI has no custom request-building layer today (PublicApiClientLive uses HttpApiClient.make(PublicApi, ...) directly); adding one purely to work around this would be a much larger, generator-contract-violating change for a problem the generator itself can encode correctly. Verified server compatibility by testing this repo's Hono router (the same pattern this monorepo already uses server-side in packages/api-runtime/src/colon-method-params.ts to solve the mirror problem on the routing side): a literal `:resume` suffix and a percent-encoded `%3Aresume` suffix produce an identical captured param value, confirming Hono decodes %3A before route matching, so the request is unchanged on the wire. Tested: bun test (167 pass, including two new end-to-end regression tests exercising clusters.resume and machines.resume through the real generatedCommandView command-execution path with an intercepted fetch, asserting the exact compiled request URL); confirmed RED ("Missing path parameter: resume") before the patch and GREEN after; bun run generate (regenerated src/generated/openapi-api.gen.ts, touching every :action-suffixed operation in the public API, proving the fix is systemic); bun run generate:check; bun run build; mise run check. --- ...t%2Fopenapi-generator@4.0.0-beta.106.patch | 61 +++++++--- src/generated/openapi-api.gen.ts | 108 +++++++++--------- test/generated-command.test.ts | 71 ++++++++++++ 3 files changed, 172 insertions(+), 68 deletions(-) diff --git a/patches/@effect%2Fopenapi-generator@4.0.0-beta.106.patch b/patches/@effect%2Fopenapi-generator@4.0.0-beta.106.patch index 7f8081d..6679d31 100644 --- a/patches/@effect%2Fopenapi-generator@4.0.0-beta.106.patch +++ b/patches/@effect%2Fopenapi-generator@4.0.0-beta.106.patch @@ -1,7 +1,19 @@ +diff --git a/node_modules/@effect/openapi-generator/.bun-tag-d2163dbfd250c038 b/.bun-tag-d2163dbfd250c038 +new file mode 100644 +index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/dist/HttpApiTransformer.js b/dist/HttpApiTransformer.js -index aad332ba5abf58db21178942761dee147b382235..49c363e2070a2e8ab21b42f83706fc6241e5c683 100644 +index aad332ba5abf58db21178942761dee147b382235..4c062f5928cc75c85de4186d60c32d221242668d 100644 --- a/dist/HttpApiTransformer.js +++ b/dist/HttpApiTransformer.js +@@ -149,7 +149,7 @@ const renderPayload = operation => { + return; + } + if (operation.requestBody?.required === false) { +- payloads.unshift("HttpApiSchema.NoContent"); ++ payloads.push("HttpApiSchema.NoContent"); + } + return joinSchemas(payloads); + }; @@ -165,11 +165,11 @@ const renderResponseSet = (responses, target) => { continue; } @@ -29,6 +41,15 @@ index aad332ba5abf58db21178942761dee147b382235..49c363e2070a2e8ab21b42f83706fc62 return `HttpApiSchema.StreamSse(${options})`; } if (media.effectStream === "uint8array") { +@@ -359,7 +361,7 @@ const renderSecurityScheme = securityScheme => { + return source; + }; + const toOperationKey = operation => `${operation.method}:${operation.path}`; +-const toHttpApiPath = path => path.replace(/{([^}]+)}/g, ":$1"); ++const toHttpApiPath = path => path.replace(/:/g, "%3A").replace(/{([^}]+)}/g, ":$1"); + const toStatus = status => { + if (!/^\d{3}$/.test(status)) { + return; diff --git a/dist/OpenApiGenerator.js b/dist/OpenApiGenerator.js index a1c2dce0a23131373824e1a0f11fdfb361faabb5..4207d1022f8b6741ddcd47ed95b875dc20ec3529 100644 --- a/dist/OpenApiGenerator.js @@ -123,23 +144,35 @@ index a1c2dce0a23131373824e1a0f11fdfb361faabb5..4207d1022f8b6741ddcd47ed95b875dc return true; } continue; -diff --git a/dist/HttpApiTransformer.js b/dist/HttpApiTransformer.js ---- a/dist/HttpApiTransformer.js -+++ b/dist/HttpApiTransformer.js -@@ -149,7 +149,7 @@ const renderPayload = operation => { - return; - } - if (operation.requestBody?.required === false) { -- payloads.unshift("HttpApiSchema.NoContent"); -+ payloads.push("HttpApiSchema.NoContent"); - } - return joinSchemas(payloads); - }; diff --git a/src/HttpApiTransformer.ts b/src/HttpApiTransformer.ts +index 02be5759b1bcbc48da4a3e72cb2a852c9143558e..b71826d7f02e66659737d726327656d032cd2203 100644 --- a/src/HttpApiTransformer.ts +++ b/src/HttpApiTransformer.ts -@@ -252,3 +252,3 @@ const renderPayload = (operation: ParsedOperation): string | undefined => { +@@ -250,7 +250,7 @@ const renderPayload = (operation: ParsedOperation): string | undefined => { + } + if (operation.requestBody?.required === false) { - payloads.unshift("HttpApiSchema.NoContent") + payloads.push("HttpApiSchema.NoContent") } + + return joinSchemas(payloads) +@@ -513,7 +513,17 @@ const renderSecurityScheme = (securityScheme: ParsedOpenApiSecurityScheme): stri + + const toOperationKey = (operation: ParsedOperation): string => `${operation.method}:${operation.path}` + +-const toHttpApiPath = (path: string): string => path.replace(/{([^}]+)}/g, ":$1") ++// REST-RPC-style OpenAPI paths (e.g. `/clusters/{id}:resume`) embed a literal ++// `:action` suffix alongside the `{param}` placeholder. Effect's HttpApiClient ++// path compiler treats every `:word` occurrence in a compiled endpoint path as ++// an Express-style path parameter, with no way to distinguish a literal colon ++// from a parameter marker. Percent-encoding literal colons before rewriting ++// `{param}` to `:param` keeps the parameter rewrite unambiguous: only colons ++// we just introduced remain unescaped, so the client compiler no longer ++// mistakes the literal action suffix for a second path parameter. The server ++// (and any RFC 3986-compliant router) decodes `%3A` back to `:` before route ++// matching, so the request is unchanged on the wire. ++const toHttpApiPath = (path: string): string => path.replace(/:/g, "%3A").replace(/{([^}]+)}/g, ":$1") + + const toStatus = (status: string): number | undefined => { + if (!/^\d{3}$/.test(status)) { diff --git a/src/generated/openapi-api.gen.ts b/src/generated/openapi-api.gen.ts index d5ea188..4d29fad 100644 --- a/src/generated/openapi-api.gen.ts +++ b/src/generated/openapi-api.gen.ts @@ -3706,12 +3706,12 @@ class SnippetsGroup extends HttpApiGroup.make("Snippets") .annotate(OpenApi.Identifier, "snippets.create") .annotate(OpenApi.Summary, "Create snippet") .annotate(OpenApi.Description, "Creates a reusable code snippet in the workspace. Snippets are async JavaScript functions executed in a sandboxed runtime with access to platform.request() for API calls."), - HttpApiEndpoint.get("snippetsListUsage", "/snippets:usage", { headers: SnippetsListUsageHeaders, success: SnippetsListUsage200, error: [SnippetsListUsage401.pipe(HttpApiSchema.status(401)), SnippetsListUsage403.pipe(HttpApiSchema.status(403))] }) + HttpApiEndpoint.get("snippetsListUsage", "/snippets%3Ausage", { headers: SnippetsListUsageHeaders, success: SnippetsListUsage200, error: [SnippetsListUsage401.pipe(HttpApiSchema.status(401)), SnippetsListUsage403.pipe(HttpApiSchema.status(403))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "snippets.listUsage") .annotate(OpenApi.Summary, "List snippet dashboard usage in workspace") .annotate(OpenApi.Description, "Returns dashboard references for snippets in the workspace specified in the workspace context."), - HttpApiEndpoint.get("snippetsGetUsage", "/snippets/:id:usage", { params: SnippetsGetUsagePathParams, success: SnippetsGetUsage200, error: [SnippetsGetUsage401.pipe(HttpApiSchema.status(401)), SnippetsGetUsage403.pipe(HttpApiSchema.status(403)), SnippetsGetUsage404.pipe(HttpApiSchema.status(404))] }) + HttpApiEndpoint.get("snippetsGetUsage", "/snippets/:id%3Ausage", { params: SnippetsGetUsagePathParams, success: SnippetsGetUsage200, error: [SnippetsGetUsage401.pipe(HttpApiSchema.status(401)), SnippetsGetUsage403.pipe(HttpApiSchema.status(403)), SnippetsGetUsage404.pipe(HttpApiSchema.status(404))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "snippets.getUsage") .annotate(OpenApi.Summary, "Get snippet dashboard usage") @@ -3731,12 +3731,12 @@ class SnippetsGroup extends HttpApiGroup.make("Snippets") .annotate(OpenApi.Identifier, "snippets.update") .annotate(OpenApi.Summary, "Update snippet") .annotate(OpenApi.Description, "Updates snippet properties. Only provided fields are changed. Dashboards using this snippet will pick up code changes on next run."), - HttpApiEndpoint.post("snippetsExecute", "/snippets:execute", { headers: SnippetsExecuteHeaders, payload: [SnippetsExecuteRequestJson, HttpApiSchema.NoContent], success: SnippetsExecute200, error: [SnippetsExecute401.pipe(HttpApiSchema.status(401)), SnippetsExecute403.pipe(HttpApiSchema.status(403)), SnippetsExecute422.pipe(HttpApiSchema.status(422))] }) + HttpApiEndpoint.post("snippetsExecute", "/snippets%3Aexecute", { headers: SnippetsExecuteHeaders, payload: [SnippetsExecuteRequestJson, HttpApiSchema.NoContent], success: SnippetsExecute200, error: [SnippetsExecute401.pipe(HttpApiSchema.status(401)), SnippetsExecute403.pipe(HttpApiSchema.status(403)), SnippetsExecute422.pipe(HttpApiSchema.status(422))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "snippets.execute") .annotate(OpenApi.Summary, "Execute ad-hoc snippet code") .annotate(OpenApi.Description, "Executes ad-hoc JavaScript in the snippet sandbox and returns the synchronous result. Does not persist a snippet or run resource."), - HttpApiEndpoint.post("snippetsExecuteStored", "/snippets/:id:execute", { params: SnippetsExecuteStoredPathParams, payload: [SnippetsExecuteStoredRequestJson, HttpApiSchema.NoContent], success: SnippetsExecuteStored200, error: [SnippetsExecuteStored401.pipe(HttpApiSchema.status(401)), SnippetsExecuteStored404.pipe(HttpApiSchema.status(404)), SnippetsExecuteStored409.pipe(HttpApiSchema.status(409)), SnippetsExecuteStored422.pipe(HttpApiSchema.status(422))] }) + HttpApiEndpoint.post("snippetsExecuteStored", "/snippets/:id%3Aexecute", { params: SnippetsExecuteStoredPathParams, payload: [SnippetsExecuteStoredRequestJson, HttpApiSchema.NoContent], success: SnippetsExecuteStored200, error: [SnippetsExecuteStored401.pipe(HttpApiSchema.status(401)), SnippetsExecuteStored404.pipe(HttpApiSchema.status(404)), SnippetsExecuteStored409.pipe(HttpApiSchema.status(409)), SnippetsExecuteStored422.pipe(HttpApiSchema.status(422))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "snippets.executeStored") .annotate(OpenApi.Summary, "Execute stored snippet") @@ -3816,12 +3816,12 @@ class DashboardsGroup extends HttpApiGroup.make("Dashboards") .annotate(OpenApi.Identifier, "dashboards.getRevision") .annotate(OpenApi.Summary, "Get dashboard revision") .annotate(OpenApi.Description, "Returns one immutable dashboard revision and its snippet version drift."), - HttpApiEndpoint.post("dashboardsRestoreRevision", "/dashboards/:id/revisions/:revision_id:restore", { params: DashboardsRestoreRevisionPathParams, headers: DashboardsRestoreRevisionHeaders, payload: DashboardsRestoreRevisionRequestJson, success: HttpApiSchema.WithHeaders(DashboardsRestoreRevision201.pipe(HttpApiSchema.status(201)), DashboardsRestoreRevision201Headers), error: [DashboardsRestoreRevision401.pipe(HttpApiSchema.status(401)), DashboardsRestoreRevision403.pipe(HttpApiSchema.status(403)), DashboardsRestoreRevision404.pipe(HttpApiSchema.status(404)), DashboardsRestoreRevision409.pipe(HttpApiSchema.status(409)), DashboardsRestoreRevision422.pipe(HttpApiSchema.status(422))] }) + HttpApiEndpoint.post("dashboardsRestoreRevision", "/dashboards/:id/revisions/:revision_id%3Arestore", { params: DashboardsRestoreRevisionPathParams, headers: DashboardsRestoreRevisionHeaders, payload: DashboardsRestoreRevisionRequestJson, success: HttpApiSchema.WithHeaders(DashboardsRestoreRevision201.pipe(HttpApiSchema.status(201)), DashboardsRestoreRevision201Headers), error: [DashboardsRestoreRevision401.pipe(HttpApiSchema.status(401)), DashboardsRestoreRevision403.pipe(HttpApiSchema.status(403)), DashboardsRestoreRevision404.pipe(HttpApiSchema.status(404)), DashboardsRestoreRevision409.pipe(HttpApiSchema.status(409)), DashboardsRestoreRevision422.pipe(HttpApiSchema.status(422))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "dashboards.restoreRevision") .annotate(OpenApi.Summary, "Restore dashboard revision") .annotate(OpenApi.Description, "Restores an immutable snapshot by creating and activating a new dashboard revision."), - HttpApiEndpoint.post("dashboardsResetToRecommended", "/dashboards/:id:resetToRecommended", { params: DashboardsResetToRecommendedPathParams, headers: DashboardsResetToRecommendedHeaders, success: HttpApiSchema.WithHeaders(DashboardsResetToRecommended201.pipe(HttpApiSchema.status(201)), DashboardsResetToRecommended201Headers), error: [DashboardsResetToRecommended401.pipe(HttpApiSchema.status(401)), DashboardsResetToRecommended403.pipe(HttpApiSchema.status(403)), DashboardsResetToRecommended404.pipe(HttpApiSchema.status(404)), DashboardsResetToRecommended409.pipe(HttpApiSchema.status(409)), DashboardsResetToRecommended422.pipe(HttpApiSchema.status(422))] }) + HttpApiEndpoint.post("dashboardsResetToRecommended", "/dashboards/:id%3AresetToRecommended", { params: DashboardsResetToRecommendedPathParams, headers: DashboardsResetToRecommendedHeaders, success: HttpApiSchema.WithHeaders(DashboardsResetToRecommended201.pipe(HttpApiSchema.status(201)), DashboardsResetToRecommended201Headers), error: [DashboardsResetToRecommended401.pipe(HttpApiSchema.status(401)), DashboardsResetToRecommended403.pipe(HttpApiSchema.status(403)), DashboardsResetToRecommended404.pipe(HttpApiSchema.status(404)), DashboardsResetToRecommended409.pipe(HttpApiSchema.status(409)), DashboardsResetToRecommended422.pipe(HttpApiSchema.status(422))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "dashboards.resetToRecommended") .annotate(OpenApi.Summary, "Reset the Workspace overview to the recommended configuration") @@ -3874,12 +3874,12 @@ class ProductsGroup extends HttpApiGroup.make("Products") .annotate(OpenApi.Identifier, "products.update") .annotate(OpenApi.Summary, "Update product") .annotate(OpenApi.Description, "Updates Product metadata, marketplace listing state, and the package version pin for future offers and installs."), - HttpApiEndpoint.post("productsArchive", "/products/:id:archive", { params: ProductsArchivePathParams, headers: ProductsArchiveHeaders, payload: [ProductsArchiveRequestJson, HttpApiSchema.NoContent], success: ProductsArchive200, error: [ProductsArchive401.pipe(HttpApiSchema.status(401)), ProductsArchive403.pipe(HttpApiSchema.status(403)), ProductsArchive404.pipe(HttpApiSchema.status(404)), ProductsArchive409.pipe(HttpApiSchema.status(409)), ProductsArchive422.pipe(HttpApiSchema.status(422))] }) + HttpApiEndpoint.post("productsArchive", "/products/:id%3Aarchive", { params: ProductsArchivePathParams, headers: ProductsArchiveHeaders, payload: [ProductsArchiveRequestJson, HttpApiSchema.NoContent], success: ProductsArchive200, error: [ProductsArchive401.pipe(HttpApiSchema.status(401)), ProductsArchive403.pipe(HttpApiSchema.status(403)), ProductsArchive404.pipe(HttpApiSchema.status(404)), ProductsArchive409.pipe(HttpApiSchema.status(409)), ProductsArchive422.pipe(HttpApiSchema.status(422))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "products.archive") .annotate(OpenApi.Summary, "Archive product") .annotate(OpenApi.Description, "Archives the product and removes it from active marketplace listings. The row persists with state `archived` for audit/history/revenue-attribution. Active installs and past orders are unaffected; backing packages remain independently managed and unchanged. Dashboard callers can still render historical product context."), - HttpApiEndpoint.post("productsUnarchive", "/products/:id:unarchive", { params: ProductsUnarchivePathParams, headers: ProductsUnarchiveHeaders, payload: [ProductsUnarchiveRequestJson, HttpApiSchema.NoContent], success: ProductsUnarchive200, error: [ProductsUnarchive401.pipe(HttpApiSchema.status(401)), ProductsUnarchive403.pipe(HttpApiSchema.status(403)), ProductsUnarchive404.pipe(HttpApiSchema.status(404)), ProductsUnarchive409.pipe(HttpApiSchema.status(409)), ProductsUnarchive422.pipe(HttpApiSchema.status(422))] }) + HttpApiEndpoint.post("productsUnarchive", "/products/:id%3Aunarchive", { params: ProductsUnarchivePathParams, headers: ProductsUnarchiveHeaders, payload: [ProductsUnarchiveRequestJson, HttpApiSchema.NoContent], success: ProductsUnarchive200, error: [ProductsUnarchive401.pipe(HttpApiSchema.status(401)), ProductsUnarchive403.pipe(HttpApiSchema.status(403)), ProductsUnarchive404.pipe(HttpApiSchema.status(404)), ProductsUnarchive409.pipe(HttpApiSchema.status(409)), ProductsUnarchive422.pipe(HttpApiSchema.status(422))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "products.unarchive") .annotate(OpenApi.Summary, "Unarchive product") @@ -3922,7 +3922,7 @@ class ClustersGroup extends HttpApiGroup.make("Clusters") .annotate(OpenApi.Identifier, "clusters.update") .annotate(OpenApi.Summary, "Update cluster") .annotate(OpenApi.Description, "Updates editable cluster fields."), - HttpApiEndpoint.post("clustersImport", "/clusters:import", { headers: ClustersImportHeaders, payload: [ClustersImportRequestJson, HttpApiSchema.NoContent], success: ClustersImport200, error: [ClustersImport401.pipe(HttpApiSchema.status(401)), ClustersImport403.pipe(HttpApiSchema.status(403))] }) + HttpApiEndpoint.post("clustersImport", "/clusters%3Aimport", { headers: ClustersImportHeaders, payload: [ClustersImportRequestJson, HttpApiSchema.NoContent], success: ClustersImport200, error: [ClustersImport401.pipe(HttpApiSchema.status(401)), ClustersImport403.pipe(HttpApiSchema.status(403))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "clusters.import") .annotate(OpenApi.Summary, "Import cluster") @@ -3932,17 +3932,17 @@ class ClustersGroup extends HttpApiGroup.make("Clusters") .annotate(OpenApi.Identifier, "clusters.getCapabilities") .annotate(OpenApi.Summary, "Get cluster capabilities") .annotate(OpenApi.Description, "Gets the last observed Kubernetes capability snapshot for a cluster."), - HttpApiEndpoint.post("clustersRefreshCapabilities", "/clusters/:id/capabilities:refresh", { params: ClustersRefreshCapabilitiesPathParams, headers: ClustersRefreshCapabilitiesHeaders, success: ClustersRefreshCapabilities202.pipe(HttpApiSchema.status(202)), error: [ClustersRefreshCapabilities401.pipe(HttpApiSchema.status(401)), ClustersRefreshCapabilities403.pipe(HttpApiSchema.status(403)), ClustersRefreshCapabilities404.pipe(HttpApiSchema.status(404)), ClustersRefreshCapabilities409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("clustersRefreshCapabilities", "/clusters/:id/capabilities%3Arefresh", { params: ClustersRefreshCapabilitiesPathParams, headers: ClustersRefreshCapabilitiesHeaders, success: ClustersRefreshCapabilities202.pipe(HttpApiSchema.status(202)), error: [ClustersRefreshCapabilities401.pipe(HttpApiSchema.status(401)), ClustersRefreshCapabilities403.pipe(HttpApiSchema.status(403)), ClustersRefreshCapabilities404.pipe(HttpApiSchema.status(404)), ClustersRefreshCapabilities409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "clusters.refreshCapabilities") .annotate(OpenApi.Summary, "Refresh cluster capabilities") .annotate(OpenApi.Description, "Refreshes observed cluster capability facts. The API returns an Operation envelope because cluster inspection runs asynchronously."), - HttpApiEndpoint.post("clustersSuspend", "/clusters/:id:suspend", { params: ClustersSuspendPathParams, headers: ClustersSuspendHeaders, success: ClustersSuspend202.pipe(HttpApiSchema.status(202)), error: [ClustersSuspend401.pipe(HttpApiSchema.status(401)), ClustersSuspend403.pipe(HttpApiSchema.status(403)), ClustersSuspend404.pipe(HttpApiSchema.status(404)), ClustersSuspend409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("clustersSuspend", "/clusters/:id%3Asuspend", { params: ClustersSuspendPathParams, headers: ClustersSuspendHeaders, success: ClustersSuspend202.pipe(HttpApiSchema.status(202)), error: [ClustersSuspend401.pipe(HttpApiSchema.status(401)), ClustersSuspend403.pipe(HttpApiSchema.status(403)), ClustersSuspend404.pipe(HttpApiSchema.status(404)), ClustersSuspend409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "clusters.suspend") .annotate(OpenApi.Summary, "Suspend cluster") .annotate(OpenApi.Description, "Suspends a running cluster. The API returns an Operation envelope because teardown and drain are asynchronous."), - HttpApiEndpoint.post("clustersResume", "/clusters/:id:resume", { params: ClustersResumePathParams, headers: ClustersResumeHeaders, success: ClustersResume202.pipe(HttpApiSchema.status(202)), error: [ClustersResume401.pipe(HttpApiSchema.status(401)), ClustersResume403.pipe(HttpApiSchema.status(403)), ClustersResume404.pipe(HttpApiSchema.status(404)), ClustersResume409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("clustersResume", "/clusters/:id%3Aresume", { params: ClustersResumePathParams, headers: ClustersResumeHeaders, success: ClustersResume202.pipe(HttpApiSchema.status(202)), error: [ClustersResume401.pipe(HttpApiSchema.status(401)), ClustersResume403.pipe(HttpApiSchema.status(403)), ClustersResume404.pipe(HttpApiSchema.status(404)), ClustersResume409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "clusters.resume") .annotate(OpenApi.Summary, "Resume cluster") @@ -3952,12 +3952,12 @@ class ClustersGroup extends HttpApiGroup.make("Clusters") .annotate(OpenApi.Identifier, "clusters.getKubeconfig") .annotate(OpenApi.Summary, "Get cluster kubeconfig") .annotate(OpenApi.Description, "Returns a cluster admin kubeconfig for workspace owners and admins. Successful credential disclosure is fail-closed on a cluster-scoped audit record."), - HttpApiEndpoint.get("clustersProxyKube", "/clusters/:id/kube_proxy/:path:*", { params: ClustersProxyKubePathParams, headers: ClustersProxyKubeHeaders, success: HttpApiSchema.Empty(200), error: [ClustersProxyKube401.pipe(HttpApiSchema.status(401)), ClustersProxyKube403.pipe(HttpApiSchema.status(403)), ClustersProxyKube404.pipe(HttpApiSchema.status(404))] }) + HttpApiEndpoint.get("clustersProxyKube", "/clusters/:id/kube_proxy/:path%3A*", { params: ClustersProxyKubePathParams, headers: ClustersProxyKubeHeaders, success: HttpApiSchema.Empty(200), error: [ClustersProxyKube401.pipe(HttpApiSchema.status(401)), ClustersProxyKube403.pipe(HttpApiSchema.status(403)), ClustersProxyKube404.pipe(HttpApiSchema.status(404))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "clusters.proxyKube") .annotate(OpenApi.Summary, "Proxy cluster API") .annotate(OpenApi.Description, "Proxy kube-apiserver traffic through the public API. `path:*` is forwarded verbatim to upstream."), - HttpApiEndpoint.post("clustersExec", "/clusters/:id:exec", { params: ClustersExecPathParams, headers: ClustersExecHeaders, payload: [ClustersExecRequestJson, HttpApiSchema.NoContent], success: ClustersExec200, error: [ClustersExec401.pipe(HttpApiSchema.status(401)), ClustersExec403.pipe(HttpApiSchema.status(403)), ClustersExec404.pipe(HttpApiSchema.status(404)), ClustersExec409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("clustersExec", "/clusters/:id%3Aexec", { params: ClustersExecPathParams, headers: ClustersExecHeaders, payload: [ClustersExecRequestJson, HttpApiSchema.NoContent], success: ClustersExec200, error: [ClustersExec401.pipe(HttpApiSchema.status(401)), ClustersExec403.pipe(HttpApiSchema.status(403)), ClustersExec404.pipe(HttpApiSchema.status(404)), ClustersExec409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "clusters.exec") .annotate(OpenApi.Summary, "Execute command in cluster") @@ -3977,7 +3977,7 @@ class ClustersGroup extends HttpApiGroup.make("Clusters") .annotate(OpenApi.Identifier, "clusters.getWorkerBootstrap") .annotate(OpenApi.Summary, "Get cluster worker bootstrap") .annotate(OpenApi.Description, "Returns a single worker-bootstrap token by ID, including its current status and expiry timestamp."), - HttpApiEndpoint.post("clustersRevokeWorkerBootstrap", "/clusters/:id/worker_bootstraps/:wbs_id:revoke", { params: ClustersRevokeWorkerBootstrapPathParams, headers: ClustersRevokeWorkerBootstrapHeaders, success: ClustersRevokeWorkerBootstrap202.pipe(HttpApiSchema.status(202)), error: [ClustersRevokeWorkerBootstrap401.pipe(HttpApiSchema.status(401)), ClustersRevokeWorkerBootstrap403.pipe(HttpApiSchema.status(403)), ClustersRevokeWorkerBootstrap404.pipe(HttpApiSchema.status(404)), ClustersRevokeWorkerBootstrap409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("clustersRevokeWorkerBootstrap", "/clusters/:id/worker_bootstraps/:wbs_id%3Arevoke", { params: ClustersRevokeWorkerBootstrapPathParams, headers: ClustersRevokeWorkerBootstrapHeaders, success: ClustersRevokeWorkerBootstrap202.pipe(HttpApiSchema.status(202)), error: [ClustersRevokeWorkerBootstrap401.pipe(HttpApiSchema.status(401)), ClustersRevokeWorkerBootstrap403.pipe(HttpApiSchema.status(403)), ClustersRevokeWorkerBootstrap404.pipe(HttpApiSchema.status(404)), ClustersRevokeWorkerBootstrap409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "clusters.revokeWorkerBootstrap") .annotate(OpenApi.Summary, "Revoke worker bootstrap") @@ -4010,12 +4010,12 @@ class MachinesGroup extends HttpApiGroup.make("Machines") .annotate(OpenApi.Identifier, "machines.update") .annotate(OpenApi.Summary, "Update machine") .annotate(OpenApi.Description, "Updates machine metadata."), - HttpApiEndpoint.post("machinesSuspend", "/machines/:id:suspend", { params: MachinesSuspendPathParams, headers: MachinesSuspendHeaders, payload: [MachinesSuspendRequestJson, HttpApiSchema.NoContent], success: MachinesSuspend202.pipe(HttpApiSchema.status(202)), error: [MachinesSuspend401.pipe(HttpApiSchema.status(401)), MachinesSuspend403.pipe(HttpApiSchema.status(403)), MachinesSuspend404.pipe(HttpApiSchema.status(404)), MachinesSuspend409.pipe(HttpApiSchema.status(409)), MachinesSuspend503.pipe(HttpApiSchema.status(503))] }) + HttpApiEndpoint.post("machinesSuspend", "/machines/:id%3Asuspend", { params: MachinesSuspendPathParams, headers: MachinesSuspendHeaders, payload: [MachinesSuspendRequestJson, HttpApiSchema.NoContent], success: MachinesSuspend202.pipe(HttpApiSchema.status(202)), error: [MachinesSuspend401.pipe(HttpApiSchema.status(401)), MachinesSuspend403.pipe(HttpApiSchema.status(403)), MachinesSuspend404.pipe(HttpApiSchema.status(404)), MachinesSuspend409.pipe(HttpApiSchema.status(409)), MachinesSuspend503.pipe(HttpApiSchema.status(503))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "machines.suspend") .annotate(OpenApi.Summary, "Suspend machine") .annotate(OpenApi.Description, "Suspends a machine and returns a long-running operation envelope. On providers that recycle instances during suspension, the machine ID stays stable but the public IP usually changes on resume unless a floating IP is attached."), - HttpApiEndpoint.post("machinesResume", "/machines/:id:resume", { params: MachinesResumePathParams, headers: MachinesResumeHeaders, payload: [MachinesResumeRequestJson, HttpApiSchema.NoContent], success: MachinesResume202.pipe(HttpApiSchema.status(202)), error: [MachinesResume401.pipe(HttpApiSchema.status(401)), MachinesResume403.pipe(HttpApiSchema.status(403)), MachinesResume404.pipe(HttpApiSchema.status(404)), MachinesResume409.pipe(HttpApiSchema.status(409)), MachinesResume503.pipe(HttpApiSchema.status(503))] }) + HttpApiEndpoint.post("machinesResume", "/machines/:id%3Aresume", { params: MachinesResumePathParams, headers: MachinesResumeHeaders, payload: [MachinesResumeRequestJson, HttpApiSchema.NoContent], success: MachinesResume202.pipe(HttpApiSchema.status(202)), error: [MachinesResume401.pipe(HttpApiSchema.status(401)), MachinesResume403.pipe(HttpApiSchema.status(403)), MachinesResume404.pipe(HttpApiSchema.status(404)), MachinesResume409.pipe(HttpApiSchema.status(409)), MachinesResume503.pipe(HttpApiSchema.status(503))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "machines.resume") .annotate(OpenApi.Summary, "Resume machine") @@ -4096,16 +4096,16 @@ class OffersGroup extends HttpApiGroup.make("Offers") .annotate(OpenApi.Identifier, "offers.get") .annotate(OpenApi.Summary, "Get offer details") .annotate(OpenApi.Description, "Returns the full offer record including pre-fill values and email allowlist. Caller must be a member of the offer’s workspace."), - HttpApiEndpoint.get("offersResolve", "/offers:resolve", { query: OffersResolveQuery, success: OffersResolve200, error: [OffersResolve403.pipe(HttpApiSchema.status(403)), OffersResolve404.pipe(HttpApiSchema.status(404))] }) + HttpApiEndpoint.get("offersResolve", "/offers%3Aresolve", { query: OffersResolveQuery, success: OffersResolve200, error: [OffersResolve403.pipe(HttpApiSchema.status(403)), OffersResolve404.pipe(HttpApiSchema.status(404))] }) .annotate(OpenApi.Identifier, "offers.resolve") .annotate(OpenApi.Summary, "Resolve offer by short hash") .annotate(OpenApi.Description, "Resolves an offer from the customer-clicked URL `/i/`. Anonymous callers receive the publicly-safe subset of fields needed to render the landing page (product name, logo, seller name, status, tier). Authenticated callers authorized to claim through an open allowlist or a verified-email match additionally receive entitled pre-fill values. Customer resolve never returns seller owner metadata or the configured email allowlist. Authenticated callers whose email is not on a configured allowlist receive 403."), - HttpApiEndpoint.post("offersArchive", "/offers/:id:archive", { params: OffersArchivePathParams, headers: OffersArchiveHeaders, success: OffersArchive200, error: [OffersArchive401.pipe(HttpApiSchema.status(401)), OffersArchive403.pipe(HttpApiSchema.status(403)), OffersArchive404.pipe(HttpApiSchema.status(404)), OffersArchive409.pipe(HttpApiSchema.status(409)), OffersArchive422.pipe(HttpApiSchema.status(422))] }) + HttpApiEndpoint.post("offersArchive", "/offers/:id%3Aarchive", { params: OffersArchivePathParams, headers: OffersArchiveHeaders, success: OffersArchive200, error: [OffersArchive401.pipe(HttpApiSchema.status(401)), OffersArchive403.pipe(HttpApiSchema.status(403)), OffersArchive404.pipe(HttpApiSchema.status(404)), OffersArchive409.pipe(HttpApiSchema.status(409)), OffersArchive422.pipe(HttpApiSchema.status(422))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "offers.archive") .annotate(OpenApi.Summary, "Archive offer") .annotate(OpenApi.Description, "Archives the offer and blocks new redemptions. The offer row remains for history and audit; callers receive the full Offer with `status: \"archived\"`. Idempotent: already-archived offers return unchanged. If-Match is required for optimistic concurrency."), - HttpApiEndpoint.post("offersUnarchive", "/offers/:id:unarchive", { params: OffersUnarchivePathParams, headers: OffersUnarchiveHeaders, success: OffersUnarchive200, error: [OffersUnarchive401.pipe(HttpApiSchema.status(401)), OffersUnarchive403.pipe(HttpApiSchema.status(403)), OffersUnarchive404.pipe(HttpApiSchema.status(404)), OffersUnarchive409.pipe(HttpApiSchema.status(409)), OffersUnarchive422.pipe(HttpApiSchema.status(422))] }) + HttpApiEndpoint.post("offersUnarchive", "/offers/:id%3Aunarchive", { params: OffersUnarchivePathParams, headers: OffersUnarchiveHeaders, success: OffersUnarchive200, error: [OffersUnarchive401.pipe(HttpApiSchema.status(401)), OffersUnarchive403.pipe(HttpApiSchema.status(403)), OffersUnarchive404.pipe(HttpApiSchema.status(404)), OffersUnarchive409.pipe(HttpApiSchema.status(409)), OffersUnarchive422.pipe(HttpApiSchema.status(422))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "offers.unarchive") .annotate(OpenApi.Summary, "Unarchive offer") @@ -4128,27 +4128,27 @@ class OrderDraftsGroup extends HttpApiGroup.make("Order Drafts") .annotate(OpenApi.Identifier, "orderDrafts.get") .annotate(OpenApi.Summary, "Get order draft details") .annotate(OpenApi.Description, "Returns the order draft row. The order draft’s own customer sees the full record including `field_values`; workspace members of the parent offer see the row with `field_values` omitted."), - HttpApiEndpoint.post("orderDraftsCancel", "/order_drafts/:id:cancel", { params: OrderDraftsCancelPathParams, headers: OrderDraftsCancelHeaders, success: OrderDraftsCancel200, error: [OrderDraftsCancel401.pipe(HttpApiSchema.status(401)), OrderDraftsCancel403.pipe(HttpApiSchema.status(403)), OrderDraftsCancel404.pipe(HttpApiSchema.status(404)), OrderDraftsCancel409.pipe(HttpApiSchema.status(409)), OrderDraftsCancel422.pipe(HttpApiSchema.status(422))] }) + HttpApiEndpoint.post("orderDraftsCancel", "/order_drafts/:id%3Acancel", { params: OrderDraftsCancelPathParams, headers: OrderDraftsCancelHeaders, success: OrderDraftsCancel200, error: [OrderDraftsCancel401.pipe(HttpApiSchema.status(401)), OrderDraftsCancel403.pipe(HttpApiSchema.status(403)), OrderDraftsCancel404.pipe(HttpApiSchema.status(404)), OrderDraftsCancel409.pipe(HttpApiSchema.status(409)), OrderDraftsCancel422.pipe(HttpApiSchema.status(422))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "orderDrafts.cancel") .annotate(OpenApi.Summary, "Cancel order draft") .annotate(OpenApi.Description, "Terminates an in-flight order draft, returning the updated record. Allowed for the order draft’s own customer or any member of the parent offer’s workspace. Idempotent — cancelling a terminated order draft returns it unchanged. Allocated cluster resources are released and any in-flight install workflow is terminated; previously-completed work is NOT undone."), - HttpApiEndpoint.post("orderDraftsClaim", "/order_drafts/:id:claim", { params: OrderDraftsClaimPathParams, headers: OrderDraftsClaimHeaders, payload: OrderDraftsClaimRequestJson, success: OrderDraftsClaim200, error: [OrderDraftsClaim401.pipe(HttpApiSchema.status(401)), OrderDraftsClaim403.pipe(HttpApiSchema.status(403)), OrderDraftsClaim404.pipe(HttpApiSchema.status(404)), OrderDraftsClaim409.pipe(HttpApiSchema.status(409)), OrderDraftsClaim422.pipe(HttpApiSchema.status(422))] }) + HttpApiEndpoint.post("orderDraftsClaim", "/order_drafts/:id%3Aclaim", { params: OrderDraftsClaimPathParams, headers: OrderDraftsClaimHeaders, payload: OrderDraftsClaimRequestJson, success: OrderDraftsClaim200, error: [OrderDraftsClaim401.pipe(HttpApiSchema.status(401)), OrderDraftsClaim403.pipe(HttpApiSchema.status(403)), OrderDraftsClaim404.pipe(HttpApiSchema.status(404)), OrderDraftsClaim409.pipe(HttpApiSchema.status(409)), OrderDraftsClaim422.pipe(HttpApiSchema.status(422))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "orderDrafts.claim") .annotate(OpenApi.Summary, "Claim order draft") .annotate(OpenApi.Description, "Binds the calling authenticated user to an anonymous order draft using a one-time claim token (R13 + AIP-147 INPUT_ONLY)."), - HttpApiEndpoint.post("orderDraftsSelectWorkspace", "/order_drafts/:id:selectWorkspace", { params: OrderDraftsSelectWorkspacePathParams, headers: OrderDraftsSelectWorkspaceHeaders, payload: OrderDraftsSelectWorkspaceRequestJson, success: OrderDraftsSelectWorkspace200, error: [OrderDraftsSelectWorkspace401.pipe(HttpApiSchema.status(401)), OrderDraftsSelectWorkspace403.pipe(HttpApiSchema.status(403)), OrderDraftsSelectWorkspace404.pipe(HttpApiSchema.status(404)), OrderDraftsSelectWorkspace409.pipe(HttpApiSchema.status(409)), OrderDraftsSelectWorkspace422.pipe(HttpApiSchema.status(422))] }) + HttpApiEndpoint.post("orderDraftsSelectWorkspace", "/order_drafts/:id%3AselectWorkspace", { params: OrderDraftsSelectWorkspacePathParams, headers: OrderDraftsSelectWorkspaceHeaders, payload: OrderDraftsSelectWorkspaceRequestJson, success: OrderDraftsSelectWorkspace200, error: [OrderDraftsSelectWorkspace401.pipe(HttpApiSchema.status(401)), OrderDraftsSelectWorkspace403.pipe(HttpApiSchema.status(403)), OrderDraftsSelectWorkspace404.pipe(HttpApiSchema.status(404)), OrderDraftsSelectWorkspace409.pipe(HttpApiSchema.status(409)), OrderDraftsSelectWorkspace422.pipe(HttpApiSchema.status(422))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "orderDrafts.selectWorkspace") .annotate(OpenApi.Summary, "Select order workspace") .annotate(OpenApi.Description, "Selects the customer workspace for an order draft, or creates a new workspace for the order. This advances the draft from workspace selection to compute allocation."), - HttpApiEndpoint.post("orderDraftsSubmitConfigure", "/order_drafts/:id:submitConfiguration", { params: OrderDraftsSubmitConfigurePathParams, headers: OrderDraftsSubmitConfigureHeaders, payload: OrderDraftsSubmitConfigureRequestJson, success: OrderDraftsSubmitConfigure200, error: [OrderDraftsSubmitConfigure401.pipe(HttpApiSchema.status(401)), OrderDraftsSubmitConfigure403.pipe(HttpApiSchema.status(403)), OrderDraftsSubmitConfigure404.pipe(HttpApiSchema.status(404)), OrderDraftsSubmitConfigure409.pipe(HttpApiSchema.status(409)), OrderDraftsSubmitConfigure422.pipe(HttpApiSchema.status(422))] }) + HttpApiEndpoint.post("orderDraftsSubmitConfigure", "/order_drafts/:id%3AsubmitConfiguration", { params: OrderDraftsSubmitConfigurePathParams, headers: OrderDraftsSubmitConfigureHeaders, payload: OrderDraftsSubmitConfigureRequestJson, success: OrderDraftsSubmitConfigure200, error: [OrderDraftsSubmitConfigure401.pipe(HttpApiSchema.status(401)), OrderDraftsSubmitConfigure403.pipe(HttpApiSchema.status(403)), OrderDraftsSubmitConfigure404.pipe(HttpApiSchema.status(404)), OrderDraftsSubmitConfigure409.pipe(HttpApiSchema.status(409)), OrderDraftsSubmitConfigure422.pipe(HttpApiSchema.status(422))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "orderDrafts.submitConfigure") .annotate(OpenApi.Summary, "Submit configure values") .annotate(OpenApi.Description, "Persists the customer’s configure-form values on the order draft and advances it to the payment phase, returning the updated record. Values are validated against the package version’s schema."), - HttpApiEndpoint.post("orderDraftsCreateWorkerBootstrap", "/order_drafts/:id:createWorkerBootstrap", { params: OrderDraftsCreateWorkerBootstrapPathParams, payload: [OrderDraftsCreateWorkerBootstrapRequestJson, HttpApiSchema.NoContent], success: OrderDraftsCreateWorkerBootstrap200, error: [OrderDraftsCreateWorkerBootstrap401.pipe(HttpApiSchema.status(401)), OrderDraftsCreateWorkerBootstrap403.pipe(HttpApiSchema.status(403)), OrderDraftsCreateWorkerBootstrap404.pipe(HttpApiSchema.status(404)), OrderDraftsCreateWorkerBootstrap409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("orderDraftsCreateWorkerBootstrap", "/order_drafts/:id%3AcreateWorkerBootstrap", { params: OrderDraftsCreateWorkerBootstrapPathParams, payload: [OrderDraftsCreateWorkerBootstrapRequestJson, HttpApiSchema.NoContent], success: OrderDraftsCreateWorkerBootstrap200, error: [OrderDraftsCreateWorkerBootstrap401.pipe(HttpApiSchema.status(401)), OrderDraftsCreateWorkerBootstrap403.pipe(HttpApiSchema.status(403)), OrderDraftsCreateWorkerBootstrap404.pipe(HttpApiSchema.status(404)), OrderDraftsCreateWorkerBootstrap409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "orderDrafts.createWorkerBootstrap") .annotate(OpenApi.Summary, "Create order draft worker bootstrap") @@ -4189,12 +4189,12 @@ class OperationsGroup extends HttpApiGroup.make("Operations") .annotate(OpenApi.Identifier, "operations.get") .annotate(OpenApi.Summary, "Get operation details + steps") .annotate(OpenApi.Description, "Returns the operation row plus its per-step progress events. Clients poll this endpoint while `done` is false; when `done` flips true, `response` carries the typed result (`SUCCEEDED`) or `error.message` carries the failure reason. Prefer `POST /v1/operations/{id}:wait` for sync \"wait until done\" semantics — it long-polls server-side instead of asking the client to tight-poll.\n\nState for in-flight operations is eventually consistent — may lag actual execution by a few seconds. State for completed operations (`done: true`) is immutable."), - HttpApiEndpoint.post("operationsWait", "/operations/:id:wait", { params: OperationsWaitPathParams, query: OperationsWaitQuery, success: OperationsWait200, error: [OperationsWait401.pipe(HttpApiSchema.status(401)), OperationsWait403.pipe(HttpApiSchema.status(403)), OperationsWait404.pipe(HttpApiSchema.status(404))] }) + HttpApiEndpoint.post("operationsWait", "/operations/:id%3Await", { params: OperationsWaitPathParams, query: OperationsWaitQuery, success: OperationsWait200, error: [OperationsWait401.pipe(HttpApiSchema.status(401)), OperationsWait403.pipe(HttpApiSchema.status(403)), OperationsWait404.pipe(HttpApiSchema.status(404))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "operations.wait") .annotate(OpenApi.Summary, "Wait for an operation to reach a terminal state") .annotate(OpenApi.Description, "Long-polls server-side until the operation reaches a terminal state (`SUCCEEDED`, `FAILED`, `CANCELLED`) or the timeout elapses. Returns the latest `Operation` either way — check `done` to distinguish.\n\nServer-side this is a live subscription on the operation row, not a polling loop, so the response fires within milliseconds of the workflow reaching its terminal state.\n\nState for in-flight operations is eventually consistent — may lag actual execution by a few seconds. State for completed operations (`done: true`) is immutable."), - HttpApiEndpoint.post("operationsCancel", "/operations/:id:cancel", { params: OperationsCancelPathParams, headers: OperationsCancelHeaders, success: OperationsCancel202.pipe(HttpApiSchema.status(202)), error: [OperationsCancel401.pipe(HttpApiSchema.status(401)), OperationsCancel403.pipe(HttpApiSchema.status(403)), OperationsCancel404.pipe(HttpApiSchema.status(404)), OperationsCancel422.pipe(HttpApiSchema.status(422))] }) + HttpApiEndpoint.post("operationsCancel", "/operations/:id%3Acancel", { params: OperationsCancelPathParams, headers: OperationsCancelHeaders, success: OperationsCancel202.pipe(HttpApiSchema.status(202)), error: [OperationsCancel401.pipe(HttpApiSchema.status(401)), OperationsCancel403.pipe(HttpApiSchema.status(403)), OperationsCancel404.pipe(HttpApiSchema.status(404)), OperationsCancel422.pipe(HttpApiSchema.status(422))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "operations.cancel") .annotate(OpenApi.Summary, "Request operation cancellation") @@ -4247,7 +4247,7 @@ class PackagesGroup extends HttpApiGroup.make("Packages") .annotate(OpenApi.Identifier, "packages.getArtifacthubValuesSchema") .annotate(OpenApi.Summary, "Resolve Artifact Hub chart values + schema") .annotate(OpenApi.Description, "Fetches a chart's values from Artifact Hub and returns them with a JSON Schema — the chart's published schema (when present) merged over a schema inferred from the values, so the editor always has defaults to show."), - HttpApiEndpoint.post("packagesImportPublished", "/packages:import", { headers: PackagesImportPublishedHeaders, payload: [PackagesImportPublishedRequestJson, HttpApiSchema.NoContent], success: PackagesImportPublished201.pipe(HttpApiSchema.status(201)), error: [PackagesImportPublished401.pipe(HttpApiSchema.status(401)), PackagesImportPublished403.pipe(HttpApiSchema.status(403)), PackagesImportPublished409.pipe(HttpApiSchema.status(409)), PackagesImportPublished422.pipe(HttpApiSchema.status(422))] }) + HttpApiEndpoint.post("packagesImportPublished", "/packages%3Aimport", { headers: PackagesImportPublishedHeaders, payload: [PackagesImportPublishedRequestJson, HttpApiSchema.NoContent], success: PackagesImportPublished201.pipe(HttpApiSchema.status(201)), error: [PackagesImportPublished401.pipe(HttpApiSchema.status(401)), PackagesImportPublished403.pipe(HttpApiSchema.status(403)), PackagesImportPublished409.pipe(HttpApiSchema.status(409)), PackagesImportPublished422.pipe(HttpApiSchema.status(422))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "packages.importPublished") .annotate(OpenApi.Summary, "Import published package") @@ -4315,22 +4315,22 @@ class WorkspacesGroup extends HttpApiGroup.make("Workspaces") .annotate(OpenApi.Identifier, "workspaces.getManagement") .annotate(OpenApi.Summary, "Get workspace management access") .annotate(OpenApi.Description, "Returns the organization currently allowed to manage this customer-owned workspace."), - HttpApiEndpoint.post("workspacesRevokeManagement", "/workspaces/:id:revokeManagement", { params: WorkspacesRevokeManagementPathParams, headers: WorkspacesRevokeManagementHeaders, success: HttpApiSchema.Empty(204), error: [WorkspacesRevokeManagement401.pipe(HttpApiSchema.status(401)), WorkspacesRevokeManagement403.pipe(HttpApiSchema.status(403)), WorkspacesRevokeManagement404.pipe(HttpApiSchema.status(404)), WorkspacesRevokeManagement409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("workspacesRevokeManagement", "/workspaces/:id%3ArevokeManagement", { params: WorkspacesRevokeManagementPathParams, headers: WorkspacesRevokeManagementHeaders, success: HttpApiSchema.Empty(204), error: [WorkspacesRevokeManagement401.pipe(HttpApiSchema.status(401)), WorkspacesRevokeManagement403.pipe(HttpApiSchema.status(403)), WorkspacesRevokeManagement404.pipe(HttpApiSchema.status(404)), WorkspacesRevokeManagement409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "workspaces.revokeManagement") .annotate(OpenApi.Summary, "Revoke workspace management access") .annotate(OpenApi.Description, "Removes organization-level management access while preserving customer ownership and the running workspace."), - HttpApiEndpoint.post("workspacesCancelSubscription", "/workspaces/:id/subscription:cancel", { params: WorkspacesCancelSubscriptionPathParams, headers: WorkspacesCancelSubscriptionHeaders, payload: [WorkspacesCancelSubscriptionRequestJson, HttpApiSchema.NoContent], success: WorkspacesCancelSubscription202.pipe(HttpApiSchema.status(202)), error: [WorkspacesCancelSubscription401.pipe(HttpApiSchema.status(401)), WorkspacesCancelSubscription403.pipe(HttpApiSchema.status(403)), WorkspacesCancelSubscription404.pipe(HttpApiSchema.status(404)), WorkspacesCancelSubscription409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("workspacesCancelSubscription", "/workspaces/:id/subscription%3Acancel", { params: WorkspacesCancelSubscriptionPathParams, headers: WorkspacesCancelSubscriptionHeaders, payload: [WorkspacesCancelSubscriptionRequestJson, HttpApiSchema.NoContent], success: WorkspacesCancelSubscription202.pipe(HttpApiSchema.status(202)), error: [WorkspacesCancelSubscription401.pipe(HttpApiSchema.status(401)), WorkspacesCancelSubscription403.pipe(HttpApiSchema.status(403)), WorkspacesCancelSubscription404.pipe(HttpApiSchema.status(404)), WorkspacesCancelSubscription409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "workspaces.cancelSubscription") .annotate(OpenApi.Summary, "Cancel workspace subscription") .annotate(OpenApi.Description, "Cancels or schedules cancellation for a workspace subscription."), - HttpApiEndpoint.post("workspacesReactivateSubscription", "/workspaces/:id/subscription:reactivate", { params: WorkspacesReactivateSubscriptionPathParams, headers: WorkspacesReactivateSubscriptionHeaders, payload: [WorkspacesReactivateSubscriptionRequestJson, HttpApiSchema.NoContent], success: WorkspacesReactivateSubscription202.pipe(HttpApiSchema.status(202)), error: [WorkspacesReactivateSubscription401.pipe(HttpApiSchema.status(401)), WorkspacesReactivateSubscription403.pipe(HttpApiSchema.status(403)), WorkspacesReactivateSubscription404.pipe(HttpApiSchema.status(404)), WorkspacesReactivateSubscription409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("workspacesReactivateSubscription", "/workspaces/:id/subscription%3Areactivate", { params: WorkspacesReactivateSubscriptionPathParams, headers: WorkspacesReactivateSubscriptionHeaders, payload: [WorkspacesReactivateSubscriptionRequestJson, HttpApiSchema.NoContent], success: WorkspacesReactivateSubscription202.pipe(HttpApiSchema.status(202)), error: [WorkspacesReactivateSubscription401.pipe(HttpApiSchema.status(401)), WorkspacesReactivateSubscription403.pipe(HttpApiSchema.status(403)), WorkspacesReactivateSubscription404.pipe(HttpApiSchema.status(404)), WorkspacesReactivateSubscription409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "workspaces.reactivateSubscription") .annotate(OpenApi.Summary, "Reactivate workspace subscription") .annotate(OpenApi.Description, "Reactivates a workspace subscription and clears cancellation schedule."), - HttpApiEndpoint.post("workspacesChangeSubscriptionTier", "/workspaces/:id/subscription:changeTier", { params: WorkspacesChangeSubscriptionTierPathParams, headers: WorkspacesChangeSubscriptionTierHeaders, payload: [WorkspacesChangeSubscriptionTierRequestJson, HttpApiSchema.NoContent], success: WorkspacesChangeSubscriptionTier202.pipe(HttpApiSchema.status(202)), error: [WorkspacesChangeSubscriptionTier401.pipe(HttpApiSchema.status(401)), WorkspacesChangeSubscriptionTier403.pipe(HttpApiSchema.status(403)), WorkspacesChangeSubscriptionTier404.pipe(HttpApiSchema.status(404)), WorkspacesChangeSubscriptionTier409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("workspacesChangeSubscriptionTier", "/workspaces/:id/subscription%3AchangeTier", { params: WorkspacesChangeSubscriptionTierPathParams, headers: WorkspacesChangeSubscriptionTierHeaders, payload: [WorkspacesChangeSubscriptionTierRequestJson, HttpApiSchema.NoContent], success: WorkspacesChangeSubscriptionTier202.pipe(HttpApiSchema.status(202)), error: [WorkspacesChangeSubscriptionTier401.pipe(HttpApiSchema.status(401)), WorkspacesChangeSubscriptionTier403.pipe(HttpApiSchema.status(403)), WorkspacesChangeSubscriptionTier404.pipe(HttpApiSchema.status(404)), WorkspacesChangeSubscriptionTier409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "workspaces.changeSubscriptionTier") .annotate(OpenApi.Summary, "Change workspace subscription tier") @@ -4385,12 +4385,12 @@ class InstallsGroup extends HttpApiGroup.make("Installs") .annotate(OpenApi.Identifier, "installs.delete") .annotate(OpenApi.Summary, "Delete install") .annotate(OpenApi.Description, "Deletes an install and cascades resource cleanup asynchronously. Returns an Operation envelope to poll for progress."), - HttpApiEndpoint.post("installsUpdateVersion", "/installs/:id:update", { params: InstallsUpdateVersionPathParams, headers: InstallsUpdateVersionHeaders, payload: InstallsUpdateVersionRequestJson, success: InstallsUpdateVersion202.pipe(HttpApiSchema.status(202)), error: [InstallsUpdateVersion401.pipe(HttpApiSchema.status(401)), InstallsUpdateVersion403.pipe(HttpApiSchema.status(403)), InstallsUpdateVersion404.pipe(HttpApiSchema.status(404)), InstallsUpdateVersion409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("installsUpdateVersion", "/installs/:id%3Aupdate", { params: InstallsUpdateVersionPathParams, headers: InstallsUpdateVersionHeaders, payload: InstallsUpdateVersionRequestJson, success: InstallsUpdateVersion202.pipe(HttpApiSchema.status(202)), error: [InstallsUpdateVersion401.pipe(HttpApiSchema.status(401)), InstallsUpdateVersion403.pipe(HttpApiSchema.status(403)), InstallsUpdateVersion404.pipe(HttpApiSchema.status(404)), InstallsUpdateVersion409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "installs.updateVersion") .annotate(OpenApi.Summary, "Update install version") .annotate(OpenApi.Description, "Requests a selected PackageVersion from the same Package. The current version changes only after the new git-backed render deploys successfully."), - HttpApiEndpoint.post("installsRestore", "/installs/:id:restore", { params: InstallsRestorePathParams, headers: InstallsRestoreHeaders, payload: InstallsRestoreRequestJson, success: InstallsRestore202.pipe(HttpApiSchema.status(202)), error: [InstallsRestore401.pipe(HttpApiSchema.status(401)), InstallsRestore403.pipe(HttpApiSchema.status(403)), InstallsRestore404.pipe(HttpApiSchema.status(404)), InstallsRestore409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("installsRestore", "/installs/:id%3Arestore", { params: InstallsRestorePathParams, headers: InstallsRestoreHeaders, payload: InstallsRestoreRequestJson, success: InstallsRestore202.pipe(HttpApiSchema.status(202)), error: [InstallsRestore401.pipe(HttpApiSchema.status(401)), InstallsRestore403.pipe(HttpApiSchema.status(403)), InstallsRestore404.pipe(HttpApiSchema.status(404)), InstallsRestore409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "installs.restore") .annotate(OpenApi.Summary, "Restore install render") @@ -4456,7 +4456,7 @@ class RepositoryChangeRequestsGroup extends HttpApiGroup.make("Repository change .annotate(OpenApi.Identifier, "repositoryChangeRequests.create") .annotate(OpenApi.Summary, "Create repository change request") .annotate(OpenApi.Description, "Creates a fork-backed repository change request. Fork write credentials are minted separately."), - HttpApiEndpoint.post("repositoryChangeRequestsCreateToken", "/repository_change_requests/:id:createToken", { params: RepositoryChangeRequestsCreateTokenPathParams, headers: RepositoryChangeRequestsCreateTokenHeaders, payload: [RepositoryChangeRequestsCreateTokenRequestJson, HttpApiSchema.NoContent], success: RepositoryChangeRequestsCreateToken201.pipe(HttpApiSchema.status(201)), error: [RepositoryChangeRequestsCreateToken400.pipe(HttpApiSchema.status(400)), RepositoryChangeRequestsCreateToken401.pipe(HttpApiSchema.status(401)), RepositoryChangeRequestsCreateToken403.pipe(HttpApiSchema.status(403)), RepositoryChangeRequestsCreateToken404.pipe(HttpApiSchema.status(404)), RepositoryChangeRequestsCreateToken409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("repositoryChangeRequestsCreateToken", "/repository_change_requests/:id%3AcreateToken", { params: RepositoryChangeRequestsCreateTokenPathParams, headers: RepositoryChangeRequestsCreateTokenHeaders, payload: [RepositoryChangeRequestsCreateTokenRequestJson, HttpApiSchema.NoContent], success: RepositoryChangeRequestsCreateToken201.pipe(HttpApiSchema.status(201)), error: [RepositoryChangeRequestsCreateToken400.pipe(HttpApiSchema.status(400)), RepositoryChangeRequestsCreateToken401.pipe(HttpApiSchema.status(401)), RepositoryChangeRequestsCreateToken403.pipe(HttpApiSchema.status(403)), RepositoryChangeRequestsCreateToken404.pipe(HttpApiSchema.status(404)), RepositoryChangeRequestsCreateToken409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "repositoryChangeRequests.createToken") .annotate(OpenApi.Summary, "Create repository change request token") @@ -4466,17 +4466,17 @@ class RepositoryChangeRequestsGroup extends HttpApiGroup.make("Repository change .annotate(OpenApi.Identifier, "repositoryChangeRequests.get") .annotate(OpenApi.Summary, "Get repository change request") .annotate(OpenApi.Description, "Returns one fork-backed repository change request."), - HttpApiEndpoint.post("repositoryChangeRequestsAccept", "/repository_change_requests/:id:accept", { params: RepositoryChangeRequestsAcceptPathParams, headers: RepositoryChangeRequestsAcceptHeaders, success: RepositoryChangeRequestsAccept200, error: [RepositoryChangeRequestsAccept401.pipe(HttpApiSchema.status(401)), RepositoryChangeRequestsAccept403.pipe(HttpApiSchema.status(403)), RepositoryChangeRequestsAccept404.pipe(HttpApiSchema.status(404)), RepositoryChangeRequestsAccept409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("repositoryChangeRequestsAccept", "/repository_change_requests/:id%3Aaccept", { params: RepositoryChangeRequestsAcceptPathParams, headers: RepositoryChangeRequestsAcceptHeaders, success: RepositoryChangeRequestsAccept200, error: [RepositoryChangeRequestsAccept401.pipe(HttpApiSchema.status(401)), RepositoryChangeRequestsAccept403.pipe(HttpApiSchema.status(403)), RepositoryChangeRequestsAccept404.pipe(HttpApiSchema.status(404)), RepositoryChangeRequestsAccept409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "repositoryChangeRequests.accept") .annotate(OpenApi.Summary, "Accept repository change request") .annotate(OpenApi.Description, "Validates and merges a repository change request into its parent repository."), - HttpApiEndpoint.post("repositoryChangeRequestsReject", "/repository_change_requests/:id:reject", { params: RepositoryChangeRequestsRejectPathParams, headers: RepositoryChangeRequestsRejectHeaders, payload: [RepositoryChangeRequestsRejectRequestJson, HttpApiSchema.NoContent], success: RepositoryChangeRequestsReject200, error: [RepositoryChangeRequestsReject401.pipe(HttpApiSchema.status(401)), RepositoryChangeRequestsReject403.pipe(HttpApiSchema.status(403)), RepositoryChangeRequestsReject404.pipe(HttpApiSchema.status(404)), RepositoryChangeRequestsReject409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("repositoryChangeRequestsReject", "/repository_change_requests/:id%3Areject", { params: RepositoryChangeRequestsRejectPathParams, headers: RepositoryChangeRequestsRejectHeaders, payload: [RepositoryChangeRequestsRejectRequestJson, HttpApiSchema.NoContent], success: RepositoryChangeRequestsReject200, error: [RepositoryChangeRequestsReject401.pipe(HttpApiSchema.status(401)), RepositoryChangeRequestsReject403.pipe(HttpApiSchema.status(403)), RepositoryChangeRequestsReject404.pipe(HttpApiSchema.status(404)), RepositoryChangeRequestsReject409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "repositoryChangeRequests.reject") .annotate(OpenApi.Summary, "Reject repository change request") .annotate(OpenApi.Description, "Rejects a repository change request and records the reason."), - HttpApiEndpoint.post("repositoryChangeRequestsWithdraw", "/repository_change_requests/:id:withdraw", { params: RepositoryChangeRequestsWithdrawPathParams, headers: RepositoryChangeRequestsWithdrawHeaders, success: RepositoryChangeRequestsWithdraw200, error: [RepositoryChangeRequestsWithdraw401.pipe(HttpApiSchema.status(401)), RepositoryChangeRequestsWithdraw403.pipe(HttpApiSchema.status(403)), RepositoryChangeRequestsWithdraw404.pipe(HttpApiSchema.status(404)), RepositoryChangeRequestsWithdraw409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("repositoryChangeRequestsWithdraw", "/repository_change_requests/:id%3Awithdraw", { params: RepositoryChangeRequestsWithdrawPathParams, headers: RepositoryChangeRequestsWithdrawHeaders, success: RepositoryChangeRequestsWithdraw200, error: [RepositoryChangeRequestsWithdraw401.pipe(HttpApiSchema.status(401)), RepositoryChangeRequestsWithdraw403.pipe(HttpApiSchema.status(403)), RepositoryChangeRequestsWithdraw404.pipe(HttpApiSchema.status(404)), RepositoryChangeRequestsWithdraw409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "repositoryChangeRequests.withdraw") .annotate(OpenApi.Summary, "Withdraw repository change request") @@ -4509,7 +4509,7 @@ class SecretsGroup extends HttpApiGroup.make("Secrets") .annotate(OpenApi.Identifier, "secrets.update") .annotate(OpenApi.Summary, "Update secret") .annotate(OpenApi.Description, "Updates secret metadata only. Rotate values with POST /secrets/{id}/versions."), - HttpApiEndpoint.post("secretsUndelete", "/secrets/:id:undelete", { params: SecretsUndeletePathParams, headers: SecretsUndeleteHeaders, success: SecretsUndelete200, error: [SecretsUndelete401.pipe(HttpApiSchema.status(401)), SecretsUndelete403.pipe(HttpApiSchema.status(403)), SecretsUndelete404.pipe(HttpApiSchema.status(404)), SecretsUndelete409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("secretsUndelete", "/secrets/:id%3Aundelete", { params: SecretsUndeletePathParams, headers: SecretsUndeleteHeaders, success: SecretsUndelete200, error: [SecretsUndelete401.pipe(HttpApiSchema.status(401)), SecretsUndelete403.pipe(HttpApiSchema.status(403)), SecretsUndelete404.pipe(HttpApiSchema.status(404)), SecretsUndelete409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "secrets.undelete") .annotate(OpenApi.Summary, "Restore a soft-deleted secret") @@ -4534,22 +4534,22 @@ class SecretsGroup extends HttpApiGroup.make("Secrets") .annotate(OpenApi.Identifier, "secrets.getVersion") .annotate(OpenApi.Summary, "Get secret version") .annotate(OpenApi.Description, "Returns metadata for one secret version. Plaintext is never returned."), - HttpApiEndpoint.post("secretsEnableVersion", "/secrets/:id/versions/:vid:enable", { params: SecretsEnableVersionPathParams, headers: SecretsEnableVersionHeaders, success: SecretsEnableVersion200, error: [SecretsEnableVersion401.pipe(HttpApiSchema.status(401)), SecretsEnableVersion403.pipe(HttpApiSchema.status(403)), SecretsEnableVersion404.pipe(HttpApiSchema.status(404)), SecretsEnableVersion409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("secretsEnableVersion", "/secrets/:id/versions/:vid%3Aenable", { params: SecretsEnableVersionPathParams, headers: SecretsEnableVersionHeaders, success: SecretsEnableVersion200, error: [SecretsEnableVersion401.pipe(HttpApiSchema.status(401)), SecretsEnableVersion403.pipe(HttpApiSchema.status(403)), SecretsEnableVersion404.pipe(HttpApiSchema.status(404)), SecretsEnableVersion409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "secrets.enableVersion") .annotate(OpenApi.Summary, "Enable a disabled secret version") .annotate(OpenApi.Description, "Re-enables a disabled SecretVersion. The newly-enabled version becomes eligible for `:access` calls again. Idempotent: enabling an already-enabled version returns 200 with no state change. Returns 409 ABORTED with a stale-revision hint if If-Match mismatches. Returns 409 if the version has been destroyed (irreversible)."), - HttpApiEndpoint.post("secretsDisableVersion", "/secrets/:id/versions/:vid:disable", { params: SecretsDisableVersionPathParams, headers: SecretsDisableVersionHeaders, success: SecretsDisableVersion200, error: [SecretsDisableVersion401.pipe(HttpApiSchema.status(401)), SecretsDisableVersion403.pipe(HttpApiSchema.status(403)), SecretsDisableVersion404.pipe(HttpApiSchema.status(404)), SecretsDisableVersion409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("secretsDisableVersion", "/secrets/:id/versions/:vid%3Adisable", { params: SecretsDisableVersionPathParams, headers: SecretsDisableVersionHeaders, success: SecretsDisableVersion200, error: [SecretsDisableVersion401.pipe(HttpApiSchema.status(401)), SecretsDisableVersion403.pipe(HttpApiSchema.status(403)), SecretsDisableVersion404.pipe(HttpApiSchema.status(404)), SecretsDisableVersion409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "secrets.disableVersion") .annotate(OpenApi.Summary, "Disable secret version") .annotate(OpenApi.Description, "Marks a secret version as disabled."), - HttpApiEndpoint.post("secretsDestroyVersion", "/secrets/:id/versions/:vid:destroy", { params: SecretsDestroyVersionPathParams, headers: SecretsDestroyVersionHeaders, success: SecretsDestroyVersion200, error: [SecretsDestroyVersion401.pipe(HttpApiSchema.status(401)), SecretsDestroyVersion403.pipe(HttpApiSchema.status(403)), SecretsDestroyVersion404.pipe(HttpApiSchema.status(404)), SecretsDestroyVersion409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("secretsDestroyVersion", "/secrets/:id/versions/:vid%3Adestroy", { params: SecretsDestroyVersionPathParams, headers: SecretsDestroyVersionHeaders, success: SecretsDestroyVersion200, error: [SecretsDestroyVersion401.pipe(HttpApiSchema.status(401)), SecretsDestroyVersion403.pipe(HttpApiSchema.status(403)), SecretsDestroyVersion404.pipe(HttpApiSchema.status(404)), SecretsDestroyVersion409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "secrets.destroyVersion") .annotate(OpenApi.Summary, "Destroy secret version") .annotate(OpenApi.Description, "Irreversibly destroys a secret version payload and keeps metadata for audit."), - HttpApiEndpoint.post("secretsAccessVersion", "/secrets/:id/versions/:vid:access", { params: SecretsAccessVersionPathParams, headers: SecretsAccessVersionHeaders, success: SecretsAccessVersion200, error: [SecretsAccessVersion401.pipe(HttpApiSchema.status(401)), SecretsAccessVersion403.pipe(HttpApiSchema.status(403)), SecretsAccessVersion404.pipe(HttpApiSchema.status(404)), SecretsAccessVersion409.pipe(HttpApiSchema.status(409)), SecretsAccessVersion410.pipe(HttpApiSchema.status(410))] }) + HttpApiEndpoint.post("secretsAccessVersion", "/secrets/:id/versions/:vid%3Aaccess", { params: SecretsAccessVersionPathParams, headers: SecretsAccessVersionHeaders, success: SecretsAccessVersion200, error: [SecretsAccessVersion401.pipe(HttpApiSchema.status(401)), SecretsAccessVersion403.pipe(HttpApiSchema.status(403)), SecretsAccessVersion404.pipe(HttpApiSchema.status(404)), SecretsAccessVersion409.pipe(HttpApiSchema.status(409)), SecretsAccessVersion410.pipe(HttpApiSchema.status(410))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "secrets.accessVersion") .annotate(OpenApi.Summary, "Access secret version plaintext") @@ -4722,7 +4722,7 @@ class AgentsGroup extends HttpApiGroup.make("Agents") .annotate(OpenApi.Identifier, "agents.update") .annotate(OpenApi.Summary, "Update agent") .annotate(OpenApi.Description, "Updates mutable agent configuration. Lifecycle state changes use archive."), - HttpApiEndpoint.post("agentsEnable", "/agents/:id:enable", { params: AgentsEnablePathParams, success: AgentsEnable200, error: [AgentsEnable401.pipe(HttpApiSchema.status(401)), AgentsEnable403.pipe(HttpApiSchema.status(403)), AgentsEnable404.pipe(HttpApiSchema.status(404))] }) + HttpApiEndpoint.post("agentsEnable", "/agents/:id%3Aenable", { params: AgentsEnablePathParams, success: AgentsEnable200, error: [AgentsEnable401.pipe(HttpApiSchema.status(401)), AgentsEnable403.pipe(HttpApiSchema.status(403)), AgentsEnable404.pipe(HttpApiSchema.status(404))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "agents.enable") .annotate(OpenApi.Summary, "Enable agent") @@ -4776,7 +4776,7 @@ class AgentSessionsGroup extends HttpApiGroup.make("Agent sessions") .annotate(OpenApi.Identifier, "agentSessions.create") .annotate(OpenApi.Summary, "Create agent session") .annotate(OpenApi.Description, "Creates a durable workspace-scoped session for an agent."), - HttpApiEndpoint.get("agentSessionsDetectConflicts", "/agent_sessions:detectConflicts", { query: AgentSessionsDetectConflictsQuery, headers: AgentSessionsDetectConflictsHeaders, success: AgentSessionsDetectConflicts200, error: [AgentSessionsDetectConflicts401.pipe(HttpApiSchema.status(401)), AgentSessionsDetectConflicts403.pipe(HttpApiSchema.status(403))] }) + HttpApiEndpoint.get("agentSessionsDetectConflicts", "/agent_sessions%3AdetectConflicts", { query: AgentSessionsDetectConflictsQuery, headers: AgentSessionsDetectConflictsHeaders, success: AgentSessionsDetectConflicts200, error: [AgentSessionsDetectConflicts401.pipe(HttpApiSchema.status(401)), AgentSessionsDetectConflicts403.pipe(HttpApiSchema.status(403))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "agentSessions.detectConflicts") .annotate(OpenApi.Summary, "Detect agent work conflicts") @@ -4791,7 +4791,7 @@ class AgentSessionsGroup extends HttpApiGroup.make("Agent sessions") .annotate(OpenApi.Identifier, "agentSessions.archive") .annotate(OpenApi.Summary, "Archive agent session") .annotate(OpenApi.Description, "Archives an agent session and requests runtime cleanup."), - HttpApiEndpoint.post("agentSessionsSetRetention", "/agent_sessions/:id:setRetention", { params: AgentSessionsSetRetentionPathParams, payload: [AgentSessionsSetRetentionRequestJson, HttpApiSchema.NoContent], success: AgentSessionsSetRetention200, error: [AgentSessionsSetRetention401.pipe(HttpApiSchema.status(401)), AgentSessionsSetRetention403.pipe(HttpApiSchema.status(403)), AgentSessionsSetRetention404.pipe(HttpApiSchema.status(404))] }) + HttpApiEndpoint.post("agentSessionsSetRetention", "/agent_sessions/:id%3AsetRetention", { params: AgentSessionsSetRetentionPathParams, payload: [AgentSessionsSetRetentionRequestJson, HttpApiSchema.NoContent], success: AgentSessionsSetRetention200, error: [AgentSessionsSetRetention401.pipe(HttpApiSchema.status(401)), AgentSessionsSetRetention403.pipe(HttpApiSchema.status(403)), AgentSessionsSetRetention404.pipe(HttpApiSchema.status(404))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "agentSessions.setRetention") .annotate(OpenApi.Summary, "Set agent session retention") @@ -4814,12 +4814,12 @@ class AgentTurnsGroup extends HttpApiGroup.make("Agent turns") .annotate(OpenApi.Identifier, "agentTurns.get") .annotate(OpenApi.Summary, "Get agent turn") .annotate(OpenApi.Description, "Gets one agent turn."), - HttpApiEndpoint.post("agentTurnsCancel", "/agent_turns/:id:cancel", { params: AgentTurnsCancelPathParams, headers: AgentTurnsCancelHeaders, success: AgentTurnsCancel200, error: [AgentTurnsCancel401.pipe(HttpApiSchema.status(401)), AgentTurnsCancel403.pipe(HttpApiSchema.status(403)), AgentTurnsCancel404.pipe(HttpApiSchema.status(404)), AgentTurnsCancel422.pipe(HttpApiSchema.status(422))] }) + HttpApiEndpoint.post("agentTurnsCancel", "/agent_turns/:id%3Acancel", { params: AgentTurnsCancelPathParams, headers: AgentTurnsCancelHeaders, success: AgentTurnsCancel200, error: [AgentTurnsCancel401.pipe(HttpApiSchema.status(401)), AgentTurnsCancel403.pipe(HttpApiSchema.status(403)), AgentTurnsCancel404.pipe(HttpApiSchema.status(404)), AgentTurnsCancel422.pipe(HttpApiSchema.status(422))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "agentTurns.cancel") .annotate(OpenApi.Summary, "Cancel agent turn") .annotate(OpenApi.Description, "Cancels a running or queued agent turn."), - HttpApiEndpoint.post("agentTurnsEmit", "/agent_turns/:id:emit", { params: AgentTurnsEmitPathParams, headers: AgentTurnsEmitHeaders, payload: [AgentTurnsEmitRequestJson, HttpApiSchema.NoContent], success: AgentTurnsEmit201.pipe(HttpApiSchema.status(201)), error: [AgentTurnsEmit401.pipe(HttpApiSchema.status(401)), AgentTurnsEmit403.pipe(HttpApiSchema.status(403)), AgentTurnsEmit404.pipe(HttpApiSchema.status(404)), AgentTurnsEmit409.pipe(HttpApiSchema.status(409)), AgentTurnsEmit422.pipe(HttpApiSchema.status(422))] }) + HttpApiEndpoint.post("agentTurnsEmit", "/agent_turns/:id%3Aemit", { params: AgentTurnsEmitPathParams, headers: AgentTurnsEmitHeaders, payload: [AgentTurnsEmitRequestJson, HttpApiSchema.NoContent], success: AgentTurnsEmit201.pipe(HttpApiSchema.status(201)), error: [AgentTurnsEmit401.pipe(HttpApiSchema.status(401)), AgentTurnsEmit403.pipe(HttpApiSchema.status(403)), AgentTurnsEmit404.pipe(HttpApiSchema.status(404)), AgentTurnsEmit409.pipe(HttpApiSchema.status(409)), AgentTurnsEmit422.pipe(HttpApiSchema.status(422))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "agentTurns.emit") .annotate(OpenApi.Summary, "Emit agent turn event") @@ -4834,7 +4834,7 @@ class AgentProviderExchangesGroup extends HttpApiGroup.make("Agent provider exch .annotate(OpenApi.Description, "Lists redacted provider proxy request and response metadata for one agent turn.")) {} class AgentEventsGroup extends HttpApiGroup.make("Agent events") - .add(HttpApiEndpoint.get("agentEventsStream", "/agent_events:stream", { query: AgentEventsStreamQuery, headers: AgentEventsStreamHeaders, success: HttpApiSchema.Empty(200), error: [AgentEventsStream401.pipe(HttpApiSchema.status(401)), AgentEventsStream403.pipe(HttpApiSchema.status(403))] }) + .add(HttpApiEndpoint.get("agentEventsStream", "/agent_events%3Astream", { query: AgentEventsStreamQuery, headers: AgentEventsStreamHeaders, success: HttpApiSchema.Empty(200), error: [AgentEventsStream401.pipe(HttpApiSchema.status(401)), AgentEventsStream403.pipe(HttpApiSchema.status(403))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "agentEvents.stream") .annotate(OpenApi.Summary, "Stream agent events") @@ -4852,7 +4852,7 @@ class ApprovalRequestsGroup extends HttpApiGroup.make("Approval requests") .annotate(OpenApi.Identifier, "approvalRequests.list") .annotate(OpenApi.Summary, "List approval requests") .annotate(OpenApi.Description, "Lists pending and resolved approval requests in the workspace."), - HttpApiEndpoint.post("approvalRequestsResolve", "/approval_requests/:id:resolve", { params: ApprovalRequestsResolvePathParams, headers: ApprovalRequestsResolveHeaders, payload: [ApprovalRequestsResolveRequestJson, HttpApiSchema.NoContent], success: ApprovalRequestsResolve200, error: [ApprovalRequestsResolve401.pipe(HttpApiSchema.status(401)), ApprovalRequestsResolve403.pipe(HttpApiSchema.status(403)), ApprovalRequestsResolve404.pipe(HttpApiSchema.status(404)), ApprovalRequestsResolve422.pipe(HttpApiSchema.status(422))] }) + HttpApiEndpoint.post("approvalRequestsResolve", "/approval_requests/:id%3Aresolve", { params: ApprovalRequestsResolvePathParams, headers: ApprovalRequestsResolveHeaders, payload: [ApprovalRequestsResolveRequestJson, HttpApiSchema.NoContent], success: ApprovalRequestsResolve200, error: [ApprovalRequestsResolve401.pipe(HttpApiSchema.status(401)), ApprovalRequestsResolve403.pipe(HttpApiSchema.status(403)), ApprovalRequestsResolve404.pipe(HttpApiSchema.status(404)), ApprovalRequestsResolve422.pipe(HttpApiSchema.status(422))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "approvalRequests.resolve") .annotate(OpenApi.Summary, "Resolve approval request") @@ -4864,7 +4864,7 @@ class WorkspaceSubdomainsGroup extends HttpApiGroup.make("Workspace subdomains") .annotate(OpenApi.Identifier, "workspaceSubdomains.get") .annotate(OpenApi.Summary, "Get workspace subdomain") .annotate(OpenApi.Description, "Returns the singleton workspace subdomain resource."), - HttpApiEndpoint.post("workspaceSubdomainsSetName", "/workspaces/:id/subdomain:setName", { params: WorkspaceSubdomainsSetNamePathParams, headers: WorkspaceSubdomainsSetNameHeaders, payload: [WorkspaceSubdomainsSetNameRequestJson, HttpApiSchema.NoContent], success: WorkspaceSubdomainsSetName202.pipe(HttpApiSchema.status(202)), error: [WorkspaceSubdomainsSetName401.pipe(HttpApiSchema.status(401)), WorkspaceSubdomainsSetName403.pipe(HttpApiSchema.status(403)), WorkspaceSubdomainsSetName409.pipe(HttpApiSchema.status(409))] }) + HttpApiEndpoint.post("workspaceSubdomainsSetName", "/workspaces/:id/subdomain%3AsetName", { params: WorkspaceSubdomainsSetNamePathParams, headers: WorkspaceSubdomainsSetNameHeaders, payload: [WorkspaceSubdomainsSetNameRequestJson, HttpApiSchema.NoContent], success: WorkspaceSubdomainsSetName202.pipe(HttpApiSchema.status(202)), error: [WorkspaceSubdomainsSetName401.pipe(HttpApiSchema.status(401)), WorkspaceSubdomainsSetName403.pipe(HttpApiSchema.status(403)), WorkspaceSubdomainsSetName409.pipe(HttpApiSchema.status(409))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "workspaceSubdomains.setName") .annotate(OpenApi.Summary, "Set workspace subdomain name") @@ -4887,12 +4887,12 @@ class PreviewHostnamesGroup extends HttpApiGroup.make("Preview hostnames") .annotate(OpenApi.Identifier, "previewHostnames.delete") .annotate(OpenApi.Summary, "Delete preview hostname") .annotate(OpenApi.Description, "Starts a workflow that releases a preview hostname."), - HttpApiEndpoint.post("previewHostnamesBindPinned", "/installs/:id/preview_hostnames:bindPinned", { params: PreviewHostnamesBindPinnedPathParams, headers: PreviewHostnamesBindPinnedHeaders, payload: [PreviewHostnamesBindPinnedRequestJson, HttpApiSchema.NoContent], success: PreviewHostnamesBindPinned202.pipe(HttpApiSchema.status(202)), error: [PreviewHostnamesBindPinned401.pipe(HttpApiSchema.status(401)), PreviewHostnamesBindPinned403.pipe(HttpApiSchema.status(403))] }) + HttpApiEndpoint.post("previewHostnamesBindPinned", "/installs/:id/preview_hostnames%3AbindPinned", { params: PreviewHostnamesBindPinnedPathParams, headers: PreviewHostnamesBindPinnedHeaders, payload: [PreviewHostnamesBindPinnedRequestJson, HttpApiSchema.NoContent], success: PreviewHostnamesBindPinned202.pipe(HttpApiSchema.status(202)), error: [PreviewHostnamesBindPinned401.pipe(HttpApiSchema.status(401)), PreviewHostnamesBindPinned403.pipe(HttpApiSchema.status(403))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "previewHostnames.bindPinned") .annotate(OpenApi.Summary, "Bind pinned preview hostname") .annotate(OpenApi.Description, "Starts a workflow that binds an immutable preview hostname to one render."), - HttpApiEndpoint.post("previewHostnamesBindFloating", "/installs/:id/preview_hostnames:bindFloating", { params: PreviewHostnamesBindFloatingPathParams, headers: PreviewHostnamesBindFloatingHeaders, payload: [PreviewHostnamesBindFloatingRequestJson, HttpApiSchema.NoContent], success: PreviewHostnamesBindFloating202.pipe(HttpApiSchema.status(202)), error: [PreviewHostnamesBindFloating401.pipe(HttpApiSchema.status(401)), PreviewHostnamesBindFloating403.pipe(HttpApiSchema.status(403))] }) + HttpApiEndpoint.post("previewHostnamesBindFloating", "/installs/:id/preview_hostnames%3AbindFloating", { params: PreviewHostnamesBindFloatingPathParams, headers: PreviewHostnamesBindFloatingHeaders, payload: [PreviewHostnamesBindFloatingRequestJson, HttpApiSchema.NoContent], success: PreviewHostnamesBindFloating202.pipe(HttpApiSchema.status(202)), error: [PreviewHostnamesBindFloating401.pipe(HttpApiSchema.status(401)), PreviewHostnamesBindFloating403.pipe(HttpApiSchema.status(403))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "previewHostnames.bindFloating") .annotate(OpenApi.Summary, "Bind floating preview hostname") @@ -4951,7 +4951,7 @@ class AccessDecisionsGroup extends HttpApiGroup.make("Access Decisions") .annotate(OpenApi.Identifier, "accessDecisions.explain") .annotate(OpenApi.Summary, "Explain an access decision") .annotate(OpenApi.Description, "Explains whether the authenticated requester can perform an access action and why."), - HttpApiEndpoint.post("accessDecisionsExplainBatch", "/access_decisions:explainBatch", { headers: AccessDecisionsExplainBatchHeaders, payload: [AccessDecisionsExplainBatchRequestJson, HttpApiSchema.NoContent], success: AccessDecisionsExplainBatch200, error: [AccessDecisionsExplainBatch401.pipe(HttpApiSchema.status(401)), AccessDecisionsExplainBatch403.pipe(HttpApiSchema.status(403)), AccessDecisionsExplainBatch404.pipe(HttpApiSchema.status(404)), AccessDecisionsExplainBatch422.pipe(HttpApiSchema.status(422))] }) + HttpApiEndpoint.post("accessDecisionsExplainBatch", "/access_decisions%3AexplainBatch", { headers: AccessDecisionsExplainBatchHeaders, payload: [AccessDecisionsExplainBatchRequestJson, HttpApiSchema.NoContent], success: AccessDecisionsExplainBatch200, error: [AccessDecisionsExplainBatch401.pipe(HttpApiSchema.status(401)), AccessDecisionsExplainBatch403.pipe(HttpApiSchema.status(403)), AccessDecisionsExplainBatch404.pipe(HttpApiSchema.status(404)), AccessDecisionsExplainBatch422.pipe(HttpApiSchema.status(422))] }) .middleware(BearerAuthSecurityMiddleware) .annotate(OpenApi.Identifier, "accessDecisions.explainBatch") .annotate(OpenApi.Summary, "Explain multiple access decisions") diff --git a/test/generated-command.test.ts b/test/generated-command.test.ts index 2352142..b5b9e7f 100644 --- a/test/generated-command.test.ts +++ b/test/generated-command.test.ts @@ -73,6 +73,54 @@ describe("generated public commands", () => { expect(await received?.json()).toEqual(body); }); + test("clusters.resume builds the literal :action-suffixed request path", async () => { + let received: Request | undefined; + const operation = clusterOperation(); + const result = await runGenerated( + "clusters.resume", + ["--input", "-"], + JSON.stringify({ + path: { id: "clu_123" }, + headers: { "if-match": "etag-1" }, + }), + (input, init) => { + received = new Request(input, init); + return Promise.resolve(Response.json(operation, { status: 202 })); + }, + ); + + expect(result.data).toEqual(operation); + expect(received?.method).toBe("POST"); + expect(received?.url).toBe( + "https://api.akua.dev/v1/clusters/clu_123%3Aresume", + ); + expect(received?.headers.get("if-match")).toBe("etag-1"); + }); + + test("machines.resume builds the literal :action-suffixed request path", async () => { + let received: Request | undefined; + const operation = machineOperation(); + const result = await runGenerated( + "machines.resume", + ["--input", "-"], + JSON.stringify({ + path: { id: "mch_123" }, + headers: { "if-match": "etag-1", "idempotency-key": "resume-once" }, + }), + (input, init) => { + received = new Request(input, init); + return Promise.resolve(Response.json(operation, { status: 202 })); + }, + ); + + expect(result.data).toEqual(operation); + expect(received?.method).toBe("POST"); + expect(received?.url).toBe( + "https://api.akua.dev/v1/machines/mch_123%3Aresume", + ); + expect(received?.headers.get("if-match")).toBe("etag-1"); + }); + test("rejects malformed and excess input before transport", async () => { let requests = 0; const transport = () => { @@ -656,6 +704,29 @@ function runAnonymousOffer(options: RunGeneratedOptions) { }; } +function clusterOperation() { + return { + id: "op_456", + workspace_id: "ws_123", + organization_id: null, + owner_type: "cluster", + owner_id: "clu_123", + parent_operation_id: null, + state: "RUNNING", + done: false, + html_url: "https://akua.dev/clusters/clu_123", + metadata: { + type: "cluster.resume", + cluster_id: "clu_123", + }, + response: null, + error: null, + last_error: null, + started_at: 1, + completed_at: null, + }; +} + function machineOperation() { return { id: "op_123", From 1c479c86b4eae2945f228c84385da616f2fed7b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Br=C3=A4mer?= <22003767+robinbraemer@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:22:25 +0200 Subject: [PATCH 2/4] test(cli): migrate test suite from bun:test to @effect/vitest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every test file imported `describe`/`expect`/`test` from `bun:test`, using raw `Effect.runPromise`/`await` to execute Effect programs under test. That's the one place in this repo not on the Effect-first model AGENTS.md requires for src/ and scripts/, and it left no path to Effect-native testing primitives (it.effect, TestClock) for a codebase that is entirely Effect-based end to end. Researched a Bun-native alternative first per the ask: two community packages exist (effect-bun-test by cevr, @domir/bun-test by DomiR), both single-maintainer, 0 and 3 GitHub stars, last published Mar/May 2026 with no subsequent activity. Neither is endorsed by the Effect-TS GitHub org or referenced anywhere in node_modules/effect/AGENTS.md, which documents only @effect/vitest for testing Effect programs. Not a reasonable bar for "official or reputable" given this CLI ships to users. Sibling monorepo akua-dev/cnap already standardized on @effect/vitest + a real vitest runner for the identical reason (packages/config/vitest/domain.ts) — used that as prior art. Installed @effect/vitest@4.0.0-beta.106 (exact peer match for this repo's pinned effect@4.0.0-beta.106) and vitest@^4.1.0. `bun run test` now runs `vitest run`; mise.toml's `test` task called `bun test` directly (bypassing package.json), so it's repointed at `bun run test` too. Files with no Effect execution (pure sync helpers, TS-AST/file assertions) import plain `describe`/`expect`/`test` from vitest, which @effect/vitest re-exports unchanged; files that run an Effect use @effect/vitest's `it.effect` with `Effect.gen`/`yield*` bodies instead of `await Effect.runPromise(...)`. vitest's worker pool runs under real Node, not Bun, even when invoked via `bun run vitest` — confirmed by probing `typeof Bun` (undefined) and `process.execPath` (resolves to the mise-installed node binary) in both `threads` and `forks` pool modes. That broke every test and one production helper that called `Bun.spawn`/`Bun.spawnSync`/`Bun.file`/ `Bun.sleep` directly: - Added test/bun-binary.ts + test/run-akua.ts, consolidating the two near-duplicate `runAkua` helpers (cli.test.ts, strict-effect-control-flow.test.ts) into one, spawning the real `akua` CLI via node:child_process.spawnSync against the bun binary resolved from `npm_execpath` (set by `bun run`/`npm run` to the fully resolved binary, avoiding a mise-shim hop per spawn — same intent as the code this replaces, just runtime-independent). - release.test.ts and effect-generator-patch.test.ts: swapped Bun.spawn/spawnSync/file/sleep for node:child_process.spawnSync, node:fs.existsSync, and node:timers/promises.setTimeout. - scripts/runtime/release-host-live.ts's runCommand (a `-live.ts` boundary module, so Bun usage there was AGENTS.md-compliant) used Bun.spawnSync to shell out during release packaging/verification; 7 release.test.ts cases exercise it in-process. Swapped to node:child_process.spawnSync with an explicit 64 MB maxBuffer (Node's 1 MB default is tight for tar/listing output; Bun's spawnSync had no such ceiling). Pure subprocess-spawning equivalence, not a Bun-specific fast path, so this is safe even when the compiled CLI itself still runs under Bun in production. Scoped the raw Promise/throw ban per the CNAP prior art rather than applying it blindly: CNAP fences `*.effect.ts` production files (not `*.effect.test.ts` test bodies) against `new Promise`/`throw`/`try`/ `async`/`await`, and this repo already has two TS-AST invariant tests (test/production-effect-invariants.test.ts, test/strict-effect-control-flow.test.ts) enforcing the equivalent, broader ban across all of src/ and scripts/ — broader because this whole CLI, not just suffixed files, is Effect-first. Left those tests untouched; test/ stays uncovered by the ban, matching CNAP's own choice not to fence test bodies. What's left there is legitimate process-boundary glue that has no Effect replacement: mocking the `fetch` global's Promise-returning contract (device-http.test.ts, fetch-openapi.test.ts), and a Promise-shaped AuthTestDependencies test double interface (auth-test-layer.ts) matching Node's async I/O contracts. Converted every Effect.runPromise/runPromiseExit call inside a test body to it.effect + yield* wherever it wasn't itself a boundary helper; generated-command.test.ts's runGenerated helper follows the same already-established pattern as runAkua (a contained, reused Promise-returning subprocess/fetch-mock boundary), so its internals and two direct Stream.runCollect(...) call sites were left alone rather than force-fit into Effect.gen for no readability gain. skills/effect-v4/SKILL.md's verification snippet and Types-and-tests section now say `bun run test` (not `bun test`) and document the it.effect/@effect/vitest split; updated the accompanying effect-v4-skill.test.ts substring assertion to match. Rationale: keeps this CLI's test suite on the same control-flow model as its production code, without silently regressing test coverage or inventing a fence that fights how test code legitimately talks to Promise-shaped host boundaries. Tested: bun run test (vitest run) — 19 files, 167 tests pass, same count as bun:test before this change; bun run generate:check; bun run build; mise run check (generate:check + build + test), all green. --- bun.lock | 136 +++++++++++ mise.toml | 2 +- package.json | 6 +- scripts/runtime/release-host-live.ts | 21 +- skills/effect-v4/SKILL.md | 10 +- test/auth-effect.test.ts | 272 +++++++++++----------- test/bun-binary.ts | 11 + test/cli.test.ts | 233 +++++++++--------- test/device-http.test.ts | 138 ++++++----- test/docs.test.ts | 2 +- test/effect-generator-patch.test.ts | 20 +- test/effect-runtime.test.ts | 126 +++++----- test/effect-v4-skill.test.ts | 4 +- test/fetch-openapi.test.ts | 136 ++++++----- test/generate-commands.test.ts | 50 ++-- test/generate-effect-api.test.ts | 259 +++++++++++--------- test/generated-command.test.ts | 2 +- test/generated-operation-executor.test.ts | 2 +- test/mode.test.ts | 2 +- test/production-effect-invariants.test.ts | 2 +- test/release-please-config.test.ts | 2 +- test/release.test.ts | 73 +++--- test/render.test.ts | 2 +- test/run-akua.ts | 42 ++++ test/strict-effect-control-flow.test.ts | 32 +-- test/workflows.test.ts | 2 +- vitest.config.ts | 17 ++ 27 files changed, 919 insertions(+), 685 deletions(-) create mode 100644 test/bun-binary.ts create mode 100644 test/run-akua.ts create mode 100644 vitest.config.ts diff --git a/bun.lock b/bun.lock index ab3ef45..a4af144 100644 --- a/bun.lock +++ b/bun.lock @@ -11,8 +11,10 @@ "devDependencies": { "@effect/openapi-generator": "4.0.0-beta.106", "@effect/platform-node": "4.0.0-beta.106", + "@effect/vitest": "4.0.0-beta.106", "@types/bun": "^1.3.0", "typescript": "^5.9.0", + "vitest": "^4.1.0", }, }, }, @@ -47,10 +49,14 @@ "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.106", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.106" } }, "sha512-TRTzLmhCwVM8G7dXz16lI3wE2vZFSs1jNs/+3WhnuOZgOUeJz9vzBK/KKlGRbbi0QbtEqmW4rAy1y9Fj4MZfsw=="], + "@effect/vitest": ["@effect/vitest@4.0.0-beta.106", "", { "peerDependencies": { "effect": "^4.0.0-beta.106", "vitest": ">=4.1.0 <5.0.0" } }, "sha512-0w799orFqjFNlKh9GrnzMIJLn2/uPaVu8qmT3hJKkYxS46tUS9ZbfjeYNsd1BddvY+3sdS/vXVpnTE64z6+QaQ=="], + "@exodus/schemasafe": ["@exodus/schemasafe@1.3.0", "", {}, "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw=="], "@ioredis/commands": ["@ioredis/commands@1.10.0", "", {}, "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="], @@ -63,24 +69,80 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], + "@oxc-project/types": ["@oxc-project/types@0.144.0", "", {}, "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.4", "", { "os": "android", "cpu": "arm64" }, "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.4", "", { "os": "none", "cpu": "arm64" }, "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@vitest/expect": ["@vitest/expect@4.1.10", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], + + "@vitest/runner": ["@vitest/runner@4.1.10", "", { "dependencies": { "@vitest/utils": "4.1.10", "pathe": "^2.0.3" } }, "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="], + + "@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="], + + "@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "call-me-maybe": ["call-me-maybe@1.0.2", "", {}, "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="], @@ -89,6 +151,8 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], @@ -99,10 +163,16 @@ "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], + "es6-promise": ["es6-promise@3.3.1", "", {}, "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], + "fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -111,6 +181,10 @@ "fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "http2-client": ["http2-client@1.3.5", "", {}, "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA=="], @@ -123,6 +197,32 @@ "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -131,6 +231,8 @@ "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], + "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "node-fetch-h2": ["node-fetch-h2@2.3.0", "", { "dependencies": { "http2-client": "^1.2.5" } }, "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg=="], @@ -149,6 +251,16 @@ "oas-validator": ["oas-validator@5.0.8", "", { "dependencies": { "call-me-maybe": "^1.0.1", "oas-kit-common": "^1.0.8", "oas-linter": "^3.2.2", "oas-resolver": "^2.5.6", "oas-schema-walker": "^1.1.5", "reftools": "^1.1.9", "should": "^13.2.1", "yaml": "^1.10.0" } }, "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw=="], + "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="], "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], @@ -161,6 +273,8 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "rolldown": ["rolldown@1.2.4", "", { "dependencies": { "@oxc-project/types": "=0.144.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.4", "@rolldown/binding-darwin-arm64": "1.2.4", "@rolldown/binding-darwin-x64": "1.2.4", "@rolldown/binding-freebsd-x64": "1.2.4", "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", "@rolldown/binding-linux-arm64-gnu": "1.2.4", "@rolldown/binding-linux-arm64-musl": "1.2.4", "@rolldown/binding-linux-ppc64-gnu": "1.2.4", "@rolldown/binding-linux-s390x-gnu": "1.2.4", "@rolldown/binding-linux-x64-gnu": "1.2.4", "@rolldown/binding-linux-x64-musl": "1.2.4", "@rolldown/binding-openharmony-arm64": "1.2.4", "@rolldown/binding-win32-arm64-msvc": "1.2.4", "@rolldown/binding-win32-x64-msvc": "1.2.4" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w=="], + "should": ["should@13.2.3", "", { "dependencies": { "should-equal": "^2.0.0", "should-format": "^3.0.3", "should-type": "^1.4.0", "should-type-adaptors": "^1.0.1", "should-util": "^1.0.0" } }, "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ=="], "should-equal": ["should-equal@2.0.0", "", { "dependencies": { "should-type": "^1.4.0" } }, "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA=="], @@ -173,14 +287,30 @@ "should-util": ["should-util@1.0.1", "", {}, "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], + "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "swagger2openapi": ["swagger2openapi@7.0.8", "", { "dependencies": { "call-me-maybe": "^1.0.1", "node-fetch": "^2.6.1", "node-fetch-h2": "^2.3.0", "node-readfiles": "^0.2.0", "oas-kit-common": "^1.0.8", "oas-resolver": "^2.5.6", "oas-schema-walker": "^1.1.5", "oas-validator": "^5.0.8", "reftools": "^1.1.9", "yaml": "^1.10.0", "yargs": "^17.0.1" }, "bin": { "swagger2openapi": "swagger2openapi.js", "oas-validate": "oas-validate.js", "boast": "boast.js" } }, "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -191,10 +321,16 @@ "uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="], + "vite": ["vite@8.2.1", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.25", "rolldown": "~1.2.1", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw=="], + + "vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], diff --git a/mise.toml b/mise.toml index d067cae..29c011c 100644 --- a/mise.toml +++ b/mise.toml @@ -30,7 +30,7 @@ run = "bun run release:smoke" [tasks.test] description = "Run tests" -run = "bun test" +run = "bun run test" [tasks."spec:fetch"] description = "Fetch the production OpenAPI spec snapshot" diff --git a/package.json b/package.json index 5db6c02..26690ed 100644 --- a/package.json +++ b/package.json @@ -17,13 +17,15 @@ "release:package": "bun scripts/release.ts package --version $npm_package_version --output dist/release", "release:verify": "bun scripts/release.ts verify --version $npm_package_version --output dist/release", "release:smoke": "bun scripts/release.ts smoke --version $npm_package_version --output dist/release", - "test": "bun test" + "test": "vitest run" }, "devDependencies": { "@effect/openapi-generator": "4.0.0-beta.106", "@effect/platform-node": "4.0.0-beta.106", + "@effect/vitest": "4.0.0-beta.106", "@types/bun": "^1.3.0", - "typescript": "^5.9.0" + "typescript": "^5.9.0", + "vitest": "^4.1.0" }, "engines": { "bun": ">=1.3.7" diff --git a/scripts/runtime/release-host-live.ts b/scripts/runtime/release-host-live.ts index 7272c30..5722873 100644 --- a/scripts/runtime/release-host-live.ts +++ b/scripts/runtime/release-host-live.ts @@ -1,3 +1,4 @@ +import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { chmodSync, @@ -42,6 +43,9 @@ import type { const RELEASE_REPOSITORY = "akua-dev/cli"; const ARCHIVE_TIMESTAMP_SECONDS = 315532800; const ARCHIVE_TIMESTAMP = new Date(ARCHIVE_TIMESTAMP_SECONDS * 1000); +// Node's default `maxBuffer` (1 MB) is too tight for `tar`/executable +// listing output; match Bun.spawnSync's effectively unbounded behavior. +const RELEASE_COMMAND_MAX_BUFFER = 64 * 1024 * 1024; function attempt( operation: string, @@ -744,20 +748,19 @@ function runCommand( ): Effect.Effect { return Effect.gen(function* () { const proc = yield* attempt("run release command", () => - Bun.spawnSync({ - cmd: command, - stdout: "pipe", - stderr: "pipe", + spawnSync(command[0], command.slice(1), { env: { ...process.env, ...extraEnv }, cwd, + encoding: "utf8", + maxBuffer: RELEASE_COMMAND_MAX_BUFFER, }), ); - const decoder = new TextDecoder(); - const stdout = decoder.decode(proc.stdout); - const stderr = decoder.decode(proc.stderr); + const stdout = proc.stdout; + const stderr = proc.stderr; + const exitCode = proc.status ?? -1; yield* check( - proc.exitCode === 0, - `${command[0]} failed (${proc.exitCode}): ${stderr.trim()}`, + exitCode === 0, + `${command[0]} failed (${exitCode}): ${stderr.trim()}`, ); return stdout; }); diff --git a/skills/effect-v4/SKILL.md b/skills/effect-v4/SKILL.md index 4149ba1..51b2b84 100644 --- a/skills/effect-v4/SKILL.md +++ b/skills/effect-v4/SKILL.md @@ -64,6 +64,14 @@ and test layers for time, I/O, process, browser, and console behavior; advance the clock deterministically instead of sleeping. Execute test Effects only in the test harness. +Tests run on `vitest` (`bun run test`), using `@effect/vitest`'s `it.effect` +for test bodies that execute an `Effect` — write the body as `Effect.gen` and +`yield*` instead of `await Effect.runPromise(...)`. Plain `vitest` `test`/ +`expect` stay for tests with no Effect to run (pure sync helpers, file/AST +assertions). `test/` is not covered by the production `Promise`/`throw`/ +`async`/`await` ban above; process-boundary test helpers (subprocess spawns, +`fetch` mocks) may still need them. + ## Source scan and verification Before handoff, inspect every production hit; the first scan must have no @@ -73,7 +81,7 @@ only to deliberate live-layer bridges: ```sh rg -n '\b(Promise|async|await|throw|runPromise)\b|\bas const\b|\bas [A-Za-z_{]' src scripts rg -n '\b(fetch|Bun\.(file|write|spawn)|process\.|console\.|readFile|writeFile)\b' src/commands src/runtime scripts -bun test +bun run test mise run check ``` diff --git a/test/auth-effect.test.ts b/test/auth-effect.test.ts index 883b60c..0f164bc 100644 --- a/test/auth-effect.test.ts +++ b/test/auth-effect.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, it } from "@effect/vitest"; import { Clock, Effect, Fiber, Layer } from "effect"; import { TestClock } from "effect/testing"; @@ -35,7 +35,8 @@ const testClockLayer = Layer.succeed(CliClock, { }); describe("Effect auth command", () => { - test("runs the device flow through injected services and TestClock", async () => { + it.effect("runs the device flow through injected services and TestClock", () => + Effect.gen(function* () { const requests: Array<{ url: string; body: Readonly> }> = []; const launched: string[] = []; @@ -91,9 +92,7 @@ describe("Effect auth command", () => { return yield* Fiber.join(fiber); }); - const envelope = await Effect.runPromise( - Effect.provide(program, services) as Effect.Effect, - ); + const envelope = yield* (Effect.provide(program, services) as Effect.Effect); expect(requests).toEqual([ { @@ -128,144 +127,145 @@ describe("Effect auth command", () => { authenticated: true, source: "config", }); - }); + }), + ); - test("renders terminal device authorization failures through runCli", async () => { - const render = async ( - reason: "access_denied" | "expired_token", - ): Promise<{ exitCode: number; payload: unknown }> => { - const stdout: string[] = []; - const responses = [ - { - status: 200, - body: { - device_code: "device-code", - user_code: "ABCD-EFGH", - verification_uri: "https://example.test/device", - expires_in: 60, - }, - }, - { status: 400, body: { error: reason } }, - ]; - const services = Layer.mergeAll( - Layer.succeed(Http, { - postJson: () => Effect.sync(() => responses.shift()!), - }), - Layer.succeed(Browser, { launch: () => Effect.void }), - Layer.succeed(Process, { awaitSignal: Effect.never }), - Layer.succeed(Console, { - stdoutIsTTY: false, - writeStderr: () => Effect.void, - writeStdout: (value) => Effect.sync(() => stdout.push(value)), - }), - Layer.succeed(SecureConfig, { - readToken: () => Effect.succeed(undefined), - saveToken: () => Effect.void, - removeToken: () => Effect.succeed(false), - }), - testClockLayer, - TestClock.layer(), - ); + it.effect("renders terminal device authorization failures through runCli", () => + Effect.gen(function* () { + const render = (reason: "access_denied" | "expired_token") => + Effect.gen(function* () { + const stdout: string[] = []; + const responses = [ + { + status: 200, + body: { + device_code: "device-code", + user_code: "ABCD-EFGH", + verification_uri: "https://example.test/device", + expires_in: 60, + }, + }, + { status: 400, body: { error: reason } }, + ]; + const services = Layer.mergeAll( + Layer.succeed(Http, { + postJson: () => Effect.sync(() => responses.shift()!), + }), + Layer.succeed(Browser, { launch: () => Effect.void }), + Layer.succeed(Process, { awaitSignal: Effect.never }), + Layer.succeed(Console, { + stdoutIsTTY: false, + writeStderr: () => Effect.void, + writeStdout: (value) => Effect.sync(() => stdout.push(value)), + }), + Layer.succeed(SecureConfig, { + readToken: () => Effect.succeed(undefined), + saveToken: () => Effect.void, + removeToken: () => Effect.succeed(false), + }), + testClockLayer, + TestClock.layer(), + ); - const exitCode = await Effect.runPromise( - Effect.provide( - runCli( - authView(["login", "--no-browser"], { HOME: "/test-home" }), - { mode: "json" }, - ), - services, - ) as Effect.Effect, - ); - return { exitCode, payload: JSON.parse(stdout.join("")) }; - }; + const exitCode = yield* (Effect.provide( + runCli( + authView(["login", "--no-browser"], { HOME: "/test-home" }), + { mode: "json" }, + ), + services, + ) as Effect.Effect); + return { exitCode, payload: JSON.parse(stdout.join("")) }; + }); - await expect(render("access_denied")).resolves.toMatchObject({ - exitCode: 3, - payload: { error: { code: "AKUA_DEVICE_ACCESS_DENIED" } }, - }); - await expect(render("expired_token")).resolves.toMatchObject({ - exitCode: 3, - payload: { error: { code: "AKUA_DEVICE_EXPIRED_TOKEN" } }, - }); - }); + expect(yield* render("access_denied")).toMatchObject({ + exitCode: 3, + payload: { error: { code: "AKUA_DEVICE_ACCESS_DENIED" } }, + }); + expect(yield* render("expired_token")).toMatchObject({ + exitCode: 3, + payload: { error: { code: "AKUA_DEVICE_EXPIRED_TOKEN" } }, + }); + }), + ); + + it.effect("maps tagged failures to distinct rendered error envelopes", () => + Effect.gen(function* () { + const render = ( + failure: + | UsageFailure + | ConfigFailure + | DeviceRequestFailure + | DeviceCancelledFailure + | DeviceAuthorizationFailure, + ) => + Effect.gen(function* () { + const stdout: string[] = []; + const exitCode = yield* Effect.provide( + runCli(Effect.fail(failure), { mode: "json" }), + Layer.succeed(Console, { + stdoutIsTTY: false, + writeStderr: () => Effect.void, + writeStdout: (value) => Effect.sync(() => stdout.push(value)), + }), + ); + return { exitCode, payload: JSON.parse(stdout.join("")) }; + }); - test("maps tagged failures to distinct rendered error envelopes", async () => { - const render = async ( - failure: - | UsageFailure - | ConfigFailure - | DeviceRequestFailure - | DeviceCancelledFailure - | DeviceAuthorizationFailure, - ) => { - const stdout: string[] = []; - const exitCode = await Effect.runPromise( - Effect.provide( - runCli(Effect.fail(failure), { mode: "json" }), - Layer.succeed(Console, { - stdoutIsTTY: false, - writeStderr: () => Effect.void, - writeStdout: (value) => Effect.sync(() => stdout.push(value)), + expect( + yield* render(new UsageFailure({ message: "Bad command." })), + ).toMatchObject({ + exitCode: 2, + payload: { error: { type: "usage_error", code: "AKUA_USAGE_ERROR" } }, + }); + expect( + yield* render( + new ConfigFailure({ + operation: "read", + path: "/config", + cause: new Error("denied"), }), ), - ); - return { exitCode, payload: JSON.parse(stdout.join("")) }; - }; - - await expect( - render(new UsageFailure({ message: "Bad command." })), - ).resolves.toMatchObject({ - exitCode: 2, - payload: { error: { type: "usage_error", code: "AKUA_USAGE_ERROR" } }, - }); - await expect( - render( - new ConfigFailure({ - operation: "read", - path: "/config", - cause: new Error("denied"), - }), - ), - ).resolves.toMatchObject({ - exitCode: 1, - payload: { error: { type: "runtime_error", code: "AKUA_CONFIG_ERROR" } }, - }); - await expect(render(new DeviceRequestFailure())).resolves.toMatchObject({ - exitCode: 3, - payload: { - error: { - type: "authentication_error", - code: "AKUA_DEVICE_REQUEST_FAILED", + ).toMatchObject({ + exitCode: 1, + payload: { error: { type: "runtime_error", code: "AKUA_CONFIG_ERROR" } }, + }); + expect(yield* render(new DeviceRequestFailure())).toMatchObject({ + exitCode: 3, + payload: { + error: { + type: "authentication_error", + code: "AKUA_DEVICE_REQUEST_FAILED", + }, }, - }, - }); - await expect(render(new DeviceCancelledFailure())).resolves.toMatchObject({ - exitCode: 1, - payload: { - error: { type: "runtime_error", code: "AKUA_DEVICE_CANCELLED" }, - }, - }); - await expect( - render(new DeviceAuthorizationFailure({ reason: "access_denied" })), - ).resolves.toMatchObject({ - exitCode: 3, - payload: { - error: { - type: "authentication_error", - code: "AKUA_DEVICE_ACCESS_DENIED", + }); + expect(yield* render(new DeviceCancelledFailure())).toMatchObject({ + exitCode: 1, + payload: { + error: { type: "runtime_error", code: "AKUA_DEVICE_CANCELLED" }, }, - }, - }); - await expect( - render(new DeviceAuthorizationFailure({ reason: "expired_token" })), - ).resolves.toMatchObject({ - exitCode: 3, - payload: { - error: { - type: "authentication_error", - code: "AKUA_DEVICE_EXPIRED_TOKEN", + }); + expect( + yield* render(new DeviceAuthorizationFailure({ reason: "access_denied" })), + ).toMatchObject({ + exitCode: 3, + payload: { + error: { + type: "authentication_error", + code: "AKUA_DEVICE_ACCESS_DENIED", + }, }, - }, - }); - }); + }); + expect( + yield* render(new DeviceAuthorizationFailure({ reason: "expired_token" })), + ).toMatchObject({ + exitCode: 3, + payload: { + error: { + type: "authentication_error", + code: "AKUA_DEVICE_EXPIRED_TOKEN", + }, + }, + }); + }), + ); }); diff --git a/test/bun-binary.ts b/test/bun-binary.ts new file mode 100644 index 0000000..6db693d --- /dev/null +++ b/test/bun-binary.ts @@ -0,0 +1,11 @@ +/** + * Resolves the bun executable used by tests that spawn a subprocess running + * Bun (either the `akua` CLI or `bun x `). `bun run`/`npm run` set + * `npm_execpath` to the fully resolved bun binary, which avoids paying a + * mise-shim resolution on every spawned process and stays correct now that + * tests run inside a vitest worker (a real Node process, not Bun) rather + * than `bun test`. + */ +export function resolveBunBinary(): string { + return process.env.npm_execpath ?? "bun"; +} diff --git a/test/cli.test.ts b/test/cli.test.ts index b1890ff..0353c9c 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, it, test } from "@effect/vitest"; import { chmod, mkdir, @@ -14,7 +14,6 @@ import { Effect } from "effect"; import { main } from "../src/bin/akua"; import { authView } from "../src/commands/auth"; import { renderSuccess, type RenderEnvelope } from "../src/runtime/render"; -import { toCliError } from "../src/runtime/effect-runtime"; import { CliLive } from "../src/runtime/services-live"; import { Console, @@ -22,85 +21,92 @@ import { PackageCliFailure, } from "../src/runtime/services"; import { runAuthView } from "./auth-test-layer"; +import { runAkua } from "./run-akua"; describe("akua entrypoint", () => { - test("embeds package help under the akua pkg invocation", async () => { - const calls: Array = []; - const run = (argv: readonly string[]) => Effect.runPromise( - main(argv, {}).pipe( - Effect.provideService(PackageCli, { - execute: (args) => - Effect.sync(() => { - calls.push(args); - return 0; - }), - }), - Effect.provideService(Console, { - stdoutIsTTY: true, - writeStderr: () => Effect.void, - writeStdout: () => Effect.void, - }), - Effect.provide(CliLive), - ), - ); - - expect(await run(["pkg"])).toBe(0); - expect(await run(["pkg", "render", "--help"])).toBe(0); - expect(calls).toEqual([ - ["--help"], - ["render", "--help"], - ]); - }); - - test("normalizes root output flags before dispatching package commands", async () => { - const calls: Array = []; - const run = (argv: readonly string[]) => Effect.runPromise( - main(argv, {}).pipe( - Effect.provideService(PackageCli, { - execute: (args) => - Effect.sync(() => { - calls.push(args); - return 0; - }), - }), - Effect.provide(CliLive), - ), - ); - - expect(await run(["--json", "pkg", "version"])).toBe(0); - expect(await run(["pkg", "version", "--output", "json"])).toBe(0); - expect(calls).toEqual([ - ["version", "--json"], - ["version", "--json"], - ]); - }); - - test("renders package loader failures through the Effect error boundary", async () => { - const stdout: string[] = []; - const exitCode = await Effect.runPromise( - main(["pkg", "version"], {}).pipe( - Effect.provideService(PackageCli, { - execute: () => - Effect.fail( - new PackageCliFailure({ cause: new Error("native detail") }), - ), - }), - Effect.provideService(Console, { - stdoutIsTTY: false, - writeStderr: () => Effect.void, - writeStdout: (value) => - Effect.sync(() => { - stdout.push(value); + it.effect("embeds package help under the akua pkg invocation", () => + Effect.gen(function* () { + const calls: Array = []; + const run = (argv: readonly string[]) => + main(argv, {}).pipe( + Effect.provideService(PackageCli, { + execute: (args) => + Effect.sync(() => { + calls.push(args); + return 0; + }), + }), + Effect.provideService(Console, { + stdoutIsTTY: true, + writeStderr: () => Effect.void, + writeStdout: () => Effect.void, + }), + Effect.provide(CliLive), + ); + + expect(yield* run(["pkg"])).toBe(0); + expect(yield* run(["pkg", "render", "--help"])).toBe(0); + expect(calls).toEqual([ + ["--help"], + ["render", "--help"], + ]); + }), + ); + + it.effect( + "normalizes root output flags before dispatching package commands", + () => + Effect.gen(function* () { + const calls: Array = []; + const run = (argv: readonly string[]) => + main(argv, {}).pipe( + Effect.provideService(PackageCli, { + execute: (args) => + Effect.sync(() => { + calls.push(args); + return 0; + }), }), - }), - Effect.provide(CliLive), - ), - ); + Effect.provide(CliLive), + ); + + expect(yield* run(["--json", "pkg", "version"])).toBe(0); + expect(yield* run(["pkg", "version", "--output", "json"])).toBe(0); + expect(calls).toEqual([ + ["version", "--json"], + ["version", "--json"], + ]); + }), + ); + + it.effect( + "renders package loader failures through the Effect error boundary", + () => + Effect.gen(function* () { + const stdout: string[] = []; + const exitCode = yield* main(["pkg", "version"], {}).pipe( + Effect.provideService(PackageCli, { + execute: () => + Effect.fail( + new PackageCliFailure({ cause: new Error("native detail") }), + ), + }), + Effect.provideService(Console, { + stdoutIsTTY: false, + writeStderr: () => Effect.void, + writeStdout: (value) => + Effect.sync(() => { + stdout.push(value); + }), + }), + Effect.provide(CliLive), + ); - expect(exitCode).toBe(1); - expect(stdout.join("\n")).toContain("AKUA_PACKAGE_UNAVAILABLE"); - expect(stdout.join("\n")).not.toContain("native detail"); - }); + expect(exitCode).toBe(1); + expect(stdout.join("\n")).toContain("AKUA_PACKAGE_UNAVAILABLE"); + expect(stdout.join("\n")).not.toContain("native detail"); + }), + ); test("uses Effect CLI to describe the interactive command tree", async () => { const root = await runAkua([]); @@ -831,33 +837,33 @@ describe("akua entrypoint", () => { } }); - test("auth status honors AKUA_API_TOKEN without HOME", async () => { - for (const home of [undefined, ""]) { - const envelope = await Effect.runPromise( - Effect.provide( + it.effect("auth status honors AKUA_API_TOKEN without HOME", () => + Effect.gen(function* () { + for (const home of [undefined, ""]) { + const envelope = yield* (Effect.provide( authView(["status"], { HOME: home, AKUA_API_TOKEN: "sk_akua_env", }), CliLive, - ) as Effect.Effect, - ); - const stdout = renderSuccess(envelope, "json"); - const payload = JSON.parse(stdout); - - expect(payload).toMatchObject({ - status: "ok", - command: "akua auth status", - observations: ["Authenticated with AKUA_API_TOKEN."], - data: { - authenticated: true, - source: "env", - }, - }); - expect(payload.data).not.toHaveProperty("config_path"); - expect(stdout).not.toContain("sk_akua_env"); - } - }); + ) as Effect.Effect); + const stdout = renderSuccess(envelope, "json"); + const payload = JSON.parse(stdout); + + expect(payload).toMatchObject({ + status: "ok", + command: "akua auth status", + observations: ["Authenticated with AKUA_API_TOKEN."], + data: { + authenticated: true, + source: "env", + }, + }); + expect(payload.data).not.toHaveProperty("config_path"); + expect(stdout).not.toContain("sk_akua_env"); + } + }), + ); test("auth logout removes stored token without clearing AKUA_API_TOKEN", async () => { const home = await makeTempHome(); @@ -1075,35 +1081,6 @@ describe("akua entrypoint", () => { }); -async function runAkua( - args: readonly string[], - env: Record = {}, -) { - const childEnv = { ...process.env, ...env }; - if (!("AKUA_OUTPUT" in env)) { - delete childEnv.AKUA_OUTPUT; - } - if (!("AKUA_API_TOKEN" in env)) { - delete childEnv.AKUA_API_TOKEN; - } - - const proc = Bun.spawn({ - // Use this Bun process directly: the CI PATH can be a mise shim, while - // tests need a deterministic executable for each isolated child process. - cmd: [process.execPath, "src/bin/akua.ts", ...args], - stdout: "pipe", - stderr: "pipe", - env: childEnv, - }); - - const [stdout, stderr, exitCode] = await Promise.all([ - new Response(proc.stdout).text(), - new Response(proc.stderr).text(), - proc.exited, - ]); - return { stdout, stderr, exitCode }; -} - async function makeTempHome(): Promise { return mkdtemp(join(process.cwd(), ".tmp-akua-home-")); } diff --git a/test/device-http.test.ts b/test/device-http.test.ts index 8a8a797..acaf07c 100644 --- a/test/device-http.test.ts +++ b/test/device-http.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, it } from "@effect/vitest"; import { Cause, Effect, Exit } from "effect"; import { FetchHttpClient } from "effect/unstable/http"; @@ -6,77 +6,89 @@ import { Http, HttpFailure } from "../src/runtime/services"; import { HttpLive } from "../src/runtime/services-live"; describe("device authorization HTTP", () => { - test("uses Effect's FetchHttpClient to encode JSON requests", async () => { - let received: Request | undefined; - const fetch = (input: RequestInfo | URL, init?: RequestInit) => { - received = new Request(input, init); - return Promise.resolve(Response.json({ access_token: "token" })); - }; - const program = Effect.gen(function* () { - const http = yield* Http; - return yield* http.postJson({ - url: "https://api.example.test/device/token", - body: { - client_id: "akua cli", - scope: "platform/read+write", - }, + it.effect("uses Effect's FetchHttpClient to encode JSON requests", () => + Effect.gen(function* () { + let received: Request | undefined; + // Mocks the `fetch` global's Promise-returning contract: FetchHttpClient + // requires this exact interop shape, which has no Effect replacement. + const fetch = (input: RequestInfo | URL, init?: RequestInit) => { + received = new Request(input, init); + return Promise.resolve(Response.json({ access_token: "token" })); + }; + const program = Effect.gen(function* () { + const http = yield* Http; + return yield* http.postJson({ + url: "https://api.example.test/device/token", + body: { + client_id: "akua cli", + scope: "platform/read+write", + }, + }); }); - }); - const response = await Effect.runPromise( - program.pipe( + const response = yield* program.pipe( Effect.provide(HttpLive), Effect.provideService(FetchHttpClient.Fetch, fetch), - ), - ); - - expect(response).toEqual({ status: 200, body: { access_token: "token" } }); - expect(received?.headers.get("content-type")).toContain("application/json"); - expect(await received?.json()).toEqual({ - client_id: "akua cli", - scope: "platform/read+write", - }); - }); - - test("maps invalid and oversized response bodies to HttpFailure", async () => { - const invalidJson = (input: RequestInfo | URL, init?: RequestInit) => { - return Promise.resolve( - new Response("not json", { - headers: { "content-type": "application/json" }, - }), ); - }; - const oversized = (input: RequestInfo | URL, init?: RequestInit) => { - return Promise.resolve( - new Response("x".repeat(16_385), { - headers: { "content-type": "application/json" }, - }), + + expect(response).toEqual({ status: 200, body: { access_token: "token" } }); + expect(received?.headers.get("content-type")).toContain( + "application/json", ); - }; - const request = Effect.gen(function* () { - const http = yield* Http; - return yield* http.postJson({ - url: "https://api.example.test/device/token", - body: {}, + const receivedRequest = received; + const receivedBody = + receivedRequest === undefined + ? undefined + : yield* Effect.promise(() => receivedRequest.json()); + expect(receivedBody).toEqual({ + client_id: "akua cli", + scope: "platform/read+write", }); - }); + }), + ); - const invalidResult = await Effect.runPromiseExit( - request.pipe( - Effect.provide(HttpLive), - Effect.provideService(FetchHttpClient.Fetch, invalidJson), - ), - ); - const oversizedResult = await Effect.runPromiseExit( - request.pipe( - Effect.provide(HttpLive), - Effect.provideService(FetchHttpClient.Fetch, oversized), - ), - ); + it.effect("maps invalid and oversized response bodies to HttpFailure", () => + Effect.gen(function* () { + // Mocks the `fetch` global's Promise-returning contract; see above. + const invalidJson = (_input: RequestInfo | URL, _init?: RequestInit) => { + return Promise.resolve( + new Response("not json", { + headers: { "content-type": "application/json" }, + }), + ); + }; + const oversized = (_input: RequestInfo | URL, _init?: RequestInit) => { + return Promise.resolve( + new Response("x".repeat(16_385), { + headers: { "content-type": "application/json" }, + }), + ); + }; + const request = Effect.gen(function* () { + const http = yield* Http; + return yield* http.postJson({ + url: "https://api.example.test/device/token", + body: {}, + }); + }); + + const invalidResult = yield* Effect.exit( + request.pipe( + Effect.provide(HttpLive), + Effect.provideService(FetchHttpClient.Fetch, invalidJson), + ), + ); + const oversizedResult = yield* Effect.exit( + request.pipe( + Effect.provide(HttpLive), + Effect.provideService(FetchHttpClient.Fetch, oversized), + ), + ); - expect(hasHttpFailure(invalidResult)).toBe(true); - expect(hasHttpFailure(oversizedResult)).toBe(true); - }); + expect(hasHttpFailure(invalidResult)).toBe(true); + expect(hasHttpFailure(oversizedResult)).toBe(true); + }), + ); }); function hasHttpFailure(exit: Exit.Exit): boolean { diff --git a/test/docs.test.ts b/test/docs.test.ts index 12d31aa..fcac6f2 100644 --- a/test/docs.test.ts +++ b/test/docs.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, test } from "vitest"; import { readFile } from "node:fs/promises"; async function text(path: string): Promise { diff --git a/test/effect-generator-patch.test.ts b/test/effect-generator-patch.test.ts index 7a18582..9deace7 100644 --- a/test/effect-generator-patch.test.ts +++ b/test/effect-generator-patch.test.ts @@ -1,8 +1,11 @@ -import { expect, test } from "bun:test"; +import { expect, test } from "vitest"; +import { spawnSync } from "node:child_process"; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { resolveBunBinary } from "./bun-binary"; + test("patched Effect generator preserves headers and SSE contracts without warnings", () => { const directory = mkdtempSync(join(tmpdir(), "akua-effect-generator-")); const specPath = join(directory, "public.json"); @@ -10,9 +13,9 @@ test("patched Effect generator preserves headers and SSE contracts without warni try { writeFileSync(specPath, JSON.stringify(specification())); - const result = Bun.spawnSync({ - cmd: [ - process.execPath, + const result = spawnSync( + resolveBunBinary(), + [ "x", "--no-install", "openapigen", @@ -23,12 +26,11 @@ test("patched Effect generator preserves headers and SSE contracts without warni "--name", "PublicApi", ], - stdout: "pipe", - stderr: "pipe", - }); + { encoding: "utf8" }, + ); - expect(result.exitCode).toBe(0); - expect(new TextDecoder().decode(result.stderr)).not.toContain("warning"); + expect(result.status).toBe(0); + expect(result.stderr).not.toContain("warning"); writeFileSync(outputPath, result.stdout); const output = readFileSync(outputPath, "utf8"); expect(output).toContain("HttpApiSchema.WithHeaders"); diff --git a/test/effect-runtime.test.ts b/test/effect-runtime.test.ts index 9ff65f9..c522134 100644 --- a/test/effect-runtime.test.ts +++ b/test/effect-runtime.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, it, test } from "@effect/vitest"; import { readFileSync } from "node:fs"; import { Clock, Effect, Layer, Stream } from "effect"; import { TestClock } from "effect/testing"; @@ -20,10 +20,10 @@ describe("Effect CLI runtime", () => { expect(runtime).not.toContain("function rendererMode"); }); - test("runs an Effect command through the central render boundary", async () => { - const stdout: string[] = []; - const exitCode = await Effect.runPromise( - Effect.provide( + it.effect("runs an Effect command through the central render boundary", () => + Effect.gen(function* () { + const stdout: string[] = []; + const exitCode = yield* Effect.provide( runCli( Effect.succeed({ command: "akua test", @@ -37,21 +37,21 @@ describe("Effect CLI runtime", () => { writeStderr: () => Effect.void, writeStdout: (value) => Effect.sync(() => stdout.push(value)), }), - ), - ); + ); - expect(exitCode).toBe(0); - expect(JSON.parse(stdout.join(""))).toMatchObject({ - status: "ok", - command: "akua test", - data: { runtime: "effect-v4" }, - }); - }); + expect(exitCode).toBe(0); + expect(JSON.parse(stdout.join(""))).toMatchObject({ + status: "ok", + command: "akua test", + data: { runtime: "effect-v4" }, + }); + }), + ); - test("renders streaming command results incrementally", async () => { - const stdout: string[] = []; - const exitCode = await Effect.runPromise( - Effect.provide( + it.effect("renders streaming command results incrementally", () => + Effect.gen(function* () { + const stdout: string[] = []; + const exitCode = yield* Effect.provide( runCli( Effect.succeed({ command: "akua installs get-logs", @@ -67,50 +67,54 @@ describe("Effect CLI runtime", () => { writeStderr: () => Effect.void, writeStdout: (value) => Effect.sync(() => stdout.push(value)), }), - ), - ); + ); - expect(exitCode).toBe(0); - expect(stdout.map((value) => JSON.parse(value))).toEqual([ - { - status: "ok", - command: "akua installs get-logs", - data: { event: "message", data: "first" }, - }, - { - status: "ok", - command: "akua installs get-logs", - data: { event: "end", data: "{}" }, - }, - ]); - }); + expect(exitCode).toBe(0); + expect(stdout.map((value) => JSON.parse(value))).toEqual([ + { + status: "ok", + command: "akua installs get-logs", + data: { event: "message", data: "first" }, + }, + { + status: "ok", + command: "akua installs get-logs", + data: { event: "end", data: "{}" }, + }, + ]); + }), + ); - test("uses service tags and TestClock layers without host dependencies", async () => { - const services = Layer.mergeAll( - Layer.succeed(Http, { postJson: () => Effect.die("not used") }), - Layer.succeed(Browser, { launch: () => Effect.void }), - Layer.succeed(Process, { awaitSignal: Effect.never }), - Layer.succeed(Console, { - stdoutIsTTY: false, - writeStderr: () => Effect.void, - writeStdout: () => Effect.void, - }), - Layer.succeed(SecureConfig, { - readToken: () => Effect.succeed(undefined), - saveToken: () => Effect.void, - removeToken: () => Effect.succeed(false), - }), - TestClock.layer(), - ); - const program = Effect.gen(function* () { - yield* Http; - yield* Browser; - yield* Process; - yield* Console; - yield* SecureConfig; - return yield* Clock.currentTimeMillis; - }); + it.effect( + "uses service tags and TestClock layers without host dependencies", + () => + Effect.gen(function* () { + const services = Layer.mergeAll( + Layer.succeed(Http, { postJson: () => Effect.die("not used") }), + Layer.succeed(Browser, { launch: () => Effect.void }), + Layer.succeed(Process, { awaitSignal: Effect.never }), + Layer.succeed(Console, { + stdoutIsTTY: false, + writeStderr: () => Effect.void, + writeStdout: () => Effect.void, + }), + Layer.succeed(SecureConfig, { + readToken: () => Effect.succeed(undefined), + saveToken: () => Effect.void, + removeToken: () => Effect.succeed(false), + }), + TestClock.layer(), + ); + const program = Effect.gen(function* () { + yield* Http; + yield* Browser; + yield* Process; + yield* Console; + yield* SecureConfig; + return yield* Clock.currentTimeMillis; + }); - expect(await Effect.runPromise(Effect.provide(program, services))).toBe(0); - }); + expect(yield* Effect.provide(program, services)).toBe(0); + }), + ); }); diff --git a/test/effect-v4-skill.test.ts b/test/effect-v4-skill.test.ts index 1136682..704722a 100644 --- a/test/effect-v4-skill.test.ts +++ b/test/effect-v4-skill.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, test } from "vitest"; import { existsSync, readFileSync } from "node:fs"; const EFFECT_SKILL_PATH = "skills/effect-v4/SKILL.md"; @@ -47,7 +47,7 @@ describe("Effect v4 CLI quality guidance", () => { "fiber", "## Red flags", "mise run check", - "bun test", + "bun run test", ]) { expect(skill).toContain(rule); } diff --git a/test/fetch-openapi.test.ts b/test/fetch-openapi.test.ts index 4e8d5ff..5f22eb4 100644 --- a/test/fetch-openapi.test.ts +++ b/test/fetch-openapi.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, it, test } from "@effect/vitest"; import { mkdtemp, readFile, rm } from "node:fs/promises"; import { join } from "node:path"; import { Console, Effect, Layer } from "effect"; @@ -26,40 +26,40 @@ describe("OpenAPI fetch guard", () => { ); }); - test("accepts an optional OpenAPI URL positional argument", async () => { - let requested = ""; - const services = Layer.mergeAll( - cliTestLayer, - Layer.succeed(ScriptHttp, { - getJson: (url) => - Effect.sync(() => { - requested = url.href; - return { openapi: "3.1.0", paths: {} }; - }), - }), - Layer.succeed(ScriptFiles, { - readText: () => Effect.succeed(""), - writeText: () => Effect.void, - }), - Layer.succeed(ScriptEnvironment, { - openApiUrl: Effect.succeed(undefined), - }), - ); - const testConsole = Object.assign(Object.create(console), { - error: () => {}, - }) as Console.Console; + it.effect("accepts an optional OpenAPI URL positional argument", () => + Effect.gen(function* () { + let requested = ""; + const services = Layer.mergeAll( + cliTestLayer, + Layer.succeed(ScriptHttp, { + getJson: (url) => + Effect.sync(() => { + requested = url.href; + return { openapi: "3.1.0", paths: {} }; + }), + }), + Layer.succeed(ScriptFiles, { + readText: () => Effect.succeed(""), + writeText: () => Effect.void, + }), + Layer.succeed(ScriptEnvironment, { + openApiUrl: Effect.succeed(undefined), + }), + ); + const testConsole = Object.assign(Object.create(console), { + error: () => {}, + }) as Console.Console; - await Effect.runPromise( - Command.runWith(fetchOpenApiCommand, { version: "test" })([ + yield* Command.runWith(fetchOpenApiCommand, { version: "test" })([ "https://example.test/openapi.json", ]).pipe( Effect.provide(services), Effect.provideService(Console.Console, testConsole), - ), - ); + ); - expect(requested).toBe("https://example.test/openapi.json"); - }); + expect(requested).toBe("https://example.test/openapi.json"); + }), + ); test("rejects non-https URLs", () => { expect(() => @@ -76,37 +76,51 @@ describe("OpenAPI fetch guard", () => { ).toThrow("OpenAPI 3.x"); }); - test("writes stable output when an unchanged spec is fetched repeatedly", async () => { - const originalFetch = globalThis.fetch; - const root = await mkdtemp(join(process.cwd(), ".tmp-akua-openapi-")); - const output = join(root, "public.json"); - const spec = { - paths: { "/health": { get: { operationId: "health" } } }, - openapi: "3.1.0", - }; - globalThis.fetch = (async () => - Response.json(spec)) as unknown as typeof fetch; - try { - await Effect.runPromise( - Effect.provide( - fetchOpenApi(new URL(DEFAULT_OPENAPI_URL), output), - ScriptLive, - ), - ); - const first = await readFile(output, "utf8"); - await Effect.runPromise( - Effect.provide( - fetchOpenApi(new URL(DEFAULT_OPENAPI_URL), output), - ScriptLive, - ), - ); - const second = await readFile(output, "utf8"); + it.effect( + "writes stable output when an unchanged spec is fetched repeatedly", + () => + Effect.gen(function* () { + const originalFetch = globalThis.fetch; + const root = yield* Effect.promise(() => + mkdtemp(join(process.cwd(), ".tmp-akua-openapi-")), + ); + const output = join(root, "public.json"); + const spec = { + paths: { "/health": { get: { operationId: "health" } } }, + openapi: "3.1.0", + }; + // Mocks the `fetch` global's Promise-returning contract; no Effect + // replacement exists for this interop shape. + globalThis.fetch = (async () => + Response.json(spec)) as unknown as typeof fetch; - expect(second).toBe(first); - expect(second).toBe(`${JSON.stringify(spec, null, 2)}\n`); - } finally { - globalThis.fetch = originalFetch; - await rm(root, { recursive: true, force: true }); - } - }); + yield* Effect.gen(function* () { + yield* Effect.provide( + fetchOpenApi(new URL(DEFAULT_OPENAPI_URL), output), + ScriptLive, + ); + const first = yield* Effect.promise(() => readFile(output, "utf8")); + yield* Effect.provide( + fetchOpenApi(new URL(DEFAULT_OPENAPI_URL), output), + ScriptLive, + ); + const second = yield* Effect.promise(() => readFile(output, "utf8")); + + expect(second).toBe(first); + expect(second).toBe(`${JSON.stringify(spec, null, 2)}\n`); + }).pipe( + Effect.ensuring( + Effect.sync(() => { + globalThis.fetch = originalFetch; + }).pipe( + Effect.andThen( + Effect.promise(() => + rm(root, { recursive: true, force: true }), + ), + ), + ), + ), + ); + }), + ); }); diff --git a/test/generate-commands.test.ts b/test/generate-commands.test.ts index 2a41e27..8cc0795 100644 --- a/test/generate-commands.test.ts +++ b/test/generate-commands.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, it, test } from "@effect/vitest"; import { Effect, Layer } from "effect"; import { Command } from "effect/unstable/cli"; @@ -10,28 +10,36 @@ import { ScriptFiles } from "../scripts/runtime/services"; import { cliTestLayer } from "./cli-test-layer"; describe("collectPublicCommands", () => { - test("parses --check and fails when the generated registry is stale", async () => { - let reads = 0; - const services = Layer.mergeAll( - cliTestLayer, - Layer.succeed(ScriptFiles, { - readText: () => - Effect.sync(() => { - reads += 1; - return reads === 1 ? JSON.stringify({ paths: {} }) : "out of date"; + it.effect( + "parses --check and fails when the generated registry is stale", + () => + Effect.gen(function* () { + let reads = 0; + const services = Layer.mergeAll( + cliTestLayer, + Layer.succeed(ScriptFiles, { + readText: () => + Effect.sync(() => { + reads += 1; + return reads === 1 + ? JSON.stringify({ paths: {} }) + : "out of date"; + }), + writeText: () => Effect.void, }), - writeText: () => Effect.void, - }), - ); + ); - await expect( - Effect.runPromise( - Command.runWith(generateCommandsCommand, { version: "test" })([ - "--check", - ]).pipe(Effect.provide(services)), - ), - ).rejects.toThrow("src/generated/commands.gen.ts is out of date"); - }); + const failure = yield* Effect.flip( + Command.runWith(generateCommandsCommand, { version: "test" })([ + "--check", + ]).pipe(Effect.provide(services)), + ); + + expect(failure.message).toContain( + "src/generated/commands.gen.ts is out of date", + ); + }), + ); test("includes public operations and excludes non-public operations", () => { const commands = Effect.runSync( diff --git a/test/generate-effect-api.test.ts b/test/generate-effect-api.test.ts index fa8b002..2f4ece6 100644 --- a/test/generate-effect-api.test.ts +++ b/test/generate-effect-api.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "bun:test"; +import { expect, it } from "@effect/vitest"; import { readFileSync } from "node:fs"; import { Effect, Layer } from "effect"; import ts from "typescript"; @@ -14,138 +14,167 @@ const sourcePath = "openapi/public.json"; const outputPath = "src/generated/openapi-api.gen.ts"; const executorPath = "src/generated/public-operation-executor.gen.ts"; -test("generates a typed HttpApi module from the public OpenAPI contract", async () => { - let writtenApi = ""; - let writtenExecutor = ""; - const layer = Layer.succeed(ScriptFiles, { - readText: () => Effect.succeed(JSON.stringify(publicSpec())), - writeText: (path, contents) => - Effect.sync(() => { - if (path === outputPath) writtenApi = contents; - if (path === executorPath) writtenExecutor = contents; - }), - }); +it.effect( + "generates a typed HttpApi module from the public OpenAPI contract", + () => + Effect.gen(function* () { + let writtenApi = ""; + let writtenExecutor = ""; + const layer = Layer.succeed(ScriptFiles, { + readText: () => Effect.succeed(JSON.stringify(publicSpec())), + writeText: (path, contents) => + Effect.sync(() => { + if (path === outputPath) writtenApi = contents; + if (path === executorPath) writtenExecutor = contents; + }), + }); - const generated = await Effect.runPromise( - generateEffectApi(sourcePath, outputPath).pipe(Effect.provide(layer)), - ); + const generated = yield* generateEffectApi(sourcePath, outputPath).pipe( + Effect.provide(layer), + ); - expect(writtenApi).toBe(generated); - expect(writtenExecutor).toContain('case "secrets.create":'); - expect(generated).toContain( - 'HttpApiEndpoint.post("secretsCreate", "/v1/secrets"', - ); - expect(generated).toContain('annotate(OpenApi.Identifier, "secrets.create")'); - expect(typeAssertions(generated)).toEqual([]); - expect(generated).not.toMatch(/[ \t]+$/m); -}); + expect(writtenApi).toBe(generated); + expect(writtenExecutor).toContain('case "secrets.create":'); + expect(generated).toContain( + 'HttpApiEndpoint.post("secretsCreate", "/v1/secrets"', + ); + expect(generated).toContain( + 'annotate(OpenApi.Identifier, "secrets.create")', + ); + expect(typeAssertions(generated)).toEqual([]); + expect(generated).not.toMatch(/[ \t]+$/m); + }), +); -test("generates only PUBLIC operations", async () => { - const layer = Layer.succeed(ScriptFiles, { - readText: () => Effect.succeed(JSON.stringify(specWithMixedVisibility())), - writeText: () => Effect.void, - }); +it.effect("generates only PUBLIC operations", () => + Effect.gen(function* () { + const layer = Layer.succeed(ScriptFiles, { + readText: () => Effect.succeed(JSON.stringify(specWithMixedVisibility())), + writeText: () => Effect.void, + }); - const generated = await Effect.runPromise( - generateEffectApi(sourcePath, outputPath).pipe(Effect.provide(layer)), - ); + const generated = yield* generateEffectApi(sourcePath, outputPath).pipe( + Effect.provide(layer), + ); - expect(generated).toContain( - 'HttpApiEndpoint.get("secretsList", "/v1/secrets"', - ); - expect(generated).toContain("pageSize"); - expect(generated).not.toContain("adminListSecrets"); -}); + expect(generated).toContain( + 'HttpApiEndpoint.get("secretsList", "/v1/secrets"', + ); + expect(generated).toContain("pageSize"); + expect(generated).not.toContain("adminListSecrets"); + }), +); -test("fails with a typed error when the generator reports a public contract warning", async () => { - const layer = Layer.succeed(ScriptFiles, { - readText: () => Effect.succeed(JSON.stringify(specWithUnannotatedSse())), - writeText: () => Effect.void, - }); +it.effect( + "fails with a typed error when the generator reports a public contract warning", + () => + Effect.gen(function* () { + const layer = Layer.succeed(ScriptFiles, { + readText: () => Effect.succeed(JSON.stringify(specWithUnannotatedSse())), + writeText: () => Effect.void, + }); - await expect( - Effect.runPromise( - generateEffectApi(sourcePath, outputPath).pipe(Effect.provide(layer)), - ), - ).rejects.toBeInstanceOf(EffectApiGenerationFailure); -}); + const failure = yield* Effect.flip( + generateEffectApi(sourcePath, outputPath).pipe(Effect.provide(layer)), + ); -test("maps generator defects to a typed generation error", async () => { - const layer = Layer.succeed(ScriptFiles, { - readText: () => Effect.succeed(JSON.stringify(specWithInvalidPattern())), - writeText: () => Effect.void, - }); + expect(failure).toBeInstanceOf(EffectApiGenerationFailure); + }), +); - await expect( - Effect.runPromise( +it.effect("maps generator defects to a typed generation error", () => + Effect.gen(function* () { + const layer = Layer.succeed(ScriptFiles, { + readText: () => Effect.succeed(JSON.stringify(specWithInvalidPattern())), + writeText: () => Effect.void, + }); + + const failure = yield* Effect.flip( generateEffectApi(sourcePath, outputPath).pipe(Effect.provide(layer)), - ), - ).rejects.toBeInstanceOf(EffectApiGenerationFailure); -}); + ); -test("detects generated API drift without overwriting the checked-in artifact", async () => { - let writes = 0; - const layer = Layer.succeed(ScriptFiles, { - readText: (path) => - Effect.succeed( - path === sourcePath ? JSON.stringify(publicSpec()) : "stale artifact", - ), - writeText: () => - Effect.sync(() => { - writes += 1; - }), - }); + expect(failure).toBeInstanceOf(EffectApiGenerationFailure); + }), +); - await expect( - Effect.runPromise( - checkEffectApi(sourcePath, outputPath).pipe(Effect.provide(layer)), - ), - ).rejects.toBeInstanceOf(EffectApiGenerationFailure); +it.effect( + "detects generated API drift without overwriting the checked-in artifact", + () => + Effect.gen(function* () { + let writes = 0; + const layer = Layer.succeed(ScriptFiles, { + readText: (path) => + Effect.succeed( + path === sourcePath + ? JSON.stringify(publicSpec()) + : "stale artifact", + ), + writeText: () => + Effect.sync(() => { + writes += 1; + }), + }); - expect(writes).toBe(0); -}); + const failure = yield* Effect.flip( + checkEffectApi(sourcePath, outputPath).pipe(Effect.provide(layer)), + ); -test("propagates generated artifact read failures instead of treating them as drift", async () => { - const layer = Layer.succeed(ScriptFiles, { - readText: (path) => - path === sourcePath - ? Effect.succeed(JSON.stringify(publicSpec())) - : Effect.fail(new ScriptHostFailure({ cause: "permission denied" })), - writeText: () => Effect.void, - }); + expect(failure).toBeInstanceOf(EffectApiGenerationFailure); + expect(writes).toBe(0); + }), +); - await expect( - Effect.runPromise( - checkEffectApi(sourcePath, outputPath).pipe(Effect.provide(layer)), - ), - ).rejects.toBeInstanceOf(ScriptHostFailure); -}); +it.effect( + "propagates generated artifact read failures instead of treating them as drift", + () => + Effect.gen(function* () { + const layer = Layer.succeed(ScriptFiles, { + readText: (path) => + path === sourcePath + ? Effect.succeed(JSON.stringify(publicSpec())) + : Effect.fail( + new ScriptHostFailure({ cause: "permission denied" }), + ), + writeText: () => Effect.void, + }); -test("checked-in public contract produces the committed strict Effect API artifact", async () => { - const source = readFileSync(sourcePath, "utf8"); - const artifact = readFileSync(outputPath, "utf8"); - const executor = readFileSync(executorPath, "utf8"); - const layer = Layer.succeed(ScriptFiles, { - readText: (path) => - Effect.succeed( - path === sourcePath - ? source - : path === outputPath - ? artifact - : executor, - ), - writeText: () => Effect.void, - }); + const failure = yield* Effect.flip( + checkEffectApi(sourcePath, outputPath).pipe(Effect.provide(layer)), + ); - await Effect.runPromise( - checkEffectApi(sourcePath, outputPath).pipe(Effect.provide(layer)), - ); + expect(failure).toBeInstanceOf(ScriptHostFailure); + }), +); + +it.effect( + "checked-in public contract produces the committed strict Effect API artifact", + () => + Effect.gen(function* () { + const source = readFileSync(sourcePath, "utf8"); + const artifact = readFileSync(outputPath, "utf8"); + const executor = readFileSync(executorPath, "utf8"); + const layer = Layer.succeed(ScriptFiles, { + readText: (path) => + Effect.succeed( + path === sourcePath + ? source + : path === outputPath + ? artifact + : executor, + ), + writeText: () => Effect.void, + }); + + yield* checkEffectApi(sourcePath, outputPath).pipe(Effect.provide(layer)); - expect(artifact).toContain('annotate(OpenApi.Identifier, "secrets.create")'); - expect(typeAssertions(artifact)).toEqual([]); - expect(artifact).not.toMatch(/[ \t]+$/m); - expect(executor).toContain('case "machines.create":'); -}); + expect(artifact).toContain( + 'annotate(OpenApi.Identifier, "secrets.create")', + ); + expect(typeAssertions(artifact)).toEqual([]); + expect(artifact).not.toMatch(/[ \t]+$/m); + expect(executor).toContain('case "machines.create":'); + }), +); function publicSpec() { return { diff --git a/test/generated-command.test.ts b/test/generated-command.test.ts index b5b9e7f..c1b1373 100644 --- a/test/generated-command.test.ts +++ b/test/generated-command.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, test } from "vitest"; import { Effect, Layer, Stream } from "effect"; import { FetchHttpClient } from "effect/unstable/http"; diff --git a/test/generated-operation-executor.test.ts b/test/generated-operation-executor.test.ts index d279861..5d36c21 100644 --- a/test/generated-operation-executor.test.ts +++ b/test/generated-operation-executor.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "bun:test"; +import { expect, test } from "vitest"; import { existsSync, readFileSync } from "node:fs"; import ts from "typescript"; import { commandRegistry } from "../src/generated/commands.gen"; diff --git a/test/mode.test.ts b/test/mode.test.ts index 41c243f..c6da182 100644 --- a/test/mode.test.ts +++ b/test/mode.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, test } from "vitest"; import { Effect } from "effect"; import { detectOutputMode } from "../src/runtime/mode"; diff --git a/test/production-effect-invariants.test.ts b/test/production-effect-invariants.test.ts index 24e36bf..cba0f8d 100644 --- a/test/production-effect-invariants.test.ts +++ b/test/production-effect-invariants.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "bun:test"; +import { expect, test } from "vitest"; import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import ts from "typescript"; diff --git a/test/release-please-config.test.ts b/test/release-please-config.test.ts index 62abec3..2130888 100644 --- a/test/release-please-config.test.ts +++ b/test/release-please-config.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, test } from "vitest"; import { readFileSync } from "node:fs"; interface ReleasePleaseConfig { diff --git a/test/release.test.ts b/test/release.test.ts index 754d231..6e0a572 100644 --- a/test/release.test.ts +++ b/test/release.test.ts @@ -1,5 +1,7 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, it, test } from "@effect/vitest"; +import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; +import { existsSync } from "node:fs"; import { chmod, copyFile, @@ -12,6 +14,7 @@ import { writeFile, } from "node:fs/promises"; import { parse, join } from "node:path"; +import { setTimeout as sleep } from "node:timers/promises"; import { Console, Effect, Layer } from "effect"; import { Command } from "effect/unstable/cli"; @@ -86,8 +89,8 @@ async function makePackageRuntimeFixture(root: string): Promise { return packageRoot; } -test("release packaging has a dedicated implementation module", async () => { - expect(await Bun.file("scripts/release.ts").exists()).toBe(true); +test("release packaging has a dedicated implementation module", () => { + expect(existsSync("scripts/release.ts")).toBe(true); }); test("keeps exported release contract helpers free of host APIs", async () => { @@ -96,32 +99,30 @@ test("keeps exported release contract helpers free of host APIs", async () => { expect(helpers).not.toContain('from "node:crypto"'); expect(helpers).not.toContain("process.platform"); expect(helpers).not.toContain("process.arch"); - expect(await Bun.file("scripts/runtime/release-host-live.ts").exists()).toBe( - true, - ); + expect(existsSync("scripts/runtime/release-host-live.ts")).toBe(true); }); describe("release target contract", () => { - test("renders the release matrix as JSON through the matrix subcommand", async () => { - const stdout: string[] = []; - const testConsole = Object.assign(Object.create(console), { - log: (value: string) => stdout.push(value), - }) as Console.Console; - - await Effect.runPromise( - Command.runWith(releaseCommand, { version: "test" })(["matrix"]).pipe( + it.effect("renders the release matrix as JSON through the matrix subcommand", () => + Effect.gen(function* () { + const stdout: string[] = []; + const testConsole = Object.assign(Object.create(console), { + log: (value: string) => stdout.push(value), + }) as Console.Console; + + yield* Command.runWith(releaseCommand, { version: "test" })(["matrix"]).pipe( Effect.provide(Layer.mergeAll(cliTestLayer, ReleaseHostLive)), Effect.provideService(Console.Console, testConsole), - ), - ); + ); - expect(JSON.parse(stdout.join("\n"))).toEqual({ - include: RELEASE_TARGETS.map((target) => ({ - target: target.id, - runner: target.runner, - })), - }); - }); + expect(JSON.parse(stdout.join("\n"))).toEqual({ + include: RELEASE_TARGETS.map((target) => ({ + target: target.id, + runner: target.runner, + })), + }); + }), + ); test("public release operations require ReleaseHost and never provide its live layer", async () => { const release = await import("../scripts/release"); @@ -475,22 +476,17 @@ describe("release target contract", () => { const extractDir = join(root, "extract"); await mkdir(extractDir); - const proc = Bun.spawn({ - cmd: [ - "tar", + const extract = spawnSync( + "tar", + [ "-xzf", join(outputDir, "akua-v1.2.3-linux-x64.tar.gz"), "-C", extractDir, ], - stdout: "pipe", - stderr: "pipe", - }); - const [exitCode] = await Promise.all([ - proc.exited, - new Response(proc.stderr).text(), - ]); - expect(exitCode).toBe(0); + { encoding: "utf8" }, + ); + expect(extract.status).toBe(0); expect((await stat(join(extractDir, "akua"))).mode & 0o777).toBe(0o755); expect(await readFile(join(extractDir, "akua"))).toEqual( await readFile(source), @@ -564,7 +560,7 @@ describe("release target contract", () => { packageRoot, }), ); - await Bun.sleep(2100); + await sleep(2100); runRelease( packageExistingExecutables({ version: "1.2.3", @@ -921,11 +917,10 @@ describe("release target contract", () => { const archivePath = join(outputDir, artifactName("1.2.3", target)); const extractDir = join(root, "extracted"); await mkdir(extractDir, { recursive: true }); - const extract = Bun.spawnSync({ - cmd: ["tar", "-xzf", archivePath, "-C", extractDir], - stderr: "pipe", + const extract = spawnSync("tar", ["-xzf", archivePath, "-C", extractDir], { + encoding: "utf8", }); - expect(extract.exitCode).toBe(0); + expect(extract.status).toBe(0); const sdkDir = join(extractDir, "node_modules", "@akua-dev", "sdk"); expect(await readFile(join(sdkDir, "dist", "mod.js"), "utf8")).toBe( diff --git a/test/render.test.ts b/test/render.test.ts index ce68200..f55235f 100644 --- a/test/render.test.ts +++ b/test/render.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, test } from "vitest"; import { AkuaCliError } from "../src/runtime/errors"; import { renderError, renderSuccess } from "../src/runtime/render"; diff --git a/test/run-akua.ts b/test/run-akua.ts new file mode 100644 index 0000000..b1925b4 --- /dev/null +++ b/test/run-akua.ts @@ -0,0 +1,42 @@ +import { spawnSync } from "node:child_process"; + +import { resolveBunBinary } from "./bun-binary"; + +/** + * Shared subprocess helper for tests that exercise the real `akua` CLI + * entrypoint end to end. Genuine process-boundary glue: it necessarily spawns + * an OS process and is exempt from this repo's Effect-only rule for `src/` + * and `scripts/` (test/ is not covered by that rule; see AGENTS.md). + */ +export interface RunAkuaResult { + readonly stdout: string; + readonly stderr: string; + readonly exitCode: number; +} + +export function runAkua( + args: readonly string[], + env: Record = {}, +): RunAkuaResult { + const childEnv: Record = { + ...process.env, + ...env, + }; + if (!("AKUA_OUTPUT" in env)) { + delete childEnv.AKUA_OUTPUT; + } + if (!("AKUA_API_TOKEN" in env)) { + delete childEnv.AKUA_API_TOKEN; + } + + const result = spawnSync(resolveBunBinary(), ["src/bin/akua.ts", ...args], { + env: childEnv, + encoding: "utf8", + }); + + return { + stdout: result.stdout ?? "", + stderr: result.stderr ?? "", + exitCode: result.status ?? -1, + }; +} diff --git a/test/strict-effect-control-flow.test.ts b/test/strict-effect-control-flow.test.ts index cd7e9d8..902a433 100644 --- a/test/strict-effect-control-flow.test.ts +++ b/test/strict-effect-control-flow.test.ts @@ -1,9 +1,11 @@ -import { expect, test } from "bun:test"; +import { expect, test } from "vitest"; import { readFileSync, readdirSync } from "node:fs"; import { join } from "node:path"; import ts from "typescript"; +import { runAkua } from "./run-akua"; + const productionFiles = [ ...findTypeScriptFiles("src"), ...findTypeScriptFiles("scripts"), @@ -97,31 +99,3 @@ function findAliasedImports(file: string): string[] { visit(source); return aliases; } - -async function runAkua( - args: readonly string[], - env: Record = {}, -) { - const childEnv = { ...process.env, ...env }; - if (!("AKUA_OUTPUT" in env)) { - delete childEnv.AKUA_OUTPUT; - } - if (!("AKUA_API_TOKEN" in env)) { - delete childEnv.AKUA_API_TOKEN; - } - - const proc = Bun.spawn({ - cmd: ["bun", "src/bin/akua.ts", ...args], - stdout: "pipe", - stderr: "pipe", - env: childEnv, - }); - - const [stdout, stderr, exitCode] = await Promise.all([ - new Response(proc.stdout).text(), - new Response(proc.stderr).text(), - proc.exited, - ]); - - return { stdout, stderr, exitCode }; -} diff --git a/test/workflows.test.ts b/test/workflows.test.ts index 3945234..0efb7ec 100644 --- a/test/workflows.test.ts +++ b/test/workflows.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, test } from "vitest"; import { readFile } from "node:fs/promises"; describe("distribution workflows", () => { diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..fe59625 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "vitest/config"; + +/** + * Vitest replaces `bun test` as this package's test runner. Effect is this + * CLI's control-flow model end to end (see AGENTS.md), so tests need + * `@effect/vitest`'s `it.effect`/`TestClock` primitives, which only run + * under vitest, not `bun:test`. Tests still execute inside the Bun process + * (`bun run test` -> `vitest run`), so `Bun.spawn` and other Bun globals used + * by process-boundary tests remain available. + */ +export default defineConfig({ + test: { + include: ["test/**/*.test.ts"], + environment: "node", + testTimeout: 30_000, + }, +}); From 6063c53cffbc5bc6cb5c3b7204ad244f5b1ff3e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Br=C3=A4mer?= <22003767+robinbraemer@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:49:23 +0200 Subject: [PATCH 3/4] chore(deps): bump effect and @effect/* to 4.0.0-rc.109 Moves the CLI off the beta.106 pre-release track onto the release candidate line now that effect, @effect/openapi-generator, @effect/platform-node, and @effect/vitest all publish matching 4.0.0-rc.* versions together (confirmed via npm registry metadata: all four shipped on 2026-08-14). 4.0.0-rc.110 exists but is excluded by this machine's global Bun install guardrail (~/.bunfig.toml minimumReleaseAge = 259200s / 3 days, published 2026-08-17, ~1.5 days old at bump time). Used 4.0.0-rc.109 instead, the newest RC that clears the 3-day supply-chain window for every package in the set. Regenerated both committed patches (patches/effect@*.patch, patches/@effect%2Fopenapi-generator@*.patch) against the rc.109 tarballs via `bun patch`/`bun patch --commit`, after first diffing beta.106 vs rc.109 upstream sources: every file either patch touches (HttpApiTransformer.ts, OpenApiGenerator.ts, HttpApiEndpoint.ts, CliOutput.ts, toCodeDocument.ts, and their dist/ builds) is byte- identical between the two versions, so the underlying colon-suffixed- path fix from #46 and the pre-existing type/response-header patches still apply unchanged; the regenerated patch files differ from the old ones only in git blob index hashes and bun's diff-context formatting, not in the actual patched code. Updated the two other hardcoded beta.106 references outside package.json/bun.lock: skills/effect-v4/SKILL.md's audited-version line and its regression assertion in test/effect-v4-skill.test.ts. Left docs/superpowers/plans/2026-08-12-openapi-effect-executor.md untouched as a historical decision record. Rationale: keep the CLI on the same effect release track the rest of the org is adopting, without jumping past this machine's minimum- release-age security gate, which exists specifically to blunt just-published supply-chain compromises. Risk: low. No source changes in src/ or scripts/; the fix and both patches are unchanged in content, `bun run generate` reproduced src/generated/openapi-api.gen.ts byte-for-byte (zero diff), and the full suite is green. Tested: bun run generate (no diff vs. committed generated output); bun run generate:check; bun run build (tsc --noEmit + bundle); bun run test (vitest run) - 19 files, 167 tests pass, same count as before the bump, including the two clusters.resume/machines.resume colon-suffix regression tests and both production-effect-invariants.test.ts / strict-effect-control-flow.test.ts purity gates; mise run check (generate:check + build + test), all green. --- bun.lock | 54 +++++++--------- package.json | 12 ++-- ...ct%2Fopenapi-generator@4.0.0-rc.109.patch} | 3 - ...ta.106.patch => effect@4.0.0-rc.109.patch} | 61 ++++++++++--------- skills/effect-v4/SKILL.md | 2 +- test/effect-v4-skill.test.ts | 2 +- 6 files changed, 64 insertions(+), 70 deletions(-) rename patches/{@effect%2Fopenapi-generator@4.0.0-beta.106.patch => @effect%2Fopenapi-generator@4.0.0-rc.109.patch} (97%) rename patches/{effect@4.0.0-beta.106.patch => effect@4.0.0-rc.109.patch} (83%) diff --git a/bun.lock b/bun.lock index a4af144..2a740f5 100644 --- a/bun.lock +++ b/bun.lock @@ -6,12 +6,12 @@ "name": "@akua-dev/cli", "dependencies": { "@akua-dev/sdk": "^0.9.4", - "effect": "4.0.0-beta.106", + "effect": "4.0.0-rc.109", }, "devDependencies": { - "@effect/openapi-generator": "4.0.0-beta.106", - "@effect/platform-node": "4.0.0-beta.106", - "@effect/vitest": "4.0.0-beta.106", + "@effect/openapi-generator": "4.0.0-rc.109", + "@effect/platform-node": "4.0.0-rc.109", + "@effect/vitest": "4.0.0-rc.109", "@types/bun": "^1.3.0", "typescript": "^5.9.0", "vitest": "^4.1.0", @@ -19,8 +19,8 @@ }, }, "patchedDependencies": { - "effect@4.0.0-beta.106": "patches/effect@4.0.0-beta.106.patch", - "@effect/openapi-generator@4.0.0-beta.106": "patches/@effect%2Fopenapi-generator@4.0.0-beta.106.patch", + "@effect/openapi-generator@4.0.0-rc.109": "patches/@effect%2Fopenapi-generator@4.0.0-rc.109.patch", + "effect@4.0.0-rc.109": "patches/effect@4.0.0-rc.109.patch", }, "packages": { "@akua-dev/native": ["@akua-dev/native@0.9.4", "", { "dependencies": { "@akua-dev/native-engines": "0.9.4" }, "optionalDependencies": { "@akua-dev/native-darwin-arm64": "0.9.4", "@akua-dev/native-darwin-x64": "0.9.4", "@akua-dev/native-linux-arm64-gnu": "0.9.4", "@akua-dev/native-linux-arm64-musl": "0.9.4", "@akua-dev/native-linux-x64-gnu": "0.9.4", "@akua-dev/native-linux-x64-musl": "0.9.4", "@akua-dev/native-win32-x64-msvc": "0.9.4" } }, "sha512-hkc2yUDjhhlVl4zqiHNCC1+LrRYYH626MRmnKLYi3byBMZGKyDJ+8roeVfdgKv89Wr2ISMRINijF8eikRujUFQ=="], @@ -43,18 +43,16 @@ "@akua-dev/sdk": ["@akua-dev/sdk@0.9.4", "", { "dependencies": { "@akua-dev/native": "0.9.4", "@standard-schema/spec": "^1.1.0", "ajv": "^8.18.0", "yaml": "^2.8.3" } }, "sha512-v4vm8iWxVzT9i664BKKnVFiG8f6mvmoElOVKGCH7xrvbwiZ6LRWZia6uqb1N1Rq2uUEMmArokvqBJDm2O6xzeA=="], - "@effect/openapi-generator": ["@effect/openapi-generator@4.0.0-beta.106", "", { "dependencies": { "swagger2openapi": "^7.0.8" }, "peerDependencies": { "@effect/platform-node": "^4.0.0-beta.106", "effect": "^4.0.0-beta.106" }, "bin": { "openapigen": "dist/bin.js" } }, "sha512-ae/aOOJgbF97/iyOom6QALqzV1c7GHGdGzc4cDNHt2PqtjRF0uuRJt4ptR/CiDDD0LMOsAzKyBpW/PwJECZvvQ=="], + "@effect/openapi-generator": ["@effect/openapi-generator@4.0.0-rc.109", "", { "dependencies": { "swagger2openapi": "^7.0.8" }, "peerDependencies": { "@effect/platform-node": "^4.0.0-rc.109", "effect": "^4.0.0-rc.109" }, "bin": { "openapigen": "./dist/bin.js" } }, "sha512-CCnzH1uubdg7UCl9EFxfKxLH6eX/bzTdBtmVJSGqcShgkomXTxl36IqNMo5TK6FErFJQi+6nvBZA2YES07Uk/g=="], - "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.106", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.106", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.106", "ioredis": ">=5.7.0 <6.0.0" } }, "sha512-Me7uorWWroWp4OuWHbFg7EqyIex45QzZ3EPwh2p8y/t3VCWVy4KSFtL9AKOKpd7QK1krUWxRK/26J/hTsqvwZg=="], + "@effect/platform-node": ["@effect/platform-node@4.0.0-rc.109", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.109", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-rc.109", "redis": ">=5.0.0 <7.0.0" } }, "sha512-LE/GTh6MCZ0uzbEFH9WmxQkC9snjWuFyh8vW04AAgbKKxSHXsepHSK9RcE/2oYKr6RhrOTD0C2wC0i+J9qoe2g=="], - "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.106", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.106" } }, "sha512-TRTzLmhCwVM8G7dXz16lI3wE2vZFSs1jNs/+3WhnuOZgOUeJz9vzBK/KKlGRbbi0QbtEqmW4rAy1y9Fj4MZfsw=="], + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-rc.109", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-rc.109" } }, "sha512-jhZJnf81N7Z5+Z41Qr0cKT75WzGu6M1W05xrSdu/V8PK0kSFtU5hJMBxSGDWNlgWhBwK0WRZtnMHY8HLdONg1w=="], - "@effect/vitest": ["@effect/vitest@4.0.0-beta.106", "", { "peerDependencies": { "effect": "^4.0.0-beta.106", "vitest": ">=4.1.0 <5.0.0" } }, "sha512-0w799orFqjFNlKh9GrnzMIJLn2/uPaVu8qmT3hJKkYxS46tUS9ZbfjeYNsd1BddvY+3sdS/vXVpnTE64z6+QaQ=="], + "@effect/vitest": ["@effect/vitest@4.0.0-rc.109", "", { "peerDependencies": { "effect": "^4.0.0-rc.109", "vitest": ">=4.1.0 <5.0.0" } }, "sha512-vu5ZkidJ/gM/+a0M07Jnq1PV4duFlYc+4Vj9TsJysK4Us7LL5bwV6uXiT7mi1/IM/3HgfAwc7Q9ROj8M65G4pA=="], "@exodus/schemasafe": ["@exodus/schemasafe@1.3.0", "", {}, "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw=="], - "@ioredis/commands": ["@ioredis/commands@1.10.0", "", {}, "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], @@ -71,6 +69,16 @@ "@oxc-project/types": ["@oxc-project/types@0.144.0", "", {}, "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg=="], + "@redis/bloom": ["@redis/bloom@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-huQgNLaCIZfQ9SeLn4q9124uOUd8HbZDYHwwUzNcRgHqCHiHKl2dDxMqJCeWh8cMqZAoWuHR8XnWbDMIf+o7ag=="], + + "@redis/client": ["@redis/client@6.2.1", "", { "dependencies": { "cluster-key-slot": "1.1.2" }, "peerDependencies": { "@node-rs/xxhash": "^1.1.0", "@opentelemetry/api": ">=1 <2" }, "optionalPeers": ["@node-rs/xxhash", "@opentelemetry/api"] }, "sha512-LzxBY7SIBvvJiyCgcaJZZakE3fJrZZ++i24+EDW9fKpCl68D35uJcKFpZZwCfOoG9WZTbyZlMzMeM0gtOAMU9Q=="], + + "@redis/json": ["@redis/json@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-AFIUJ8Gj0DaaSBHYuSt8+O0oYWM+50OK1c0OmodB7XERIA8+BbyV3O4v76f9iccWasd1/7qjfZTpuzexUaZtrQ=="], + + "@redis/search": ["@redis/search@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-2vfOAOyYFE7UUw3sBBlkqqruBtOUS4HRY5MtW4hp83llrwvtrTE4r22CEqXddlV+54zkLxBE4nmsIJ/dpezQrQ=="], + + "@redis/time-series": ["@redis/time-series@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-kiYniph04dJOole+L359B6C9E+jYS2uDP7hca6Onj0xF38ZIpyxARO0Iq0W4ZRn1e8Q6vqW00QFZVSMRA/2Ijw=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.4", "", { "os": "android", "cpu": "arm64" }, "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ=="], @@ -145,7 +153,7 @@ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="], + "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -153,13 +161,9 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "effect": ["effect@4.0.0-beta.106", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "uuid": "^14.0.1" } }, "sha512-Sb1eNPYP8UdkE6xDOEEnrrOh/Vu2ocdRTPSeZTjZXukUrEYT2NtuQpbS90sEWaa7b6qLt8dfEYPpa6IVe5giHw=="], + "effect": ["effect@4.0.0-rc.109", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "msgpackr": "^2.0.4" } }, "sha512-6ubcOCtfdbmFO5+vgcT2HsTw5s+n3aMUj4eAIbVpUxP7+VYCwXxxcBHgiWgizOrGO1eGmuOBFek3mM0dFcwaWA=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -189,14 +193,10 @@ "http2-client": ["http2-client@1.3.5", "", {}, "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA=="], - "ioredis": ["ioredis@5.11.1", "", { "dependencies": { "@ioredis/commands": "1.10.0", "cluster-key-slot": "1.1.1", "debug": "4.4.3", "denque": "2.1.0", "redis-errors": "1.2.0", "redis-parser": "3.0.0", "standard-as-callback": "2.1.0" } }, "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], - "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], @@ -225,8 +225,6 @@ "mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msgpackr": ["msgpackr@2.0.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA=="], "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], @@ -263,9 +261,7 @@ "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="], - "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], - - "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], + "redis": ["redis@6.2.1", "", { "dependencies": { "@redis/bloom": "6.2.1", "@redis/client": "6.2.1", "@redis/json": "6.2.1", "@redis/search": "6.2.1", "@redis/time-series": "6.2.1" } }, "sha512-Z9VHtgYs48PiQC77X9O2Er8Hj4T+5BtFjT91/vi5Is1D04N72cA946ZslM1ImJw8ZctFBZWAVjM7S5wJNeHMpg=="], "reftools": ["reftools@1.1.9", "", {}, "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w=="], @@ -293,8 +289,6 @@ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], - "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -319,8 +313,6 @@ "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], - "uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="], - "vite": ["vite@8.2.1", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.25", "rolldown": "~1.2.1", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw=="], "vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="], diff --git a/package.json b/package.json index 26690ed..d116156 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "test": "vitest run" }, "devDependencies": { - "@effect/openapi-generator": "4.0.0-beta.106", - "@effect/platform-node": "4.0.0-beta.106", - "@effect/vitest": "4.0.0-beta.106", + "@effect/openapi-generator": "4.0.0-rc.109", + "@effect/platform-node": "4.0.0-rc.109", + "@effect/vitest": "4.0.0-rc.109", "@types/bun": "^1.3.0", "typescript": "^5.9.0", "vitest": "^4.1.0" @@ -32,10 +32,10 @@ }, "dependencies": { "@akua-dev/sdk": "^0.9.4", - "effect": "4.0.0-beta.106" + "effect": "4.0.0-rc.109" }, "patchedDependencies": { - "effect@4.0.0-beta.106": "patches/effect@4.0.0-beta.106.patch", - "@effect/openapi-generator@4.0.0-beta.106": "patches/@effect%2Fopenapi-generator@4.0.0-beta.106.patch" + "@effect/openapi-generator@4.0.0-rc.109": "patches/@effect%2Fopenapi-generator@4.0.0-rc.109.patch", + "effect@4.0.0-rc.109": "patches/effect@4.0.0-rc.109.patch" } } diff --git a/patches/@effect%2Fopenapi-generator@4.0.0-beta.106.patch b/patches/@effect%2Fopenapi-generator@4.0.0-rc.109.patch similarity index 97% rename from patches/@effect%2Fopenapi-generator@4.0.0-beta.106.patch rename to patches/@effect%2Fopenapi-generator@4.0.0-rc.109.patch index 6679d31..26aeefb 100644 --- a/patches/@effect%2Fopenapi-generator@4.0.0-beta.106.patch +++ b/patches/@effect%2Fopenapi-generator@4.0.0-rc.109.patch @@ -1,6 +1,3 @@ -diff --git a/node_modules/@effect/openapi-generator/.bun-tag-d2163dbfd250c038 b/.bun-tag-d2163dbfd250c038 -new file mode 100644 -index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/dist/HttpApiTransformer.js b/dist/HttpApiTransformer.js index aad332ba5abf58db21178942761dee147b382235..4c062f5928cc75c85de4186d60c32d221242668d 100644 --- a/dist/HttpApiTransformer.js diff --git a/patches/effect@4.0.0-beta.106.patch b/patches/effect@4.0.0-rc.109.patch similarity index 83% rename from patches/effect@4.0.0-beta.106.patch rename to patches/effect@4.0.0-rc.109.patch index 7a56292..08bcf56 100644 --- a/patches/effect@4.0.0-beta.106.patch +++ b/patches/effect@4.0.0-rc.109.patch @@ -1,8 +1,8 @@ diff --git a/dist/internal/schema/toCodeDocument.js b/dist/internal/schema/toCodeDocument.js -index 59399d367e3b0755e2394e384e3b1586e5032363..c3d282057f7d7d4c788b5495b7f96d41005da418 100644 +index 9600dfc7693953a58a79b2f13abe4f414859022d..0dd9af5ddd95dff7dede3344cf93af31a9f29e4c 100644 --- a/dist/internal/schema/toCodeDocument.js +++ b/dist/internal/schema/toCodeDocument.js -@@ -418,7 +418,8 @@ +@@ -418,7 +418,8 @@ export function toCodeDocument(document) { return makeCode(`Schema.Record(${signature.parameter.runtime}, ${signature.type.runtime})`, `{ readonly [x: ${signature.parameter.Type}]: ${signature.type.Type} }`); } const indexRuntimes = indexSignatures.map(signature => `Schema.Record(${signature.parameter.runtime}, ${signature.type.runtime})`).join(", "); @@ -11,8 +11,22 @@ index 59399d367e3b0755e2394e384e3b1586e5032363..c3d282057f7d7d4c788b5495b7f96d41 + const indexTypes = indexSignatures.map(signature => `readonly [x: ${signature.parameter.Type}]: ${signature.type.Type}${indexValueType}`).join(", "); return makeCode(`Schema.StructWithRest(Schema.Struct({ ${propertyRuntimes} }), [${indexRuntimes}])`, `{ ${propertyTypes}${properties.length > 0 ? ", " : ""}${indexTypes} }`); } - case "Union": + case "Union": +diff --git a/dist/unstable/cli/CliOutput.js b/dist/unstable/cli/CliOutput.js +index 874e19f9968cd1f1a072803730ef3e46ffbb808c..7230a334b76f5c201d6f6fd7225ab6338eafd146 100644 +--- a/dist/unstable/cli/CliOutput.js ++++ b/dist/unstable/cli/CliOutput.js +@@ -195,7 +195,7 @@ const renderTable = (rows, widthCap) => { + return rows.map(({ + left, + right +- }) => ` ${pad(left, col)}${right}`).join("\n"); ++ }) => ` ${pad(left, Math.max(col, visualLength(left) + 2))}${right}`).join("\n"); + }; + const formatSubcommandName = (name, alias) => alias ? `${name}, ${alias}` : name; + /** diff --git a/dist/unstable/httpapi/HttpApiEndpoint.d.ts b/dist/unstable/httpapi/HttpApiEndpoint.d.ts +index e95cfc448c7fd374d1781c59b601dec9703fd9a1..e2047691b17e9a16fbaa86b752161f8c6e89b000 100644 --- a/dist/unstable/httpapi/HttpApiEndpoint.d.ts +++ b/dist/unstable/httpapi/HttpApiEndpoint.d.ts @@ -283,12 +283,10 @@ export type ClientRequest, widthCap?: number) => { + const maxColumn = Math.max(...rows.map((r) => visualLength(r.left))) + 4 + const col = widthCap === undefined ? maxColumn : Math.min(maxColumn, widthCap) +- return rows.map(({ left, right }) => ` ${pad(left, col)}${right}`).join("\n") ++ return rows ++ .map(({ left, right }) => ` ${pad(left, Math.max(col, visualLength(left) + 2))}${right}`) ++ .join("\n") + } + + const formatSubcommandName = (name: string, alias: string | undefined): string => alias ? `${name}, ${alias}` : name diff --git a/src/unstable/httpapi/HttpApiEndpoint.ts b/src/unstable/httpapi/HttpApiEndpoint.ts +index 331b4ff84e85e70193eda8371ce63ebed659b9b9..c30c0fc94cb3751b8b6a43f551e3999c7d10d04d 100644 --- a/src/unstable/httpapi/HttpApiEndpoint.ts +++ b/src/unstable/httpapi/HttpApiEndpoint.ts @@ -502,11 +502,18 @@ export type ClientRequest< @@ -57,28 +87,3 @@ diff --git a/src/unstable/httpapi/HttpApiEndpoint.ts b/src/unstable/httpapi/Http ) extends infer Req ? keyof Req extends never ? (void | { readonly responseMode?: ResponseMode }) : Req & { readonly responseMode?: ResponseMode } : void -diff --git a/dist/unstable/cli/CliOutput.js b/dist/unstable/cli/CliOutput.js ---- a/dist/unstable/cli/CliOutput.js -+++ b/dist/unstable/cli/CliOutput.js -@@ -192,8 +192,8 @@ const renderTable = (rows, widthCap) => { - const renderTable = (rows, widthCap) => { - const maxColumn = Math.max(...rows.map(r => visualLength(r.left))) + 4; - const col = widthCap === undefined ? maxColumn : Math.min(maxColumn, widthCap); - return rows.map(({ - left, - right -- }) => ` ${pad(left, col)}${right}`).join("\n"); -+ }) => ` ${pad(left, Math.max(col, visualLength(left) + 2))}${right}`).join("\n"); - }; -diff --git a/src/unstable/cli/CliOutput.ts b/src/unstable/cli/CliOutput.ts ---- a/src/unstable/cli/CliOutput.ts -+++ b/src/unstable/cli/CliOutput.ts -@@ -411,5 +411,7 @@ const renderTable = (rows: ReadonlyArray, widthCap?: number) => { - const renderTable = (rows: ReadonlyArray, widthCap?: number) => { - const maxColumn = Math.max(...rows.map((r) => visualLength(r.left))) + 4 - const col = widthCap === undefined ? maxColumn : Math.min(maxColumn, widthCap) -- return rows.map(({ left, right }) => ` ${pad(left, col)}${right}`).join("\n") -+ return rows -+ .map(({ left, right }) => ` ${pad(left, Math.max(col, visualLength(left) + 2))}${right}`) -+ .join("\n") - } diff --git a/skills/effect-v4/SKILL.md b/skills/effect-v4/SKILL.md index 51b2b84..8791bff 100644 --- a/skills/effect-v4/SKILL.md +++ b/skills/effect-v4/SKILL.md @@ -5,7 +5,7 @@ description: Use when creating, refactoring, reviewing, or debugging production # Effect v4 CLI quality -Use the audited dependency exactly as locked: `effect@4.0.0-beta.106`. Do not +Use the audited dependency exactly as locked: `effect@4.0.0-rc.109`. Do not apply Effect v3 examples or upgrade guidance without a separate dependency audit. diff --git a/test/effect-v4-skill.test.ts b/test/effect-v4-skill.test.ts index 704722a..3ff1644 100644 --- a/test/effect-v4-skill.test.ts +++ b/test/effect-v4-skill.test.ts @@ -26,7 +26,7 @@ describe("Effect v4 CLI quality guidance", () => { expect(skill).toMatch(/^---\nname: effect-v4\ndescription: Use when .*Effect v4.*CLI/m); for (const rule of [ - "effect@4.0.0-beta.106", + "effect@4.0.0-rc.109", "Effect services and layers", "Data.TaggedError", "TestClock", From a5189f596586f8dac5feb206107626faa76538c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Br=C3=A4mer?= <22003767+robinbraemer@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:57:07 +0200 Subject: [PATCH 4/4] refactor(cli): eliminate remaining Effect anti-patterns from the vitest migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up code review pass on this branch's earlier vitest migration found four real Effect-idiom violations, all fixed: 1. test/fetch-openapi.test.ts monkey-patched globalThis.fetch with an `as unknown as typeof fetch` cast, claiming no Effect replacement existed. It does: fetchOpenApi already depends on the ScriptHttp service (not Effect's HttpClient module), so the fix is a Layer.succeed(ScriptHttp, {...}) test double merged with the real ScriptFilesLive — the exact pattern the same file already uses one test above. No global mutation, no cast. 2. Eleven test files imported describe/expect/test directly from "vitest" instead of exclusively from "@effect/vitest", which re-exports them unchanged. Fixed all eleven; added a regression test (test/production-effect-invariants.test.ts) that AST-scans test/ for stray "vitest" imports and for globalThis.fetch assignments so both classes can't silently return. 3. The vitest-migration commit swapped Bun.spawn/Bun.file/Bun.sleep for raw node:child_process/node:fs/node:crypto/node:timers/promises calls — a lateral move, not a real fix. scripts/runtime/ release-host-live.ts (the ~1100-line release packaging/verification pipeline) is rewritten on effect/FileSystem, effect/Path, effect/unstable/process's ChildProcessSpawner, and effect/Crypto throughout, using @effect/platform-node's NodeServices.layer at each ReleaseHost boundary method. The one raw import that stays is node:fs's lstatSync in the symlink-attack check: FileSystem.stat always follows symlinks in this effect version (delegates to Node's fs.stat) and there is no lstat-equivalent, so that check cannot be expressed through the service — documented in place. test/release.test.ts and test/effect-generator-patch.test.ts got the same treatment for the calls that mirror what the production code now does (subprocess spawn, SHA-256 digest, sleep, existence checks); the ~20 independent temp-fixture node:fs/promises calls in release.test.ts that never touch the Effect pipeline under test are left as documented, in-scope exceptions rather than force-fit into Effect.gen bodies across 30 unrelated test cases. test/run-akua.ts's spawnSync (45+ call sites across two large test files, deliberately black-box testing the compiled binary from outside the runtime it spawns) is likewise a documented exception. 4. Named functions that just wrapped Effect.gen are now Effect.fn per node_modules/effect/AGENTS.md's documented style (no prior local usage existed to match, so this follows the upstream doc pattern directly). Plain ternary/ combinator functions that never wrapped Effect.gen were left as-is, matching the finding's scope. release-host-live.ts's async FileSystem/ChildProcessSpawner service calls mean its effects can no longer resolve synchronously, so test/release.test.ts's runRelease() helper moved from Effect.runSync to Effect.runPromise; every call site and the sync-throw assertions that depended on it were updated to await/.rejects accordingly. Rationale: `as unknown as` is a banned escape hatch in this repo's own conventions, and raw node:* imports where a real Effect service exists undermine the Effect-first model this whole CLI is built on — both undo the point of the vitest migration this branch just did. Risk: release-host-live.ts is the release packaging/verification/smoke pipeline; a behavior regression here would ship broken release artifacts. Verified beyond the test suite by running the real pipeline end to end on this host: `release:package` (cross-compiled all 5 targets), `release:verify`, and `release:smoke` all succeeded against freshly built archives. Tested: bun run test (vitest run) — 19 files, 169 tests pass (167 baseline + 2 new regression checks); bun run generate:check; bun run build (tsc --noEmit + bundle); mise run check, all green; mise run release:package && release:verify && release:smoke against a real compiled host archive, all exit 0. --- scripts/runtime/release-host-live.ts | 1329 +++++++++++---------- scripts/runtime/release-services.ts | 14 + test/docs.test.ts | 2 +- test/effect-generator-patch.test.ts | 112 +- test/effect-v4-skill.test.ts | 2 +- test/fetch-openapi.test.ts | 31 +- test/generated-command.test.ts | 2 +- test/generated-operation-executor.test.ts | 2 +- test/mode.test.ts | 2 +- test/production-effect-invariants.test.ts | 46 +- test/release-please-config.test.ts | 2 +- test/release.test.ts | 172 ++- test/render.test.ts | 2 +- test/run-akua.ts | 9 + test/strict-effect-control-flow.test.ts | 2 +- test/workflows.test.ts | 2 +- 16 files changed, 954 insertions(+), 777 deletions(-) diff --git a/scripts/runtime/release-host-live.ts b/scripts/runtime/release-host-live.ts index 5722873..6e07736 100644 --- a/scripts/runtime/release-host-live.ts +++ b/scripts/runtime/release-host-live.ts @@ -1,21 +1,16 @@ -import { spawnSync } from "node:child_process"; -import { createHash } from "node:crypto"; -import { - chmodSync, - copyFileSync, - lstatSync, - mkdirSync, - mkdtempSync, - readFileSync, - readdirSync, - rmSync, - statSync, - utimesSync, - writeFileSync, -} from "node:fs"; -import { tmpdir } from "node:os"; -import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; -import { Effect, Layer } from "effect"; +// FileSystem.stat always follows symlinks (delegates to Node's fs.stat; see +// node_modules/@effect/platform-node-shared/src/NodeFileSystem.ts), and this +// effect version exposes no lstat-equivalent (non-symlink-following stat) on +// the FileSystem service. assertSafeOutputDirectory's symlink-attack check +// below needs exactly that non-following behavior, so it stays on raw +// node:fs, bridged through Effect.try like any other live-adapter host +// boundary. +import { lstatSync } from "node:fs"; + +import { NodeServices } from "@effect/platform-node"; +import { Crypto, Effect, FileSystem, Layer, Path, Stream } from "effect"; +import type { PlatformError } from "effect/PlatformError"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { RELEASE_TARGETS, @@ -24,6 +19,8 @@ import { archiveExtractCommand, artifactName, assertCompiledExecutable, + bytesEqual, + bytesToHex, checksumLine, homebrewManifestName, releaseAssetNames, @@ -43,9 +40,6 @@ import type { const RELEASE_REPOSITORY = "akua-dev/cli"; const ARCHIVE_TIMESTAMP_SECONDS = 315532800; const ARCHIVE_TIMESTAMP = new Date(ARCHIVE_TIMESTAMP_SECONDS * 1000); -// Node's default `maxBuffer` (1 MB) is too tight for `tar`/executable -// listing output; match Bun.spawnSync's effectively unbounded behavior. -const RELEASE_COMMAND_MAX_BUFFER = 64 * 1024 * 1024; function attempt( operation: string, @@ -61,6 +55,11 @@ function attempt( }); } +function toReleaseFailure(operation: string) { + return (cause: PlatformError): ReleaseFailure => + new ReleaseFailure({ message: `Release host ${operation} failed`, cause }); +} + function check( condition: boolean, message: string, @@ -68,81 +67,74 @@ function check( return condition ? Effect.void : releaseFailure(message); } -function planReleaseUploads( +const planReleaseUploads = Effect.fn("planReleaseUploads")(function* ( candidateDirInput: string, existingDirInput: string, version: string, -): Effect.Effect { - return Effect.gen(function* () { - const candidateDir = yield* attempt("resolve candidate directory", () => - resolve(candidateDirInput), - ); - const existingDir = yield* attempt("resolve existing directory", () => - resolve(existingDirInput), - ); - const expectedNames = yield* releaseAssetNames(version); - const candidateNames = yield* attempt("read candidate directory", () => - readdirSync(candidateDir).sort(), - ); +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const candidateDir = path.resolve(candidateDirInput); + const existingDir = path.resolve(existingDirInput); + const expectedNames = yield* releaseAssetNames(version); + const candidateNames = ( + yield* fs + .readDirectory(candidateDir) + .pipe(Effect.mapError(toReleaseFailure("read candidate directory"))) + ).sort(); + yield* check( + JSON.stringify(candidateNames) === + JSON.stringify([...expectedNames].sort()), + `Unexpected release files: ${candidateNames.join(", ")}`, + ); + const existingNames = yield* fs + .readDirectory(existingDir) + .pipe(Effect.mapError(toReleaseFailure("read existing directory"))); + const expectedNameSet = new Set(expectedNames); + for (const name of existingNames) { yield* check( - JSON.stringify(candidateNames) === - JSON.stringify([...expectedNames].sort()), - `Unexpected release files: ${candidateNames.join(", ")}`, + expectedNameSet.has(name), + `Unexpected existing release asset: ${name}`, ); - const existingNames = yield* attempt("read existing directory", () => - readdirSync(existingDir), + const candidate = yield* fs + .readFile(path.join(candidateDir, name)) + .pipe(Effect.mapError(toReleaseFailure("read candidate asset"))); + const existing = yield* fs + .readFile(path.join(existingDir, name)) + .pipe(Effect.mapError(toReleaseFailure("read existing asset"))); + yield* check( + bytesEqual(candidate, existing), + `Existing release asset does not match candidate: ${name}`, ); - const expectedNameSet = new Set(expectedNames); - for (const name of existingNames) { - yield* check( - expectedNameSet.has(name), - `Unexpected existing release asset: ${name}`, - ); - const candidate = yield* attempt("read candidate asset", () => - readFileSync(join(candidateDir, name)), - ); - const existing = yield* attempt("read existing asset", () => - readFileSync(join(existingDir, name)), - ); - yield* check( - candidate.equals(existing), - `Existing release asset does not match candidate: ${name}`, - ); - } - const existingNameSet = new Set(existingNames); - return expectedNames - .filter((name) => !existingNameSet.has(name)) - .map((name) => join(candidateDir, name)); - }); -} + } + const existingNameSet = new Set(existingNames); + return expectedNames + .filter((name) => !existingNameSet.has(name)) + .map((name) => path.join(candidateDir, name)); +}); -function assertSafeOutputDirectory( - outputDirInput: string, -): Effect.Effect { - return Effect.gen(function* () { - const outputDir = yield* attempt("resolve output directory", () => - resolve(outputDirInput), - ); - const workspace = yield* attempt("read workspace directory", () => - resolve(process.cwd()), - ); - const releaseOutputRoot = join(workspace, "dist", "release"); - const releaseRelativePath = relative(releaseOutputRoot, outputDir); +const assertSafeOutputDirectory = Effect.fn("assertSafeOutputDirectory")( + function* (outputDirInput: string) { + const path = yield* Path.Path; + const outputDir = path.resolve(outputDirInput); + const workspace = path.resolve(process.cwd()); + const releaseOutputRoot = path.join(workspace, "dist", "release"); + const releaseRelativePath = path.relative(releaseOutputRoot, outputDir); yield* check( !( releaseRelativePath === ".." || releaseRelativePath.startsWith( `..${process.platform === "win32" ? "\\" : "/"}`, ) || - isAbsolute(releaseRelativePath) + path.isAbsolute(releaseRelativePath) ), `Unsafe release output directory: ${outputDir}`, ); let currentPath = workspace; - const segments = relative(workspace, outputDir).split(sep); + const segments = path.relative(workspace, outputDir).split(path.sep); for (const segment of segments) { - currentPath = join(currentPath, segment); + currentPath = path.join(currentPath, segment); const stats = yield* lstatIfPresent(currentPath); if (stats !== undefined) { yield* check( @@ -151,8 +143,8 @@ function assertSafeOutputDirectory( ); } } - }); -} + }, +); function lstatIfPresent( path: string, @@ -167,22 +159,20 @@ function lstatIfPresent( ); } -function packageExistingExecutables( - input: PackageExistingExecutablesInput, -): Effect.Effect { - return Effect.gen(function* () { +const packageExistingExecutables = Effect.fn("packageExistingExecutables")( + function* (input: PackageExistingExecutablesInput) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; yield* validateVersion(input.version); - const outputDir = yield* attempt("resolve output directory", () => - resolve(input.outputDir), - ); + const outputDir = path.resolve(input.outputDir); yield* assertSafeOutputDirectory(outputDir); - const stagingRoot = join(outputDir, ".staging"); - yield* attempt("clear release output", () => - rmSync(outputDir, { recursive: true, force: true }), - ); - yield* attempt("create staging directory", () => - mkdirSync(stagingRoot, { recursive: true }), - ); + const stagingRoot = path.join(outputDir, ".staging"); + yield* fs + .remove(outputDir, { recursive: true, force: true }) + .pipe(Effect.mapError(toReleaseFailure("clear release output"))); + yield* fs + .makeDirectory(stagingRoot, { recursive: true }) + .pipe(Effect.mapError(toReleaseFailure("create staging directory"))); const assets: ReleaseAsset[] = []; const packageAssets = Effect.gen(function* () { @@ -193,32 +183,42 @@ function packageExistingExecutables( `Missing compiled executable for ${target.id}`, ); } - const stagingDir = join(stagingRoot, target.id); - const stagedExecutable = join(stagingDir, target.executable); + const stagingDir = path.join(stagingRoot, target.id); + const stagedExecutable = path.join(stagingDir, target.executable); const archive = artifactName(input.version, target); - const archivePath = join(outputDir, archive); - yield* attempt("create target staging directory", () => - mkdirSync(stagingDir, { recursive: true }), - ); - const sourceBytes = yield* attempt("read compiled executable", () => - readFileSync(source), - ); - yield* attempt("stage compiled executable", () => - writeFileSync(stagedExecutable, sourceBytes), - ); - const stagedBytes = yield* attempt("verify staged executable", () => - readFileSync(stagedExecutable), - ); + const archivePath = path.join(outputDir, archive); + yield* fs + .makeDirectory(stagingDir, { recursive: true }) + .pipe( + Effect.mapError(toReleaseFailure("create target staging directory")), + ); + const sourceBytes = yield* fs + .readFile(source) + .pipe(Effect.mapError(toReleaseFailure("read compiled executable"))); + yield* fs + .writeFile(stagedExecutable, sourceBytes) + .pipe(Effect.mapError(toReleaseFailure("stage compiled executable"))); + const stagedBytes = yield* fs + .readFile(stagedExecutable) + .pipe( + Effect.mapError(toReleaseFailure("verify staged executable")), + ); yield* check( - stagedBytes.equals(sourceBytes), + bytesEqual(stagedBytes, sourceBytes), `Staged executable does not match source for ${target.id}`, ); - yield* attempt("set staged executable mode", () => - chmodSync(stagedExecutable, target.os === "windows" ? 0o644 : 0o755), - ); - yield* attempt("set staged executable timestamp", () => - utimesSync(stagedExecutable, ARCHIVE_TIMESTAMP, ARCHIVE_TIMESTAMP), - ); + yield* fs + .chmod(stagedExecutable, target.os === "windows" ? 0o644 : 0o755) + .pipe( + Effect.mapError(toReleaseFailure("set staged executable mode")), + ); + yield* fs + .utimes(stagedExecutable, ARCHIVE_TIMESTAMP, ARCHIVE_TIMESTAMP) + .pipe( + Effect.mapError( + toReleaseFailure("set staged executable timestamp"), + ), + ); const runtimeFiles = yield* stagePackageRuntime( input.packageRoot, stagingDir, @@ -273,19 +273,17 @@ function packageExistingExecutables( stagingDir, ); } - const bytes = new Uint8Array( - yield* attempt("read packaged archive", () => - readFileSync(archivePath), - ), - ); + const bytes = yield* fs + .readFile(archivePath) + .pipe(Effect.mapError(toReleaseFailure("read packaged archive"))); const digest = yield* sha256(bytes); const checksumFile = `${archive}.sha256`; - yield* attempt("write archive checksum", () => - writeFileSync( - join(outputDir, checksumFile), + yield* fs + .writeFileString( + path.join(outputDir, checksumFile), checksumLine(archive, digest), - ), - ); + ) + .pipe(Effect.mapError(toReleaseFailure("write archive checksum"))); assets.push({ target: target.id, bun_target: target.bunTarget, @@ -314,215 +312,231 @@ function packageExistingExecutables( input.version, assets, ); - yield* attempt("write aggregate checksums", () => - writeFileSync( - join(outputDir, "checksums.txt"), + yield* fs + .writeFileString( + path.join(outputDir, "checksums.txt"), assets .map((asset) => checksumLine(asset.file, asset.sha256)) .join(""), - ), - ); - yield* attempt("write release manifest", () => - writeFileSync(join(outputDir, manifestName), stableJson(manifest)), - ); - yield* attempt("write Homebrew manifest", () => - writeFileSync( - join(outputDir, homebrewName), + ) + .pipe(Effect.mapError(toReleaseFailure("write aggregate checksums"))); + yield* fs + .writeFileString( + path.join(outputDir, manifestName), + stableJson(manifest), + ) + .pipe(Effect.mapError(toReleaseFailure("write release manifest"))); + yield* fs + .writeFileString( + path.join(outputDir, homebrewName), stableJson(homebrewManifest), - ), - ); + ) + .pipe(Effect.mapError(toReleaseFailure("write Homebrew manifest"))); }); yield* packageAssets.pipe( Effect.ensuring( - attempt("remove staging directory", () => - rmSync(stagingRoot, { recursive: true, force: true }), - ).pipe(Effect.ignore), + fs + .remove(stagingRoot, { recursive: true, force: true }) + .pipe( + Effect.mapError(toReleaseFailure("remove staging directory")), + Effect.ignore, + ), ), ); yield* verifyReleaseDirectory(outputDir, input.version); - }); -} + }, +); -function packageRelease( +const packageRelease = Effect.fn("packageRelease")(function* ( input: PackageReleaseInput, -): Effect.Effect { - return Effect.gen(function* () { - yield* validateVersion(input.version); - const binaryBuildParent = yield* attempt( - "resolve binary build directory", - () => join(process.cwd(), "dist"), - ); - yield* attempt("create binary build directory", () => - mkdirSync(binaryBuildParent, { recursive: true }), - ); - const binaryRoot = yield* attempt( - "create binary build staging directory", - () => mkdtempSync(join(binaryBuildParent, ".tmp-akua-release-build-")), +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* validateVersion(input.version); + const binaryBuildParent = path.join(process.cwd(), "dist"); + yield* fs + .makeDirectory(binaryBuildParent, { recursive: true }) + .pipe(Effect.mapError(toReleaseFailure("create binary build directory"))); + const binaryRoot = yield* fs + .makeTempDirectory({ + directory: binaryBuildParent, + prefix: ".tmp-akua-release-build-", + }) + .pipe( + Effect.mapError( + toReleaseFailure("create binary build staging directory"), + ), ); - const buildBinaries = Effect.gen(function* () { - const binaries: Record = {}; - for (const target of RELEASE_TARGETS) { - const binaryPath = join(binaryRoot, target.id, target.executable); - yield* attempt("create target binary directory", () => - mkdirSync(join(binaryRoot, target.id), { recursive: true }), - ); - yield* runCommand([ - "bun", - "build", - input.entrypoint ?? "src/bin/akua.ts", - "--compile", - `--target=${target.bunTarget}`, - "--no-compile-autoload-dotenv", - "--no-compile-autoload-bunfig", - // Keep @akua-dev/* as a real runtime import instead of bundling - // it: @akua-dev/native's platform .node binding is a binary file - // the bundler cannot inline. The compiled binary resolves it via - // process.execPath at runtime (src/runtime/services-live.ts). - "--external", - "@akua-dev/*", - `--outfile=${binaryPath}`, - ]); - const bytes = yield* attempt("read compiled executable", () => - readFileSync(binaryPath), + const buildBinaries = Effect.gen(function* () { + const binaries: Record = {}; + for (const target of RELEASE_TARGETS) { + const binaryPath = path.join(binaryRoot, target.id, target.executable); + yield* fs + .makeDirectory(path.join(binaryRoot, target.id), { recursive: true }) + .pipe( + Effect.mapError(toReleaseFailure("create target binary directory")), ); - yield* assertCompiledExecutable(target, bytes); - binaries[target.id] = binaryPath; - } - yield* packageExistingExecutables({ - version: input.version, - outputDir: input.outputDir, - binaries, - packageRoot: - input.packageRoot ?? join(process.cwd(), "node_modules", "@akua-dev"), - }); + yield* runCommand([ + "bun", + "build", + input.entrypoint ?? "src/bin/akua.ts", + "--compile", + `--target=${target.bunTarget}`, + "--no-compile-autoload-dotenv", + "--no-compile-autoload-bunfig", + // Keep @akua-dev/* as a real runtime import instead of bundling + // it: @akua-dev/native's platform .node binding is a binary file + // the bundler cannot inline. The compiled binary resolves it via + // process.execPath at runtime (src/runtime/services-live.ts). + "--external", + "@akua-dev/*", + `--outfile=${binaryPath}`, + ]); + const bytes = yield* fs + .readFile(binaryPath) + .pipe(Effect.mapError(toReleaseFailure("read compiled executable"))); + yield* assertCompiledExecutable(target, bytes); + binaries[target.id] = binaryPath; + } + yield* packageExistingExecutables({ + version: input.version, + outputDir: input.outputDir, + binaries, + packageRoot: + input.packageRoot ?? path.join(process.cwd(), "node_modules", "@akua-dev"), }); - yield* buildBinaries.pipe( - Effect.ensuring( - attempt("remove binary build staging directory", () => - rmSync(binaryRoot, { recursive: true, force: true }), - ).pipe(Effect.ignore), - ), - ); }); -} + yield* buildBinaries.pipe( + Effect.ensuring( + fs + .remove(binaryRoot, { recursive: true, force: true }) + .pipe( + Effect.mapError( + toReleaseFailure("remove binary build staging directory"), + ), + Effect.ignore, + ), + ), + ); +}); -function smokeReleaseArtifact(input: { - version: string; - outputDir: string; - targetId: string; -}): Effect.Effect { - return Effect.gen(function* () { - yield* validateVersion(input.version); - const target = RELEASE_TARGETS.find( - (candidate) => candidate.id === input.targetId, +const smokeReleaseArtifact = Effect.fn("smokeReleaseArtifact")(function* ( + input: { version: string; outputDir: string; targetId: string }, +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* validateVersion(input.version); + const target = RELEASE_TARGETS.find( + (candidate) => candidate.id === input.targetId, + ); + if (!target) { + return yield* releaseFailure(`Unknown release target: ${input.targetId}`); + } + const installRoot = yield* fs + .makeTempDirectory({ prefix: "akua-release-smoke-" }) + .pipe(Effect.mapError(toReleaseFailure("create smoke directory"))); + const smoke = Effect.gen(function* () { + const archivePath = path.resolve( + input.outputDir, + artifactName(input.version, target), + ); + yield* runCommand( + archiveExtractCommand( + target.archive, + archivePath, + installRoot, + process.platform, + ), ); - if (!target) { - return yield* releaseFailure(`Unknown release target: ${input.targetId}`); + const executable = path.join(installRoot, target.executable); + if (target.os !== "windows") { + yield* fs + .chmod(executable, 0o755) + .pipe(Effect.mapError(toReleaseFailure("set smoke executable mode"))); } - const installRoot = yield* attempt("create smoke directory", () => - mkdtempSync(join(tmpdir(), "akua-release-smoke-")), + const versionOutput = yield* runCommand([ + executable, + "--version", + "--json", + ]); + const reportedVersion = yield* parseReportedVersion(versionOutput); + yield* check( + reportedVersion === input.version, + `Installed ${target.id} executable reported an unexpected version: ${versionOutput.trim()}`, ); - const smoke = Effect.gen(function* () { - const archivePath = yield* attempt("resolve smoke archive", () => - resolve(input.outputDir, artifactName(input.version, target)), - ); - yield* runCommand( - archiveExtractCommand( - target.archive, - archivePath, - installRoot, - process.platform, - ), + const helpOutput = yield* runCommand([executable, "--help"], { + AKUA_OUTPUT: "agent", + }); + yield* check( + helpOutput.trim() !== "", + `Installed ${target.id} executable returned empty help output`, + ); + const commandsOutput = yield* runCommand( + [executable, "commands", "--limit", "1"], + { + AKUA_OUTPUT: "agent", + }, + ); + yield* check( + commandsOutput.trim() !== "", + `Installed ${target.id} executable returned empty command output`, + ); + const packageSmokeRoot = path.join(installRoot, "package-smoke"); + yield* fs + .makeDirectory(packageSmokeRoot, { recursive: true }) + .pipe( + Effect.mapError(toReleaseFailure("create package smoke directory")), ); - const executable = join(installRoot, target.executable); - if (target.os !== "windows") { - yield* attempt("set smoke executable mode", () => - chmodSync(executable, 0o755), - ); - } - const versionOutput = yield* runCommand([ + yield* runCommand( + [executable, "pkg", "version", "--json"], + {}, + packageSmokeRoot, + ); + yield* runCommand( + [executable, "pkg", "init", "demo", "--json"], + {}, + packageSmokeRoot, + ); + const packageDirectory = path.join(packageSmokeRoot, "demo"); + yield* runCommand( + [executable, "pkg", "check", "--json"], + {}, + packageDirectory, + ); + yield* runCommand( + [ executable, - "--version", + "pkg", + "render", + "--inputs", + "inputs.example.yaml", + "--out", + "deploy", "--json", - ]); - const reportedVersion = yield* parseReportedVersion(versionOutput); - yield* check( - reportedVersion === input.version, - `Installed ${target.id} executable reported an unexpected version: ${versionOutput.trim()}`, - ); - const helpOutput = yield* runCommand([executable, "--help"], { - AKUA_OUTPUT: "agent", - }); - yield* check( - helpOutput.trim() !== "", - `Installed ${target.id} executable returned empty help output`, - ); - const commandsOutput = yield* runCommand( - [executable, "commands", "--limit", "1"], - { - AKUA_OUTPUT: "agent", - }, - ); - yield* check( - commandsOutput.trim() !== "", - `Installed ${target.id} executable returned empty command output`, - ); - const packageSmokeRoot = join(installRoot, "package-smoke"); - yield* attempt("create package smoke directory", () => - mkdirSync(packageSmokeRoot, { recursive: true }), - ); - yield* runCommand( - [executable, "pkg", "version", "--json"], - {}, - packageSmokeRoot, - ); - yield* runCommand( - [executable, "pkg", "init", "demo", "--json"], - {}, - packageSmokeRoot, - ); - const packageDirectory = join(packageSmokeRoot, "demo"); - yield* runCommand( - [executable, "pkg", "check", "--json"], - {}, - packageDirectory, - ); - yield* runCommand( - [ - executable, - "pkg", - "render", - "--inputs", - "inputs.example.yaml", - "--out", - "deploy", - "--json", - ], - {}, - packageDirectory, - ); - const renderedFiles = yield* attempt("read package smoke output", () => - readdirSync(join(packageDirectory, "deploy")), - ); - yield* check( - renderedFiles.length > 0, - `Installed ${target.id} package renderer returned no manifests`, - ); - yield* runCommand( - [executable, "pkg", "inspect", "--json"], - {}, - packageDirectory, - ); - }); - yield* smoke.pipe( - Effect.ensuring( - attempt("remove smoke directory", () => - rmSync(installRoot, { recursive: true, force: true }), - ).pipe(Effect.ignore), - ), + ], + {}, + packageDirectory, + ); + const renderedFiles = yield* fs + .readDirectory(path.join(packageDirectory, "deploy")) + .pipe(Effect.mapError(toReleaseFailure("read package smoke output"))); + yield* check( + renderedFiles.length > 0, + `Installed ${target.id} package renderer returned no manifests`, ); + yield* runCommand([executable, "pkg", "inspect", "--json"], {}, packageDirectory); }); -} + yield* smoke.pipe( + Effect.ensuring( + fs + .remove(installRoot, { recursive: true, force: true }) + .pipe( + Effect.mapError(toReleaseFailure("remove smoke directory")), + Effect.ignore, + ), + ), + ); +}); function parseReportedVersion( output: string, @@ -535,330 +549,348 @@ function parseReportedVersion( ); } -function verifyReleaseDirectory( +const verifyReleaseDirectory = Effect.fn("verifyReleaseDirectory")(function* ( outputDirInput: string, version: string, -): Effect.Effect { - return Effect.gen(function* () { - yield* validateVersion(version); - const outputDir = yield* attempt("resolve release directory", () => - resolve(outputDirInput), - ); - const manifestName = yield* releaseManifestName(version); - const manifestContents = yield* attempt("read release manifest", () => - readFileSync(join(outputDir, manifestName), "utf8"), - ); - const manifestJson = yield* attempt("parse release manifest", () => - JSON.parse(manifestContents), - ); - const manifest = yield* parseReleaseManifest(manifestJson); - const homebrewName = yield* homebrewManifestName(version); +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* validateVersion(version); + const outputDir = path.resolve(outputDirInput); + const manifestName = yield* releaseManifestName(version); + const manifestContents = yield* fs + .readFileString(path.join(outputDir, manifestName)) + .pipe(Effect.mapError(toReleaseFailure("read release manifest"))); + const manifestJson = yield* attempt("parse release manifest", () => + JSON.parse(manifestContents), + ); + const manifest = yield* parseReleaseManifest(manifestJson); + const homebrewName = yield* homebrewManifestName(version); + yield* check( + manifest.schema_version === 1 && + manifest.version === version && + manifest.executable === "akua" && + manifest.checksums === "checksums.txt" && + manifest.homebrew_manifest === homebrewName && + manifest.assets.length === RELEASE_TARGETS.length, + "Release manifest does not match the requested release contract", + ); + const aggregateLines: string[] = []; + for (let index = 0; index < RELEASE_TARGETS.length; index += 1) { + const target = RELEASE_TARGETS[index]; + const asset = manifest.assets[index]; + const expectedFile = artifactName(version, target); yield* check( - manifest.schema_version === 1 && - manifest.version === version && - manifest.executable === "akua" && - manifest.checksums === "checksums.txt" && - manifest.homebrew_manifest === homebrewName && - manifest.assets.length === RELEASE_TARGETS.length, - "Release manifest does not match the requested release contract", - ); - const aggregateLines: string[] = []; - for (let index = 0; index < RELEASE_TARGETS.length; index += 1) { - const target = RELEASE_TARGETS[index]; - const asset = manifest.assets[index]; - const expectedFile = artifactName(version, target); - yield* check( - asset.target === target.id && - asset.bun_target === target.bunTarget && - asset.os === target.os && - asset.arch === target.arch && - asset.archive === target.archive && - asset.file === expectedFile && - asset.checksum_file === `${expectedFile}.sha256` && - asset.executable === target.executable && - asset.contents[0] === target.executable && - asset.contents.includes( - "node_modules/@akua-dev/native/package.json", - ) && - asset.contents.includes( - "node_modules/@akua-dev/native-engines/helm-engine.wasm", - ) && - asset.contents.includes( - "node_modules/@akua-dev/native-engines/kustomize-engine.wasm", - ) && - asset.contents.filter((entry) => entry.endsWith(".node")).length === - 1, - `Release manifest target mismatch for ${target.id}`, - ); - const bytes = new Uint8Array( - yield* attempt("read release archive", () => - readFileSync(join(outputDir, asset.file)), - ), - ); - const digest = yield* sha256(bytes); - yield* check( - digest === asset.sha256, - `Release asset checksum mismatch: ${asset.file}`, - ); - const expectedLine = checksumLine(asset.file, digest); - const adjacent = yield* attempt("read release checksum", () => - readFileSync(join(outputDir, asset.checksum_file), "utf8"), - ); - yield* check( - adjacent === expectedLine, - `Release checksum file mismatch: ${asset.checksum_file}`, - ); - const size = yield* attempt( - "read release archive metadata", - () => statSync(join(outputDir, asset.file)).size, - ); - yield* check( - size === asset.size, - `Release asset size mismatch: ${asset.file}`, - ); - aggregateLines.push(expectedLine); - yield* verifyArchive(outputDir, target, asset.file, asset.contents); - } - const aggregate = yield* attempt("read aggregate checksums", () => - readFileSync(join(outputDir, manifest.checksums), "utf8"), + asset.target === target.id && + asset.bun_target === target.bunTarget && + asset.os === target.os && + asset.arch === target.arch && + asset.archive === target.archive && + asset.file === expectedFile && + asset.checksum_file === `${expectedFile}.sha256` && + asset.executable === target.executable && + asset.contents[0] === target.executable && + asset.contents.includes( + "node_modules/@akua-dev/native/package.json", + ) && + asset.contents.includes( + "node_modules/@akua-dev/native-engines/helm-engine.wasm", + ) && + asset.contents.includes( + "node_modules/@akua-dev/native-engines/kustomize-engine.wasm", + ) && + asset.contents.filter((entry) => entry.endsWith(".node")).length === + 1, + `Release manifest target mismatch for ${target.id}`, ); + const bytes = yield* fs + .readFile(path.join(outputDir, asset.file)) + .pipe(Effect.mapError(toReleaseFailure("read release archive"))); + const digest = yield* sha256(bytes); yield* check( - aggregate === aggregateLines.join(""), - "Aggregate checksum file mismatch", - ); - const homebrewManifest = yield* attempt("read Homebrew manifest", () => - readFileSync(join(outputDir, manifest.homebrew_manifest), "utf8"), - ); - const expectedHomebrewManifest = stableJson( - yield* createHomebrewManifest(version, manifest.assets), + digest === asset.sha256, + `Release asset checksum mismatch: ${asset.file}`, ); + const expectedLine = checksumLine(asset.file, digest); + const adjacent = yield* fs + .readFileString(path.join(outputDir, asset.checksum_file)) + .pipe(Effect.mapError(toReleaseFailure("read release checksum"))); yield* check( - homebrewManifest === expectedHomebrewManifest, - "Homebrew manifest mismatch", + adjacent === expectedLine, + `Release checksum file mismatch: ${asset.checksum_file}`, ); - const actualNames = yield* attempt("read release directory", () => - readdirSync(outputDir).sort(), - ); - const expectedNames = (yield* releaseAssetNames(version)).sort(); + const info = yield* fs + .stat(path.join(outputDir, asset.file)) + .pipe(Effect.mapError(toReleaseFailure("read release archive metadata"))); + const size = Number(info.size); yield* check( - JSON.stringify(actualNames) === JSON.stringify(expectedNames), - `Unexpected release files: ${actualNames.join(", ")}`, + size === asset.size, + `Release asset size mismatch: ${asset.file}`, ); - }); -} + aggregateLines.push(expectedLine); + yield* verifyArchive(outputDir, target, asset.file, asset.contents); + } + const aggregate = yield* fs + .readFileString(path.join(outputDir, manifest.checksums)) + .pipe(Effect.mapError(toReleaseFailure("read aggregate checksums"))); + yield* check( + aggregate === aggregateLines.join(""), + "Aggregate checksum file mismatch", + ); + const homebrewManifest = yield* fs + .readFileString(path.join(outputDir, manifest.homebrew_manifest)) + .pipe(Effect.mapError(toReleaseFailure("read Homebrew manifest"))); + const expectedHomebrewManifest = stableJson( + yield* createHomebrewManifest(version, manifest.assets), + ); + yield* check( + homebrewManifest === expectedHomebrewManifest, + "Homebrew manifest mismatch", + ); + const actualNames = ( + yield* fs + .readDirectory(outputDir) + .pipe(Effect.mapError(toReleaseFailure("read release directory"))) + ).sort(); + const expectedNames = (yield* releaseAssetNames(version)).sort(); + yield* check( + JSON.stringify(actualNames) === JSON.stringify(expectedNames), + `Unexpected release files: ${actualNames.join(", ")}`, + ); +}); -function createHomebrewManifest( +const createHomebrewManifest = Effect.fn("createHomebrewManifest")(function* ( version: string, assets: readonly ReleaseAsset[], -): Effect.Effect< - { - schema_version: number; - formula: string; - version: string; - release: string; - platforms: Record< - string, - { artifact: string; url: string; sha256: string } - >; - }, - ReleaseFailure -> { - return Effect.gen(function* () { - const platforms: Record< - string, - { artifact: string; url: string; sha256: string } - > = {}; - for (const target of RELEASE_TARGETS) { - if (!target.homebrew) continue; - const asset = assets.find((candidate) => candidate.target === target.id); - if (!asset) { - return yield* releaseFailure( - `Missing Homebrew release asset for ${target.id}`, - ); - } - const key = `${target.homebrew.os}_${target.homebrew.arch}`; - platforms[key] = { - artifact: asset.file, - url: `https://github.com/${RELEASE_REPOSITORY}/releases/download/v${version}/${asset.file}`, - sha256: asset.sha256, - }; +) { + const platforms: Record< + string, + { artifact: string; url: string; sha256: string } + > = {}; + for (const target of RELEASE_TARGETS) { + if (!target.homebrew) continue; + const asset = assets.find((candidate) => candidate.target === target.id); + if (!asset) { + return yield* releaseFailure( + `Missing Homebrew release asset for ${target.id}`, + ); } - return { - schema_version: 1, - formula: "akua", - version, - release: `https://github.com/${RELEASE_REPOSITORY}/releases/tag/v${version}`, - platforms, + const key = `${target.homebrew.os}_${target.homebrew.arch}`; + platforms[key] = { + artifact: asset.file, + url: `https://github.com/${RELEASE_REPOSITORY}/releases/download/v${version}/${asset.file}`, + sha256: asset.sha256, }; - }); -} + } + return { + schema_version: 1, + formula: "akua", + version, + release: `https://github.com/${RELEASE_REPOSITORY}/releases/tag/v${version}`, + platforms, + }; +}); -function verifyArchive( +const verifyArchive = Effect.fn("verifyArchive")(function* ( outputDir: string, target: ReleaseTarget, file: string, contents: readonly string[], -): Effect.Effect { - return Effect.gen(function* () { - const archivePath = join(outputDir, file); - const listCommand = - target.archive === "zip" - ? ["unzip", "-Z1", archivePath] - : ["tar", "-tzf", archivePath]; - const listedFiles = (yield* runCommand(listCommand)) - .trim() - .split("\n") - .filter((entry) => entry !== "" && !entry.endsWith("/")) - .sort(); - const expectedFiles = [...contents].sort(); - yield* check( - JSON.stringify(listedFiles) === JSON.stringify(expectedFiles), - `Release archive ${file} has unexpected files: ${listedFiles.join(", ")}`, - ); - if (target.os === "windows") return; - const extractDir = yield* attempt( - "create archive verification directory", - () => mkdtempSync(join(tmpdir(), "akua-release-verify-")), +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const archivePath = path.join(outputDir, file); + const listCommand = + target.archive === "zip" + ? ["unzip", "-Z1", archivePath] + : ["tar", "-tzf", archivePath]; + const listedFiles = (yield* runCommand(listCommand)) + .trim() + .split("\n") + .filter((entry) => entry !== "" && !entry.endsWith("/")) + .sort(); + const expectedFiles = [...contents].sort(); + yield* check( + JSON.stringify(listedFiles) === JSON.stringify(expectedFiles), + `Release archive ${file} has unexpected files: ${listedFiles.join(", ")}`, + ); + if (target.os === "windows") return; + const extractDir = yield* fs + .makeTempDirectory({ prefix: "akua-release-verify-" }) + .pipe( + Effect.mapError( + toReleaseFailure("create archive verification directory"), + ), ); - const verifyMode = Effect.gen(function* () { - yield* runCommand(["tar", "-xzf", archivePath, "-C", extractDir]); - const mode = yield* attempt( - "read extracted executable metadata", - () => statSync(join(extractDir, target.executable)).mode & 0o777, + const verifyMode = Effect.gen(function* () { + yield* runCommand(["tar", "-xzf", archivePath, "-C", extractDir]); + const info = yield* fs + .stat(path.join(extractDir, target.executable)) + .pipe( + Effect.mapError(toReleaseFailure("read extracted executable metadata")), ); - yield* check( - mode === 0o755, - `Release executable mode mismatch for ${target.id}: ${mode.toString(8)}`, - ); - }); - yield* verifyMode.pipe( - Effect.ensuring( - attempt("remove archive verification directory", () => - rmSync(extractDir, { recursive: true, force: true }), - ).pipe(Effect.ignore), - ), + const mode = info.mode & 0o777; + yield* check( + mode === 0o755, + `Release executable mode mismatch for ${target.id}: ${mode.toString(8)}`, ); }); -} + yield* verifyMode.pipe( + Effect.ensuring( + fs + .remove(extractDir, { recursive: true, force: true }) + .pipe( + Effect.mapError( + toReleaseFailure("remove archive verification directory"), + ), + Effect.ignore, + ), + ), + ); +}); -function runCommand( +const runCommand = Effect.fn("runCommand")(function* ( command: string[], extraEnv: Record = {}, cwd?: string, -): Effect.Effect { - return Effect.gen(function* () { - const proc = yield* attempt("run release command", () => - spawnSync(command[0], command.slice(1), { - env: { ...process.env, ...extraEnv }, - cwd, - encoding: "utf8", - maxBuffer: RELEASE_COMMAND_MAX_BUFFER, - }), - ); - const stdout = proc.stdout; - const stderr = proc.stderr; - const exitCode = proc.status ?? -1; - yield* check( - exitCode === 0, - `${command[0]} failed (${exitCode}): ${stderr.trim()}`, - ); - return stdout; - }); -} +) { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const [stdout, stderr, exitCode] = yield* Effect.scoped( + Effect.gen(function* () { + const handle = yield* spawner.spawn( + ChildProcess.make(command[0], command.slice(1), { + cwd, + env: extraEnv, + extendEnv: true, + }), + ); + return yield* Effect.all( + [ + handle.stdout.pipe(Stream.decodeText(), Stream.mkString), + handle.stderr.pipe(Stream.decodeText(), Stream.mkString), + handle.exitCode, + ], + { concurrency: "unbounded" }, + ); + }), + ).pipe(Effect.mapError(toReleaseFailure("run release command"))); + yield* check( + exitCode === ChildProcessSpawner.ExitCode(0), + `${command[0]} failed (${exitCode}): ${stderr.trim()}`, + ); + return stdout; +}); -function stagePackageRuntime( +const stagePackageRuntime = Effect.fn("stagePackageRuntime")(function* ( packageRoot: string, stagingDir: string, target: ReleaseTarget, -): Effect.Effect { - return Effect.gen(function* () { - const scopeRoot = join(stagingDir, "node_modules", "@akua-dev"); - const nativeDestination = join(scopeRoot, "native"); - const archiveFiles: string[] = []; - const runtimeDirectories = new Set(); - for (const packageName of ["native", "native-engines", "sdk"]) { - const manifest = yield* readPackageManifest(packageRoot, packageName); - const declaredFiles = yield* packageManifestFiles(manifest, packageName); - const expandedFiles = yield* expandPackageManifestFiles( - packageRoot, - packageName, - declaredFiles, - ); - for (const file of ["package.json", ...expandedFiles]) { - const destination = join(scopeRoot, packageName, file); - yield* stagePackageRuntimeFile( - join(packageRoot, packageName, file), - destination, - ); - archiveFiles.push( - `node_modules/@akua-dev/${packageName}/${file.replaceAll("\\", "/")}`, - ); - runtimeDirectories.add(dirname(destination)); - } - } - const bindingManifest = yield* readPackageManifest( +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const scopeRoot = path.join(stagingDir, "node_modules", "@akua-dev"); + const nativeDestination = path.join(scopeRoot, "native"); + const archiveFiles: string[] = []; + const runtimeDirectories = new Set(); + for (const packageName of ["native", "native-engines", "sdk"]) { + const manifest = yield* readPackageManifest(packageRoot, packageName); + const declaredFiles = yield* packageManifestFiles(path, manifest, packageName); + const expandedFiles = yield* expandPackageManifestFiles( packageRoot, - target.bindingPackage, - ); - const bindingFile = yield* packageManifestMain( - bindingManifest, - target.bindingPackage, - ); - const bindingDestination = join(nativeDestination, bindingFile); - yield* stagePackageRuntimeFile( - join(packageRoot, target.bindingPackage, bindingFile), - bindingDestination, - ); - archiveFiles.push( - `node_modules/@akua-dev/native/${bindingFile.replaceAll("\\", "/")}`, + packageName, + declaredFiles, ); - runtimeDirectories.add(dirname(bindingDestination)); - runtimeDirectories.add(scopeRoot); - runtimeDirectories.add(join(stagingDir, "node_modules")); - for (const directory of runtimeDirectories) { - yield* attempt("set package runtime directory timestamp", () => - utimesSync(directory, ARCHIVE_TIMESTAMP, ARCHIVE_TIMESTAMP), + for (const file of ["package.json", ...expandedFiles]) { + const destination = path.join(scopeRoot, packageName, file); + yield* stagePackageRuntimeFile( + path.join(packageRoot, packageName, file), + destination, ); + archiveFiles.push( + `node_modules/@akua-dev/${packageName}/${file.replaceAll("\\", "/")}`, + ); + runtimeDirectories.add(path.dirname(destination)); } - return archiveFiles; - }); -} + } + const bindingManifest = yield* readPackageManifest( + packageRoot, + target.bindingPackage, + ); + const bindingFile = yield* packageManifestMain( + path, + bindingManifest, + target.bindingPackage, + ); + const bindingDestination = path.join(nativeDestination, bindingFile); + yield* stagePackageRuntimeFile( + path.join(packageRoot, target.bindingPackage, bindingFile), + bindingDestination, + ); + archiveFiles.push( + `node_modules/@akua-dev/native/${bindingFile.replaceAll("\\", "/")}`, + ); + runtimeDirectories.add(path.dirname(bindingDestination)); + runtimeDirectories.add(scopeRoot); + runtimeDirectories.add(path.join(stagingDir, "node_modules")); + for (const directory of runtimeDirectories) { + yield* fs + .utimes(directory, ARCHIVE_TIMESTAMP, ARCHIVE_TIMESTAMP) + .pipe( + Effect.mapError( + toReleaseFailure("set package runtime directory timestamp"), + ), + ); + } + return archiveFiles; +}); -function stagePackageRuntimeFile( - source: string, - destination: string, -): Effect.Effect { - return Effect.gen(function* () { - yield* attempt("create package runtime file directory", () => - mkdirSync(dirname(destination), { recursive: true }), - ); - yield* attempt("stage package runtime file", () => - copyFileSync(source, destination), - ); - yield* attempt("set package runtime file mode", () => - chmodSync(destination, 0o644), - ); - yield* attempt("set package runtime file timestamp", () => - utimesSync(destination, ARCHIVE_TIMESTAMP, ARCHIVE_TIMESTAMP), - ); - }); -} +const stagePackageRuntimeFile = Effect.fn("stagePackageRuntimeFile")( + function* (source: string, destination: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs + .makeDirectory(path.dirname(destination), { recursive: true }) + .pipe( + Effect.mapError( + toReleaseFailure("create package runtime file directory"), + ), + ); + yield* fs + .copyFile(source, destination) + .pipe(Effect.mapError(toReleaseFailure("stage package runtime file"))); + yield* fs + .chmod(destination, 0o644) + .pipe( + Effect.mapError(toReleaseFailure("set package runtime file mode")), + ); + yield* fs + .utimes(destination, ARCHIVE_TIMESTAMP, ARCHIVE_TIMESTAMP) + .pipe( + Effect.mapError( + toReleaseFailure("set package runtime file timestamp"), + ), + ); + }, +); -function readPackageManifest( +const readPackageManifest = Effect.fn("readPackageManifest")(function* ( packageRoot: string, packageName: string, -): Effect.Effect { - return Effect.gen(function* () { - const source = yield* attempt("read package runtime manifest", () => - readFileSync(join(packageRoot, packageName, "package.json"), "utf8"), +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const source = yield* fs + .readFileString(path.join(packageRoot, packageName, "package.json")) + .pipe( + Effect.mapError(toReleaseFailure("read package runtime manifest")), ); - return yield* attempt("parse package runtime manifest", () => - JSON.parse(source), - ); - }); -} + return yield* attempt("parse package runtime manifest", () => + JSON.parse(source), + ); +}); function packageManifestFiles( + path: Path.Path, value: unknown, packageName: string, ): Effect.Effect { @@ -866,78 +898,85 @@ function packageManifestFiles( return releaseFailure(`Package runtime files are invalid for ${packageName}`); } return Effect.all( - value.files.map((file) => safePackageRelativePath(file, packageName)), + value.files.map((file) => safePackageRelativePath(path, file, packageName)), ); } // @akua-dev/sdk declares directory entries (e.g. "dist") in package.json's // `files`, unlike native/native-engines' flat file lists — expand every // directory entry into its individual files so each one gets staged. -function expandPackageManifestFiles( - packageRoot: string, - packageName: string, - files: string[], -): Effect.Effect { - return Effect.gen(function* () { +const expandPackageManifestFiles = Effect.fn("expandPackageManifestFiles")( + function* (packageRoot: string, packageName: string, files: string[]) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; const expanded: string[] = []; for (const file of files) { - const absolute = join(packageRoot, packageName, file); - const info = yield* attempt("stat package runtime file", () => - statSync(absolute), - ); - if (info.isDirectory()) { + const absolute = path.join(packageRoot, packageName, file); + const info = yield* fs + .stat(absolute) + .pipe(Effect.mapError(toReleaseFailure("stat package runtime file"))); + if (info.type === "Directory") { expanded.push(...(yield* walkPackageDirectory(absolute, file))); } else { expanded.push(file); } } return expanded; - }); -} + }, +); -function walkPackageDirectory( +const walkPackageDirectory = Effect.fn("walkPackageDirectory")(function* ( absoluteDirectory: string, relativeDirectory: string, -): Effect.Effect { - return Effect.gen(function* () { - const entries = yield* attempt("read package runtime directory", () => - readdirSync(absoluteDirectory, { withFileTypes: true }), +): Effect.fn.Return< + string[], + ReleaseFailure, + FileSystem.FileSystem | Path.Path +> { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const entries = yield* fs + .readDirectory(absoluteDirectory) + .pipe( + Effect.mapError(toReleaseFailure("read package runtime directory")), ); - const files: string[] = []; - for (const entry of entries) { - const relativePath = join(relativeDirectory, entry.name); - if (entry.isDirectory()) { - files.push( - ...(yield* walkPackageDirectory( - join(absoluteDirectory, entry.name), - relativePath, - )), - ); - } else { - files.push(relativePath); - } + const files: string[] = []; + for (const entry of entries) { + const absoluteEntry = path.join(absoluteDirectory, entry); + const relativePath = path.join(relativeDirectory, entry); + const info = yield* fs + .stat(absoluteEntry) + .pipe(Effect.mapError(toReleaseFailure("stat package runtime entry"))); + if (info.type === "Directory") { + files.push( + ...(yield* walkPackageDirectory(absoluteEntry, relativePath)), + ); + } else { + files.push(relativePath); } - return files; - }); -} + } + return files; +}); function packageManifestMain( + path: Path.Path, value: unknown, packageName: string, ): Effect.Effect { return isRecord(value) - ? safePackageRelativePath(value.main, packageName) + ? safePackageRelativePath(path, value.main, packageName) : releaseFailure(`Package runtime main is invalid for ${packageName}`); } function safePackageRelativePath( + path: Path.Path, value: unknown, packageName: string, ): Effect.Effect { if ( typeof value !== "string" || value === "" || - isAbsolute(value) || + path.isAbsolute(value) || value.split(/[\\/]/).some((segment) => segment === "" || segment === "..") ) { return releaseFailure(`Package runtime path is invalid for ${packageName}`); @@ -945,11 +984,13 @@ function safePackageRelativePath( return Effect.succeed(value); } -function sha256(bytes: Uint8Array): Effect.Effect { - return attempt("calculate SHA-256", () => - createHash("sha256").update(bytes).digest("hex"), - ); -} +const sha256 = Effect.fn("sha256")(function* (bytes: Uint8Array) { + const crypto = yield* Crypto.Crypto; + const digest = yield* crypto + .digest("SHA-256", bytes) + .pipe(Effect.mapError(toReleaseFailure("calculate SHA-256"))); + return bytesToHex(digest); +}); function stableJson(value: unknown): string { return `${JSON.stringify(value, null, 2)}\n`; @@ -1105,7 +1146,7 @@ function isRecord(value: unknown): value is Record { } export const ReleaseHostLive = Layer.succeed(ReleaseHost, { - sha256, + sha256: (bytes) => sha256(bytes).pipe(Effect.provide(NodeServices.layer)), hostTargetId: attempt("read release host", () => ({ platform: process.platform, arch: process.arch, @@ -1114,10 +1155,20 @@ export const ReleaseHostLive = Layer.succeed(ReleaseHost, { releaseTargetIdForHost(platform, arch), ), ), - planUploads: planReleaseUploads, - assertSafeOutputDirectory, - packageExistingExecutables, - packageRelease, - smokeReleaseArtifact, - verifyReleaseDirectory, + planUploads: (candidateDir, existingDir, version) => + planReleaseUploads(candidateDir, existingDir, version).pipe( + Effect.provide(NodeServices.layer), + ), + assertSafeOutputDirectory: (outputDir) => + assertSafeOutputDirectory(outputDir).pipe(Effect.provide(NodeServices.layer)), + packageExistingExecutables: (input) => + packageExistingExecutables(input).pipe(Effect.provide(NodeServices.layer)), + packageRelease: (input) => + packageRelease(input).pipe(Effect.provide(NodeServices.layer)), + smokeReleaseArtifact: (input) => + smokeReleaseArtifact(input).pipe(Effect.provide(NodeServices.layer)), + verifyReleaseDirectory: (outputDir, version) => + verifyReleaseDirectory(outputDir, version).pipe( + Effect.provide(NodeServices.layer), + ), }); diff --git a/scripts/runtime/release-services.ts b/scripts/runtime/release-services.ts index e5e00c1..c2a4da8 100644 --- a/scripts/runtime/release-services.ts +++ b/scripts/runtime/release-services.ts @@ -149,6 +149,20 @@ export function checksumLine(name: string, digest: string): string { return `${digest} ${name}\n`; } +export function bytesToHex(bytes: Uint8Array): string { + return Array.from(bytes) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); +} + +export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) return false; + for (let index = 0; index < a.length; index += 1) { + if (a[index] !== b[index]) return false; + } + return true; +} + export function validateVersion( version: string, ): Effect.Effect { diff --git a/test/docs.test.ts b/test/docs.test.ts index fcac6f2..c73d6c5 100644 --- a/test/docs.test.ts +++ b/test/docs.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "vitest"; +import { describe, expect, test } from "@effect/vitest"; import { readFile } from "node:fs/promises"; async function text(path: string): Promise { diff --git a/test/effect-generator-patch.test.ts b/test/effect-generator-patch.test.ts index 9deace7..d774e38 100644 --- a/test/effect-generator-patch.test.ts +++ b/test/effect-generator-patch.test.ts @@ -1,59 +1,73 @@ -import { expect, test } from "vitest"; -import { spawnSync } from "node:child_process"; -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { expect, it } from "@effect/vitest"; +import { NodeServices } from "@effect/platform-node"; +import { Effect, FileSystem, Path, Stream } from "effect"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { resolveBunBinary } from "./bun-binary"; -test("patched Effect generator preserves headers and SSE contracts without warnings", () => { - const directory = mkdtempSync(join(tmpdir(), "akua-effect-generator-")); - const specPath = join(directory, "public.json"); - const outputPath = join(directory, "public-api.gen.ts"); +it.effect( + "patched Effect generator preserves headers and SSE contracts without warnings", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const directory = yield* fs.makeTempDirectoryScoped({ + prefix: "akua-effect-generator-", + }); + const specPath = path.join(directory, "public.json"); + const outputPath = path.join(directory, "public-api.gen.ts"); - try { - writeFileSync(specPath, JSON.stringify(specification())); - const result = spawnSync( - resolveBunBinary(), - [ - "x", - "--no-install", - "openapigen", - "--spec", - specPath, - "--format", - "httpapi", - "--name", - "PublicApi", - ], - { encoding: "utf8" }, - ); + yield* fs.writeFileString(specPath, JSON.stringify(specification())); + const handle = yield* spawner.spawn( + ChildProcess.make(resolveBunBinary(), [ + "x", + "--no-install", + "openapigen", + "--spec", + specPath, + "--format", + "httpapi", + "--name", + "PublicApi", + ]), + ); + const [stdout, stderr, exitCode] = yield* Effect.all( + [ + handle.stdout.pipe(Stream.decodeText(), Stream.mkString), + handle.stderr.pipe(Stream.decodeText(), Stream.mkString), + handle.exitCode, + ], + { concurrency: "unbounded" }, + ); - expect(result.status).toBe(0); - expect(result.stderr).not.toContain("warning"); - writeFileSync(outputPath, result.stdout); - const output = readFileSync(outputPath, "utf8"); - expect(output).toContain("HttpApiSchema.WithHeaders"); - expect(output).toContain("WidgetsCreate201Headers"); - expect(output).toContain("HttpApiSchema.StreamSse({ events:"); - expect(output).toContain( - "payload: [WidgetsCreateRequestJson, HttpApiSchema.NoContent]", - ); - expect(output).toContain("readonly [x: string]: Schema.Json | undefined"); - } finally { - rmSync(directory, { force: true, recursive: true }); - } -}); + expect(exitCode).toBe(ChildProcessSpawner.ExitCode(0)); + expect(stderr).not.toContain("warning"); + yield* fs.writeFileString(outputPath, stdout); + const output = yield* fs.readFileString(outputPath); + expect(output).toContain("HttpApiSchema.WithHeaders"); + expect(output).toContain("WidgetsCreate201Headers"); + expect(output).toContain("HttpApiSchema.StreamSse({ events:"); + expect(output).toContain( + "payload: [WidgetsCreateRequestJson, HttpApiSchema.NoContent]", + ); + expect(output).toContain("readonly [x: string]: Schema.Json | undefined"); + }).pipe(Effect.provide(NodeServices.layer)), +); -test("patched Effect client preserves optional multipart as FormData or void", () => { - const clientTypes = readFileSync( - "node_modules/effect/dist/unstable/httpapi/HttpApiEndpoint.d.ts", - "utf8", - ); +it.effect( + "patched Effect client preserves optional multipart as FormData or void", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const clientTypes = yield* fs.readFileString( + "node_modules/effect/dist/unstable/httpapi/HttpApiEndpoint.d.ts", + ); - expect(clientTypes).toContain('Extract { @@ -80,7 +80,6 @@ describe("OpenAPI fetch guard", () => { "writes stable output when an unchanged spec is fetched repeatedly", () => Effect.gen(function* () { - const originalFetch = globalThis.fetch; const root = yield* Effect.promise(() => mkdtemp(join(process.cwd(), ".tmp-akua-openapi-")), ); @@ -89,20 +88,26 @@ describe("OpenAPI fetch guard", () => { paths: { "/health": { get: { operationId: "health" } } }, openapi: "3.1.0", }; - // Mocks the `fetch` global's Promise-returning contract; no Effect - // replacement exists for this interop shape. - globalThis.fetch = (async () => - Response.json(spec)) as unknown as typeof fetch; + // Test double for the ScriptHttp service (the seam fetchOpenApi + // already depends on), paired with the real ScriptFilesLive so this + // test still exercises real disk writes/reads for the stable-output + // assertion. No global fetch mutation, no unsafe cast. + const services = Layer.mergeAll( + Layer.succeed(ScriptHttp, { + getJson: () => Effect.succeed(spec), + }), + ScriptFilesLive, + ); yield* Effect.gen(function* () { yield* Effect.provide( fetchOpenApi(new URL(DEFAULT_OPENAPI_URL), output), - ScriptLive, + services, ); const first = yield* Effect.promise(() => readFile(output, "utf8")); yield* Effect.provide( fetchOpenApi(new URL(DEFAULT_OPENAPI_URL), output), - ScriptLive, + services, ); const second = yield* Effect.promise(() => readFile(output, "utf8")); @@ -110,15 +115,7 @@ describe("OpenAPI fetch guard", () => { expect(second).toBe(`${JSON.stringify(spec, null, 2)}\n`); }).pipe( Effect.ensuring( - Effect.sync(() => { - globalThis.fetch = originalFetch; - }).pipe( - Effect.andThen( - Effect.promise(() => - rm(root, { recursive: true, force: true }), - ), - ), - ), + Effect.promise(() => rm(root, { recursive: true, force: true })), ), ); }), diff --git a/test/generated-command.test.ts b/test/generated-command.test.ts index c1b1373..74ca626 100644 --- a/test/generated-command.test.ts +++ b/test/generated-command.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "vitest"; +import { describe, expect, test } from "@effect/vitest"; import { Effect, Layer, Stream } from "effect"; import { FetchHttpClient } from "effect/unstable/http"; diff --git a/test/generated-operation-executor.test.ts b/test/generated-operation-executor.test.ts index 5d36c21..4f1a7aa 100644 --- a/test/generated-operation-executor.test.ts +++ b/test/generated-operation-executor.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "@effect/vitest"; import { existsSync, readFileSync } from "node:fs"; import ts from "typescript"; import { commandRegistry } from "../src/generated/commands.gen"; diff --git a/test/mode.test.ts b/test/mode.test.ts index c6da182..b33feba 100644 --- a/test/mode.test.ts +++ b/test/mode.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "vitest"; +import { describe, expect, test } from "@effect/vitest"; import { Effect } from "effect"; import { detectOutputMode } from "../src/runtime/mode"; diff --git a/test/production-effect-invariants.test.ts b/test/production-effect-invariants.test.ts index cba0f8d..63262f4 100644 --- a/test/production-effect-invariants.test.ts +++ b/test/production-effect-invariants.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "@effect/vitest"; import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import ts from "typescript"; @@ -207,6 +207,50 @@ test("runtime handoff inspection requires a block-bodied import.meta.main guard" ]); }); +test("test files import vitest primitives only through @effect/vitest", () => { + const violations = collectTypeScriptFiles("test").flatMap((file) => { + const source = readFileSync(file, "utf8"); + const sourceFile = ts.createSourceFile( + file, + source, + ts.ScriptTarget.Latest, + true, + ); + const found: Violation[] = []; + visit(sourceFile, (node) => { + if ( + ts.isImportDeclaration(node) && + ts.isStringLiteral(node.moduleSpecifier) && + node.moduleSpecifier.text === "vitest" + ) { + found.push({ + file, + rule: 'import from "vitest" instead of "@effect/vitest"', + }); + } + }); + return found; + }); + + expect(violations).toEqual([]); +}); + +test("test files never monkey-patch globalThis.fetch", () => { + const violations = collectTypeScriptFiles("test").flatMap((file) => { + const source = readFileSync(file, "utf8"); + return /globalThis\.fetch\s*=/.test(source) + ? [ + { + file, + rule: "globalThis.fetch assignment; use FetchHttpClient.Fetch or a service test layer instead", + }, + ] + : []; + }); + + expect(violations).toEqual([]); +}); + function productionFiles(): string[] { return productionRoots.flatMap(collectTypeScriptFiles); } diff --git a/test/release-please-config.test.ts b/test/release-please-config.test.ts index 2130888..1101646 100644 --- a/test/release-please-config.test.ts +++ b/test/release-please-config.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "vitest"; +import { describe, expect, test } from "@effect/vitest"; import { readFileSync } from "node:fs"; interface ReleasePleaseConfig { diff --git a/test/release.test.ts b/test/release.test.ts index 6e0a572..a47e865 100644 --- a/test/release.test.ts +++ b/test/release.test.ts @@ -1,7 +1,17 @@ import { describe, expect, it, test } from "@effect/vitest"; -import { spawnSync } from "node:child_process"; -import { createHash } from "node:crypto"; -import { existsSync } from "node:fs"; +// This file's remaining node:fs/promises and node:path imports (below) are +// deliberate: they build/inspect ~20 independent temp-directory fixtures +// around the production release pipeline (writing fake binaries and +// package.json manifests, then reading packaged output back out for +// assertions). They never exercise release-host-live.ts's own FileSystem +// service — converting them would mean rewriting every test in this +// describe block into an Effect.gen body for no behavior-relevant gain, since +// none of this is part of the Effect pipeline under test. This matches the +// "process-boundary test helper" carve-out already documented in +// AGENTS.md/skills/effect-v4/SKILL.md for test/. Where a call *does* +// independently exercise the same class of host API the production code +// under test now uses (subprocess spawn, SHA-256 hashing, sleeping, +// existence checks), it's routed through Effect below instead. import { chmod, copyFile, @@ -14,17 +24,61 @@ import { writeFile, } from "node:fs/promises"; import { parse, join } from "node:path"; -import { setTimeout as sleep } from "node:timers/promises"; -import { Console, Effect, Layer } from "effect"; +import { NodeServices } from "@effect/platform-node"; +import { Console, Crypto, Effect, FileSystem, Layer } from "effect"; import { Command } from "effect/unstable/cli"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { RELEASE_TARGETS, releaseCommand } from "../scripts/release"; -import { ReleaseHost } from "../scripts/runtime/release-services"; +import { bytesToHex, ReleaseHost } from "../scripts/runtime/release-services"; import { ReleaseHostLive } from "../scripts/runtime/release-host-live"; import { cliTestLayer } from "./cli-test-layer"; -function runRelease(program: Effect.Effect): A { - return Effect.runSync(Effect.provide(program, ReleaseHostLive)); +function runRelease( + program: Effect.Effect, +): Promise { + return Effect.runPromise(Effect.provide(program, ReleaseHostLive)); +} + +// Shared runner for the handful of independent-oracle/verification host +// calls below that do have a real Effect equivalent (crypto digest, +// subprocess spawn, file existence). +function runNode( + effect: Effect.Effect, +): Promise { + return Effect.runPromise(Effect.provide(effect, NodeServices.layer)); +} + +function sha256Oracle(bytes: Uint8Array): Promise { + return runNode( + Effect.gen(function* () { + const crypto = yield* Crypto.Crypto; + const digest = yield* crypto.digest("SHA-256", bytes); + return bytesToHex(digest); + }), + ); +} + +function fileExists(path: string): Promise { + return runNode(Effect.flatMap(FileSystem.FileSystem, (fs) => fs.exists(path))); +} + +function extractTarSync( + archivePath: string, + extractDir: string, +): Promise<{ status: number }> { + return runNode( + Effect.scoped( + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const handle = yield* spawner.spawn( + ChildProcess.make("tar", ["-xzf", archivePath, "-C", extractDir]), + ); + const exitCode = yield* handle.exitCode; + return { status: exitCode }; + }), + ), + ); } async function makeReleaseTempDir(): Promise { @@ -89,8 +143,8 @@ async function makePackageRuntimeFixture(root: string): Promise { return packageRoot; } -test("release packaging has a dedicated implementation module", () => { - expect(existsSync("scripts/release.ts")).toBe(true); +test("release packaging has a dedicated implementation module", async () => { + expect(await fileExists("scripts/release.ts")).toBe(true); }); test("keeps exported release contract helpers free of host APIs", async () => { @@ -99,7 +153,7 @@ test("keeps exported release contract helpers free of host APIs", async () => { expect(helpers).not.toContain('from "node:crypto"'); expect(helpers).not.toContain("process.platform"); expect(helpers).not.toContain("process.arch"); - expect(existsSync("scripts/runtime/release-host-live.ts")).toBe(true); + expect(await fileExists("scripts/runtime/release-host-live.ts")).toBe(true); }); describe("release target contract", () => { @@ -273,9 +327,9 @@ describe("release target contract", () => { digest: string, ) => string; const bytes = new TextEncoder().encode("akua\n"); - const digest = createHash("sha256").update(bytes).digest("hex"); + const digest = await sha256Oracle(bytes); - expect(runRelease(sha256(bytes))).toBe(digest); + expect(await runRelease(sha256(bytes))).toBe(digest); expect(checksumLine("akua-v1.2.3-linux-x64.tar.gz", digest)).toBe( `${digest} akua-v1.2.3-linux-x64.tar.gz\n`, ); @@ -347,7 +401,7 @@ describe("release target contract", () => { ); expect( - runRelease(planReleaseUploads(candidateDir, existingDir, "1.2.3")), + await runRelease(planReleaseUploads(candidateDir, existingDir, "1.2.3")), ).toEqual( assetNames .filter((_, index) => index !== 0 && index !== 4) @@ -388,9 +442,9 @@ describe("release target contract", () => { } await writeFile(join(existingDir, assetNames[0]), "different bytes\n"); - expect(() => + await expect( runRelease(planReleaseUploads(candidateDir, existingDir, "1.2.3")), - ).toThrow( + ).rejects.toThrow( `Existing release asset does not match candidate: ${assetNames[0]}`, ); } finally { @@ -426,7 +480,7 @@ describe("release target contract", () => { const packageRoot = await makePackageRuntimeFixture(root); await writeFile(source, "#!/bin/sh\necho akua fixture\n"); await chmod(source, 0o755); - runRelease( + await runRelease( packageExistingExecutables({ version: "1.2.3", outputDir, @@ -438,7 +492,7 @@ describe("release target contract", () => { ); expect( - runRelease(verifyReleaseDirectory(outputDir, "1.2.3")), + await runRelease(verifyReleaseDirectory(outputDir, "1.2.3")), ).toBeUndefined(); const manifest = JSON.parse( await readFile(join(outputDir, "akua-v1.2.3-manifest.json"), "utf8"), @@ -476,15 +530,9 @@ describe("release target contract", () => { const extractDir = join(root, "extract"); await mkdir(extractDir); - const extract = spawnSync( - "tar", - [ - "-xzf", - join(outputDir, "akua-v1.2.3-linux-x64.tar.gz"), - "-C", - extractDir, - ], - { encoding: "utf8" }, + const extract = await extractTarSync( + join(outputDir, "akua-v1.2.3-linux-x64.tar.gz"), + extractDir, ); expect(extract.status).toBe(0); expect((await stat(join(extractDir, "akua"))).mode & 0o777).toBe(0o755); @@ -552,7 +600,7 @@ describe("release target contract", () => { await writeFile(source, "#!/bin/sh\necho akua fixture\n"); await chmod(source, 0o755); - runRelease( + await runRelease( packageExistingExecutables({ version: "1.2.3", outputDir: firstOutputDir, @@ -560,8 +608,8 @@ describe("release target contract", () => { packageRoot, }), ); - await sleep(2100); - runRelease( + await Effect.runPromise(Effect.sleep("2100 millis")); + await runRelease( packageExistingExecutables({ version: "1.2.3", outputDir: secondOutputDir, @@ -609,7 +657,7 @@ describe("release target contract", () => { const packageRoot = await makePackageRuntimeFixture(root); await writeFile(source, "#!/bin/sh\necho akua fixture\n"); await chmod(source, 0o755); - runRelease( + await runRelease( packageExistingExecutables({ version: "1.2.3", outputDir, @@ -624,9 +672,9 @@ describe("release target contract", () => { "tampered", ); - expect(() => + await expect( runRelease(verifyReleaseDirectory(outputDir, "1.2.3")), - ).toThrow("checksum mismatch"); + ).rejects.toThrow("checksum mismatch"); } finally { await rm(root, { recursive: true, force: true }); } @@ -641,23 +689,23 @@ describe("release target contract", () => { outputDir: string, ) => Effect.Effect; - expect(() => runRelease(assertSafeOutputDirectory(process.cwd()))).toThrow( - "Unsafe release output directory", - ); - expect(() => + await expect( + runRelease(assertSafeOutputDirectory(process.cwd())), + ).rejects.toThrow("Unsafe release output directory"); + await expect( runRelease(assertSafeOutputDirectory(parse(process.cwd()).root)), - ).toThrow("Unsafe release output directory"); - expect(() => + ).rejects.toThrow("Unsafe release output directory"); + await expect( runRelease(assertSafeOutputDirectory(join(process.cwd(), "src"))), - ).toThrow("Unsafe release output directory"); - expect(() => + ).rejects.toThrow("Unsafe release output directory"); + await expect( runRelease(assertSafeOutputDirectory(join(process.cwd(), "docs"))), - ).toThrow("Unsafe release output directory"); - expect(() => + ).rejects.toThrow("Unsafe release output directory"); + await expect( runRelease(assertSafeOutputDirectory(join(process.cwd(), "dist", "js"))), - ).toThrow("Unsafe release output directory"); + ).rejects.toThrow("Unsafe release output directory"); expect( - runRelease( + await runRelease( assertSafeOutputDirectory(join(process.cwd(), "dist", "release")), ), ).toBeUndefined(); @@ -679,9 +727,9 @@ describe("release target contract", () => { try { await symlink(target, linkedDirectory, "dir"); - expect(() => + await expect( runRelease(assertSafeOutputDirectory(join(linkedDirectory, "release"))), - ).toThrow("symlink"); + ).rejects.toThrow("symlink"); } finally { await rm(root, { recursive: true, force: true }); await rm(target, { recursive: true, force: true }); @@ -713,7 +761,7 @@ describe("release target contract", () => { const packageRoot = await makePackageRuntimeFixture(root); await writeFile(source, "#!/bin/sh\necho akua fixture\n"); await chmod(source, 0o755); - runRelease( + await runRelease( packageExistingExecutables({ version: "1.2.3", outputDir, @@ -728,9 +776,9 @@ describe("release target contract", () => { homebrew.platforms.linux_intel.sha256 = "0".repeat(64); await writeFile(manifestPath, `${JSON.stringify(homebrew, null, 2)}\n`); - expect(() => + await expect( runRelease(verifyReleaseDirectory(outputDir, "1.2.3")), - ).toThrow("Homebrew manifest mismatch"); + ).rejects.toThrow("Homebrew manifest mismatch"); } finally { await rm(root, { recursive: true, force: true }); } @@ -830,7 +878,7 @@ describe("release target contract", () => { `#!/bin/sh\nprintf '%s\\n' "$*" >> '${smokeLog}'\ncase "$1" in\n --version) echo '{"status":"ok","data":{"version":"1.2.3"}}' ;;\n --help) echo 'Usage: akua' ;;\n commands) echo 'commands[1]' ;;\n pkg)\n case "$2" in\n version) echo '{"version":"0.8.26"}' ;;\n init) mkdir -p demo; echo '{}' ;;\n check) echo '{}' ;;\n render) mkdir -p deploy; echo manifest > deploy/manifest.yaml; echo '{}' ;;\n inspect) echo '{}' ;;\n *) exit 2 ;;\n esac\n ;;\n *) exit 2 ;;\nesac\n`, ); await chmod(source, 0o755); - runRelease( + await runRelease( packageExistingExecutables({ version: "1.2.3", outputDir, @@ -841,12 +889,13 @@ describe("release target contract", () => { }), ); + const targetId = await runRelease(hostTargetId()); expect( - runRelease( + await runRelease( smokeReleaseArtifact({ version: "1.2.3", outputDir, - targetId: runRelease(hostTargetId()), + targetId, }), ), ).toBeUndefined(); @@ -897,7 +946,7 @@ describe("release target contract", () => { const { RELEASE_TARGETS: targets } = release as { RELEASE_TARGETS: Array<{ id: string }>; }; - runRelease( + await runRelease( packageExistingExecutables({ version: "1.2.3", outputDir, @@ -911,15 +960,13 @@ describe("release target contract", () => { // The staging directory is cleaned up after packaging, so verify the // real produced archive contents (what an installer actually // extracts), not the intermediate .staging tree. - const targetId = runRelease(hostTargetId()); + const targetId = await runRelease(hostTargetId()); const target = targets.find((candidate) => candidate.id === targetId); if (!target) throw new Error(`Unknown host target: ${targetId}`); const archivePath = join(outputDir, artifactName("1.2.3", target)); const extractDir = join(root, "extracted"); await mkdir(extractDir, { recursive: true }); - const extract = spawnSync("tar", ["-xzf", archivePath, "-C", extractDir], { - encoding: "utf8", - }); + const extract = await extractTarSync(archivePath, extractDir); expect(extract.status).toBe(0); const sdkDir = join(extractDir, "node_modules", "@akua-dev", "sdk"); @@ -974,7 +1021,7 @@ describe("release target contract", () => { '#!/bin/sh\ncase "$1" in\n --version) echo \'{"status":"ok","data":{"version":"11.2.3"}}\' ;;\n --help) echo \'Usage: akua\' ;;\n commands) echo \'commands[1]\' ;;\n *) exit 2 ;;\nesac\n', ); await chmod(source, 0o755); - runRelease( + await runRelease( packageExistingExecutables({ version: "1.2.3", outputDir, @@ -985,15 +1032,16 @@ describe("release target contract", () => { }), ); - expect(() => + const targetId = await runRelease(hostTargetId()); + await expect( runRelease( smokeReleaseArtifact({ version: "1.2.3", outputDir, - targetId: runRelease(hostTargetId()), + targetId, }), ), - ).toThrow("unexpected version"); + ).rejects.toThrow("unexpected version"); } finally { await rm(root, { recursive: true, force: true }); } diff --git a/test/render.test.ts b/test/render.test.ts index f55235f..2a5c71a 100644 --- a/test/render.test.ts +++ b/test/render.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "vitest"; +import { describe, expect, test } from "@effect/vitest"; import { AkuaCliError } from "../src/runtime/errors"; import { renderError, renderSuccess } from "../src/runtime/render"; diff --git a/test/run-akua.ts b/test/run-akua.ts index b1925b4..d5782cb 100644 --- a/test/run-akua.ts +++ b/test/run-akua.ts @@ -7,6 +7,15 @@ import { resolveBunBinary } from "./bun-binary"; * entrypoint end to end. Genuine process-boundary glue: it necessarily spawns * an OS process and is exempt from this repo's Effect-only rule for `src/` * and `scripts/` (test/ is not covered by that rule; see AGENTS.md). + * + * Deliberately kept on raw node:child_process rather than + * effect/unstable/process's ChildProcessSpawner: this helper black-box + * tests the compiled CLI entrypoint from outside the Effect pipeline it + * spawns (that's the point — it never touches the runtime under test), and + * it is called synchronously from 45+ plain (non-Effect) assertions across + * test/cli.test.ts and test/strict-effect-control-flow.test.ts. Converting + * it to an Effect-returning spawn would force every one of those call sites + * into an Effect.gen/it.effect body for no behavior-relevant gain. */ export interface RunAkuaResult { readonly stdout: string; diff --git a/test/strict-effect-control-flow.test.ts b/test/strict-effect-control-flow.test.ts index 902a433..64ece23 100644 --- a/test/strict-effect-control-flow.test.ts +++ b/test/strict-effect-control-flow.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "@effect/vitest"; import { readFileSync, readdirSync } from "node:fs"; import { join } from "node:path"; diff --git a/test/workflows.test.ts b/test/workflows.test.ts index 0efb7ec..43cd325 100644 --- a/test/workflows.test.ts +++ b/test/workflows.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "vitest"; +import { describe, expect, test } from "@effect/vitest"; import { readFile } from "node:fs/promises"; describe("distribution workflows", () => {