@tuongaz/seeflow 0.1.64 → 0.1.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/web/assets/{index-BAEA18IR.js → index-DL6aaddE.js} +985 -985
- package/dist/web/assets/{index.es-DZEdTXNJ.js → index.es-CVm3MRo3.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-DT1Li8zz.js → jspdf.es.min-C06OvDJX.js} +3 -3
- package/dist/web/index.html +1 -1
- package/examples/ecommerce-platform/flow.json +28 -30
- package/examples/order-pipeline/flow.json +16 -16
- package/package.json +1 -1
- package/src/api.ts +19 -23
- package/src/cli-e2e.ts +6 -7
- package/src/cli-manifest.ts +3 -5
- package/src/cli.ts +26 -0
- package/src/diagram.ts +8 -5
- package/src/layout.ts +22 -26
- package/src/mcp.ts +2 -2
- package/src/merge.ts +4 -3
- package/src/node-files.ts +1 -1
- package/src/operations.ts +68 -50
- package/src/runtime.ts +37 -0
- package/src/schema-catalog.ts +23 -11
- package/src/schema.ts +147 -271
- package/src/server.ts +1 -1
- package/src/status-runner.ts +2 -1
- package/src/watcher.ts +7 -7
package/src/schema.ts
CHANGED
|
@@ -5,11 +5,8 @@ const PositionSchema = z.object({
|
|
|
5
5
|
y: z.number(),
|
|
6
6
|
});
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// US-008 cut resetAction the same way, no schema in this file uses HttpAction
|
|
11
|
-
// anymore. `HttpMethodSchema` is still used for the optional `method` field on
|
|
12
|
-
// connectors (documentation metadata).
|
|
8
|
+
// `HttpMethodSchema` is documentation metadata on connectors. No node schema
|
|
9
|
+
// uses it — PlayAction/StatusAction are script-based.
|
|
13
10
|
const HttpMethodSchema = z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
|
|
14
11
|
|
|
15
12
|
// Curated palette tokens. Stored on disk as readable names; the frontend maps
|
|
@@ -26,7 +23,8 @@ export const ColorTokenSchema = z.enum([
|
|
|
26
23
|
]);
|
|
27
24
|
|
|
28
25
|
// Visual fields shared by every node type (functional + decorative). All
|
|
29
|
-
// optional —
|
|
26
|
+
// optional — every visual must work without per-field opinions. Live on
|
|
27
|
+
// resolved nodes; the disk-side flow.json strips them into style.json.
|
|
30
28
|
const NodeVisualBaseShape = {
|
|
31
29
|
width: z.number().positive().optional(),
|
|
32
30
|
height: z.number().positive().optional(),
|
|
@@ -39,19 +37,20 @@ const NodeVisualBaseShape = {
|
|
|
39
37
|
cornerRadius: z.number().min(0).optional(),
|
|
40
38
|
};
|
|
41
39
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// spread NodeVisualBaseShape.
|
|
48
|
-
const NodeDescriptionBaseShape = {
|
|
40
|
+
// Semantic-data fields shared by every node type. `name` is optional —
|
|
41
|
+
// every visual works without a label. `icon` is decorative-by-default; the
|
|
42
|
+
// `type:'icon'` variant overrides it to required.
|
|
43
|
+
const NodeSemanticBaseShape = {
|
|
44
|
+
name: z.string().optional(),
|
|
49
45
|
description: z.string().optional(),
|
|
50
46
|
detail: z.string().optional(),
|
|
47
|
+
// Decorative header glyph. Lucide icon name (kebab-case) resolved by the
|
|
48
|
+
// canvas <Icon> primitive; falls back to a placeholder when unknown.
|
|
49
|
+
icon: z.string().optional(),
|
|
51
50
|
};
|
|
52
51
|
|
|
53
|
-
//
|
|
54
|
-
//
|
|
52
|
+
// Relative-path safety refine (textual). Mirrors the same rule used for
|
|
53
|
+
// image-node and html-node script paths. Realpath verification is layered on
|
|
55
54
|
// top by the proxy/status-runner before any spawn (symlink-escape defense).
|
|
56
55
|
const isCleanRelativePath = (s: string): boolean => {
|
|
57
56
|
if (s.length === 0) return false;
|
|
@@ -81,11 +80,7 @@ const ScriptActionSchema = z.object({
|
|
|
81
80
|
|
|
82
81
|
export const PlayActionSchema = ScriptActionSchema;
|
|
83
82
|
|
|
84
|
-
//
|
|
85
|
-
// script (interpreter + args + scriptPath + optional input/timeoutMs) but
|
|
86
|
-
// invoked from the /reset endpoint. The studio kills every live play and
|
|
87
|
-
// status script for the demo before running this script, so the running app
|
|
88
|
-
// sees a clean baseline when wiping its state.
|
|
83
|
+
// resetAction is a one-shot script with the same shape as a play script.
|
|
89
84
|
export const ResetActionSchema = ScriptActionSchema;
|
|
90
85
|
|
|
91
86
|
// Long-running status script. Same spawn shape as ScriptAction (interpreter +
|
|
@@ -117,54 +112,23 @@ export const StateSourceSchema = z.discriminatedUnion('kind', [
|
|
|
117
112
|
z.object({ kind: z.literal('event') }),
|
|
118
113
|
]);
|
|
119
114
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// Decorative header glyph. Lucide icon name (kebab-case) resolved by the
|
|
127
|
-
// canvas <Icon> primitive; falls back to a placeholder when unknown.
|
|
128
|
-
icon: z.string().optional(),
|
|
129
|
-
...NodeVisualBaseShape,
|
|
130
|
-
...NodeDescriptionBaseShape,
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
const PlayNodeDataSchema = NodeDataBaseSchema.extend({
|
|
134
|
-
playAction: PlayActionSchema,
|
|
135
|
-
statusAction: StatusActionSchema.optional(),
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const StateNodeDataSchema = NodeDataBaseSchema.extend({
|
|
115
|
+
// Capabilities — any subset of these makes a node Playable / Stateful. All
|
|
116
|
+
// optional, valid on every node type. A node is Playable iff `playAction` is
|
|
117
|
+
// set; Stateful iff `statusAction` is set; Both iff both. `stateSource` is
|
|
118
|
+
// informational metadata that pairs with statusAction. `handlerModule` is
|
|
119
|
+
// reserved for a future skills runtime and is schema-only at v1.
|
|
120
|
+
const NodeCapabilitiesShape = {
|
|
139
121
|
playAction: PlayActionSchema.optional(),
|
|
140
122
|
statusAction: StatusActionSchema.optional(),
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const NodeBaseShape = {
|
|
144
|
-
id: z.string().min(1),
|
|
145
|
-
position: PositionSchema,
|
|
123
|
+
stateSource: StateSourceSchema.optional(),
|
|
124
|
+
handlerModule: z.string().optional(),
|
|
146
125
|
};
|
|
147
126
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const StateNodeSchema = z.object({
|
|
155
|
-
...NodeBaseShape,
|
|
156
|
-
type: z.literal('stateNode'),
|
|
157
|
-
data: StateNodeDataSchema,
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// Decorative annotation node — rectangle / ellipse / sticky. No semantic
|
|
161
|
-
// payload (no kind/stateSource/playAction); reuses NodeVisualBaseShape so
|
|
162
|
-
// users can theme it the same way as functional nodes.
|
|
163
|
-
// US-009 added `database` as the first illustrative shape (cylinder rendered
|
|
164
|
-
// via inline SVG inside shape-node.tsx). Illustrative shapes share the same
|
|
165
|
-
// shapeNode wrapper and color/border fields but own their own visuals via a
|
|
166
|
-
// per-shape component under `apps/web/src/components/nodes/shapes/`.
|
|
167
|
-
export const ShapeKindSchema = z.enum([
|
|
127
|
+
// 12 flat node types. The first 9 are geometric/illustrative and share
|
|
128
|
+
// GeometricNodeData. `image`, `html`, `icon` carry per-type fields.
|
|
129
|
+
// The renderer picks the SVG / chrome by `type`; the schema treats them
|
|
130
|
+
// (apart from the per-type fields below) as identical.
|
|
131
|
+
export const GEOMETRIC_NODE_TYPES = [
|
|
168
132
|
'rectangle',
|
|
169
133
|
'ellipse',
|
|
170
134
|
'sticky',
|
|
@@ -174,148 +138,102 @@ export const ShapeKindSchema = z.enum([
|
|
|
174
138
|
'user',
|
|
175
139
|
'queue',
|
|
176
140
|
'cloud',
|
|
177
|
-
]
|
|
141
|
+
] as const;
|
|
178
142
|
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
...NodeVisualBaseShape,
|
|
183
|
-
...NodeDescriptionBaseShape,
|
|
184
|
-
});
|
|
143
|
+
export const NodeTypeSchema = z.enum([...GEOMETRIC_NODE_TYPES, 'image', 'html', 'icon']);
|
|
144
|
+
|
|
145
|
+
// ---- Resolved (in-memory) per-type data -------------------------------------
|
|
185
146
|
|
|
186
|
-
const
|
|
187
|
-
...
|
|
188
|
-
|
|
189
|
-
|
|
147
|
+
const ResolvedGeometricNodeData = z.object({
|
|
148
|
+
...NodeSemanticBaseShape,
|
|
149
|
+
...NodeVisualBaseShape,
|
|
150
|
+
...NodeCapabilitiesShape,
|
|
190
151
|
});
|
|
191
152
|
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
153
|
+
// Image node — references a file under the project root by relative path.
|
|
154
|
+
// `path` is constrained to live under `nodes/<id>/` (post-validate refine on
|
|
155
|
+
// ResolvedFlowSchema below) so the delete_node cascade owns cleanup.
|
|
156
|
+
const ResolvedImageNodeData = z.object({
|
|
157
|
+
...NodeSemanticBaseShape,
|
|
158
|
+
...NodeVisualBaseShape,
|
|
159
|
+
...NodeCapabilitiesShape,
|
|
198
160
|
path: z.string().min(1).refine(isCleanRelativePath, {
|
|
199
161
|
message: 'path must be a relative path under the project root (no absolute / traversal)',
|
|
200
162
|
}),
|
|
201
163
|
alt: z.string().optional(),
|
|
202
|
-
...NodeVisualBaseShape,
|
|
203
|
-
...NodeDescriptionBaseShape,
|
|
204
164
|
borderWidth: z.number().min(1).max(8).optional(),
|
|
205
165
|
});
|
|
206
166
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// inline via `data.html`. The studio externalizes the content to
|
|
216
|
-
// `<project>/nodes/<id>/view.html` and stores a `file://` ref in flow.json;
|
|
217
|
-
// the resolver inlines the content back on read so consumers see
|
|
218
|
-
// the resolved HTML string. The renderer sanitizes before injection
|
|
219
|
-
// (US-013/US-014). Spreads NodeVisualBaseShape so authors can theme the
|
|
220
|
-
// wrapper (border / background / radius / font) with the same fields
|
|
221
|
-
// available on every other visual node.
|
|
222
|
-
export const HtmlNodeDataSchema = z.object({
|
|
167
|
+
// Html node — escape-hatch for content the curated visuals don't cover.
|
|
168
|
+
// `html` is externalized to `<project>/nodes/<id>/view.html` on write; the
|
|
169
|
+
// file-ref resolver inlines it back on read. The renderer sanitizes before
|
|
170
|
+
// injection. `autoSize:true` lets the renderer size around the content.
|
|
171
|
+
const ResolvedHtmlNodeData = z.object({
|
|
172
|
+
...NodeSemanticBaseShape,
|
|
173
|
+
...NodeVisualBaseShape,
|
|
174
|
+
...NodeCapabilitiesShape,
|
|
223
175
|
html: z.string().optional(),
|
|
224
|
-
name: z.string().optional(),
|
|
225
|
-
// Decorative caption glyph. Lucide icon name (kebab-case) resolved by the
|
|
226
|
-
// canvas <Icon> primitive; rendered inline with the caption when set.
|
|
227
|
-
icon: z.string().optional(),
|
|
228
|
-
// When true (or absent), the renderer measures the HTML content and React
|
|
229
|
-
// Flow sizes the wrapper around it (capped at 800×600 by the renderer's
|
|
230
|
-
// measuring container styles). The studio adapter (`mergeNodeUpdates`)
|
|
231
|
-
// enforces the invariant that `autoSize === true` and persisted
|
|
232
|
-
// `width`/`height` never coexist: writing width/height flips autoSize to
|
|
233
|
-
// false; writing autoSize: true strips width/height.
|
|
234
176
|
autoSize: z.boolean().optional(),
|
|
235
|
-
...NodeVisualBaseShape,
|
|
236
|
-
...NodeDescriptionBaseShape,
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
const HtmlNodeSchema = z.object({
|
|
240
|
-
...NodeBaseShape,
|
|
241
|
-
type: z.literal('htmlNode'),
|
|
242
|
-
data: HtmlNodeDataSchema,
|
|
243
177
|
});
|
|
244
178
|
|
|
245
|
-
//
|
|
246
|
-
// (
|
|
247
|
-
|
|
248
|
-
|
|
179
|
+
// Icon node — renders a Lucide glyph as its main visual. `icon` is required
|
|
180
|
+
// here (overrides the optional decorative `icon` from NodeSemanticBaseShape).
|
|
181
|
+
const ResolvedIconNodeData = z.object({
|
|
182
|
+
...NodeSemanticBaseShape,
|
|
183
|
+
...NodeVisualBaseShape,
|
|
184
|
+
...NodeCapabilitiesShape,
|
|
249
185
|
icon: z.string().min(1),
|
|
250
186
|
color: ColorTokenSchema.optional(),
|
|
251
187
|
strokeWidth: z.number().min(0.5).max(4).optional(),
|
|
252
|
-
width: z.number().positive().optional(),
|
|
253
|
-
height: z.number().positive().optional(),
|
|
254
188
|
alt: z.string().optional(),
|
|
255
|
-
// US-002: optional visible caption rendered below the icon. Distinct from
|
|
256
|
-
// `alt` (screen-reader text). Absent / empty → no caption rendered and the
|
|
257
|
-
// node's bounding box is byte-identical to the unlabeled layout.
|
|
258
|
-
name: z.string().optional(),
|
|
259
|
-
...NodeDescriptionBaseShape,
|
|
260
189
|
});
|
|
261
190
|
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
191
|
+
const NodeBaseShape = {
|
|
192
|
+
id: z.string().min(1),
|
|
193
|
+
position: PositionSchema,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const makeResolvedGeometricSchema = (type: (typeof GEOMETRIC_NODE_TYPES)[number]) =>
|
|
197
|
+
z.object({
|
|
198
|
+
...NodeBaseShape,
|
|
199
|
+
type: z.literal(type),
|
|
200
|
+
data: ResolvedGeometricNodeData,
|
|
201
|
+
});
|
|
267
202
|
|
|
268
203
|
const NodeSchema = z.discriminatedUnion('type', [
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
204
|
+
makeResolvedGeometricSchema('rectangle'),
|
|
205
|
+
makeResolvedGeometricSchema('ellipse'),
|
|
206
|
+
makeResolvedGeometricSchema('sticky'),
|
|
207
|
+
makeResolvedGeometricSchema('text'),
|
|
208
|
+
makeResolvedGeometricSchema('database'),
|
|
209
|
+
makeResolvedGeometricSchema('server'),
|
|
210
|
+
makeResolvedGeometricSchema('user'),
|
|
211
|
+
makeResolvedGeometricSchema('queue'),
|
|
212
|
+
makeResolvedGeometricSchema('cloud'),
|
|
213
|
+
z.object({ ...NodeBaseShape, type: z.literal('image'), data: ResolvedImageNodeData }),
|
|
214
|
+
z.object({ ...NodeBaseShape, type: z.literal('html'), data: ResolvedHtmlNodeData }),
|
|
215
|
+
z.object({ ...NodeBaseShape, type: z.literal('icon'), data: ResolvedIconNodeData }),
|
|
275
216
|
]);
|
|
276
217
|
|
|
277
|
-
// Connector
|
|
278
|
-
// Edge from each connector at render time (id/source/target are reused;
|
|
279
|
-
// `label` becomes the edge label; visual style comes from optional
|
|
280
|
-
// `style`/`color` fields). v1 has no separate `edges[]` array — connectors
|
|
281
|
-
// are the sole source of truth for inter-node connections. Optional
|
|
282
|
-
// metadata fields (`method`/`url`/`eventName`/`queueName`) may be present
|
|
283
|
-
// for documentation purposes; the renderer does not branch on them.
|
|
218
|
+
// Connector — unchanged by the flat-types refactor.
|
|
284
219
|
const ConnectorStyleSchema = z.enum(['solid', 'dashed', 'dotted']);
|
|
285
220
|
const ConnectorDirectionSchema = z.enum(['forward', 'backward', 'both', 'none']);
|
|
286
|
-
// Path geometry — orthogonal to `style` (which means the dash pattern). Absent
|
|
287
|
-
// → renders as today's smooth bezier curve. 'step' renders as a smoothstep
|
|
288
|
-
// (right-angle / zigzag) path. (US-017)
|
|
289
221
|
const ConnectorPathSchema = z.enum(['curve', 'step']);
|
|
290
222
|
|
|
291
|
-
// Visual fields shared by every connector. All optional — existing
|
|
292
|
-
// demo files predate them and must continue to parse. `direction` defaults
|
|
293
|
-
// to 'forward' when absent (the historical behavior).
|
|
294
223
|
const ConnectorVisualBaseShape = {
|
|
295
224
|
style: ConnectorStyleSchema.optional(),
|
|
296
225
|
color: ColorTokenSchema.optional(),
|
|
297
226
|
direction: ConnectorDirectionSchema.optional(),
|
|
298
227
|
borderSize: z.number().positive().optional(),
|
|
299
228
|
path: ConnectorPathSchema.optional(),
|
|
300
|
-
// US-018: per-connector label font size in CSS pixels. Absent → fall back to
|
|
301
|
-
// the editable-edge default (11px). Mirrors NodeVisualBaseShape.fontSize.
|
|
302
229
|
fontSize: z.number().positive().optional(),
|
|
303
230
|
};
|
|
304
231
|
|
|
305
|
-
// Handle ids — every node type
|
|
306
|
-
//
|
|
307
|
-
// `sourceHandle` MUST be a source-side id and `targetHandle` MUST be a
|
|
308
|
-
// target-side id; sending the wrong role leaves a stranded endpoint at render
|
|
309
|
-
// time, so the schema rejects it (US-022).
|
|
232
|
+
// Handle ids — every node type uses the same four-handle layout:
|
|
233
|
+
// target-only on top + left, source-only on right + bottom.
|
|
310
234
|
export const SourceHandleIdSchema = z.enum(['r', 'b']);
|
|
311
235
|
export const TargetHandleIdSchema = z.enum(['t', 'l']);
|
|
312
236
|
|
|
313
|
-
// US-006: pinned endpoint position. `side` names one of the four perimeter
|
|
314
|
-
// sides of the connected node; `t` is the parameterized position along that
|
|
315
|
-
// side, [0, 1], measured from the top-left corner of the side (top/bottom →
|
|
316
|
-
// left-to-right; left/right → top-to-bottom). Pins are persisted so they
|
|
317
|
-
// survive node moves and resizes without drifting toward the other endpoint's
|
|
318
|
-
// center the way floating endpoints do.
|
|
319
237
|
const EdgePinSideSchema = z.enum(['top', 'right', 'bottom', 'left']);
|
|
320
238
|
export const EdgePinSchema = z.object({
|
|
321
239
|
side: EdgePinSideSchema,
|
|
@@ -326,21 +244,10 @@ const ConnectorBaseShape = {
|
|
|
326
244
|
id: z.string().min(1),
|
|
327
245
|
source: z.string().min(1),
|
|
328
246
|
target: z.string().min(1),
|
|
329
|
-
// Optional — connectors authored before the four-handle layout omit them and
|
|
330
|
-
// React Flow falls back to the first matching handle.
|
|
331
247
|
sourceHandle: SourceHandleIdSchema.optional(),
|
|
332
248
|
targetHandle: TargetHandleIdSchema.optional(),
|
|
333
|
-
// US-021: tracks whether each endpoint's handle was auto-picked by the
|
|
334
|
-
// facing-handle picker (true) or pinned by an explicit user handle drop
|
|
335
|
-
// (false / absent). Auto-picked endpoints get re-routed when nodes move so
|
|
336
|
-
// the connector keeps facing the other end; user-pinned ones never do.
|
|
337
249
|
sourceHandleAutoPicked: z.boolean().optional(),
|
|
338
250
|
targetHandleAutoPicked: z.boolean().optional(),
|
|
339
|
-
// US-006: optional explicit perimeter positions for each endpoint. When
|
|
340
|
-
// set, the endpoint is computed from `(side, t)` against the connected
|
|
341
|
-
// node's current bbox at render time — the position parameterizes with the
|
|
342
|
-
// node so the pin survives moves and resizes. Absent → floating /
|
|
343
|
-
// handle-based endpoint behavior (back-compat).
|
|
344
251
|
sourcePin: EdgePinSchema.optional(),
|
|
345
252
|
targetPin: EdgePinSchema.optional(),
|
|
346
253
|
label: z.string().optional(),
|
|
@@ -363,9 +270,8 @@ export const ResolvedFlowSchema = z
|
|
|
363
270
|
nodes: z.array(NodeSchema),
|
|
364
271
|
connectors: z.array(ConnectorSchema),
|
|
365
272
|
// Optional one-shot script the studio runs when the user clicks Restart.
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
// script (US-008), so the script sees no stragglers.
|
|
273
|
+
// The studio kills every live play + status script for the flow BEFORE
|
|
274
|
+
// invoking this script, so the script sees no stragglers.
|
|
369
275
|
resetAction: ResetActionSchema.optional(),
|
|
370
276
|
})
|
|
371
277
|
.superRefine((resolved, ctx) => {
|
|
@@ -386,18 +292,18 @@ export const ResolvedFlowSchema = z
|
|
|
386
292
|
});
|
|
387
293
|
}
|
|
388
294
|
});
|
|
389
|
-
//
|
|
295
|
+
// type:'image' upload paths must live under the node's own
|
|
390
296
|
// `nodes/<id>/` folder so delete_node's removeNodeDir cascade is the
|
|
391
297
|
// single source of cleanup.
|
|
392
298
|
resolved.nodes.forEach((node, idx) => {
|
|
393
|
-
if (node.type !== '
|
|
299
|
+
if (node.type !== 'image') return;
|
|
394
300
|
const path = (node.data as { path?: string }).path;
|
|
395
301
|
const expected = `nodes/${node.id}/`;
|
|
396
302
|
if (typeof path === 'string' && !path.startsWith(expected)) {
|
|
397
303
|
ctx.addIssue({
|
|
398
304
|
code: z.ZodIssueCode.custom,
|
|
399
305
|
path: ['nodes', idx, 'data', 'path'],
|
|
400
|
-
message: `
|
|
306
|
+
message: `image node path must start with "${expected}"`,
|
|
401
307
|
});
|
|
402
308
|
}
|
|
403
309
|
});
|
|
@@ -405,12 +311,7 @@ export const ResolvedFlowSchema = z
|
|
|
405
311
|
|
|
406
312
|
export type ResolvedFlow = z.infer<typeof ResolvedFlowSchema>;
|
|
407
313
|
export type ResolvedFlowNode = z.infer<typeof NodeSchema>;
|
|
408
|
-
export type
|
|
409
|
-
export type ImageNode = z.infer<typeof ImageNodeSchema>;
|
|
410
|
-
export type IconNode = z.infer<typeof IconNodeSchema>;
|
|
411
|
-
export type HtmlNode = z.infer<typeof HtmlNodeSchema>;
|
|
412
|
-
export type HtmlNodeData = z.infer<typeof HtmlNodeDataSchema>;
|
|
413
|
-
export type ShapeKind = z.infer<typeof ShapeKindSchema>;
|
|
314
|
+
export type NodeType = z.infer<typeof NodeTypeSchema>;
|
|
414
315
|
export type ColorToken = z.infer<typeof ColorTokenSchema>;
|
|
415
316
|
export type Connector = z.infer<typeof ConnectorSchema>;
|
|
416
317
|
export type ConnectorStyle = z.infer<typeof ConnectorStyleSchema>;
|
|
@@ -429,63 +330,38 @@ export type StateSource = z.infer<typeof StateSourceSchema>;
|
|
|
429
330
|
// What lives on disk in <project>/flow.json after the split.
|
|
430
331
|
// =============================================================================
|
|
431
332
|
|
|
432
|
-
const
|
|
433
|
-
name: z.string().min(1),
|
|
434
|
-
stateSource: StateSourceSchema,
|
|
435
|
-
handlerModule: z.string().optional(),
|
|
436
|
-
icon: z.string().optional(),
|
|
437
|
-
...NodeDescriptionBaseShape,
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
const FlowPlayNodeDataSchema = z
|
|
441
|
-
.object({
|
|
442
|
-
...FlowNodeDataBaseShape,
|
|
443
|
-
playAction: PlayActionSchema,
|
|
444
|
-
statusAction: StatusActionSchema.optional(),
|
|
445
|
-
})
|
|
446
|
-
.strict();
|
|
447
|
-
|
|
448
|
-
const FlowStateNodeDataSchema = z
|
|
333
|
+
const FlowGeometricNodeData = z
|
|
449
334
|
.object({
|
|
450
|
-
...
|
|
451
|
-
|
|
452
|
-
statusAction: StatusActionSchema.optional(),
|
|
335
|
+
...NodeSemanticBaseShape,
|
|
336
|
+
...NodeCapabilitiesShape,
|
|
453
337
|
})
|
|
454
338
|
.strict();
|
|
455
339
|
|
|
456
|
-
const
|
|
457
|
-
.object({
|
|
458
|
-
shape: ShapeKindSchema,
|
|
459
|
-
name: z.string().optional(),
|
|
460
|
-
...NodeDescriptionBaseShape,
|
|
461
|
-
})
|
|
462
|
-
.strict();
|
|
463
|
-
|
|
464
|
-
const FlowImageNodeDataSchema = z
|
|
340
|
+
const FlowImageNodeData = z
|
|
465
341
|
.object({
|
|
342
|
+
...NodeSemanticBaseShape,
|
|
343
|
+
...NodeCapabilitiesShape,
|
|
466
344
|
path: z.string().min(1).refine(isCleanRelativePath, {
|
|
467
345
|
message: 'path must be a relative path under the project root (no absolute / traversal)',
|
|
468
346
|
}),
|
|
469
347
|
alt: z.string().optional(),
|
|
470
|
-
...NodeDescriptionBaseShape,
|
|
471
348
|
})
|
|
472
349
|
.strict();
|
|
473
350
|
|
|
474
|
-
const
|
|
351
|
+
const FlowHtmlNodeData = z
|
|
475
352
|
.object({
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
...NodeDescriptionBaseShape,
|
|
353
|
+
...NodeSemanticBaseShape,
|
|
354
|
+
...NodeCapabilitiesShape,
|
|
355
|
+
html: z.string().optional(),
|
|
480
356
|
})
|
|
481
357
|
.strict();
|
|
482
358
|
|
|
483
|
-
const
|
|
359
|
+
const FlowIconNodeData = z
|
|
484
360
|
.object({
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
icon: z.string().
|
|
488
|
-
|
|
361
|
+
...NodeSemanticBaseShape,
|
|
362
|
+
...NodeCapabilitiesShape,
|
|
363
|
+
icon: z.string().min(1),
|
|
364
|
+
alt: z.string().optional(),
|
|
489
365
|
})
|
|
490
366
|
.strict();
|
|
491
367
|
|
|
@@ -493,61 +369,62 @@ const FlowNodeBaseShape = {
|
|
|
493
369
|
id: z.string().min(1),
|
|
494
370
|
};
|
|
495
371
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
export const
|
|
513
|
-
|
|
514
|
-
...FlowNodeBaseShape,
|
|
515
|
-
type: z.literal('shapeNode'),
|
|
516
|
-
data: FlowShapeNodeDataSchema,
|
|
517
|
-
})
|
|
518
|
-
.strict();
|
|
372
|
+
const makeFlowGeometricSchema = (type: (typeof GEOMETRIC_NODE_TYPES)[number]) =>
|
|
373
|
+
z
|
|
374
|
+
.object({
|
|
375
|
+
...FlowNodeBaseShape,
|
|
376
|
+
type: z.literal(type),
|
|
377
|
+
data: FlowGeometricNodeData,
|
|
378
|
+
})
|
|
379
|
+
.strict();
|
|
380
|
+
|
|
381
|
+
export const FlowRectangleNodeSchema = makeFlowGeometricSchema('rectangle');
|
|
382
|
+
export const FlowEllipseNodeSchema = makeFlowGeometricSchema('ellipse');
|
|
383
|
+
export const FlowStickyNodeSchema = makeFlowGeometricSchema('sticky');
|
|
384
|
+
export const FlowTextNodeSchema = makeFlowGeometricSchema('text');
|
|
385
|
+
export const FlowDatabaseNodeSchema = makeFlowGeometricSchema('database');
|
|
386
|
+
export const FlowServerNodeSchema = makeFlowGeometricSchema('server');
|
|
387
|
+
export const FlowUserNodeSchema = makeFlowGeometricSchema('user');
|
|
388
|
+
export const FlowQueueNodeSchema = makeFlowGeometricSchema('queue');
|
|
389
|
+
export const FlowCloudNodeSchema = makeFlowGeometricSchema('cloud');
|
|
519
390
|
|
|
520
391
|
export const FlowImageNodeSchema = z
|
|
521
392
|
.object({
|
|
522
393
|
...FlowNodeBaseShape,
|
|
523
|
-
type: z.literal('
|
|
524
|
-
data:
|
|
394
|
+
type: z.literal('image'),
|
|
395
|
+
data: FlowImageNodeData,
|
|
525
396
|
})
|
|
526
397
|
.strict();
|
|
527
398
|
|
|
528
|
-
export const
|
|
399
|
+
export const FlowHtmlNodeSchema = z
|
|
529
400
|
.object({
|
|
530
401
|
...FlowNodeBaseShape,
|
|
531
|
-
type: z.literal('
|
|
532
|
-
data:
|
|
402
|
+
type: z.literal('html'),
|
|
403
|
+
data: FlowHtmlNodeData,
|
|
533
404
|
})
|
|
534
405
|
.strict();
|
|
535
406
|
|
|
536
|
-
export const
|
|
407
|
+
export const FlowIconNodeSchema = z
|
|
537
408
|
.object({
|
|
538
409
|
...FlowNodeBaseShape,
|
|
539
|
-
type: z.literal('
|
|
540
|
-
data:
|
|
410
|
+
type: z.literal('icon'),
|
|
411
|
+
data: FlowIconNodeData,
|
|
541
412
|
})
|
|
542
413
|
.strict();
|
|
543
414
|
|
|
544
415
|
const FlowNodeSchema = z.discriminatedUnion('type', [
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
416
|
+
FlowRectangleNodeSchema,
|
|
417
|
+
FlowEllipseNodeSchema,
|
|
418
|
+
FlowStickyNodeSchema,
|
|
419
|
+
FlowTextNodeSchema,
|
|
420
|
+
FlowDatabaseNodeSchema,
|
|
421
|
+
FlowServerNodeSchema,
|
|
422
|
+
FlowUserNodeSchema,
|
|
423
|
+
FlowQueueNodeSchema,
|
|
424
|
+
FlowCloudNodeSchema,
|
|
548
425
|
FlowImageNodeSchema,
|
|
549
|
-
FlowIconNodeSchema,
|
|
550
426
|
FlowHtmlNodeSchema,
|
|
427
|
+
FlowIconNodeSchema,
|
|
551
428
|
]);
|
|
552
429
|
|
|
553
430
|
const FlowConnectorBaseShape = {
|
|
@@ -602,11 +479,10 @@ export type FlowNode = z.infer<typeof FlowNodeSchema>;
|
|
|
602
479
|
export type FlowConnector = z.infer<typeof FlowConnectorSchema>;
|
|
603
480
|
|
|
604
481
|
// Envelope-only flow shape for the `seeflow schema flow` surface. The full
|
|
605
|
-
// FlowSchema validates the whole graph; this companion
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
// detailed shapes. Not used for validation — only the catalog reads it.
|
|
482
|
+
// FlowSchema validates the whole graph; this companion describes the top-level
|
|
483
|
+
// shape without inlining every node variant or the connector shape, so the
|
|
484
|
+
// runtime-introspectable JSON Schema stays compact. Authors drill into
|
|
485
|
+
// `seeflow schema node` / `seeflow schema connector` for the detailed shapes.
|
|
610
486
|
export const FlowEnvelopeSchema = z
|
|
611
487
|
.object({
|
|
612
488
|
version: z.literal(2),
|
|
@@ -635,12 +511,12 @@ const NodeStyleSchema = z
|
|
|
635
511
|
fontSize: z.number().positive().optional(),
|
|
636
512
|
textColor: ColorTokenSchema.optional(),
|
|
637
513
|
cornerRadius: z.number().min(0).optional(),
|
|
638
|
-
//
|
|
514
|
+
// type:'image'-specific
|
|
639
515
|
borderWidth: z.number().min(1).max(8).optional(),
|
|
640
|
-
//
|
|
516
|
+
// type:'icon'-specific
|
|
641
517
|
color: ColorTokenSchema.optional(),
|
|
642
518
|
strokeWidth: z.number().min(0.5).max(4).optional(),
|
|
643
|
-
//
|
|
519
|
+
// type:'html'-specific
|
|
644
520
|
autoSize: z.boolean().optional(),
|
|
645
521
|
})
|
|
646
522
|
.strict();
|
package/src/server.ts
CHANGED
|
@@ -100,7 +100,7 @@ export function createApp(options: CreateAppOptions = {}): Hono {
|
|
|
100
100
|
// don't churn. Route is unauthenticated and stateless.
|
|
101
101
|
app.get('/healthz', (c) => c.json({ status: 'ok' }));
|
|
102
102
|
|
|
103
|
-
// Vendored runtime assets (e.g. @tailwindcss/browser@4 for
|
|
103
|
+
// Vendored runtime assets (e.g. @tailwindcss/browser@4 for type:'html').
|
|
104
104
|
// Served identically in dev and prod so they don't depend on the web
|
|
105
105
|
// bundle. The `{[A-Za-z0-9._-]+}` regex constrains :file to a single safe
|
|
106
106
|
// segment, making traversal (`..`, `/`) impossible by construction.
|
package/src/status-runner.ts
CHANGED
|
@@ -159,7 +159,8 @@ interface StatusNode {
|
|
|
159
159
|
function collectStatusNodes(demo: ResolvedFlow): StatusNode[] {
|
|
160
160
|
const out: StatusNode[] = [];
|
|
161
161
|
for (const node of demo.nodes) {
|
|
162
|
-
|
|
162
|
+
// Capabilities are valid on every node type post-flat-types — gate purely
|
|
163
|
+
// on `statusAction` presence, not on the visual kind.
|
|
163
164
|
const action = node.data.statusAction;
|
|
164
165
|
if (!action) continue;
|
|
165
166
|
out.push({ nodeId: node.id, action });
|