@pipelex/sdk 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifacts.d.ts +4 -1
- package/dist/artifacts.js +6 -1
- package/dist/artifacts.js.map +1 -1
- package/dist/client.d.ts +7 -13
- package/dist/client.js +15 -16
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +30 -5
- package/dist/errors.js +9 -5
- package/dist/errors.js.map +1 -1
- package/dist/hooks/check-core.d.ts +97 -8
- package/dist/hooks/check-core.js +164 -15
- package/dist/hooks/check-core.js.map +1 -1
- package/dist/hooks/claude-mthds-check.d.ts +9 -1
- package/dist/hooks/claude-mthds-check.js +48 -31
- package/dist/hooks/claude-mthds-check.js.map +1 -1
- package/dist/hooks/patch-envelope.d.ts +66 -0
- package/dist/hooks/patch-envelope.js +127 -0
- package/dist/hooks/patch-envelope.js.map +1 -0
- package/dist/hooks/shell-script.d.ts +92 -0
- package/dist/hooks/shell-script.js +1304 -0
- package/dist/hooks/shell-script.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/product-models.d.ts +1 -9
- package/dist/product-models.js +1 -1
- package/dist/runs.d.ts +10 -1
- package/dist/runs.js +17 -1
- package/dist/runs.js.map +1 -1
- package/dist/timers.d.ts +11 -0
- package/dist/timers.js +14 -0
- package/dist/timers.js.map +1 -0
- package/dist/upload-grant.d.ts +38 -15
- package/dist/upload-grant.js +200 -67
- package/dist/upload-grant.js.map +1 -1
- package/dist/upload.js +9 -2
- package/dist/upload.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/architecture.md +4 -4
- package/docs/artifact-download.md +1 -1
- package/docs/hook-bundle.md +88 -0
- package/docs/input-preparation.md +14 -8
- package/package.json +2 -2
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# The `.mthds` hook bundle
|
|
2
|
+
|
|
3
|
+
`dist-hooks/check.mjs` is the post-edit hook that checks a `.mthds` file after a coding agent edits it. It is built from `src/hooks/` by `npm run build:hook` and vendored by `pipelex-plugins`, which runs it after edits in Claude Code, Codex and Mistral Vibe (see [`architecture.md`](./architecture.md#hook-bundle-distcheckmjs) for how it is built and distributed). This page covers which file the hook checks on each platform, and in particular how it finds the file a Codex patch wrote.
|
|
4
|
+
|
|
5
|
+
## What the hook does with a file
|
|
6
|
+
|
|
7
|
+
Each file goes through three stages: a local lint that blocks on any diagnostic, a local format that rewrites the file in place when its layout changes, and a validation by the hosted API (`POST /v1/validate`) that blocks on an invalid verdict and adds a non-blocking note about pending pipe signatures. The hook fails open: when the engine cannot load, when `PIPELEX_API_KEY` is unset or the API cannot be reached, the stage that could not run passes silently and the others stand. When several files are checked, one block among them wins and the notes are joined, and each file is read when its turn comes, so an edit made to it while an earlier file was being validated is checked rather than overwritten. The hook always exits 0, since its verdict is on stdout.
|
|
8
|
+
|
|
9
|
+
## Which file it checks
|
|
10
|
+
|
|
11
|
+
| Platform | Where the edited file comes from |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| Claude Code | `tool_input.file_path` of the `PostToolUse` payload. |
|
|
14
|
+
| Mistral Vibe | The `post_tool` payload's `tool_output.file`, `tool_output.path`, `tool_input.file_path` or `tool_input.path`, resolved against the payload's `cwd`, when `tool_status` is `success`. |
|
|
15
|
+
| Codex | The patch in `tool_input.command`, which may name several files, read as described below. |
|
|
16
|
+
|
|
17
|
+
A file that no longer exists is not checked.
|
|
18
|
+
|
|
19
|
+
## Codex: the patch
|
|
20
|
+
|
|
21
|
+
Codex edits files with patches in its own envelope: `*** Begin Patch`, one section per file, `*** End Patch`. A section opens with `*** Add File: <path>`, `*** Update File: <path>` (optionally followed by `*** Move to: <path>`) or `*** Delete File: <path>`, and its `+` lines are the lines it adds, which the patch program writes verbatim.
|
|
22
|
+
|
|
23
|
+
The hook reads the envelope as sections and applies them in order to find the `.mthds` files the patch leaves on disk: an added or updated file is one, a moved file is replaced by its destination, and a deleted file is dropped. A relative path resolves against the session directory, which is the payload's `cwd` when it is absolute, and the hook's own working directory otherwise (Codex starts the hook in that same directory).
|
|
24
|
+
|
|
25
|
+
A patch made with Codex's `apply_patch` tool arrives with `tool_name: "apply_patch"`, and its paths are relative to the session directory by construction. Every file it leaves on disk is checked.
|
|
26
|
+
|
|
27
|
+
## Codex: a patch run through the shell
|
|
28
|
+
|
|
29
|
+
A Codex model can also run the patch program from a shell script, for instance `cd sub; apply_patch <<'EOF' … EOF`. Codex reports that as `tool_name: "Bash"`, with the whole script in `tool_input.command`, and the patch's relative paths are then relative to wherever the script had moved when `apply_patch` ran. The payload carries nothing else: the directory Codex's `exec_command` may have been given as `workdir` is not in it.
|
|
30
|
+
|
|
31
|
+
So for a `Bash` payload, the hook reads the script as a POSIX shell script (the dialect bash and zsh share for everything read here), follows its working directory from the session directory, and places each patch section at the directory of the `apply_patch` command that reads it.
|
|
32
|
+
|
|
33
|
+
### What reads the patch
|
|
34
|
+
|
|
35
|
+
A section is placed only where a patch command reads it: the words of an `apply_patch` command (including a quoted argument spanning several lines) and the heredocs it opens, and the text of a command feeding one through a pipeline (`cat <<'EOF' | apply_patch`) or through a substitution in its words (`apply_patch "$(cat <<'EOF' … EOF\n)"`). A patch fed by a substitution is placed at the patch command's directory, whatever the substitution does inside. `apply_patch` is recognised by name or by path, and after assignments or `command`; `applypatch` is the same program.
|
|
36
|
+
|
|
37
|
+
A section held by any other command is not placed. That includes a patch kept in a variable, written to a file for later, run through `bash -c`, `eval` or a function, or fed to a patch command that sits inside a subshell or a group, because what reads it later is not something the reading follows. Its relative paths are unplaced, and its absolute paths go through the content check below, since the script may never have applied the patch.
|
|
38
|
+
|
|
39
|
+
### How the directory is followed
|
|
40
|
+
|
|
41
|
+
| Construct | How it is read |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `cd DIR`, with one operand that is a literal word, optionally after `-L`, `-P` or `--` | The directory becomes `DIR`, resolved against the current one, assuming the `cd` succeeds. An absolute `DIR` makes an unknown directory known again. |
|
|
44
|
+
| `cd` with no operand, `cd -`, an operand starting with `~`, an operand holding `$`, a backtick or a glob character, several operands, or an unknown option | The directory becomes unknown. |
|
|
45
|
+
| `pushd`, `popd`, `eval`, `source`, `.`, and any function definition | The directory becomes unknown. |
|
|
46
|
+
| `( … )`, `$( … )`, `<( … )` and `>( … )`, the last two also as a redirection's target (`done < <(find …)`) | A scope: a `cd` inside it ends at the closing parenthesis. |
|
|
47
|
+
| A list sent to the background with `&`, and every member of a pipeline but the last | A subshell in bash and zsh alike, so a `cd` inside it ends with it. |
|
|
48
|
+
| A pipeline's last member | The directory becomes unknown when it moves, because zsh runs that member in the current shell and bash does not. |
|
|
49
|
+
| `{ … }` and the separators `&&`, `;` and newline | Followed in order, assuming each command succeeds. |
|
|
50
|
+
| `!` before a pipeline | Inverts its status, so a negated `cd` fails on the path the reading follows. |
|
|
51
|
+
| `\|\|`, the branches of `if` and `case`, and the passes of `for`, `select`, `while` and `until` loops | Where only one of several paths runs, the directory after is kept when every path leaves it the same, and becomes unknown otherwise. What runs only when a list that moved the shell failed, after `\|\|`, in `elif` and `else`, in an `until` loop's body or after a `while` loop, starts from an unknown directory, since the list may have failed anywhere; when it does not move the shell itself, such as `exit 1`, it leaves the directory to the path that succeeded. A loop whose passes start in different directories is read from an unknown one. |
|
|
52
|
+
| Heredoc bodies, quoted strings, comments, and the expression of a `[[ … ]]` | Never read as commands. |
|
|
53
|
+
| An unterminated quote, substitution, backtick or compound command, a heredoc inside backticks, an unbalanced `)`, or a closing word such as `fi` with nothing to close | The script is unparsed, and every relative path in it is unplaced. |
|
|
54
|
+
|
|
55
|
+
A relative path under an unknown directory, or anywhere in an unparsed script, is not resolved at all: the hook does not fall back to the session directory, because an unknown directory means the script moved somewhere the hook cannot follow, which is exactly when the session directory is the wrong guess. An absolute path resolves wherever the script ran. When one script applies several patches, in heredocs or as quoted arguments, each is read on its own, so a patch deleting a file in one directory never hides another patch's edit of a same-named file, and a file any of them leaves on disk is a target.
|
|
56
|
+
|
|
57
|
+
### The content check
|
|
58
|
+
|
|
59
|
+
The reading can be wrong, and a script run with `exec_command`'s `workdir` looks like one run in the session directory. So before checking a file that a `Bash` patch named by a relative path, or by an absolute path in text no patch command reads, the hook confirms the file carries the lines the patch added, in order. Each line is compared with its trailing whitespace trimmed, and blank lines are skipped. Only added lines count, because they are the only lines the patch program writes verbatim: it matches a patch's context lines loosely, so comparing them would reject the right file. When several patches in the script wrote the file, the lines of any one of them confirm it.
|
|
60
|
+
|
|
61
|
+
The lines are compared as the patch program received them. In a quoted heredoc (`<<'EOF'`), that is the script's text. In quotes, the shell removes the quotes a line closes and reopens, as `'\''` does to write an apostrophe in a single-quoted patch, and in double quotes or an unquoted heredoc it removes backslash escapes too, so `\"` in a double-quoted patch is compared as `"`. A line holding a `$` or backtick expansion is left out of the comparison, since the hook cannot know what it expanded to. When a line ends in other quoting than it began in, or in a backslash that joins it to the next line, the lines after it are read in quoting the hook does not follow, so none of that patch's lines is compared.
|
|
62
|
+
|
|
63
|
+
A file in another directory than the one the patch wrote almost never holds the lines the patch just added, so it is skipped rather than checked and reformatted. A section that adds no line, a pure deletion or a bare rename, gives the hook nothing to compare, so its file is not checked either.
|
|
64
|
+
|
|
65
|
+
An absolute path that a patch command reads names its file for certain, but the reading does not know whether the script reached that command: it reads a branch that did not run, or a function nobody called, like any other. So the hook checks such a file unless it lacks the lines the patch added, which shows the script did not write it. A section that adds no line shows nothing, so its file is checked.
|
|
66
|
+
|
|
67
|
+
### The note
|
|
68
|
+
|
|
69
|
+
When a relative `.mthds` path from a `Bash` patch goes unchecked, because it is unplaced, because the file there does not carry the patch's added lines, or because no file exists where it resolves (unless the patch itself deleted or moved it), the hook names it in one non-blocking note, sent as additional context:
|
|
70
|
+
|
|
71
|
+
> The .mthds hook did not check `broken.mthds`: it could not confirm which file this shell command patched. Name the file by its absolute path, or edit it with the apply_patch tool, and the hook will check it.
|
|
72
|
+
|
|
73
|
+
An absolute path that fails the content check is dropped without a note: its file does not hold the lines of a patch the script only stored or never reached, so the script did not write it.
|
|
74
|
+
|
|
75
|
+
The note never blocks, and a block from another file in the same call wins over it. It is sent even when the engine that runs the checks cannot load. It exists because a missing file or an unknown directory is something the agent can fix, unlike a missing API key, and without it the agent would read the hook's silence as a clean check. It is never sent for the `apply_patch` tool, whose paths are anchored reliably.
|
|
76
|
+
|
|
77
|
+
### Known limits
|
|
78
|
+
|
|
79
|
+
- **A near-copy can still be checked.** A same-named file elsewhere that already holds every line the patch added passes the content check. It takes a copy of the method in two places and a patch run in the other one.
|
|
80
|
+
- **Another hook may run first.** A user's own `PostToolUse` hook that reformats `.mthds` files on `Bash` before this one would make the added lines differ from the file, and the hook would skip the file, with the note for a relative path and without one for an absolute path.
|
|
81
|
+
- **An unreached patch that only removes lines is checked.** An absolute path in such a patch, in a branch the script did not take or a function it never called, is checked, since the patch added nothing that could show it did not run.
|
|
82
|
+
- **A delete in another branch can hide the note.** When one branch of a script writes a relative path and another deletes it, and no file exists where the hook resolved the path, the hook takes the absence for the delete's doing and sends no note.
|
|
83
|
+
- **A header path holding an expansion is not read.** `*** Update File: $FILE` in an unquoted heredoc names no `.mthds` file as written, so the hook neither checks the file nor names it.
|
|
84
|
+
- **Every member of a pipeline is read as feeding its patch command.** In `apply_patch <<REAL | cat <<OTHER`, the text of `OTHER` is read as a patch the command applied, although a pipeline's data flows only to the right.
|
|
85
|
+
- **`CDPATH`, login profiles and failing commands are ignored.** A `cd` is assumed to succeed and to go where its operand says, and so is a command before `&&`, so `[[ -d build ]] && cd build` is read as moving into `build`. The content check catches the cases where it did not.
|
|
86
|
+
- **A heredoc opened with `<<-` and indented patch lines.** The shell strips leading tabs from such a body, but a patch header is read only at the start of a line, so a tab-indented header is not seen.
|
|
87
|
+
- **Only POSIX shells are read.** A PowerShell script is read as a POSIX one; the content check keeps a misreading from checking the wrong file.
|
|
88
|
+
- **A patch Codex applies itself fires no hook.** Codex intercepts a script that is exactly `apply_patch <<'EOF' … EOF`, alone or after one `cd <dir> &&`, and applies the patch in process without any `PostToolUse` event, so nothing in the bundle sees it.
|
|
@@ -54,7 +54,7 @@ The file is described, never sent. The grant (`UploadGrant`) is a presigned, cre
|
|
|
54
54
|
|
|
55
55
|
**The grant is a bearer capability.** Until it expires, whoever holds it can write that one object, so the SDK never logs it, and its caller should not either. The route never replays a grant for a repeated request: a caller that lost one asks for a new one.
|
|
56
56
|
|
|
57
|
-
**Sending the file** is the standalone `uploadWithGrant(grant, file, { signal? })`, which takes a `Blob` or a `File` and returns `{ uri }`:
|
|
57
|
+
**Sending the file** is the standalone `uploadWithGrant(grant, file, { signal?, timeoutMs? })`, which takes a `Blob` or a `File` and returns `{ uri }`:
|
|
58
58
|
|
|
59
59
|
```ts
|
|
60
60
|
import { uploadWithGrant, RejectedAssetError } from "@pipelex/sdk/upload";
|
|
@@ -64,6 +64,8 @@ const { uri } = await uploadWithGrant(grant, file);
|
|
|
64
64
|
|
|
65
65
|
It sends the file as the raw body of a `PUT` to `grant.url`, with `grant.headers` exactly as received. The grant signed `Content-Length` too, and the body sets it, so the file must be exactly the declared size. Redirects are refused rather than followed, because a presigned URL is valid only at the address it was signed for. **The `uri` names nothing until the `PUT` has succeeded**, so fill a form field or a run input with it only after `uploadWithGrant` resolves.
|
|
66
66
|
|
|
67
|
+
**The time limit.** The whole exchange with storage runs under a time limit — the request, the file going out, the answer coming back and the read of an error body — so a stalled upload settles even when the caller passes no signal. By default the limit is 60 s plus 1 s for every started 128 KiB of the file, which assumes about 1 Mbit/s and comes to 460 s for a 50 MiB file. `timeoutMs` replaces the default, for a slower link or a caller that wants a tighter bound; it must be a positive number no larger than 2147483647, the longest delay a timer honours, or the call throws an `InputPreparationError` before anything is sent. A `signal` ends the upload whenever it fires, so it can shorten the limit but never lengthen it, and a caller's abort still propagates as the signal's own reason even when the limit ran out too. When the limit runs out before storage answers, the call throws an `UploadTransportError` whose `code` is `timeout` and whose message names the seconds allowed. When it runs out while storage's error body is still arriving, storage has already answered, so the call settles as that answer instead, classified from its status and whatever part of the body arrived. After a timeout, whether storage stored the file is unknown: retrying with the same grant before it expires either stores the file or answers the `412` of a used grant, and then the grant's `uri` already names the file. The grant's expiry does not bound the upload, because storage checks the signature when the request starts, not when it ends.
|
|
68
|
+
|
|
67
69
|
**The browser-safe entry.** `uploadWithGrant`, the grant types and the three error classes it raises (`InputPreparationError`, `RejectedAssetError`, `UploadTransportError`) are exported from `@pipelex/sdk/upload`, whose import graph reaches no Node builtin, no `undici` and not the client, so a page bundles it with nothing marked external. They are the same function and the same classes the main entry exports, so an `instanceof` check agrees whichever entry the error came through. The unit suite bundles the entry for the browser with esbuild and fails if anything Node-only becomes reachable.
|
|
68
70
|
|
|
69
71
|
**Storage's refusals** map onto the input-preparation errors, so a caller that already handles `uploadFile` needs no new case:
|
|
@@ -75,14 +77,16 @@ It sends the file as the raw body of a `PUT` to `grant.url`, with `grant.headers
|
|
|
75
77
|
| `403 SignatureDoesNotMatch` | `RejectedAssetError` | `signature_mismatch` | The file's size, content type or metadata differ from what the grant signed. |
|
|
76
78
|
| `403 AccessDenied`, "Request has expired" | `RejectedAssetError` | `grant_expired` | The grant expired. |
|
|
77
79
|
| `403 AccessDenied`, headers "not signed" | `RejectedAssetError` | `unsigned_header` | The request carried a storage header the grant did not sign. |
|
|
78
|
-
| `400 RequestTimeout` | `UploadTransportError` | | Storage stopped waiting for the body and wrote nothing, so the same grant can be retried while it is valid. |
|
|
80
|
+
| `400 RequestTimeout` | `UploadTransportError` | `storage_timeout` | Storage stopped waiting for the body and wrote nothing, so the same grant can be retried while it is valid. |
|
|
81
|
+
| `409 ConditionalRequestConflict` | `UploadTransportError` | `conflict` | Another upload with the same grant was still in progress. Whether the object was written is unknown, so the same-grant retry applies. A grant is meant to be sent once at a time. |
|
|
79
82
|
| any other `4xx` | `RejectedAssetError` | `store_refused` | Storage's own message is relayed. |
|
|
80
|
-
| `5xx` | `UploadTransportError` | | Whether the object was written is unknown: retrying with the same grant either stores it or answers the `412` of a used grant. |
|
|
81
|
-
| a redirect | `UploadTransportError` | | Refused, not followed. |
|
|
82
|
-
| no
|
|
83
|
-
|
|
|
83
|
+
| `5xx` | `UploadTransportError` | `server_error` | Whether the object was written is unknown: retrying with the same grant either stores it or answers the `412` of a used grant. |
|
|
84
|
+
| a redirect | `UploadTransportError` | `redirected` | Refused, not followed. |
|
|
85
|
+
| no answer within the time limit | `UploadTransportError` | `timeout` | The limit above ran out before storage answered. Whether the object was written is unknown, so the same-grant retry applies. |
|
|
86
|
+
| no response | `UploadTransportError` | `unreachable` | Storage unreachable. In a browser a refused cross-origin request looks exactly like this, so the message also points at the page's CSP `connect-src`. If the connection failed after the file went out, the object may have been written, so the message gives the same-grant retry too. |
|
|
87
|
+
| nothing sent | `UploadTransportError` | `invalid_grant_url` | The grant's `url` is not an absolute `http(s)` URL free of user info, so it is refused before any request. A browser would resolve a relative URL against the page and send the credential there. |
|
|
84
88
|
|
|
85
|
-
`RejectedAssetError.code`
|
|
89
|
+
`RejectedAssetError.code` and `UploadTransportError.code` are what a caller branches on — to request a new grant on `grant_used` or `grant_expired`, to retry with the same grant on `timeout` or `server_error`, or to tell a timeout from an unreachable host — rather than the message, the status or the error's `name`, since an expired grant and an unsigned header both answer `403 AccessDenied` and only S3's message tells them apart. The SDK sets `code` on every error it raises. `RejectedAssetError.status` is storage's, and its `filename` is the `File`'s name, or the object name the grant's `uri` ends in for a nameless `Blob`. `UploadTransportError.status` is storage's status when storage answered, and undefined when it did not. Neither error wraps storage's response, and only S3's error code and message reach a message: its error body can echo the signed request, credential included, so it is never kept, and only its first 16 KiB are read, which is where S3 writes its code and message. **No error carries the grant's URL.** The runtime's network error can: Node and Chrome name the whole URL when they cannot build a request from it, and Bun puts it on the error's `path`. So an unreachable storage is described by its origin and by the error names and codes along the cause chain (`TypeError, caused by Error ENOTFOUND`), and the runtime's error is neither relayed nor kept as `cause`. A caller's abort propagates as the signal's own reason, unwrapped, including one that lands while storage's error body is still arriving.
|
|
86
90
|
|
|
87
91
|
### `prepareInputs` — signature-driven input preparation
|
|
88
92
|
|
|
@@ -225,7 +229,9 @@ The contract distinguishes these semantic outcomes, each a typed subclass of `In
|
|
|
225
229
|
- **rejected asset** (`RejectedAssetError`) — the server refused it (e.g. a `413` past the service-defined size cap — see "Storage policy" — surfaced as a clear rejection with `code` `too_large`, not a raw transport error), or storage refused an upload with a grant (a used or expired grant, or a file that differs from what the grant signed — see the table above for each `code`);
|
|
226
230
|
- **unsupported server capability** (`UnsupportedUploadCapabilityError`) — the configured deployment has no upload route;
|
|
227
231
|
- **authentication / authorization failure** (`UploadAuthenticationError`) — `401` / `403`;
|
|
228
|
-
- **transport failure** (`UploadTransportError`) — a network or server fault,
|
|
232
|
+
- **transport failure** (`UploadTransportError`) — a network or server fault, or any other unexpected upload failure; its `status` is the HTTP status when a response produced it, and its `code` says which failure it was. From `uploadFile` the code is `server_error` for a `5xx`, `timeout` when the client's own request timeout ran out, `unreachable` when no response came back, and `unexpected` for any other status or failure; `uploadWithGrant`'s codes are in the table above.
|
|
233
|
+
|
|
234
|
+
A malformed data URL — no comma, bad base64, bad percent-encoding — is a plain `InputPreparationError`, raised while the asset is read and before anything is uploaded.
|
|
229
235
|
|
|
230
236
|
All preparation failures are raised **before any run is created**.
|
|
231
237
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipelex/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "TypeScript SDK for the Pipelex hosted API — execute methods, manage runs, and call the product surface.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@eslint/js": "^9.39.4",
|
|
55
|
-
"@pipelex/tools-wasm": "
|
|
55
|
+
"@pipelex/tools-wasm": "0.3.0",
|
|
56
56
|
"@types/node": "^22.12.0",
|
|
57
57
|
"@vitest/coverage-v8": "^4.1.9",
|
|
58
58
|
"dependency-cruiser": "^17.4.3",
|