@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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -0
  3. package/dist/artifacts.d.ts +59 -0
  4. package/dist/artifacts.d.ts.map +1 -0
  5. package/dist/artifacts.js +274 -0
  6. package/dist/artifacts.js.map +1 -0
  7. package/dist/cache.d.ts +43 -0
  8. package/dist/cache.d.ts.map +1 -0
  9. package/dist/cache.js +0 -0
  10. package/dist/cache.js.map +1 -0
  11. package/dist/d2/diagnostics.d.ts +25 -0
  12. package/dist/d2/diagnostics.d.ts.map +1 -0
  13. package/dist/d2/diagnostics.js +77 -0
  14. package/dist/d2/diagnostics.js.map +1 -0
  15. package/dist/d2/fonts.d.ts +23 -0
  16. package/dist/d2/fonts.d.ts.map +1 -0
  17. package/dist/d2/fonts.js +255 -0
  18. package/dist/d2/fonts.js.map +1 -0
  19. package/dist/d2/preflight.d.ts +20 -0
  20. package/dist/d2/preflight.d.ts.map +1 -0
  21. package/dist/d2/preflight.js +217 -0
  22. package/dist/d2/preflight.js.map +1 -0
  23. package/dist/d2/profiles.d.ts +34 -0
  24. package/dist/d2/profiles.d.ts.map +1 -0
  25. package/dist/d2/profiles.js +118 -0
  26. package/dist/d2/profiles.js.map +1 -0
  27. package/dist/d2/runner.d.ts +95 -0
  28. package/dist/d2/runner.d.ts.map +1 -0
  29. package/dist/d2/runner.js +350 -0
  30. package/dist/d2/runner.js.map +1 -0
  31. package/dist/display.d.ts +48 -0
  32. package/dist/display.d.ts.map +1 -0
  33. package/dist/display.js +120 -0
  34. package/dist/display.js.map +1 -0
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +5 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/normalize.d.ts +23 -0
  40. package/dist/normalize.d.ts.map +1 -0
  41. package/dist/normalize.js +83 -0
  42. package/dist/normalize.js.map +1 -0
  43. package/dist/process.d.ts +38 -0
  44. package/dist/process.d.ts.map +1 -0
  45. package/dist/process.js +87 -0
  46. package/dist/process.js.map +1 -0
  47. package/dist/raster.d.ts +40 -0
  48. package/dist/raster.d.ts.map +1 -0
  49. package/dist/raster.js +193 -0
  50. package/dist/raster.js.map +1 -0
  51. package/dist/render.d.ts +55 -0
  52. package/dist/render.d.ts.map +1 -0
  53. package/dist/render.js +229 -0
  54. package/dist/render.js.map +1 -0
  55. package/dist/tools.d.ts +58 -0
  56. package/dist/tools.d.ts.map +1 -0
  57. package/dist/tools.js +284 -0
  58. package/dist/tools.js.map +1 -0
  59. package/package.json +101 -0
  60. package/src/artifacts.ts +418 -0
  61. package/src/cache.ts +0 -0
  62. package/src/d2/diagnostics.ts +114 -0
  63. package/src/d2/fonts.ts +289 -0
  64. package/src/d2/preflight.ts +270 -0
  65. package/src/d2/profiles.ts +157 -0
  66. package/src/d2/runner.ts +513 -0
  67. package/src/display.ts +201 -0
  68. package/src/index.ts +5 -0
  69. package/src/normalize.ts +119 -0
  70. package/src/process.ts +134 -0
  71. package/src/raster.ts +258 -0
  72. package/src/render.ts +338 -0
  73. 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
+ [![CI](https://github.com/mcuste/pi-diagram/actions/workflows/ci.yml/badge.svg)](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
+ ![Request lifecycle](diagrams/request-lifecycle.svg)
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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"}
@@ -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