@mcuste/pi-diagram 0.0.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/LICENSE +21 -0
- package/README.md +240 -0
- package/dist/artifacts.d.ts +59 -0
- package/dist/artifacts.d.ts.map +1 -0
- package/dist/artifacts.js +274 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/cache.d.ts +43 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +0 -0
- package/dist/cache.js.map +1 -0
- package/dist/d2/diagnostics.d.ts +25 -0
- package/dist/d2/diagnostics.d.ts.map +1 -0
- package/dist/d2/diagnostics.js +77 -0
- package/dist/d2/diagnostics.js.map +1 -0
- package/dist/d2/fonts.d.ts +23 -0
- package/dist/d2/fonts.d.ts.map +1 -0
- package/dist/d2/fonts.js +255 -0
- package/dist/d2/fonts.js.map +1 -0
- package/dist/d2/preflight.d.ts +20 -0
- package/dist/d2/preflight.d.ts.map +1 -0
- package/dist/d2/preflight.js +217 -0
- package/dist/d2/preflight.js.map +1 -0
- package/dist/d2/profiles.d.ts +34 -0
- package/dist/d2/profiles.d.ts.map +1 -0
- package/dist/d2/profiles.js +118 -0
- package/dist/d2/profiles.js.map +1 -0
- package/dist/d2/runner.d.ts +95 -0
- package/dist/d2/runner.d.ts.map +1 -0
- package/dist/d2/runner.js +350 -0
- package/dist/d2/runner.js.map +1 -0
- package/dist/display.d.ts +48 -0
- package/dist/display.d.ts.map +1 -0
- package/dist/display.js +120 -0
- package/dist/display.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/normalize.d.ts +23 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/normalize.js +83 -0
- package/dist/normalize.js.map +1 -0
- package/dist/process.d.ts +38 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +87 -0
- package/dist/process.js.map +1 -0
- package/dist/raster.d.ts +40 -0
- package/dist/raster.d.ts.map +1 -0
- package/dist/raster.js +193 -0
- package/dist/raster.js.map +1 -0
- package/dist/render.d.ts +55 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +229 -0
- package/dist/render.js.map +1 -0
- package/dist/tools.d.ts +58 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +284 -0
- package/dist/tools.js.map +1 -0
- package/package.json +101 -0
- package/src/artifacts.ts +418 -0
- package/src/cache.ts +0 -0
- package/src/d2/diagnostics.ts +114 -0
- package/src/d2/fonts.ts +289 -0
- package/src/d2/preflight.ts +270 -0
- package/src/d2/profiles.ts +157 -0
- package/src/d2/runner.ts +513 -0
- package/src/display.ts +201 -0
- package/src/index.ts +5 -0
- package/src/normalize.ts +119 -0
- package/src/process.ts +134 -0
- package/src/raster.ts +258 -0
- package/src/render.ts +338 -0
- package/src/tools.ts +455 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mcuste
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# pi-diagram
|
|
2
|
+
|
|
3
|
+
[](https://github.com/mcuste/pi-diagram/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
An extension for the [Pi](https://github.com/earendil-works/pi) and
|
|
6
|
+
[Oh My Pi](https://github.com/can1357/oh-my-pi) terminal coding agents. It gives the model one
|
|
7
|
+
`diagram` tool that turns declarative [D2](https://d2lang.com) source into a rendered diagram in the
|
|
8
|
+
transcript, or into documentation artifacts checked into the repository.
|
|
9
|
+
|
|
10
|
+
If any of those names are new to you:
|
|
11
|
+
|
|
12
|
+
- **D2** is a declarative diagram language. Text such as `client -> gateway -> api` compiles to a
|
|
13
|
+
laid-out diagram, and the same language covers containers, sequence diagrams, and SQL tables.
|
|
14
|
+
- **Pi** and **Oh My Pi** are terminal coding agents. An **extension** is an npm package they load
|
|
15
|
+
at startup to add tools the model can call.
|
|
16
|
+
|
|
17
|
+
> **Status: images, text, and files work.** In a terminal that supports inline images the diagram
|
|
18
|
+
> itself is shown; everywhere else it is box drawing or plain ASCII. Files land outside the
|
|
19
|
+
> repository unless a destination is named. The design being followed is
|
|
20
|
+
> [docs/terminal_diagram_tool_proposal.md](docs/terminal_diagram_tool_proposal.md).
|
|
21
|
+
|
|
22
|
+
## Why
|
|
23
|
+
|
|
24
|
+
Coding agents explain spatial things in prose: "A calls B, B queues C, C writes D, D emits an event
|
|
25
|
+
back to A." The user rebuilds a graph in their head that the agent could have drawn. A fenced
|
|
26
|
+
Mermaid block moves the problem rather than solving it, because a terminal shows the source instead
|
|
27
|
+
of the picture.
|
|
28
|
+
|
|
29
|
+
This extension gives the model one tool for that. The model writes the meaning: nodes, edges,
|
|
30
|
+
groups, labels, emphasis. The tool owns the appearance: layout engine, theme, spacing, fonts, and
|
|
31
|
+
which representation the current terminal can actually display. Diagrams then look consistent
|
|
32
|
+
across calls, because the model is not styling them one at a time.
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
- Node.js 22 or newer
|
|
37
|
+
- The D2 CLI, version 0.8.0 or newer, on `PATH` or named by `D2_BIN`
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
brew install d2 # ships 0.8.1
|
|
41
|
+
go install github.com/d2lang/d2@v0.8.1 # or a pinned build from source
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The prebuilt binaries on D2's GitHub releases page stop at 0.7.1, which draws SQL tables as empty
|
|
45
|
+
boxes, so they are below the supported floor.
|
|
46
|
+
|
|
47
|
+
D2 is an external dependency on purpose. The extension never downloads a renderer during a tool
|
|
48
|
+
call. Without it, the extension still loads and a call explains how to install it.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
Pi:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pi install npm:@mcuste/pi-diagram
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Oh My Pi:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
omp plugin install @mcuste/pi-diagram
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or through the Oh My Pi marketplace:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
/marketplace add mcuste/pi-diagram
|
|
68
|
+
/marketplace install pi-diagram@pi-diagram
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
From a local checkout:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pnpm install
|
|
75
|
+
pi install /absolute/path/to/pi-diagram
|
|
76
|
+
omp plugin link /absolute/path/to/pi-diagram
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## What the tool does
|
|
80
|
+
|
|
81
|
+
One tool named `diagram`. Give it D2 source and it draws the diagram in the terminal:
|
|
82
|
+
|
|
83
|
+
```d2
|
|
84
|
+
edge: Edge { gateway }
|
|
85
|
+
core: Core { api; worker }
|
|
86
|
+
edge.gateway -> core.api
|
|
87
|
+
core.api -> core.worker: enqueue
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
┌───────────────────┐
|
|
92
|
+
│ Edge │
|
|
93
|
+
│ │
|
|
94
|
+
│ ┌──────────┐ │
|
|
95
|
+
│ │ gateway │ │
|
|
96
|
+
│ └──────────┘ │
|
|
97
|
+
│ │ │
|
|
98
|
+
└──────────│────────┘
|
|
99
|
+
│
|
|
100
|
+
┌─────────│─────────┐
|
|
101
|
+
│ Core │
|
|
102
|
+
│ ▼ │
|
|
103
|
+
│ ┌──────┐ │
|
|
104
|
+
│ │ api │ │
|
|
105
|
+
│ └──────┘ │
|
|
106
|
+
│ │ │
|
|
107
|
+
│ enqueue │
|
|
108
|
+
│ ▼ │
|
|
109
|
+
│ ┌─────────┐ │
|
|
110
|
+
│ │ worker │ │
|
|
111
|
+
│ └─────────┘ │
|
|
112
|
+
└───────────────────┘
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Containers, sequence diagrams, SQL tables, class shapes, and state flows all work from the same
|
|
116
|
+
language.
|
|
117
|
+
|
|
118
|
+
| Field | Purpose |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| `source` | The diagram, in D2 |
|
|
121
|
+
| `title` | Label shown above the diagram |
|
|
122
|
+
| `render` | `auto` and `image` show a picture where the terminal can, `unicode` draws box drawing, `ascii` plain 7-bit, `source` echoes the D2 |
|
|
123
|
+
| `profile` | What the diagram is for. It sets the layout engine, theme, and spacing |
|
|
124
|
+
| `formats` | Files to produce: `source`, `svg`, `png`, `txt`. Written outside the repository |
|
|
125
|
+
| `save` | Also copy them into the repository. `dir` is required |
|
|
126
|
+
|
|
127
|
+
## What a profile changes
|
|
128
|
+
|
|
129
|
+
The model says what a diagram is for, and the tool decides how it looks:
|
|
130
|
+
|
|
131
|
+
| Profile | For | Drawn as |
|
|
132
|
+
| --- | --- | --- |
|
|
133
|
+
| `explain` | A diagram inside an answer | Hand drawn, neutral theme, small margins |
|
|
134
|
+
| `architecture` | System and component views | Neutral theme, more room between rows so edges stay separable |
|
|
135
|
+
| `data` | Schemas, tables, class relationships | Neutral theme, tight spacing: tables are tall already |
|
|
136
|
+
| `docs` | Diagrams checked into a repository | Grey theme that prints in greyscale, page-sized margins |
|
|
137
|
+
| `tree` | A hierarchy: an org chart, a call tree, a file layout | Dagre, which fans children out under their parent |
|
|
138
|
+
| `c4` | Architecture written in the C4 convention | Architecture spacing under the C4 palette |
|
|
139
|
+
| `dependency` | A graph with more nodes than usual | The tightest spacing, cutting the room edges take past nodes |
|
|
140
|
+
|
|
141
|
+
`explain` is the default and is drawn by hand: an answer in a conversation is a rough model, and a
|
|
142
|
+
crisp diagram claims more precision than it has. Every other profile is crisp.
|
|
143
|
+
|
|
144
|
+
Every profile also sets a dark theme, so a saved SVG adapts to dark mode.
|
|
145
|
+
|
|
146
|
+
Only the picture changes. D2 draws text in character cells, so every profile gives the same box
|
|
147
|
+
drawing.
|
|
148
|
+
|
|
149
|
+
## Images in the terminal
|
|
150
|
+
|
|
151
|
+
On Kitty, Ghostty, WezTerm, iTerm2, and anything else that speaks a terminal image protocol,
|
|
152
|
+
`auto` shows the drawn diagram. Everywhere else the same call shows box drawing, and text is
|
|
153
|
+
always rendered too, because whether a terminal can display an image is only settled when the
|
|
154
|
+
result reaches the screen.
|
|
155
|
+
|
|
156
|
+
Nothing needs configuring. The terminal's own capabilities decide, and a terminal without an
|
|
157
|
+
image protocol is never sent one. Note that a multiplexer between the terminal and the agent has
|
|
158
|
+
to forward the protocol: tmux needs `allow-passthrough`, and herdr needs
|
|
159
|
+
`experimental.kitty_graphics`.
|
|
160
|
+
|
|
161
|
+
The image never enters the model's context. It is written to a private temporary directory and
|
|
162
|
+
read back when the row is displayed.
|
|
163
|
+
|
|
164
|
+
D2 exports PNG by driving a headless browser it downloads on first use, which this tool will not
|
|
165
|
+
do during a call. Instead the SVG it already produces is rasterized locally by
|
|
166
|
+
[resvg](https://github.com/yisibl/resvg-js), which needs no browser and no network. Labels are
|
|
167
|
+
drawn with the fonts the SVG carries, so the picture matches the boxes D2 measured. Characters
|
|
168
|
+
those fonts do not cover, such as CJK, fall back to the fonts on the machine and say so.
|
|
169
|
+
|
|
170
|
+
## What the model gets back
|
|
171
|
+
|
|
172
|
+
In a terminal this extension draws the row, so the model reads one line: `Drew "Request path" as
|
|
173
|
+
an image. It is on the user's screen, so it is not repeated here.` Saved paths and notes come with
|
|
174
|
+
it, so a conversation full of diagrams costs about what a conversation of text costs. Print, RPC,
|
|
175
|
+
and JSON modes have no row to draw, so there the diagram is the result text.
|
|
176
|
+
|
|
177
|
+
Expanding a row adds the render mode, the profile, the D2 version, the file paths, diagnostics,
|
|
178
|
+
and the source.
|
|
179
|
+
|
|
180
|
+
## Where files go
|
|
181
|
+
|
|
182
|
+
Most diagrams explain something in passing and should leave nothing behind. So nothing is written
|
|
183
|
+
unless it is asked for, and the repository is never the default:
|
|
184
|
+
|
|
185
|
+
| Call | Files | Approval |
|
|
186
|
+
| --- | --- | --- |
|
|
187
|
+
| `{ source }` | none, the diagram is only in the transcript | read |
|
|
188
|
+
| `{ source, formats: ["svg"] }` | an SVG in a private temp directory, path returned | read |
|
|
189
|
+
| `{ source, title, save: { dir: "docs/diagrams" } }` | copied into the repository | write, prompts with the exact files |
|
|
190
|
+
|
|
191
|
+
`save.dir` has no default. There is no directory convention that holds across repositories, so
|
|
192
|
+
the destination has to be named. Only pass `save` when the user asked to keep the diagram. A
|
|
193
|
+
saved `.d2` is put through `d2 fmt` first, because people edit it later.
|
|
194
|
+
|
|
195
|
+
A saved pair is editable source plus a viewable rendering:
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
docs/diagrams/request-lifecycle.d2
|
|
199
|
+
docs/diagrams/request-lifecycle.svg
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Markdown then needs no D2 or Mermaid runtime:
|
|
203
|
+
|
|
204
|
+
```markdown
|
|
205
|
+

|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
For documentation prefer `svg`: it scales, and Markdown renders it anywhere. `png` exists for
|
|
209
|
+
places where SVG support is weak.
|
|
210
|
+
|
|
211
|
+
Repository paths stay inside the workspace. Absolute paths, `..`, and symlinks pointing outside
|
|
212
|
+
are all refused.
|
|
213
|
+
|
|
214
|
+
What D2 drew is also kept in a temporary store, so the same diagram drawn again is about ten times
|
|
215
|
+
faster, including in a later session. A new D2 version, or any change to a profile, is a different
|
|
216
|
+
entry. The store holds 64 MB for a week.
|
|
217
|
+
|
|
218
|
+
Layout engine, theme, padding, and font are deliberately not in the schema. They are policy here,
|
|
219
|
+
and a model given those knobs spends tokens on styling and produces a different look every call.
|
|
220
|
+
|
|
221
|
+
## What is deliberately missing
|
|
222
|
+
|
|
223
|
+
- **Model-drawn images.** The model never emits SVG coordinates or ASCII art directly. It writes
|
|
224
|
+
source, and a renderer lays it out.
|
|
225
|
+
- **Imports and external assets.** Diagram source cannot pull in files, local images, or remote
|
|
226
|
+
icons. Those turn a rendering request into file and network access.
|
|
227
|
+
- **Interactive editing.** The tool renders; it is not a diagram editor in the terminal.
|
|
228
|
+
- **Every D2 feature.** A safe subset is supported, and anything outside it is reported as an error
|
|
229
|
+
the model can correct rather than being silently dropped.
|
|
230
|
+
|
|
231
|
+
## Documentation
|
|
232
|
+
|
|
233
|
+
- [Safety model](docs/safety.md)
|
|
234
|
+
- [Design proposal](docs/terminal_diagram_tool_proposal.md)
|
|
235
|
+
- [Development and release](docs/development.md)
|
|
236
|
+
- [Changelog](CHANGELOG.md)
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
declare const EXTENSIONS: {
|
|
2
|
+
readonly source: ".d2";
|
|
3
|
+
readonly svg: ".svg";
|
|
4
|
+
readonly png: ".png";
|
|
5
|
+
readonly txt: ".txt";
|
|
6
|
+
};
|
|
7
|
+
export type ArtifactFormat = keyof typeof EXTENSIONS;
|
|
8
|
+
declare const safeBasenameBrand: unique symbol;
|
|
9
|
+
declare const artifactDirectoryBrand: unique symbol;
|
|
10
|
+
/** A file name stem with no path separators, extension, or reserved meaning. */
|
|
11
|
+
type SafeBasename = string & {
|
|
12
|
+
readonly [safeBasenameBrand]: true;
|
|
13
|
+
};
|
|
14
|
+
/** An absolute directory that is either the session temp store or inside the workspace root. */
|
|
15
|
+
type ArtifactDirectory = string & {
|
|
16
|
+
readonly [artifactDirectoryBrand]: true;
|
|
17
|
+
};
|
|
18
|
+
type ArtifactLocation = "temp" | "workspace";
|
|
19
|
+
export interface ArtifactNames {
|
|
20
|
+
/** Workspace-relative destination, or `undefined` to keep the files out of the repository. */
|
|
21
|
+
readonly directory: string | undefined;
|
|
22
|
+
readonly basename: SafeBasename;
|
|
23
|
+
readonly formats: readonly ArtifactFormat[];
|
|
24
|
+
}
|
|
25
|
+
export interface ArtifactTarget {
|
|
26
|
+
readonly location: ArtifactLocation;
|
|
27
|
+
readonly directory: ArtifactDirectory;
|
|
28
|
+
readonly names: ArtifactNames;
|
|
29
|
+
readonly root: string;
|
|
30
|
+
}
|
|
31
|
+
export interface WrittenArtifact {
|
|
32
|
+
readonly format: ArtifactFormat;
|
|
33
|
+
readonly location: ArtifactLocation;
|
|
34
|
+
/** Workspace-relative for a repository file, absolute for a temporary one. */
|
|
35
|
+
readonly path: string;
|
|
36
|
+
}
|
|
37
|
+
export interface ArtifactIdentity {
|
|
38
|
+
readonly title: string | undefined;
|
|
39
|
+
/** Hash of the normalized source, used to name a temporary file with no title. */
|
|
40
|
+
readonly hash: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ArtifactAsk {
|
|
43
|
+
/** Which artifacts to produce. They land in the temp store either way. */
|
|
44
|
+
readonly formats?: unknown;
|
|
45
|
+
/** Present only when the caller also wants them in the repository. */
|
|
46
|
+
readonly save?: unknown;
|
|
47
|
+
}
|
|
48
|
+
/** Resolves nothing on disk, so a bad request is refused before any rendering starts. */
|
|
49
|
+
export declare function parseArtifactNames(request: ArtifactAsk, identity: ArtifactIdentity): ArtifactNames;
|
|
50
|
+
/** The workspace-relative paths a repository write would touch, for an approval prompt. */
|
|
51
|
+
export declare function workspacePaths(names: ArtifactNames): readonly string[];
|
|
52
|
+
export declare function parseArtifactTarget(cwd: unknown, names: ArtifactNames): Promise<ArtifactTarget>;
|
|
53
|
+
/**
|
|
54
|
+
* Writes each artifact through a temporary file and a rename, so a reader never sees a half
|
|
55
|
+
* written diagram and a failed render leaves the previous version in place.
|
|
56
|
+
*/
|
|
57
|
+
export declare function writeArtifacts(target: ArtifactTarget, contents: ReadonlyMap<ArtifactFormat, string | Uint8Array>): Promise<readonly WrittenArtifact[]>;
|
|
58
|
+
export {};
|
|
59
|
+
//# sourceMappingURL=artifacts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifacts.d.ts","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"AA2BA,QAAA,MAAM,UAAU;aACd,MAAM,EAAE,KAAK;aACb,GAAG,EAAE,MAAM;aACX,GAAG,EAAE,MAAM;aACX,GAAG,EAAE,MAAM;CACH,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG,MAAM,OAAO,UAAU,CAAC;AAerD,OAAO,CAAC,MAAM,iBAAiB,EAAE,OAAO,MAAM,CAAC;AAC/C,OAAO,CAAC,MAAM,sBAAsB,EAAE,OAAO,MAAM,CAAC;AAEpD,gFAAgF;AAChF,KAAK,YAAY,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,CAAC,iBAAiB,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAEpE,gGAAgG;AAChG,KAAK,iBAAiB,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,CAAC,sBAAsB,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAE9E,KAAK,gBAAgB,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7C,MAAM,WAAW,aAAa;IAC5B,8FAA8F;IAC9F,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;CAC7C;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AA8HD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,sEAAsE;IACtE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,yFAAyF;AACzF,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,gBAAgB,GACzB,aAAa,CAQf;AAED,2FAA2F;AAC3F,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,MAAM,EAAE,CAMtE;AAsDD,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,aAAa,GACnB,OAAO,CAAC,cAAc,CAAC,CAgCzB;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,UAAU,CAAC,GACzD,OAAO,CAAC,SAAS,eAAe,EAAE,CAAC,CAyCrC"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, mkdtemp, readdir, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { DiagramSourceError } from "./d2/diagnostics.js";
|
|
6
|
+
/**
|
|
7
|
+
* Writes diagram artifacts. Files land in a private temporary directory unless a call names a
|
|
8
|
+
* repository destination, because most diagrams explain something in passing and have no
|
|
9
|
+
* business being committed.
|
|
10
|
+
*/
|
|
11
|
+
const MAX_BASENAME_LENGTH = 60;
|
|
12
|
+
const MAX_DIRECTORY_LENGTH = 255;
|
|
13
|
+
/** Keeps a long session from filling the temp directory with diagrams nobody opened. */
|
|
14
|
+
const MAX_TEMP_FILES = 64;
|
|
15
|
+
const EXTENSIONS = {
|
|
16
|
+
source: ".d2",
|
|
17
|
+
svg: ".svg",
|
|
18
|
+
png: ".png",
|
|
19
|
+
txt: ".txt",
|
|
20
|
+
};
|
|
21
|
+
/** Editable source plus a viewable rendering. */
|
|
22
|
+
const DEFAULT_FORMATS = ["source", "svg"];
|
|
23
|
+
/** Names Windows treats as devices rather than files, whatever extension follows. */
|
|
24
|
+
const RESERVED_NAMES = new Set([
|
|
25
|
+
"con",
|
|
26
|
+
"prn",
|
|
27
|
+
"aux",
|
|
28
|
+
"nul",
|
|
29
|
+
...Array.from({ length: 9 }, (_, index) => `com${index + 1}`),
|
|
30
|
+
...Array.from({ length: 9 }, (_, index) => `lpt${index + 1}`),
|
|
31
|
+
]);
|
|
32
|
+
function refuse(summary, message, hint) {
|
|
33
|
+
throw new DiagramSourceError(summary, [
|
|
34
|
+
{ code: "D2_SOURCE", message, ...(hint === undefined ? {} : { hint }) },
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
function parseFormats(requested) {
|
|
38
|
+
if (requested === undefined) {
|
|
39
|
+
return DEFAULT_FORMATS;
|
|
40
|
+
}
|
|
41
|
+
if (!Array.isArray(requested) || requested.length === 0) {
|
|
42
|
+
refuse("Diagram save formats are not usable.", `Expected a non-empty list, got ${typeof requested}.`);
|
|
43
|
+
}
|
|
44
|
+
const formats = [];
|
|
45
|
+
for (const format of requested) {
|
|
46
|
+
if (!Object.hasOwn(EXTENSIONS, format)) {
|
|
47
|
+
refuse("Diagram save formats are not usable.", `${JSON.stringify(format)} is not a format.`, `Use ${Object.keys(EXTENSIONS).join(", ")}.`);
|
|
48
|
+
}
|
|
49
|
+
if (!formats.includes(format)) {
|
|
50
|
+
formats.push(format);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return formats;
|
|
54
|
+
}
|
|
55
|
+
/** A repository destination has to be named. There is no directory convention worth assuming. */
|
|
56
|
+
function parseDirectory(requested) {
|
|
57
|
+
if (typeof requested !== "string" || requested.trim().length === 0) {
|
|
58
|
+
refuse("Saving a diagram needs a directory.", requested === undefined
|
|
59
|
+
? "`save.dir` was not given."
|
|
60
|
+
: `Expected a path, got ${typeof requested}.`, "Name the directory to write into, such as docs/diagrams.");
|
|
61
|
+
}
|
|
62
|
+
const directory = requested.trim().replaceAll("\\", "/");
|
|
63
|
+
if (directory.length > MAX_DIRECTORY_LENGTH) {
|
|
64
|
+
refuse("Diagram save directory is not usable.", `It is longer than ${MAX_DIRECTORY_LENGTH} characters.`);
|
|
65
|
+
}
|
|
66
|
+
if (isAbsolute(requested) || directory.startsWith("/")) {
|
|
67
|
+
refuse("Diagram save directory is not usable.", `${JSON.stringify(requested)} is an absolute path.`, "Give a path relative to the workspace, such as docs/diagrams.");
|
|
68
|
+
}
|
|
69
|
+
const segments = directory.split("/").filter((segment) => segment.length > 0 && segment !== ".");
|
|
70
|
+
if (segments.includes("..")) {
|
|
71
|
+
refuse("Diagram save directory is not usable.", `${JSON.stringify(requested)} climbs out of the workspace.`, "Give a path inside the workspace.");
|
|
72
|
+
}
|
|
73
|
+
return segments.length === 0 ? "." : segments.join("/");
|
|
74
|
+
}
|
|
75
|
+
function parseSave(requested) {
|
|
76
|
+
if (typeof requested !== "object" || requested === null || Array.isArray(requested)) {
|
|
77
|
+
refuse("Diagram save options are not usable.", `Expected an object, got ${typeof requested}.`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
directory: parseDirectory(Reflect.get(requested, "dir")),
|
|
81
|
+
basename: Reflect.get(requested, "basename"),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function slugify(value) {
|
|
85
|
+
return value
|
|
86
|
+
.normalize("NFKD")
|
|
87
|
+
.toLowerCase()
|
|
88
|
+
.replace(/[^a-z0-9]+/gu, "-")
|
|
89
|
+
.replace(/^-+/u, "")
|
|
90
|
+
.slice(0, MAX_BASENAME_LENGTH)
|
|
91
|
+
.replace(/-+$/u, "");
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A repository file needs a name someone chose, so regenerating the diagram lands on the same
|
|
95
|
+
* path. A temporary file falls back to the source hash, so an ad-hoc diagram needs no title.
|
|
96
|
+
*/
|
|
97
|
+
function parseBasename(requested, title, fallback) {
|
|
98
|
+
const chosen = typeof requested === "string" && requested.trim().length > 0 ? requested : title;
|
|
99
|
+
if (chosen === undefined) {
|
|
100
|
+
if (fallback !== undefined) {
|
|
101
|
+
return fallback;
|
|
102
|
+
}
|
|
103
|
+
refuse("A diagram saved into the repository needs a name.", "Neither `save.basename` nor `title` was given.", "Add a title so the file keeps the same name when the diagram is regenerated.");
|
|
104
|
+
}
|
|
105
|
+
const basename = slugify(chosen);
|
|
106
|
+
if (basename.length === 0) {
|
|
107
|
+
refuse("A saved diagram needs a usable name.", `${JSON.stringify(chosen)} has no letters or digits to build a file name from.`);
|
|
108
|
+
}
|
|
109
|
+
if (RESERVED_NAMES.has(basename)) {
|
|
110
|
+
refuse("A saved diagram needs a different name.", `${basename} is a reserved device name.`);
|
|
111
|
+
}
|
|
112
|
+
return basename;
|
|
113
|
+
}
|
|
114
|
+
/** Resolves nothing on disk, so a bad request is refused before any rendering starts. */
|
|
115
|
+
export function parseArtifactNames(request, identity) {
|
|
116
|
+
const save = request.save === undefined ? undefined : parseSave(request.save);
|
|
117
|
+
const fallback = save === undefined ? `diagram-${identity.hash.slice(0, 12)}` : undefined;
|
|
118
|
+
return {
|
|
119
|
+
directory: save?.directory,
|
|
120
|
+
basename: parseBasename(save?.basename, identity.title, fallback),
|
|
121
|
+
formats: parseFormats(request.formats),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** The workspace-relative paths a repository write would touch, for an approval prompt. */
|
|
125
|
+
export function workspacePaths(names) {
|
|
126
|
+
if (names.directory === undefined) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
const prefix = names.directory === "." ? "" : `${names.directory}/`;
|
|
130
|
+
return names.formats.map((format) => `${prefix}${names.basename}${EXTENSIONS[format]}`);
|
|
131
|
+
}
|
|
132
|
+
function contains(root, candidate) {
|
|
133
|
+
return candidate === root || candidate.startsWith(root.endsWith(sep) ? root : `${root}${sep}`);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Checks the deepest existing ancestor, before any directory is created: a `docs` symlink
|
|
137
|
+
* pointing outside would otherwise have `mkdir` build the rest of the path beyond it.
|
|
138
|
+
*/
|
|
139
|
+
async function assertInsideWorkspace(realRoot, target) {
|
|
140
|
+
if (!contains(realRoot, target)) {
|
|
141
|
+
refuse("Diagram save directory is outside the workspace.", `${target} is not inside ${realRoot}.`);
|
|
142
|
+
}
|
|
143
|
+
let probe = target;
|
|
144
|
+
for (;;) {
|
|
145
|
+
try {
|
|
146
|
+
const real = await realpath(probe);
|
|
147
|
+
if (!contains(realRoot, real)) {
|
|
148
|
+
refuse("Diagram save directory leaves the workspace.", `${probe} resolves to ${real}, outside ${realRoot}.`, "A symbolic link on that path points outside the workspace.");
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error.code !== "ENOENT") {
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
const parent = dirname(probe);
|
|
157
|
+
if (parent === probe) {
|
|
158
|
+
refuse("Diagram save directory is not usable.", `No part of ${target} exists.`);
|
|
159
|
+
}
|
|
160
|
+
probe = parent;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
let sessionStore;
|
|
165
|
+
/**
|
|
166
|
+
* `mkdtemp` creates the directory owner-only, so diagrams that quote repository content are not
|
|
167
|
+
* readable by other users of a shared machine.
|
|
168
|
+
*/
|
|
169
|
+
function sessionDirectory() {
|
|
170
|
+
sessionStore ??= mkdtemp(join(tmpdir(), "pi-diagram-store-"));
|
|
171
|
+
return sessionStore;
|
|
172
|
+
}
|
|
173
|
+
export async function parseArtifactTarget(cwd, names) {
|
|
174
|
+
if (names.directory === undefined) {
|
|
175
|
+
const directory = await sessionDirectory();
|
|
176
|
+
return { location: "temp", directory: directory, names, root: directory };
|
|
177
|
+
}
|
|
178
|
+
if (typeof cwd !== "string" || cwd.length === 0 || !isAbsolute(cwd)) {
|
|
179
|
+
refuse("Diagrams cannot be saved into a repository without a workspace directory.", `The host gave ${JSON.stringify(cwd)}.`);
|
|
180
|
+
}
|
|
181
|
+
const root = resolve(cwd);
|
|
182
|
+
let realRoot;
|
|
183
|
+
try {
|
|
184
|
+
realRoot = await realpath(root);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
refuse("The workspace directory cannot be read.", `${root} could not be resolved: ${error.message}.`);
|
|
188
|
+
}
|
|
189
|
+
const directory = resolve(realRoot, names.directory);
|
|
190
|
+
await assertInsideWorkspace(realRoot, directory);
|
|
191
|
+
return {
|
|
192
|
+
location: "workspace",
|
|
193
|
+
directory: directory,
|
|
194
|
+
names,
|
|
195
|
+
root: realRoot,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Writes each artifact through a temporary file and a rename, so a reader never sees a half
|
|
200
|
+
* written diagram and a failed render leaves the previous version in place.
|
|
201
|
+
*/
|
|
202
|
+
export async function writeArtifacts(target, contents) {
|
|
203
|
+
await mkdir(target.directory, { recursive: true });
|
|
204
|
+
if (target.location === "workspace") {
|
|
205
|
+
// The directory exists now, so this catches a link created between the check and the write.
|
|
206
|
+
await assertInsideWorkspace(target.root, target.directory);
|
|
207
|
+
}
|
|
208
|
+
const written = [];
|
|
209
|
+
for (const format of target.names.formats) {
|
|
210
|
+
const content = contents.get(format);
|
|
211
|
+
if (content === undefined) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const destination = join(target.directory, `${target.names.basename}${EXTENSIONS[format]}`);
|
|
215
|
+
await assertWritable(destination);
|
|
216
|
+
const temporary = join(target.directory, `.${target.names.basename}.${randomUUID()}.tmp`);
|
|
217
|
+
try {
|
|
218
|
+
await writeFile(temporary, content, {
|
|
219
|
+
...(typeof content === "string" ? { encoding: "utf8" } : {}),
|
|
220
|
+
mode: 0o644,
|
|
221
|
+
});
|
|
222
|
+
await rename(temporary, destination);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
await rm(temporary, { force: true });
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
written.push({
|
|
229
|
+
format,
|
|
230
|
+
location: target.location,
|
|
231
|
+
path: target.location === "workspace"
|
|
232
|
+
? relative(target.root, destination).split(sep).join("/")
|
|
233
|
+
: destination,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (target.location === "temp") {
|
|
237
|
+
await evictOldest(target.directory);
|
|
238
|
+
}
|
|
239
|
+
return written;
|
|
240
|
+
}
|
|
241
|
+
/** Regenerating a diagram replaces its own files, but never anything that is not a plain file. */
|
|
242
|
+
async function assertWritable(destination) {
|
|
243
|
+
try {
|
|
244
|
+
const existing = await lstat(destination);
|
|
245
|
+
if (!existing.isFile()) {
|
|
246
|
+
refuse("That diagram path cannot be written.", `${destination} already exists and is not a regular file.`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
if (error.code !== "ENOENT") {
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async function evictOldest(directory) {
|
|
256
|
+
const names = await readdir(directory);
|
|
257
|
+
if (names.length <= MAX_TEMP_FILES) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const aged = await Promise.all(names.map(async (name) => {
|
|
261
|
+
const path = join(directory, name);
|
|
262
|
+
try {
|
|
263
|
+
return { path, at: (await stat(path)).mtimeMs };
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return { path, at: Number.POSITIVE_INFINITY };
|
|
267
|
+
}
|
|
268
|
+
}));
|
|
269
|
+
aged.sort((left, right) => left.at - right.at);
|
|
270
|
+
for (const { path } of aged.slice(0, aged.length - MAX_TEMP_FILES)) {
|
|
271
|
+
await rm(path, { force: true, recursive: true });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=artifacts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"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"}
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface CacheKeyParts {
|
|
2
|
+
readonly source: string;
|
|
3
|
+
readonly language: "d2";
|
|
4
|
+
/** Which renderer ran. Two builds can report the same version from different paths. */
|
|
5
|
+
readonly binary: string;
|
|
6
|
+
/** So no output is served after a D2 upgrade. */
|
|
7
|
+
readonly version: string;
|
|
8
|
+
/**
|
|
9
|
+
* Carries the engine, theme, and spacing a profile chose, so a policy change cannot serve a
|
|
10
|
+
* picture drawn under the old one.
|
|
11
|
+
*/
|
|
12
|
+
readonly argv: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
export interface RenderCache {
|
|
15
|
+
read(key: string): Promise<string | undefined>;
|
|
16
|
+
write(key: string, value: string): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export declare function cacheKey(parts: CacheKeyParts): string;
|
|
19
|
+
/** Length-prefixed, so no two field lists can read as one another. */
|
|
20
|
+
export declare function cacheKeyOf(fields: readonly string[]): string;
|
|
21
|
+
/** Keeps nothing. For tests, and for callers that want every render to run. */
|
|
22
|
+
export declare const noCache: RenderCache;
|
|
23
|
+
export interface FileCacheOptions {
|
|
24
|
+
readonly directory?: string;
|
|
25
|
+
readonly maxBytes?: number;
|
|
26
|
+
readonly maxAgeMs?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Entries on disk, outside the project, shared between sessions. Every operation is best-effort: a
|
|
30
|
+
* cache that cannot be read or written must not fail a diagram.
|
|
31
|
+
*/
|
|
32
|
+
export declare class FileCache implements RenderCache {
|
|
33
|
+
private readonly directory;
|
|
34
|
+
private readonly maxBytes;
|
|
35
|
+
private readonly maxAgeMs;
|
|
36
|
+
constructor(options?: FileCacheOptions);
|
|
37
|
+
read(key: string): Promise<string | undefined>;
|
|
38
|
+
write(key: string, value: string): Promise<void>;
|
|
39
|
+
/** Drops entries past the age limit, then the least recently used until the size fits. */
|
|
40
|
+
private prune;
|
|
41
|
+
private pathFor;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;IACxB,uFAAuF;IACvF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,iDAAiD;IACjD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC/C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClD;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAErD;AAED,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAM5D;AAED,+EAA+E;AAC/E,eAAO,MAAM,OAAO,EAAE,WAGrB,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;GAGG;AACH,qBAAa,SAAU,YAAW,WAAW;IAC3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC,YAAY,OAAO,GAAE,gBAAqB,EAIzC;IAEK,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAWnD;IAEK,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYrD;IAED,0FAA0F;YAC5E,KAAK;IAkCnB,OAAO,CAAC,OAAO;CAGhB"}
|
package/dist/cache.js
ADDED
|
Binary file
|