@tuongaz/seeflow 0.1.3
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/README.md +95 -0
- package/bin/seeflow +32 -0
- package/bin/seeflow-mcp +23 -0
- package/dist/web/assets/html2canvas.esm-CBrSDip1.js +22 -0
- package/dist/web/assets/index-BlhIMoXf.js +8005 -0
- package/dist/web/assets/index-CIpouxGY.css +1 -0
- package/dist/web/assets/index.es-D6Hswegt.js +18 -0
- package/dist/web/assets/purify.es-CLGrRn1w.js +3 -0
- package/dist/web/index.html +13 -0
- package/examples/ecommerce-platform/.seeflow/scripts/play.ts +2 -0
- package/examples/ecommerce-platform/.seeflow/seeflow.json +250 -0
- package/examples/order-pipeline/.seeflow/scripts/play.ts +18 -0
- package/examples/order-pipeline/.seeflow/seeflow.json +86 -0
- package/examples/order-pipeline/README.md +11 -0
- package/examples/order-pipeline/package.json +6 -0
- package/package.json +55 -0
- package/public/runtime/tailwind.js +24394 -0
- package/src/api.ts +1093 -0
- package/src/cli.ts +329 -0
- package/src/demo.ts +65 -0
- package/src/diagram.ts +432 -0
- package/src/events.ts +70 -0
- package/src/mcp-shim.ts +93 -0
- package/src/mcp.ts +540 -0
- package/src/operations.ts +1192 -0
- package/src/process-spawner.ts +75 -0
- package/src/proxy.ts +393 -0
- package/src/registry.ts +139 -0
- package/src/runtime.ts +78 -0
- package/src/schema.ts +441 -0
- package/src/sdk-template.ts +37 -0
- package/src/sdk-writer.ts +37 -0
- package/src/server.ts +211 -0
- package/src/shellout.ts +30 -0
- package/src/status-runner.ts +374 -0
- package/src/watcher.ts +383 -0
package/src/schema.ts
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const PositionSchema = z.object({
|
|
4
|
+
x: z.number(),
|
|
5
|
+
y: z.number(),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// US-008: HttpAction was the original shape for both playAction and resetAction
|
|
9
|
+
// in pre-script-action releases. After US-001 cut playAction to script-only and
|
|
10
|
+
// US-008 cut resetAction the same way, no schema in this file uses HttpAction
|
|
11
|
+
// anymore. Connectors keep the `http` *kind literal* for visual semantics, but
|
|
12
|
+
// that's an independent enum — search before re-adding any HttpActionSchema.
|
|
13
|
+
const HttpMethodSchema = z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
|
|
14
|
+
|
|
15
|
+
// Curated palette tokens. Stored on disk as readable names; the frontend maps
|
|
16
|
+
// them to actual CSS values (theme-aware, light + dark).
|
|
17
|
+
export const ColorTokenSchema = z.enum([
|
|
18
|
+
'default',
|
|
19
|
+
'slate',
|
|
20
|
+
'blue',
|
|
21
|
+
'green',
|
|
22
|
+
'amber',
|
|
23
|
+
'red',
|
|
24
|
+
'purple',
|
|
25
|
+
'pink',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
// Visual fields shared by every node type (functional + decorative). All
|
|
29
|
+
// optional — existing demo files predate them and must continue to parse.
|
|
30
|
+
// US-019: `locked` freezes a node in place (no drag / resize / delete) and
|
|
31
|
+
// renders a lock badge on its top-right corner. Absent → unlocked default.
|
|
32
|
+
// Mirrored explicitly into IconNodeDataSchema below
|
|
33
|
+
// since that variant doesn't spread this base shape.
|
|
34
|
+
const NodeVisualBaseShape = {
|
|
35
|
+
width: z.number().positive().optional(),
|
|
36
|
+
height: z.number().positive().optional(),
|
|
37
|
+
borderColor: ColorTokenSchema.optional(),
|
|
38
|
+
backgroundColor: ColorTokenSchema.optional(),
|
|
39
|
+
borderSize: z.number().positive().optional(),
|
|
40
|
+
borderStyle: z.enum(['solid', 'dashed', 'dotted']).optional(),
|
|
41
|
+
fontSize: z.number().positive().optional(),
|
|
42
|
+
textColor: ColorTokenSchema.optional(),
|
|
43
|
+
cornerRadius: z.number().min(0).optional(),
|
|
44
|
+
locked: z.boolean().optional(),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Consolidated three-field metadata shared by every node variant. `description`
|
|
48
|
+
// is the short body text rendered on the canvas under the node header (and as
|
|
49
|
+
// light-bold text in the sidebar). `detail` is the long-form free-text body
|
|
50
|
+
// rendered only in the sidebar. Both optional so unset fields round-trip
|
|
51
|
+
// unchanged. Spread into every node-data schema below since Icon doesn't
|
|
52
|
+
// spread NodeVisualBaseShape.
|
|
53
|
+
const NodeDescriptionBaseShape = {
|
|
54
|
+
description: z.string().optional(),
|
|
55
|
+
detail: z.string().optional(),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// US-001: relative-path safety refine (textual). Mirrors the same rule used
|
|
59
|
+
// for image/html-node paths further down. Realpath verification is layered on
|
|
60
|
+
// top by the proxy/status-runner before any spawn (symlink-escape defense).
|
|
61
|
+
const isCleanRelativePath = (s: string): boolean => {
|
|
62
|
+
if (s.length === 0) return false;
|
|
63
|
+
if (s.startsWith('/') || s.startsWith('\\')) return false;
|
|
64
|
+
if (/^[A-Za-z]:[\\/]/.test(s)) return false;
|
|
65
|
+
const segments = s.split(/[\\/]/);
|
|
66
|
+
return !segments.some((seg) => seg === '..');
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Script-based action: the studio spawns `<interpreter> [...args] <scriptPath>`
|
|
70
|
+
// from the project's repoPath. `scriptPath` is a relative path under
|
|
71
|
+
// `<project>/.seeflow/`; `args` (optional) prepend to the interpreter; `input`
|
|
72
|
+
// (optional) gets JSON-serialized and written to the child's stdin then closed;
|
|
73
|
+
// `timeoutMs` caps execution (default applied at the spawn layer, not here).
|
|
74
|
+
const ScriptActionSchema = z.object({
|
|
75
|
+
kind: z.literal('script'),
|
|
76
|
+
interpreter: z.string().min(1),
|
|
77
|
+
args: z.array(z.string()).optional(),
|
|
78
|
+
scriptPath: z.string().min(1).refine(isCleanRelativePath, {
|
|
79
|
+
message: 'scriptPath must be a relative path under .seeflow/ (no absolute / traversal)',
|
|
80
|
+
}),
|
|
81
|
+
input: z.unknown().optional(),
|
|
82
|
+
timeoutMs: z.number().int().positive().max(600_000).optional(),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const PlayActionSchema = ScriptActionSchema;
|
|
86
|
+
|
|
87
|
+
// US-008: resetAction is a one-shot script action — same shape as a play
|
|
88
|
+
// script (interpreter + args + scriptPath + optional input/timeoutMs) but
|
|
89
|
+
// invoked from the /reset endpoint. The studio kills every live play and
|
|
90
|
+
// status script for the demo before running this script, so the running app
|
|
91
|
+
// sees a clean baseline when wiping its state.
|
|
92
|
+
const ResetActionSchema = ScriptActionSchema;
|
|
93
|
+
|
|
94
|
+
// Long-running status script. Same spawn shape as ScriptAction (interpreter +
|
|
95
|
+
// args + scriptPath) but no stdin payload and a much longer max lifetime since
|
|
96
|
+
// these processes tick continuously and stream StatusReports to stdout.
|
|
97
|
+
const StatusActionSchema = z.object({
|
|
98
|
+
kind: z.literal('script'),
|
|
99
|
+
interpreter: z.string().min(1),
|
|
100
|
+
args: z.array(z.string()).optional(),
|
|
101
|
+
scriptPath: z.string().min(1).refine(isCleanRelativePath, {
|
|
102
|
+
message: 'scriptPath must be a relative path under .seeflow/ (no absolute / traversal)',
|
|
103
|
+
}),
|
|
104
|
+
maxLifetimeMs: z.number().int().positive().max(3_600_000).optional(),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Per-tick status report a statusAction script writes to stdout (one JSON
|
|
108
|
+
// record per line). `data` is a free-form key/value bag rendered as a table
|
|
109
|
+
// in the sidebar.
|
|
110
|
+
export const StatusReportSchema = z.object({
|
|
111
|
+
state: z.enum(['ok', 'warn', 'error', 'pending']),
|
|
112
|
+
summary: z.string().max(120).optional(),
|
|
113
|
+
detail: z.string().max(2000).optional(),
|
|
114
|
+
data: z.record(z.string(), z.unknown()).optional(),
|
|
115
|
+
ts: z.number().int().positive().optional(),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const StateSourceSchema = z.discriminatedUnion('kind', [
|
|
119
|
+
z.object({ kind: z.literal('request') }),
|
|
120
|
+
z.object({ kind: z.literal('event') }),
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
const NodeDataBaseSchema = z.object({
|
|
124
|
+
name: z.string().min(1),
|
|
125
|
+
kind: z.string().min(1),
|
|
126
|
+
stateSource: StateSourceSchema,
|
|
127
|
+
// Reserved for v2: a module path resolved by future skills runtime.
|
|
128
|
+
// Schema-only at v1 — never read at runtime.
|
|
129
|
+
handlerModule: z.string().optional(),
|
|
130
|
+
...NodeVisualBaseShape,
|
|
131
|
+
...NodeDescriptionBaseShape,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const PlayNodeDataSchema = NodeDataBaseSchema.extend({
|
|
135
|
+
playAction: PlayActionSchema,
|
|
136
|
+
statusAction: StatusActionSchema.optional(),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const StateNodeDataSchema = NodeDataBaseSchema.extend({
|
|
140
|
+
playAction: PlayActionSchema.optional(),
|
|
141
|
+
statusAction: StatusActionSchema.optional(),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const NodeBaseShape = {
|
|
145
|
+
id: z.string().min(1),
|
|
146
|
+
position: PositionSchema,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const PlayNodeSchema = z.object({
|
|
150
|
+
...NodeBaseShape,
|
|
151
|
+
type: z.literal('playNode'),
|
|
152
|
+
data: PlayNodeDataSchema,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const StateNodeSchema = z.object({
|
|
156
|
+
...NodeBaseShape,
|
|
157
|
+
type: z.literal('stateNode'),
|
|
158
|
+
data: StateNodeDataSchema,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Decorative annotation node — rectangle / ellipse / sticky. No semantic
|
|
162
|
+
// payload (no kind/stateSource/playAction); reuses NodeVisualBaseShape so
|
|
163
|
+
// users can theme it the same way as functional nodes.
|
|
164
|
+
// US-009 added `database` as the first illustrative shape (cylinder rendered
|
|
165
|
+
// via inline SVG inside shape-node.tsx). Illustrative shapes share the same
|
|
166
|
+
// shapeNode wrapper and color/border fields but own their own visuals via a
|
|
167
|
+
// per-shape component under `apps/web/src/components/nodes/shapes/`.
|
|
168
|
+
const ShapeKindSchema = z.enum([
|
|
169
|
+
'rectangle',
|
|
170
|
+
'ellipse',
|
|
171
|
+
'sticky',
|
|
172
|
+
'text',
|
|
173
|
+
'database',
|
|
174
|
+
'server',
|
|
175
|
+
'user',
|
|
176
|
+
'queue',
|
|
177
|
+
'cloud',
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
const ShapeNodeDataSchema = z.object({
|
|
181
|
+
shape: ShapeKindSchema,
|
|
182
|
+
name: z.string().optional(),
|
|
183
|
+
...NodeVisualBaseShape,
|
|
184
|
+
...NodeDescriptionBaseShape,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const ShapeNodeSchema = z.object({
|
|
188
|
+
...NodeBaseShape,
|
|
189
|
+
type: z.literal('shapeNode'),
|
|
190
|
+
data: ShapeNodeDataSchema,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Decorative image node — references a file under `<project>/.seeflow/` by
|
|
194
|
+
// relative path (US-004 hard-cut from base64 data URLs to path-backed files).
|
|
195
|
+
// `path` is the same kind of relative path as `htmlPath` on htmlNode: rooted
|
|
196
|
+
// at `.seeflow/`, no leading slash, no `..` segments. The renderer fetches via
|
|
197
|
+
// `GET /api/projects/:id/files/:path`.
|
|
198
|
+
const ImageNodeDataSchema = z.object({
|
|
199
|
+
path: z.string().min(1).refine(isCleanRelativePath, {
|
|
200
|
+
message: 'path must be a relative path under .seeflow/ (no absolute / traversal)',
|
|
201
|
+
}),
|
|
202
|
+
alt: z.string().optional(),
|
|
203
|
+
...NodeVisualBaseShape,
|
|
204
|
+
...NodeDescriptionBaseShape,
|
|
205
|
+
borderWidth: z.number().min(1).max(8).optional(),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const ImageNodeSchema = z.object({
|
|
209
|
+
...NodeBaseShape,
|
|
210
|
+
type: z.literal('imageNode'),
|
|
211
|
+
data: ImageNodeDataSchema,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// US-011 (illustrative-shapes-htmlnode): htmlNode is the escape-hatch node type
|
|
215
|
+
// for content the curated nodes don't cover — references author-written HTML at
|
|
216
|
+
// `<project>/.seeflow/<htmlPath>`. The renderer fetches via the file-serving
|
|
217
|
+
// endpoint and sanitizes before injecting (US-013/US-014). `htmlPath` uses the
|
|
218
|
+
// same path-safety refine as imageNode.path: relative under `.seeflow/`, no
|
|
219
|
+
// absolute root, no `..` traversal. Spreads NodeVisualBaseShape so authors can
|
|
220
|
+
// theme the wrapper (border / background / radius / font) with the same fields
|
|
221
|
+
// available on every other visual node.
|
|
222
|
+
//
|
|
223
|
+
// File existence is INTENTIONALLY not validated at the schema level. Missing
|
|
224
|
+
// files are a normal authoring state (author drops a node, file hasn't been
|
|
225
|
+
// written yet) and would otherwise reject the whole demo. The US-014 renderer
|
|
226
|
+
// renders a `PlaceholderCard` instead — so a missing htmlPath WARNS (via the
|
|
227
|
+
// placeholder visual) without ERRORING (without failing demo parse).
|
|
228
|
+
const HtmlNodeDataSchema = z.object({
|
|
229
|
+
htmlPath: z.string().min(1).refine(isCleanRelativePath, {
|
|
230
|
+
message: 'htmlPath must be a relative path under .seeflow/ (no absolute / traversal)',
|
|
231
|
+
}),
|
|
232
|
+
name: z.string().optional(),
|
|
233
|
+
...NodeVisualBaseShape,
|
|
234
|
+
...NodeDescriptionBaseShape,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const HtmlNodeSchema = z.object({
|
|
238
|
+
...NodeBaseShape,
|
|
239
|
+
type: z.literal('htmlNode'),
|
|
240
|
+
data: HtmlNodeDataSchema,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Decorative icon node — renders a Lucide glyph on the canvas. Unboxed
|
|
244
|
+
// (no border/cornerRadius/backgroundColor) so it does NOT spread
|
|
245
|
+
// NodeVisualBaseShape; only `width` / `height` are reused for resizing.
|
|
246
|
+
const IconNodeDataSchema = z.object({
|
|
247
|
+
icon: z.string().min(1),
|
|
248
|
+
color: ColorTokenSchema.optional(),
|
|
249
|
+
strokeWidth: z.number().min(0.5).max(4).optional(),
|
|
250
|
+
width: z.number().positive().optional(),
|
|
251
|
+
height: z.number().positive().optional(),
|
|
252
|
+
alt: z.string().optional(),
|
|
253
|
+
// US-002: optional visible caption rendered below the icon. Distinct from
|
|
254
|
+
// `alt` (screen-reader text). Absent / empty → no caption rendered and the
|
|
255
|
+
// node's bounding box is byte-identical to the unlabeled layout.
|
|
256
|
+
name: z.string().optional(),
|
|
257
|
+
// US-019: lock state mirror of NodeVisualBaseShape.locked. IconNode does
|
|
258
|
+
// not spread the visual base so we declare it here explicitly.
|
|
259
|
+
locked: z.boolean().optional(),
|
|
260
|
+
...NodeDescriptionBaseShape,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const IconNodeSchema = z.object({
|
|
264
|
+
...NodeBaseShape,
|
|
265
|
+
type: z.literal('iconNode'),
|
|
266
|
+
data: IconNodeDataSchema,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const NodeSchema = z.discriminatedUnion('type', [
|
|
270
|
+
PlayNodeSchema,
|
|
271
|
+
StateNodeSchema,
|
|
272
|
+
ShapeNodeSchema,
|
|
273
|
+
ImageNodeSchema,
|
|
274
|
+
IconNodeSchema,
|
|
275
|
+
HtmlNodeSchema,
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
// Connector is the semantic edge between two nodes — describes HOW they are
|
|
279
|
+
// connected, not just THAT they are. Discriminated on `kind`:
|
|
280
|
+
// • http — service-to-service HTTP call (method + url echo of the playAction)
|
|
281
|
+
// • event — pub/sub event (eventName)
|
|
282
|
+
// • queue — message-queue handoff (queueName)
|
|
283
|
+
// • default — user-drawn, no semantic payload (UI annotation only)
|
|
284
|
+
// The frontend derives a React Flow Edge from each connector at render time
|
|
285
|
+
// (id/source/target are reused; `label` becomes the edge label; visual style
|
|
286
|
+
// is picked from `kind`, but per-connector `style`/`color` overrides it). v1
|
|
287
|
+
// has no separate `edges[]` array — connectors are the sole source of truth
|
|
288
|
+
// for inter-node connections.
|
|
289
|
+
const ConnectorStyleSchema = z.enum(['solid', 'dashed', 'dotted']);
|
|
290
|
+
const ConnectorDirectionSchema = z.enum(['forward', 'backward', 'both', 'none']);
|
|
291
|
+
// Path geometry — orthogonal to `style` (which means the dash pattern). Absent
|
|
292
|
+
// → renders as today's smooth bezier curve. 'step' renders as a smoothstep
|
|
293
|
+
// (right-angle / zigzag) path. (US-017)
|
|
294
|
+
const ConnectorPathSchema = z.enum(['curve', 'step']);
|
|
295
|
+
|
|
296
|
+
// Visual fields shared by every connector kind. All optional — existing
|
|
297
|
+
// demo files predate them and must continue to parse. `direction` defaults
|
|
298
|
+
// to 'forward' when absent (the historical behavior).
|
|
299
|
+
const ConnectorVisualBaseShape = {
|
|
300
|
+
style: ConnectorStyleSchema.optional(),
|
|
301
|
+
color: ColorTokenSchema.optional(),
|
|
302
|
+
direction: ConnectorDirectionSchema.optional(),
|
|
303
|
+
borderSize: z.number().positive().optional(),
|
|
304
|
+
path: ConnectorPathSchema.optional(),
|
|
305
|
+
// US-018: per-connector label font size in CSS pixels. Absent → fall back to
|
|
306
|
+
// the editable-edge default (11px). Mirrors NodeVisualBaseShape.fontSize.
|
|
307
|
+
fontSize: z.number().positive().optional(),
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// Handle ids — every node kind in this codebase uses the same four-handle
|
|
311
|
+
// layout: target-only on top + left, source-only on right + bottom (US-013).
|
|
312
|
+
// `sourceHandle` MUST be a source-side id and `targetHandle` MUST be a
|
|
313
|
+
// target-side id; sending the wrong role leaves a stranded endpoint at render
|
|
314
|
+
// time, so the schema rejects it (US-022).
|
|
315
|
+
export const SourceHandleIdSchema = z.enum(['r', 'b']);
|
|
316
|
+
export const TargetHandleIdSchema = z.enum(['t', 'l']);
|
|
317
|
+
|
|
318
|
+
// US-006: pinned endpoint position. `side` names one of the four perimeter
|
|
319
|
+
// sides of the connected node; `t` is the parameterized position along that
|
|
320
|
+
// side, [0, 1], measured from the top-left corner of the side (top/bottom →
|
|
321
|
+
// left-to-right; left/right → top-to-bottom). Pins are persisted so they
|
|
322
|
+
// survive node moves and resizes without drifting toward the other endpoint's
|
|
323
|
+
// center the way floating endpoints do.
|
|
324
|
+
const EdgePinSideSchema = z.enum(['top', 'right', 'bottom', 'left']);
|
|
325
|
+
export const EdgePinSchema = z.object({
|
|
326
|
+
side: EdgePinSideSchema,
|
|
327
|
+
t: z.number().min(0).max(1),
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const ConnectorBaseShape = {
|
|
331
|
+
id: z.string().min(1),
|
|
332
|
+
source: z.string().min(1),
|
|
333
|
+
target: z.string().min(1),
|
|
334
|
+
// Optional — connectors authored before the four-handle layout omit them and
|
|
335
|
+
// React Flow falls back to the first matching handle.
|
|
336
|
+
sourceHandle: SourceHandleIdSchema.optional(),
|
|
337
|
+
targetHandle: TargetHandleIdSchema.optional(),
|
|
338
|
+
// US-021: tracks whether each endpoint's handle was auto-picked by the
|
|
339
|
+
// facing-handle picker (true) or pinned by an explicit user handle drop
|
|
340
|
+
// (false / absent). Auto-picked endpoints get re-routed when nodes move so
|
|
341
|
+
// the connector keeps facing the other end; user-pinned ones never do.
|
|
342
|
+
sourceHandleAutoPicked: z.boolean().optional(),
|
|
343
|
+
targetHandleAutoPicked: z.boolean().optional(),
|
|
344
|
+
// US-006: optional explicit perimeter positions for each endpoint. When
|
|
345
|
+
// set, the endpoint is computed from `(side, t)` against the connected
|
|
346
|
+
// node's current bbox at render time — the position parameterizes with the
|
|
347
|
+
// node so the pin survives moves and resizes. Absent → floating /
|
|
348
|
+
// handle-based endpoint behavior (back-compat).
|
|
349
|
+
sourcePin: EdgePinSchema.optional(),
|
|
350
|
+
targetPin: EdgePinSchema.optional(),
|
|
351
|
+
label: z.string().optional(),
|
|
352
|
+
...ConnectorVisualBaseShape,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const HttpConnectorSchema = z.object({
|
|
356
|
+
...ConnectorBaseShape,
|
|
357
|
+
kind: z.literal('http'),
|
|
358
|
+
method: HttpMethodSchema.optional(),
|
|
359
|
+
url: z.string().min(1).optional(),
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const EventConnectorSchema = z.object({
|
|
363
|
+
...ConnectorBaseShape,
|
|
364
|
+
kind: z.literal('event'),
|
|
365
|
+
eventName: z.string().min(1),
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const QueueConnectorSchema = z.object({
|
|
369
|
+
...ConnectorBaseShape,
|
|
370
|
+
kind: z.literal('queue'),
|
|
371
|
+
queueName: z.string().min(1),
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const DefaultConnectorSchema = z.object({
|
|
375
|
+
...ConnectorBaseShape,
|
|
376
|
+
kind: z.literal('default'),
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const ConnectorSchema = z.discriminatedUnion('kind', [
|
|
380
|
+
HttpConnectorSchema,
|
|
381
|
+
EventConnectorSchema,
|
|
382
|
+
QueueConnectorSchema,
|
|
383
|
+
DefaultConnectorSchema,
|
|
384
|
+
]);
|
|
385
|
+
|
|
386
|
+
export const DemoSchema = z
|
|
387
|
+
.object({
|
|
388
|
+
version: z.literal(1),
|
|
389
|
+
name: z.string().min(1),
|
|
390
|
+
nodes: z.array(NodeSchema),
|
|
391
|
+
connectors: z.array(ConnectorSchema),
|
|
392
|
+
// Optional one-shot script the studio runs when the user clicks Restart.
|
|
393
|
+
// Lets the running app wipe its own in-memory state. The studio kills
|
|
394
|
+
// every live play + status script for the demo BEFORE invoking this
|
|
395
|
+
// script (US-008), so the script sees no stragglers.
|
|
396
|
+
resetAction: ResetActionSchema.optional(),
|
|
397
|
+
})
|
|
398
|
+
.superRefine((demo, ctx) => {
|
|
399
|
+
const nodeIds = new Set(demo.nodes.map((n) => n.id));
|
|
400
|
+
demo.connectors.forEach((c, idx) => {
|
|
401
|
+
if (!nodeIds.has(c.source)) {
|
|
402
|
+
ctx.addIssue({
|
|
403
|
+
code: z.ZodIssueCode.custom,
|
|
404
|
+
path: ['connectors', idx, 'source'],
|
|
405
|
+
message: `Connector ${c.id} references unknown source node: ${c.source}`,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
if (!nodeIds.has(c.target)) {
|
|
409
|
+
ctx.addIssue({
|
|
410
|
+
code: z.ZodIssueCode.custom,
|
|
411
|
+
path: ['connectors', idx, 'target'],
|
|
412
|
+
message: `Connector ${c.id} references unknown target node: ${c.target}`,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
export type Demo = z.infer<typeof DemoSchema>;
|
|
419
|
+
export type DemoNode = z.infer<typeof NodeSchema>;
|
|
420
|
+
export type ShapeNode = z.infer<typeof ShapeNodeSchema>;
|
|
421
|
+
export type ImageNode = z.infer<typeof ImageNodeSchema>;
|
|
422
|
+
export type IconNode = z.infer<typeof IconNodeSchema>;
|
|
423
|
+
export type HtmlNode = z.infer<typeof HtmlNodeSchema>;
|
|
424
|
+
export type HtmlNodeData = z.infer<typeof HtmlNodeDataSchema>;
|
|
425
|
+
export type ShapeKind = z.infer<typeof ShapeKindSchema>;
|
|
426
|
+
export type ColorToken = z.infer<typeof ColorTokenSchema>;
|
|
427
|
+
export type Connector = z.infer<typeof ConnectorSchema>;
|
|
428
|
+
export type HttpConnector = z.infer<typeof HttpConnectorSchema>;
|
|
429
|
+
export type EventConnector = z.infer<typeof EventConnectorSchema>;
|
|
430
|
+
export type QueueConnector = z.infer<typeof QueueConnectorSchema>;
|
|
431
|
+
export type DefaultConnector = z.infer<typeof DefaultConnectorSchema>;
|
|
432
|
+
export type ConnectorStyle = z.infer<typeof ConnectorStyleSchema>;
|
|
433
|
+
export type ConnectorDirection = z.infer<typeof ConnectorDirectionSchema>;
|
|
434
|
+
export type ConnectorPath = z.infer<typeof ConnectorPathSchema>;
|
|
435
|
+
export type EdgePin = z.infer<typeof EdgePinSchema>;
|
|
436
|
+
export type EdgePinSide = z.infer<typeof EdgePinSideSchema>;
|
|
437
|
+
export type PlayAction = z.infer<typeof PlayActionSchema>;
|
|
438
|
+
export type StatusAction = z.infer<typeof StatusActionSchema>;
|
|
439
|
+
export type StatusReport = z.infer<typeof StatusReportSchema>;
|
|
440
|
+
export type ResetAction = z.infer<typeof ResetActionSchema>;
|
|
441
|
+
export type StateSource = z.infer<typeof StateSourceSchema>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-contained `emit()` helper copied verbatim into a target repo's
|
|
3
|
+
* `.seeflow/sdk/emit.ts` by `seeflow register` when the demo declares any
|
|
4
|
+
* event-bound state node. Kept dependency-free so the user's app can call it
|
|
5
|
+
* without installing `@seeflow/sdk`.
|
|
6
|
+
*/
|
|
7
|
+
export const EMIT_TEMPLATE = `// Auto-generated by seeflow register. Safe to commit; safe to delete.
|
|
8
|
+
// Re-running register will not overwrite this file.
|
|
9
|
+
export type EmitStatus = 'running' | 'done' | 'error';
|
|
10
|
+
|
|
11
|
+
export interface EmitOptions {
|
|
12
|
+
runId?: string;
|
|
13
|
+
payload?: unknown;
|
|
14
|
+
studioUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const readEnv = (key: string): string | undefined => {
|
|
18
|
+
const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;
|
|
19
|
+
return proc?.env?.[key];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export async function emit(
|
|
23
|
+
demoId: string,
|
|
24
|
+
nodeId: string,
|
|
25
|
+
status: EmitStatus,
|
|
26
|
+
opts: EmitOptions = {},
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
const base = (
|
|
29
|
+
opts.studioUrl ?? readEnv('SEEFLOW_STUDIO_URL') ?? 'http://localhost:4321'
|
|
30
|
+
).replace(/\\/+$/, '');
|
|
31
|
+
await fetch(\`\${base}/api/emit\`, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'content-type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ demoId, nodeId, status, runId: opts.runId, payload: opts.payload }),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { Demo } from './schema.ts';
|
|
4
|
+
import { EMIT_TEMPLATE } from './sdk-template.ts';
|
|
5
|
+
|
|
6
|
+
export type SdkWriteOutcome = 'skipped' | 'written' | 'present';
|
|
7
|
+
|
|
8
|
+
export interface SdkWriteResult {
|
|
9
|
+
outcome: SdkWriteOutcome;
|
|
10
|
+
/** Absolute path of the SDK file (written or pre-existing). Null on `skipped`. */
|
|
11
|
+
filePath: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Writes `.seeflow/sdk/emit.ts` into a target repo iff the demo declares any
|
|
16
|
+
* node with `stateSource.kind === 'event'`. Idempotent: existing files are
|
|
17
|
+
* never overwritten. The only place M1's CLI mutates a user repo.
|
|
18
|
+
*/
|
|
19
|
+
export function writeSdkEmitIfNeeded(repoPath: string, demo: Demo): SdkWriteResult {
|
|
20
|
+
const hasEventState = demo.nodes.some(
|
|
21
|
+
(n) =>
|
|
22
|
+
n.type !== 'shapeNode' &&
|
|
23
|
+
n.type !== 'imageNode' &&
|
|
24
|
+
n.type !== 'iconNode' &&
|
|
25
|
+
n.type !== 'htmlNode' &&
|
|
26
|
+
n.data.stateSource.kind === 'event',
|
|
27
|
+
);
|
|
28
|
+
if (!hasEventState) return { outcome: 'skipped', filePath: null };
|
|
29
|
+
|
|
30
|
+
const sdkDir = join(repoPath, '.seeflow', 'sdk');
|
|
31
|
+
const filePath = join(sdkDir, 'emit.ts');
|
|
32
|
+
if (existsSync(filePath)) return { outcome: 'present', filePath };
|
|
33
|
+
|
|
34
|
+
mkdirSync(sdkDir, { recursive: true });
|
|
35
|
+
writeFileSync(filePath, EMIT_TEMPLATE);
|
|
36
|
+
return { outcome: 'written', filePath };
|
|
37
|
+
}
|