@tuongaz/seeflow 0.1.63 → 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-DAP_yx-l.js → index-DL6aaddE.js} +985 -985
- package/dist/web/assets/{index.es-2bA-nRVD.js → index.es-CVm3MRo3.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-C7u0-VKd.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/operations.ts
CHANGED
|
@@ -27,10 +27,10 @@ import {
|
|
|
27
27
|
EdgePinSchema,
|
|
28
28
|
type Flow,
|
|
29
29
|
FlowSchema,
|
|
30
|
+
NodeTypeSchema,
|
|
30
31
|
PlayActionSchema,
|
|
31
32
|
type ResolvedFlow,
|
|
32
33
|
ResolvedFlowSchema,
|
|
33
|
-
ShapeKindSchema,
|
|
34
34
|
SourceHandleIdSchema,
|
|
35
35
|
StateSourceSchema,
|
|
36
36
|
StatusActionSchema,
|
|
@@ -82,25 +82,16 @@ export type ReorderBody = z.infer<typeof ReorderBodySchema>;
|
|
|
82
82
|
// other key lands inside node.data. Final validity is enforced by re-parsing
|
|
83
83
|
// the whole demo through ResolvedFlowSchema after the merge — this body schema just
|
|
84
84
|
// rejects unknown top-level keys to catch typos.
|
|
85
|
-
const NodeTypeSchema = z.enum([
|
|
86
|
-
'playNode',
|
|
87
|
-
'stateNode',
|
|
88
|
-
'shapeNode',
|
|
89
|
-
'imageNode',
|
|
90
|
-
'iconNode',
|
|
91
|
-
'htmlNode',
|
|
92
|
-
]);
|
|
93
|
-
|
|
94
85
|
export const NodePatchBodySchema = z
|
|
95
86
|
.object({
|
|
96
87
|
// When supplied AND different from the node's current type, the merged
|
|
97
88
|
// node is reclassified in place: data keys not allowed on the new type's
|
|
98
89
|
// FlowDataSchema are stripped, visuals (which route to style.json) are
|
|
99
90
|
// preserved, and the post-merge ResolvedFlowSchema reparse enforces the
|
|
100
|
-
// new type's required fields (e.g.
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
91
|
+
// new type's required fields (e.g. type:'image' without a `path` in the
|
|
92
|
+
// same body surfaces as `badSchema`). The per-node folder under
|
|
93
|
+
// `nodes/<id>/` is keyed by id, so retype keeps scripts, detail.md, and
|
|
94
|
+
// view.html attached.
|
|
104
95
|
type: NodeTypeSchema.optional(),
|
|
105
96
|
position: PositionBodySchema.optional(),
|
|
106
97
|
name: z.string().optional(),
|
|
@@ -114,22 +105,22 @@ export const NodePatchBodySchema = z
|
|
|
114
105
|
cornerRadius: z.number().min(0).optional(),
|
|
115
106
|
width: z.number().positive().optional(),
|
|
116
107
|
height: z.number().positive().optional(),
|
|
117
|
-
//
|
|
108
|
+
// type:'html'-only: when true, the renderer measures content and React Flow
|
|
118
109
|
// sizes the wrapper around it. mergeNodeUpdates enforces the invariant
|
|
119
110
|
// that autoSize:true never coexists with persisted width/height.
|
|
120
111
|
autoSize: z.boolean().optional(),
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
//
|
|
112
|
+
// type:'icon'-only: stroke color token. Lands at data.color; the
|
|
113
|
+
// post-merge ResolvedFlowSchema reparse gates that this is only valid on
|
|
114
|
+
// type:'icon'.
|
|
124
115
|
color: ColorTokenSchema.optional(),
|
|
125
|
-
//
|
|
116
|
+
// type:'icon'-only: glyph stroke width. Lands at data.strokeWidth; the
|
|
126
117
|
// post-merge reparse gates the [0.5, 4] bound and arm validity.
|
|
127
118
|
strokeWidth: z.number().min(0.5).max(4).optional(),
|
|
128
|
-
//
|
|
119
|
+
// type:'icon'/type:'image'-only: accessible alt text. Lands at data.alt.
|
|
129
120
|
alt: z.string().optional(),
|
|
130
121
|
// kebab-case Lucide icon name. Lands at data.icon. The post-merge reparse
|
|
131
122
|
// enforces the schema's `.min(1)` non-empty rule for nodes that require
|
|
132
|
-
// icon (
|
|
123
|
+
// icon (type:'icon'), and gates which variants allow it. Explicit `null`
|
|
133
124
|
// clears the field (mergeNodeUpdates strips the key from disk) — mirrors
|
|
134
125
|
// the empty-string clear convention used for description / detail.
|
|
135
126
|
icon: z.string().min(1).nullable().optional(),
|
|
@@ -138,14 +129,15 @@ export const NodePatchBodySchema = z
|
|
|
138
129
|
// serialize signal — `mergeNodeUpdates` strips the key from disk.
|
|
139
130
|
description: z.string().optional(),
|
|
140
131
|
detail: z.string().optional(),
|
|
141
|
-
//
|
|
132
|
+
// type:'html'-only: inline HTML content. Externalized to
|
|
142
133
|
// `<project>/nodes/<id>/view.html` by patchNodeImpl; the file:// ref on
|
|
143
134
|
// the node persists. Empty string empties the file but keeps the ref.
|
|
144
135
|
html: z.string().optional(),
|
|
145
|
-
//
|
|
146
|
-
// behaviour onto a previously-created node without re-issuing it.
|
|
147
|
-
//
|
|
148
|
-
//
|
|
136
|
+
// Capability attach: lets the skill (or any consumer) wire executable
|
|
137
|
+
// behaviour onto a previously-created node without re-issuing it. All
|
|
138
|
+
// capabilities are valid on every node type — presence drives renderer
|
|
139
|
+
// chrome. Final validity is enforced by the post-merge
|
|
140
|
+
// ResolvedFlowSchema reparse.
|
|
149
141
|
playAction: PlayActionSchema.optional(),
|
|
150
142
|
statusAction: StatusActionSchema.optional(),
|
|
151
143
|
stateSource: StateSourceSchema.optional(),
|
|
@@ -170,7 +162,6 @@ const NODE_DATA_PATCH_KEYS = [
|
|
|
170
162
|
'width',
|
|
171
163
|
'height',
|
|
172
164
|
'autoSize',
|
|
173
|
-
'shape',
|
|
174
165
|
'color',
|
|
175
166
|
'strokeWidth',
|
|
176
167
|
'alt',
|
|
@@ -190,35 +181,63 @@ const EXTERNALIZED_FIELD_NAMES = new Set<string>(EXTERNALIZED_NODE_FIELDS.map((e
|
|
|
190
181
|
// they route to style.json on write. Everything else gets stripped from
|
|
191
182
|
// `data` when a node changes type so the post-merge ResolvedFlowSchema reparse
|
|
192
183
|
// doesn't reject lingering fields from the previous variant. Missing required
|
|
193
|
-
// fields on the new type (e.g.
|
|
184
|
+
// fields on the new type (e.g. retype to type:'image' without a `path`)
|
|
194
185
|
// surface as the normal `badSchema` outcome from the reparse.
|
|
186
|
+
const GEOMETRIC_SEMANTIC_KEYS: ReadonlySet<string> = new Set([
|
|
187
|
+
'name',
|
|
188
|
+
'description',
|
|
189
|
+
'detail',
|
|
190
|
+
'icon',
|
|
191
|
+
'stateSource',
|
|
192
|
+
'handlerModule',
|
|
193
|
+
'playAction',
|
|
194
|
+
'statusAction',
|
|
195
|
+
]);
|
|
196
|
+
|
|
195
197
|
const SEMANTIC_KEYS_BY_TYPE: Record<z.infer<typeof NodeTypeSchema>, ReadonlySet<string>> = {
|
|
196
|
-
|
|
198
|
+
rectangle: GEOMETRIC_SEMANTIC_KEYS,
|
|
199
|
+
ellipse: GEOMETRIC_SEMANTIC_KEYS,
|
|
200
|
+
sticky: GEOMETRIC_SEMANTIC_KEYS,
|
|
201
|
+
text: GEOMETRIC_SEMANTIC_KEYS,
|
|
202
|
+
database: GEOMETRIC_SEMANTIC_KEYS,
|
|
203
|
+
server: GEOMETRIC_SEMANTIC_KEYS,
|
|
204
|
+
user: GEOMETRIC_SEMANTIC_KEYS,
|
|
205
|
+
queue: GEOMETRIC_SEMANTIC_KEYS,
|
|
206
|
+
cloud: GEOMETRIC_SEMANTIC_KEYS,
|
|
207
|
+
image: new Set([
|
|
197
208
|
'name',
|
|
198
|
-
'kind',
|
|
199
|
-
'stateSource',
|
|
200
|
-
'handlerModule',
|
|
201
|
-
'icon',
|
|
202
209
|
'description',
|
|
203
210
|
'detail',
|
|
211
|
+
'icon',
|
|
212
|
+
'stateSource',
|
|
213
|
+
'handlerModule',
|
|
204
214
|
'playAction',
|
|
205
215
|
'statusAction',
|
|
216
|
+
'path',
|
|
217
|
+
'alt',
|
|
206
218
|
]),
|
|
207
|
-
|
|
219
|
+
html: new Set([
|
|
208
220
|
'name',
|
|
209
|
-
'
|
|
221
|
+
'description',
|
|
222
|
+
'detail',
|
|
223
|
+
'icon',
|
|
210
224
|
'stateSource',
|
|
211
225
|
'handlerModule',
|
|
212
|
-
'
|
|
226
|
+
'playAction',
|
|
227
|
+
'statusAction',
|
|
228
|
+
'html',
|
|
229
|
+
]),
|
|
230
|
+
icon: new Set([
|
|
231
|
+
'name',
|
|
213
232
|
'description',
|
|
214
233
|
'detail',
|
|
234
|
+
'icon',
|
|
235
|
+
'stateSource',
|
|
236
|
+
'handlerModule',
|
|
215
237
|
'playAction',
|
|
216
238
|
'statusAction',
|
|
239
|
+
'alt',
|
|
217
240
|
]),
|
|
218
|
-
shapeNode: new Set(['shape', 'name', 'description', 'detail']),
|
|
219
|
-
imageNode: new Set(['path', 'alt', 'description', 'detail']),
|
|
220
|
-
iconNode: new Set(['icon', 'alt', 'name', 'description', 'detail']),
|
|
221
|
-
htmlNode: new Set(['html', 'name', 'icon', 'description', 'detail']),
|
|
222
241
|
};
|
|
223
242
|
|
|
224
243
|
// Visual data keys — routed to style.json on write by splitFlow. Kept here
|
|
@@ -288,7 +307,7 @@ export const mergeNodeUpdates = (node: Record<string, unknown>, updates: NodePat
|
|
|
288
307
|
// doesn't reject lingering fields. The per-node folder under
|
|
289
308
|
// `nodes/<id>/` is keyed by id (unchanged), so scripts and externalized
|
|
290
309
|
// files stay attached. Missing required fields on the new type (e.g.
|
|
291
|
-
//
|
|
310
|
+
// retype to type:'image' without a `path` in the same patch) surface as
|
|
292
311
|
// `badSchema` from the ResolvedFlowSchema reparse.
|
|
293
312
|
if (updates.type !== undefined && updates.type !== node.type) {
|
|
294
313
|
node.type = updates.type;
|
|
@@ -302,12 +321,12 @@ export const mergeNodeUpdates = (node: Record<string, unknown>, updates: NodePat
|
|
|
302
321
|
}
|
|
303
322
|
}
|
|
304
323
|
|
|
305
|
-
//
|
|
324
|
+
// type:'html'-only invariant enforcement:
|
|
306
325
|
// autoSize === true ⊻ (width and height set).
|
|
307
326
|
// autoSize: true is the dominant signal — it strips width/height even if
|
|
308
327
|
// the same patch tried to write them. Writing width/height implicitly
|
|
309
328
|
// flips autoSize to false.
|
|
310
|
-
if (node.type === '
|
|
329
|
+
if (node.type === 'html') {
|
|
311
330
|
// The autoSize invariant requires `width`/`height` to be ABSENT from the
|
|
312
331
|
// serialized JSON when autoSize is true — not present with value
|
|
313
332
|
// `undefined` (which would serialize as a stray `"width": null` or get
|
|
@@ -389,7 +408,7 @@ export type GetFlowOutcome =
|
|
|
389
408
|
| { kind: 'fileNotFound'; path: string };
|
|
390
409
|
|
|
391
410
|
// Lightweight graph projection — flow + nodes + connectors with file-backed
|
|
392
|
-
// fields (`detail` on every node, `html` on
|
|
411
|
+
// fields (`detail` on every node, `html` on type:'html') stripped so the
|
|
393
412
|
// caller can navigate the topology without paying for inlined bodies.
|
|
394
413
|
export interface FlowGraphResponse {
|
|
395
414
|
id: string;
|
|
@@ -1141,6 +1160,9 @@ export async function createProjectImpl(
|
|
|
1141
1160
|
try {
|
|
1142
1161
|
mkdirSync(folderPath, { recursive: true });
|
|
1143
1162
|
writeFileSync(demoFullPath, `${JSON.stringify(scaffold, null, 2)}\n`);
|
|
1163
|
+
const tmpDir = join(folderPath, '.tmp');
|
|
1164
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
1165
|
+
writeFileSync(join(tmpDir, '.gitignore'), '*\n!.gitignore\n');
|
|
1144
1166
|
} catch (err) {
|
|
1145
1167
|
return { kind: 'scaffoldFailed', message: err instanceof Error ? err.message : String(err) };
|
|
1146
1168
|
}
|
|
@@ -1378,7 +1400,7 @@ export async function addFlowBulkImpl(
|
|
|
1378
1400
|
// schema violation surfaces honestly instead of being silently papered over.
|
|
1379
1401
|
// After the flow.json write, `removeNodeDir` cascades the node's whole
|
|
1380
1402
|
// `<project>/nodes/<id>/` folder — covering detail.md, view.html, and any
|
|
1381
|
-
//
|
|
1403
|
+
// type:'image' upload that lived there.
|
|
1382
1404
|
export async function deleteNodeImpl(
|
|
1383
1405
|
deps: OperationsDeps,
|
|
1384
1406
|
flowId: string,
|
|
@@ -1765,11 +1787,7 @@ export async function applyLayoutImpl(
|
|
|
1765
1787
|
|
|
1766
1788
|
const flow = flowParse.data;
|
|
1767
1789
|
const result = await computeLayout(
|
|
1768
|
-
flow.nodes.map((n) => ({
|
|
1769
|
-
id: n.id,
|
|
1770
|
-
type: n.type,
|
|
1771
|
-
data: n.type === 'shapeNode' ? { shape: (n.data as { shape?: string }).shape } : undefined,
|
|
1772
|
-
})),
|
|
1790
|
+
flow.nodes.map((n) => ({ id: n.id, type: n.type })),
|
|
1773
1791
|
flow.connectors.map((c) => ({ id: c.id, source: c.source, target: c.target })),
|
|
1774
1792
|
options,
|
|
1775
1793
|
);
|
package/src/runtime.ts
CHANGED
|
@@ -76,3 +76,40 @@ export function isPidAlive(pid: number): boolean {
|
|
|
76
76
|
return false;
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
// `0.0.0.0` / `::` are wildcard bind addresses, not connectable destinations —
|
|
81
|
+
// probe loopback when the caller passes one. IPv6 unspecified `::` maps to
|
|
82
|
+
// `::1`; everything else is treated as IPv4 wildcard → `127.0.0.1`.
|
|
83
|
+
function probeHost(host: string): string {
|
|
84
|
+
if (host === '::' || host === '[::]') return '::1';
|
|
85
|
+
if (host === '0.0.0.0' || host === '') return '127.0.0.1';
|
|
86
|
+
return host;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Detects whether a TCP listener is responding on host:port. Uses a short
|
|
91
|
+
* connect probe (300 ms default) so we catch actively-listening servers
|
|
92
|
+
* without padding fast paths. Returns false on any connect error or timeout.
|
|
93
|
+
*/
|
|
94
|
+
export async function portInUse(host: string, port: number, timeoutMs = 300): Promise<boolean> {
|
|
95
|
+
const hostname = probeHost(host);
|
|
96
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
97
|
+
try {
|
|
98
|
+
const socket = await Promise.race([
|
|
99
|
+
Bun.connect({
|
|
100
|
+
hostname,
|
|
101
|
+
port,
|
|
102
|
+
socket: { data() {}, open() {}, close() {}, error() {} },
|
|
103
|
+
}),
|
|
104
|
+
new Promise<never>((_, reject) => {
|
|
105
|
+
timer = setTimeout(() => reject(new Error('timeout')), timeoutMs);
|
|
106
|
+
}),
|
|
107
|
+
]);
|
|
108
|
+
socket.end();
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
} finally {
|
|
113
|
+
if (timer) clearTimeout(timer);
|
|
114
|
+
}
|
|
115
|
+
}
|
package/src/schema-catalog.ts
CHANGED
|
@@ -8,14 +8,20 @@
|
|
|
8
8
|
import type { ZodTypeAny } from 'zod';
|
|
9
9
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
10
10
|
import {
|
|
11
|
+
FlowCloudNodeSchema,
|
|
11
12
|
FlowConnectorSchema,
|
|
13
|
+
FlowDatabaseNodeSchema,
|
|
14
|
+
FlowEllipseNodeSchema,
|
|
12
15
|
FlowEnvelopeSchema,
|
|
13
16
|
FlowHtmlNodeSchema,
|
|
14
17
|
FlowIconNodeSchema,
|
|
15
18
|
FlowImageNodeSchema,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
FlowQueueNodeSchema,
|
|
20
|
+
FlowRectangleNodeSchema,
|
|
21
|
+
FlowServerNodeSchema,
|
|
22
|
+
FlowStickyNodeSchema,
|
|
23
|
+
FlowTextNodeSchema,
|
|
24
|
+
FlowUserNodeSchema,
|
|
19
25
|
PlayActionSchema,
|
|
20
26
|
ResetActionSchema,
|
|
21
27
|
StatusActionSchema,
|
|
@@ -44,7 +50,7 @@ const CATEGORIES: SchemaCategory[] = [
|
|
|
44
50
|
{
|
|
45
51
|
name: 'node',
|
|
46
52
|
description:
|
|
47
|
-
'All
|
|
53
|
+
'All 12 flat node variants (rectangle, ellipse, sticky, text, database, server, user, queue, cloud, image, html, icon). Visual kind is the type; capabilities (playAction / statusAction / stateSource) are independent optional fields on every variant.',
|
|
48
54
|
},
|
|
49
55
|
{
|
|
50
56
|
name: 'connector',
|
|
@@ -64,15 +70,21 @@ const PAYLOADS: Record<string, SchemaPayload> = {
|
|
|
64
70
|
},
|
|
65
71
|
node: {
|
|
66
72
|
schemas: {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
rectangle: toJsonSchema(FlowRectangleNodeSchema),
|
|
74
|
+
ellipse: toJsonSchema(FlowEllipseNodeSchema),
|
|
75
|
+
sticky: toJsonSchema(FlowStickyNodeSchema),
|
|
76
|
+
text: toJsonSchema(FlowTextNodeSchema),
|
|
77
|
+
database: toJsonSchema(FlowDatabaseNodeSchema),
|
|
78
|
+
server: toJsonSchema(FlowServerNodeSchema),
|
|
79
|
+
user: toJsonSchema(FlowUserNodeSchema),
|
|
80
|
+
queue: toJsonSchema(FlowQueueNodeSchema),
|
|
81
|
+
cloud: toJsonSchema(FlowCloudNodeSchema),
|
|
82
|
+
image: toJsonSchema(FlowImageNodeSchema),
|
|
83
|
+
html: toJsonSchema(FlowHtmlNodeSchema),
|
|
84
|
+
icon: toJsonSchema(FlowIconNodeSchema),
|
|
73
85
|
},
|
|
74
86
|
notes: [
|
|
75
|
-
"
|
|
87
|
+
"type:'image' data.path must start with 'nodes/<id>/'.",
|
|
76
88
|
"scriptPath in playAction/statusAction is relative to nodes/<nodeId>/ and may not contain '..' or absolute paths.",
|
|
77
89
|
],
|
|
78
90
|
},
|