omegon 0.6.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/.gitattributes +3 -0
- package/AGENTS.md +16 -0
- package/LICENSE +15 -0
- package/README.md +289 -0
- package/bin/pi.mjs +30 -0
- package/extensions/00-secrets/index.ts +1126 -0
- package/extensions/01-auth/auth.ts +401 -0
- package/extensions/01-auth/index.ts +289 -0
- package/extensions/auto-compact.ts +42 -0
- package/extensions/bootstrap/deps.ts +291 -0
- package/extensions/bootstrap/index.ts +811 -0
- package/extensions/chronos/chronos.sh +487 -0
- package/extensions/chronos/index.ts +148 -0
- package/extensions/cleave/assessment.ts +754 -0
- package/extensions/cleave/bridge.ts +31 -0
- package/extensions/cleave/conflicts.ts +250 -0
- package/extensions/cleave/dispatcher.ts +808 -0
- package/extensions/cleave/guardrails.ts +426 -0
- package/extensions/cleave/index.ts +3121 -0
- package/extensions/cleave/lifecycle-emitter.ts +20 -0
- package/extensions/cleave/openspec.ts +811 -0
- package/extensions/cleave/planner.ts +260 -0
- package/extensions/cleave/review.ts +579 -0
- package/extensions/cleave/skills.ts +355 -0
- package/extensions/cleave/types.ts +261 -0
- package/extensions/cleave/workspace.ts +861 -0
- package/extensions/cleave/worktree.ts +243 -0
- package/extensions/core-renderers.ts +253 -0
- package/extensions/dashboard/context-gauge.ts +58 -0
- package/extensions/dashboard/file-watch.ts +14 -0
- package/extensions/dashboard/footer.ts +1145 -0
- package/extensions/dashboard/git.ts +185 -0
- package/extensions/dashboard/index.ts +478 -0
- package/extensions/dashboard/memory-audit.ts +34 -0
- package/extensions/dashboard/overlay-data.ts +705 -0
- package/extensions/dashboard/overlay.ts +365 -0
- package/extensions/dashboard/render-utils.ts +54 -0
- package/extensions/dashboard/types.ts +191 -0
- package/extensions/dashboard/uri-helper.ts +45 -0
- package/extensions/debug.ts +69 -0
- package/extensions/defaults.ts +282 -0
- package/extensions/design-tree/dashboard-state.ts +161 -0
- package/extensions/design-tree/design-card.ts +362 -0
- package/extensions/design-tree/index.ts +2130 -0
- package/extensions/design-tree/lifecycle-emitter.ts +41 -0
- package/extensions/design-tree/tree.ts +1607 -0
- package/extensions/design-tree/types.ts +163 -0
- package/extensions/distill.ts +127 -0
- package/extensions/effort/index.ts +395 -0
- package/extensions/effort/tiers.ts +146 -0
- package/extensions/effort/types.ts +105 -0
- package/extensions/lib/git-state.ts +227 -0
- package/extensions/lib/local-models.ts +157 -0
- package/extensions/lib/model-preferences.ts +51 -0
- package/extensions/lib/model-routing.ts +720 -0
- package/extensions/lib/operator-fallback.ts +205 -0
- package/extensions/lib/operator-profile.ts +360 -0
- package/extensions/lib/slash-command-bridge.ts +253 -0
- package/extensions/lib/typebox-helpers.ts +16 -0
- package/extensions/local-inference/index.ts +727 -0
- package/extensions/mcp-bridge/README.md +220 -0
- package/extensions/mcp-bridge/index.ts +951 -0
- package/extensions/mcp-bridge/lib.ts +365 -0
- package/extensions/mcp-bridge/mcp.json +3 -0
- package/extensions/mcp-bridge/package.json +11 -0
- package/extensions/model-budget.ts +752 -0
- package/extensions/offline-driver.ts +403 -0
- package/extensions/openspec/archive-gate.ts +164 -0
- package/extensions/openspec/branch-cleanup.ts +64 -0
- package/extensions/openspec/dashboard-state.ts +50 -0
- package/extensions/openspec/index.ts +1917 -0
- package/extensions/openspec/lifecycle-emitter.ts +65 -0
- package/extensions/openspec/lifecycle-files.ts +70 -0
- package/extensions/openspec/lifecycle.ts +50 -0
- package/extensions/openspec/reconcile.ts +187 -0
- package/extensions/openspec/spec.ts +1385 -0
- package/extensions/openspec/types.ts +98 -0
- package/extensions/project-memory/DESIGN-global-mind.md +198 -0
- package/extensions/project-memory/README.md +202 -0
- package/extensions/project-memory/api-types.ts +382 -0
- package/extensions/project-memory/compaction-policy.ts +29 -0
- package/extensions/project-memory/core.ts +164 -0
- package/extensions/project-memory/embeddings.ts +230 -0
- package/extensions/project-memory/extraction-v2.ts +861 -0
- package/extensions/project-memory/factstore.ts +2177 -0
- package/extensions/project-memory/index.ts +3459 -0
- package/extensions/project-memory/injection-metrics.ts +91 -0
- package/extensions/project-memory/jsonl-io.ts +12 -0
- package/extensions/project-memory/lifecycle.ts +331 -0
- package/extensions/project-memory/migration.ts +293 -0
- package/extensions/project-memory/package.json +9 -0
- package/extensions/project-memory/sci-renderers.ts +7 -0
- package/extensions/project-memory/template.ts +103 -0
- package/extensions/project-memory/triggers.ts +52 -0
- package/extensions/project-memory/types.ts +102 -0
- package/extensions/render/composition/fonts/Inter-Bold.ttf +0 -0
- package/extensions/render/composition/fonts/Inter-Regular.ttf +0 -0
- package/extensions/render/composition/fonts/Tomorrow-Bold.ttf +0 -0
- package/extensions/render/composition/fonts/Tomorrow-Regular.ttf +0 -0
- package/extensions/render/composition/package-lock.json +534 -0
- package/extensions/render/composition/package.json +22 -0
- package/extensions/render/composition/render.mjs +246 -0
- package/extensions/render/composition/test-comp.tsx +87 -0
- package/extensions/render/composition/types.ts +24 -0
- package/extensions/render/excalidraw/UPSTREAM.md +81 -0
- package/extensions/render/excalidraw/elements.ts +764 -0
- package/extensions/render/excalidraw/index.ts +66 -0
- package/extensions/render/excalidraw/types.ts +223 -0
- package/extensions/render/excalidraw-renderer/pyproject.toml +8 -0
- package/extensions/render/excalidraw-renderer/render_excalidraw.py +182 -0
- package/extensions/render/excalidraw-renderer/render_template.html +59 -0
- package/extensions/render/index.ts +830 -0
- package/extensions/render/native-diagrams/index.ts +57 -0
- package/extensions/render/native-diagrams/motifs.ts +542 -0
- package/extensions/render/native-diagrams/raster.ts +8 -0
- package/extensions/render/native-diagrams/scene.ts +75 -0
- package/extensions/render/native-diagrams/spec.ts +204 -0
- package/extensions/render/native-diagrams/svg.ts +116 -0
- package/extensions/sci-ui.ts +304 -0
- package/extensions/session-log.ts +174 -0
- package/extensions/shared-state.ts +146 -0
- package/extensions/spinner-verbs.ts +91 -0
- package/extensions/style.ts +281 -0
- package/extensions/terminal-title.ts +191 -0
- package/extensions/tool-profile/index.ts +291 -0
- package/extensions/tool-profile/profiles.ts +290 -0
- package/extensions/types.d.ts +9 -0
- package/extensions/vault/index.ts +185 -0
- package/extensions/version-check.ts +90 -0
- package/extensions/view/index.ts +859 -0
- package/extensions/view/uri-resolver.ts +148 -0
- package/extensions/web-search/index.ts +182 -0
- package/extensions/web-search/providers.ts +121 -0
- package/extensions/web-ui/index.ts +110 -0
- package/extensions/web-ui/server.ts +265 -0
- package/extensions/web-ui/state.ts +462 -0
- package/extensions/web-ui/static/index.html +145 -0
- package/extensions/web-ui/types.ts +284 -0
- package/package.json +76 -0
- package/prompts/init.md +75 -0
- package/prompts/new-repo.md +54 -0
- package/prompts/oci-login.md +56 -0
- package/prompts/status.md +50 -0
- package/settings.json +4 -0
- package/skills/cleave/SKILL.md +218 -0
- package/skills/git/SKILL.md +209 -0
- package/skills/git/_reference/ci-validation.md +204 -0
- package/skills/oci/SKILL.md +338 -0
- package/skills/openspec/SKILL.md +346 -0
- package/skills/pi-extensions/SKILL.md +191 -0
- package/skills/pi-tui/SKILL.md +517 -0
- package/skills/python/SKILL.md +189 -0
- package/skills/rust/SKILL.md +268 -0
- package/skills/security/SKILL.md +206 -0
- package/skills/style/SKILL.md +264 -0
- package/skills/typescript/SKILL.md +225 -0
- package/skills/vault/SKILL.md +102 -0
- package/themes/alpharius-legacy.json +85 -0
- package/themes/alpharius.conf +59 -0
- package/themes/alpharius.json +88 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Excalidraw element factory — programmatic .excalidraw file generation.
|
|
3
|
+
*
|
|
4
|
+
* Usage in LLM tool calls:
|
|
5
|
+
* import { rect, diamond, ellipse, dot, text, arrow, line,
|
|
6
|
+
* bindArrow, createDocument, validateDocument,
|
|
7
|
+
* fanOut, timeline, grid,
|
|
8
|
+
* SEMANTIC_COLORS, TEXT_COLORS } from "./excalidraw/index.ts";
|
|
9
|
+
*
|
|
10
|
+
* See UPSTREAM.md for vendoring provenance and sync instructions.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
// Shape factories
|
|
15
|
+
rect,
|
|
16
|
+
diamond,
|
|
17
|
+
ellipse,
|
|
18
|
+
dot,
|
|
19
|
+
// Text
|
|
20
|
+
text,
|
|
21
|
+
// Connectors
|
|
22
|
+
arrow,
|
|
23
|
+
line,
|
|
24
|
+
// Binding
|
|
25
|
+
bindArrow,
|
|
26
|
+
// Document
|
|
27
|
+
createDocument,
|
|
28
|
+
validateDocument,
|
|
29
|
+
resetIndexCounter,
|
|
30
|
+
// Layout helpers
|
|
31
|
+
fanOut,
|
|
32
|
+
timeline,
|
|
33
|
+
grid,
|
|
34
|
+
} from "./elements.ts";
|
|
35
|
+
|
|
36
|
+
export type {
|
|
37
|
+
ExcalidrawElement,
|
|
38
|
+
ExcalidrawFile,
|
|
39
|
+
RectangleElement,
|
|
40
|
+
DiamondElement,
|
|
41
|
+
EllipseElement,
|
|
42
|
+
TextElement,
|
|
43
|
+
ArrowElement,
|
|
44
|
+
LineElement,
|
|
45
|
+
SemanticPurpose,
|
|
46
|
+
TextLevel,
|
|
47
|
+
Point,
|
|
48
|
+
ShapeOptions,
|
|
49
|
+
TextOptions,
|
|
50
|
+
ArrowOptions,
|
|
51
|
+
} from "./elements.ts";
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
SEMANTIC_COLORS,
|
|
55
|
+
TEXT_COLORS,
|
|
56
|
+
FONT_FAMILIES,
|
|
57
|
+
DEFAULT_ELEMENT_STYLE,
|
|
58
|
+
DEFAULT_APP_STATE,
|
|
59
|
+
} from "./types.ts";
|
|
60
|
+
|
|
61
|
+
export type {
|
|
62
|
+
ColorPair,
|
|
63
|
+
AppState,
|
|
64
|
+
ArrowBinding,
|
|
65
|
+
BoundElement,
|
|
66
|
+
} from "./types.ts";
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Excalidraw Element Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Vendored from @swiftlysingh/excalidraw-cli@1.1.0 (src/types/excalidraw.ts)
|
|
5
|
+
* with additions for semantic palette system. See UPSTREAM.md for sync guide.
|
|
6
|
+
*
|
|
7
|
+
* Based on Excalidraw JSON schema v2 (excalidraw.com)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Primitives
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export type ExcalidrawElementType =
|
|
15
|
+
| "rectangle"
|
|
16
|
+
| "diamond"
|
|
17
|
+
| "ellipse"
|
|
18
|
+
| "text"
|
|
19
|
+
| "arrow"
|
|
20
|
+
| "line"
|
|
21
|
+
| "freedraw";
|
|
22
|
+
|
|
23
|
+
export type FillStyle = "solid" | "hachure" | "cross-hatch";
|
|
24
|
+
export type StrokeStyle = "solid" | "dashed" | "dotted";
|
|
25
|
+
export type Arrowhead = "arrow" | "bar" | "dot" | "triangle" | null;
|
|
26
|
+
export type TextAlign = "left" | "center" | "right";
|
|
27
|
+
export type VerticalAlign = "top" | "middle" | "bottom";
|
|
28
|
+
|
|
29
|
+
export interface Roundness {
|
|
30
|
+
type: 1 | 2 | 3; // 1=legacy, 2=proportional, 3=adaptive
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface BoundElement {
|
|
34
|
+
id: string;
|
|
35
|
+
type: "arrow" | "text";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ArrowBinding {
|
|
39
|
+
elementId: string;
|
|
40
|
+
mode: "orbit" | "point";
|
|
41
|
+
fixedPoint: [number, number]; // Normalized [0-1] coordinates
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Element types
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
export interface ElementBase {
|
|
49
|
+
id: string;
|
|
50
|
+
type: ExcalidrawElementType;
|
|
51
|
+
x: number;
|
|
52
|
+
y: number;
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
angle: number;
|
|
56
|
+
strokeColor: string;
|
|
57
|
+
backgroundColor: string;
|
|
58
|
+
fillStyle: FillStyle;
|
|
59
|
+
strokeWidth: number;
|
|
60
|
+
strokeStyle: StrokeStyle;
|
|
61
|
+
roughness: number;
|
|
62
|
+
opacity: number;
|
|
63
|
+
groupIds: string[];
|
|
64
|
+
frameId: string | null;
|
|
65
|
+
index: string;
|
|
66
|
+
roundness: Roundness | null;
|
|
67
|
+
seed: number;
|
|
68
|
+
version: number;
|
|
69
|
+
versionNonce: number;
|
|
70
|
+
isDeleted: boolean;
|
|
71
|
+
boundElements: BoundElement[] | null;
|
|
72
|
+
updated: number;
|
|
73
|
+
link: string | null;
|
|
74
|
+
locked: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface RectangleElement extends ElementBase {
|
|
78
|
+
type: "rectangle";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface DiamondElement extends ElementBase {
|
|
82
|
+
type: "diamond";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface EllipseElement extends ElementBase {
|
|
86
|
+
type: "ellipse";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface TextElement extends ElementBase {
|
|
90
|
+
type: "text";
|
|
91
|
+
text: string;
|
|
92
|
+
fontSize: number;
|
|
93
|
+
fontFamily: number; // 1=Virgil, 2=Helvetica, 3=Cascadia, 5=Excalifont
|
|
94
|
+
textAlign: TextAlign;
|
|
95
|
+
verticalAlign: VerticalAlign;
|
|
96
|
+
containerId: string | null;
|
|
97
|
+
originalText: string;
|
|
98
|
+
autoResize: boolean;
|
|
99
|
+
lineHeight: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ArrowElement extends ElementBase {
|
|
103
|
+
type: "arrow";
|
|
104
|
+
points: Array<[number, number]>;
|
|
105
|
+
lastCommittedPoint: [number, number] | null;
|
|
106
|
+
startBinding: ArrowBinding | null;
|
|
107
|
+
endBinding: ArrowBinding | null;
|
|
108
|
+
startArrowhead: Arrowhead;
|
|
109
|
+
endArrowhead: Arrowhead;
|
|
110
|
+
elbowed: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface LineElement extends ElementBase {
|
|
114
|
+
type: "line";
|
|
115
|
+
points: Array<[number, number]>;
|
|
116
|
+
lastCommittedPoint: [number, number] | null;
|
|
117
|
+
startBinding: ArrowBinding | null;
|
|
118
|
+
endBinding: ArrowBinding | null;
|
|
119
|
+
startArrowhead: Arrowhead;
|
|
120
|
+
endArrowhead: Arrowhead;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type ExcalidrawElement =
|
|
124
|
+
| RectangleElement
|
|
125
|
+
| DiamondElement
|
|
126
|
+
| EllipseElement
|
|
127
|
+
| TextElement
|
|
128
|
+
| ArrowElement
|
|
129
|
+
| LineElement;
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// File structure
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
export interface AppState {
|
|
136
|
+
gridSize: number;
|
|
137
|
+
gridStep: number;
|
|
138
|
+
gridModeEnabled: boolean;
|
|
139
|
+
viewBackgroundColor: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface ExcalidrawFile {
|
|
143
|
+
type: "excalidraw";
|
|
144
|
+
version: 2;
|
|
145
|
+
source: string;
|
|
146
|
+
elements: ExcalidrawElement[];
|
|
147
|
+
appState: AppState;
|
|
148
|
+
files: Record<string, unknown>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Defaults
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
export const DEFAULT_APP_STATE: AppState = {
|
|
156
|
+
gridSize: 20,
|
|
157
|
+
gridStep: 5,
|
|
158
|
+
gridModeEnabled: false,
|
|
159
|
+
viewBackgroundColor: "#06080e",
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const DEFAULT_ELEMENT_STYLE = {
|
|
163
|
+
strokeColor: "#1e1e1e",
|
|
164
|
+
backgroundColor: "transparent",
|
|
165
|
+
fillStyle: "solid" as FillStyle,
|
|
166
|
+
strokeWidth: 2,
|
|
167
|
+
strokeStyle: "solid" as StrokeStyle,
|
|
168
|
+
roughness: 0, // Clean/modern default (upstream uses 1)
|
|
169
|
+
opacity: 100,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const FONT_FAMILIES = {
|
|
173
|
+
Virgil: 1,
|
|
174
|
+
Helvetica: 2,
|
|
175
|
+
Cascadia: 3,
|
|
176
|
+
Excalifont: 5,
|
|
177
|
+
} as const;
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Semantic palette (omegon addition, not from upstream)
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
export type SemanticPurpose =
|
|
184
|
+
| "primary"
|
|
185
|
+
| "secondary"
|
|
186
|
+
| "tertiary"
|
|
187
|
+
| "start"
|
|
188
|
+
| "end"
|
|
189
|
+
| "warning"
|
|
190
|
+
| "decision"
|
|
191
|
+
| "ai"
|
|
192
|
+
| "inactive"
|
|
193
|
+
| "error"
|
|
194
|
+
| "evidence";
|
|
195
|
+
|
|
196
|
+
export interface ColorPair {
|
|
197
|
+
fill: string;
|
|
198
|
+
stroke: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export const SEMANTIC_COLORS: Record<SemanticPurpose, ColorPair> = {
|
|
202
|
+
primary: { fill: "#1a4a6e", stroke: "#2ab4c8" },
|
|
203
|
+
secondary: { fill: "#1a3a5a", stroke: "#1a8898" },
|
|
204
|
+
tertiary: { fill: "#0e2a40", stroke: "#344858" },
|
|
205
|
+
start: { fill: "#0e2e20", stroke: "#1ab878" },
|
|
206
|
+
end: { fill: "#2e2010", stroke: "#b89020" },
|
|
207
|
+
warning: { fill: "#2a1808", stroke: "#c86418" },
|
|
208
|
+
decision: { fill: "#2a1010", stroke: "#c83030" },
|
|
209
|
+
ai: { fill: "#1a1040", stroke: "#6060c0" },
|
|
210
|
+
inactive: { fill: "#0e1622", stroke: "#344858" },
|
|
211
|
+
error: { fill: "#2e0e0e", stroke: "#c83030" },
|
|
212
|
+
evidence: { fill: "#06080e", stroke: "#1a3448" },
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export const TEXT_COLORS = {
|
|
216
|
+
title: "#2ab4c8",
|
|
217
|
+
subtitle: "#1a8898",
|
|
218
|
+
body: "#607888",
|
|
219
|
+
onLight: "#c4d8e4",
|
|
220
|
+
onDark: "#c4d8e4",
|
|
221
|
+
} as const;
|
|
222
|
+
|
|
223
|
+
export type TextLevel = keyof typeof TEXT_COLORS;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Render Excalidraw JSON to PNG using Playwright + headless Chromium.
|
|
2
|
+
|
|
3
|
+
Vendored from coleam00/excalidraw-diagram-skill (references/render_excalidraw.py)
|
|
4
|
+
with minor changes: pinned esm.sh version in template, improved error messages.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
cd skills/visualize/references/excalidraw
|
|
8
|
+
uv run python render_excalidraw.py <path-to-file.excalidraw> [--output path.png] [--scale 2]
|
|
9
|
+
|
|
10
|
+
First-time setup:
|
|
11
|
+
cd skills/visualize/references/excalidraw
|
|
12
|
+
uv sync
|
|
13
|
+
uv run playwright install chromium
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate_excalidraw(data: dict) -> list[str]:
|
|
25
|
+
"""Validate Excalidraw JSON structure. Returns list of errors (empty = valid)."""
|
|
26
|
+
errors: list[str] = []
|
|
27
|
+
|
|
28
|
+
if data.get("type") != "excalidraw":
|
|
29
|
+
errors.append(f"Expected type 'excalidraw', got '{data.get('type')}'")
|
|
30
|
+
|
|
31
|
+
if "elements" not in data:
|
|
32
|
+
errors.append("Missing 'elements' array")
|
|
33
|
+
elif not isinstance(data["elements"], list):
|
|
34
|
+
errors.append("'elements' must be an array")
|
|
35
|
+
elif len(data["elements"]) == 0:
|
|
36
|
+
errors.append("'elements' array is empty — nothing to render")
|
|
37
|
+
|
|
38
|
+
return errors
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def compute_bounding_box(elements: list[dict]) -> tuple[float, float, float, float]:
|
|
42
|
+
"""Compute bounding box (min_x, min_y, max_x, max_y) across all elements."""
|
|
43
|
+
min_x = float("inf")
|
|
44
|
+
min_y = float("inf")
|
|
45
|
+
max_x = float("-inf")
|
|
46
|
+
max_y = float("-inf")
|
|
47
|
+
|
|
48
|
+
for el in elements:
|
|
49
|
+
if el.get("isDeleted"):
|
|
50
|
+
continue
|
|
51
|
+
x = el.get("x", 0)
|
|
52
|
+
y = el.get("y", 0)
|
|
53
|
+
w = el.get("width", 0)
|
|
54
|
+
h = el.get("height", 0)
|
|
55
|
+
|
|
56
|
+
if el.get("type") in ("arrow", "line") and "points" in el:
|
|
57
|
+
for px, py in el["points"]:
|
|
58
|
+
min_x = min(min_x, x + px)
|
|
59
|
+
min_y = min(min_y, y + py)
|
|
60
|
+
max_x = max(max_x, x + px)
|
|
61
|
+
max_y = max(max_y, y + py)
|
|
62
|
+
else:
|
|
63
|
+
min_x = min(min_x, x)
|
|
64
|
+
min_y = min(min_y, y)
|
|
65
|
+
max_x = max(max_x, x + abs(w))
|
|
66
|
+
max_y = max(max_y, y + abs(h))
|
|
67
|
+
|
|
68
|
+
if min_x == float("inf"):
|
|
69
|
+
return (0, 0, 800, 600)
|
|
70
|
+
|
|
71
|
+
return (min_x, min_y, max_x, max_y)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def render(
|
|
75
|
+
excalidraw_path: Path,
|
|
76
|
+
output_path: Path | None = None,
|
|
77
|
+
scale: int = 2,
|
|
78
|
+
max_width: int = 1920,
|
|
79
|
+
) -> Path:
|
|
80
|
+
"""Render an .excalidraw file to PNG. Returns the output PNG path."""
|
|
81
|
+
try:
|
|
82
|
+
from playwright.sync_api import sync_playwright
|
|
83
|
+
except ImportError:
|
|
84
|
+
print("ERROR: playwright not installed.", file=sys.stderr)
|
|
85
|
+
print("Run: cd skills/visualize/references/excalidraw && uv sync && uv run playwright install chromium", file=sys.stderr)
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
raw = excalidraw_path.read_text(encoding="utf-8")
|
|
89
|
+
try:
|
|
90
|
+
data = json.loads(raw)
|
|
91
|
+
except json.JSONDecodeError as e:
|
|
92
|
+
print(f"ERROR: Invalid JSON in {excalidraw_path}: {e}", file=sys.stderr)
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
|
|
95
|
+
errors = validate_excalidraw(data)
|
|
96
|
+
if errors:
|
|
97
|
+
print(f"ERROR: Invalid Excalidraw file:", file=sys.stderr)
|
|
98
|
+
for err in errors:
|
|
99
|
+
print(f" - {err}", file=sys.stderr)
|
|
100
|
+
sys.exit(1)
|
|
101
|
+
|
|
102
|
+
elements = [e for e in data["elements"] if not e.get("isDeleted")]
|
|
103
|
+
min_x, min_y, max_x, max_y = compute_bounding_box(elements)
|
|
104
|
+
padding = 80
|
|
105
|
+
diagram_w = max_x - min_x + padding * 2
|
|
106
|
+
diagram_h = max_y - min_y + padding * 2
|
|
107
|
+
|
|
108
|
+
vp_width = min(int(diagram_w), max_width)
|
|
109
|
+
vp_height = max(int(diagram_h), 600)
|
|
110
|
+
|
|
111
|
+
if output_path is None:
|
|
112
|
+
output_path = excalidraw_path.with_suffix(".png")
|
|
113
|
+
|
|
114
|
+
template_path = Path(__file__).parent / "render_template.html"
|
|
115
|
+
if not template_path.exists():
|
|
116
|
+
print(f"ERROR: Template not found at {template_path}", file=sys.stderr)
|
|
117
|
+
sys.exit(1)
|
|
118
|
+
|
|
119
|
+
template_url = template_path.as_uri()
|
|
120
|
+
|
|
121
|
+
with sync_playwright() as p:
|
|
122
|
+
try:
|
|
123
|
+
browser = p.chromium.launch(headless=True)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
if "Executable doesn't exist" in str(e) or "browserType.launch" in str(e):
|
|
126
|
+
print("ERROR: Chromium not installed for Playwright.", file=sys.stderr)
|
|
127
|
+
print("Run: cd skills/visualize/references/excalidraw && uv run playwright install chromium", file=sys.stderr)
|
|
128
|
+
sys.exit(1)
|
|
129
|
+
raise
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
page = browser.new_page(
|
|
133
|
+
viewport={"width": vp_width, "height": vp_height},
|
|
134
|
+
device_scale_factor=scale,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
page.goto(template_url)
|
|
138
|
+
page.wait_for_function("window.__moduleReady === true", timeout=30000)
|
|
139
|
+
|
|
140
|
+
# Use Playwright argument passing instead of string interpolation
|
|
141
|
+
# to avoid JS injection issues with special characters in JSON values.
|
|
142
|
+
result = page.evaluate("(data) => window.renderDiagram(data)", data)
|
|
143
|
+
|
|
144
|
+
if not result or not result.get("success"):
|
|
145
|
+
error_msg = result.get("error", "Unknown render error") if result else "renderDiagram returned null"
|
|
146
|
+
print(f"ERROR: Render failed: {error_msg}", file=sys.stderr)
|
|
147
|
+
sys.exit(1)
|
|
148
|
+
|
|
149
|
+
# Wait for render to complete (exportToSvg is async, renderDiagram
|
|
150
|
+
# sets __renderComplete after SVG is appended to DOM).
|
|
151
|
+
page.wait_for_function("window.__renderComplete === true", timeout=15000)
|
|
152
|
+
|
|
153
|
+
svg_el = page.query_selector("#root svg")
|
|
154
|
+
if svg_el is None:
|
|
155
|
+
print("ERROR: No SVG element found after render.", file=sys.stderr)
|
|
156
|
+
sys.exit(1)
|
|
157
|
+
|
|
158
|
+
svg_el.screenshot(path=str(output_path))
|
|
159
|
+
finally:
|
|
160
|
+
browser.close()
|
|
161
|
+
|
|
162
|
+
return output_path
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def main() -> None:
|
|
166
|
+
parser = argparse.ArgumentParser(description="Render Excalidraw JSON to PNG")
|
|
167
|
+
parser.add_argument("input", type=Path, help="Path to .excalidraw JSON file")
|
|
168
|
+
parser.add_argument("--output", "-o", type=Path, default=None, help="Output PNG path")
|
|
169
|
+
parser.add_argument("--scale", "-s", type=int, default=2, help="Device scale factor (default: 2)")
|
|
170
|
+
parser.add_argument("--width", "-w", type=int, default=1920, help="Max viewport width (default: 1920)")
|
|
171
|
+
args = parser.parse_args()
|
|
172
|
+
|
|
173
|
+
if not args.input.exists():
|
|
174
|
+
print(f"ERROR: File not found: {args.input}", file=sys.stderr)
|
|
175
|
+
sys.exit(1)
|
|
176
|
+
|
|
177
|
+
png_path = render(args.input, args.output, args.scale, args.width)
|
|
178
|
+
print(str(png_path))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
main()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<style>
|
|
6
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
7
|
+
body { background: #ffffff; overflow: hidden; }
|
|
8
|
+
#root { display: inline-block; }
|
|
9
|
+
#root svg { display: block; }
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
|
|
15
|
+
<!--
|
|
16
|
+
Pin @excalidraw/excalidraw to a specific version to avoid CDN updates breaking rendering.
|
|
17
|
+
Update this version intentionally when syncing with upstream (see UPSTREAM.md).
|
|
18
|
+
Current pin: 0.18.0 (pinned 2026-03-03)
|
|
19
|
+
-->
|
|
20
|
+
<script type="module">
|
|
21
|
+
import { exportToSvg } from "https://esm.sh/@excalidraw/excalidraw@0.18.0?bundle";
|
|
22
|
+
|
|
23
|
+
window.renderDiagram = async function(jsonData) {
|
|
24
|
+
try {
|
|
25
|
+
const data = typeof jsonData === "string" ? JSON.parse(jsonData) : jsonData;
|
|
26
|
+
const elements = data.elements || [];
|
|
27
|
+
const appState = data.appState || {};
|
|
28
|
+
const files = data.files || {};
|
|
29
|
+
|
|
30
|
+
appState.viewBackgroundColor = appState.viewBackgroundColor || "#ffffff";
|
|
31
|
+
appState.exportWithDarkMode = false;
|
|
32
|
+
|
|
33
|
+
const svg = await exportToSvg({
|
|
34
|
+
elements: elements,
|
|
35
|
+
appState: {
|
|
36
|
+
...appState,
|
|
37
|
+
exportBackground: true,
|
|
38
|
+
},
|
|
39
|
+
files: files,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const root = document.getElementById("root");
|
|
43
|
+
root.innerHTML = "";
|
|
44
|
+
root.appendChild(svg);
|
|
45
|
+
|
|
46
|
+
window.__renderComplete = true;
|
|
47
|
+
window.__renderError = null;
|
|
48
|
+
return { success: true, width: svg.getAttribute("width"), height: svg.getAttribute("height") };
|
|
49
|
+
} catch (err) {
|
|
50
|
+
window.__renderComplete = true;
|
|
51
|
+
window.__renderError = err.message;
|
|
52
|
+
return { success: false, error: err.message };
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
window.__moduleReady = true;
|
|
57
|
+
</script>
|
|
58
|
+
</body>
|
|
59
|
+
</html>
|