@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/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. stateNode playNode without a
101
- // playAction in the same body surfaces as `badSchema`). The per-node
102
- // folder under `nodes/<id>/` is keyed by id, so retype keeps scripts,
103
- // detail.md, and view.html attached.
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
- // htmlNode-only: when true, the renderer measures content and React Flow
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
- shape: ShapeKindSchema.optional(),
122
- // iconNode-only: stroke color token. Lands at data.color; ResolvedFlowSchema's
123
- // post-merge reparse gates that this is only valid on an iconNode.
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
- // iconNode-only: glyph stroke width. Lands at data.strokeWidth; the
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
- // iconNode-only: accessible alt text for the icon. Lands at data.alt.
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 (iconNode), and gates which variants allow it. Explicit `null`
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
- // htmlNode-only: inline HTML content. Externalized to
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
- // P5 overlay attach: lets the skill (or any consumer) wire executable
146
- // behaviour onto a previously-created node without re-issuing it. Final
147
- // validity is enforced by the post-merge ResolvedFlowSchema reparse
148
- // e.g. statusAction is only valid on playNode / stateNode.
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. stateNode playNode without playAction)
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
- playNode: new Set([
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
- stateNode: new Set([
219
+ html: new Set([
208
220
  'name',
209
- 'kind',
221
+ 'description',
222
+ 'detail',
223
+ 'icon',
210
224
  'stateSource',
211
225
  'handlerModule',
212
- 'icon',
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
- // stateNode playNode without a playAction in the same patch) surface as
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
- // htmlNode-only invariant enforcement:
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 === 'htmlNode') {
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 htmlNode) stripped so the
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
- // imageNode upload that lived there.
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
+ }
@@ -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
- FlowPlayNodeSchema,
17
- FlowShapeNodeSchema,
18
- FlowStateNodeSchema,
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 six node variants (playNode, stateNode, shapeNode, imageNode, iconNode, htmlNode).',
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
- playNode: toJsonSchema(FlowPlayNodeSchema),
68
- stateNode: toJsonSchema(FlowStateNodeSchema),
69
- shapeNode: toJsonSchema(FlowShapeNodeSchema),
70
- imageNode: toJsonSchema(FlowImageNodeSchema),
71
- iconNode: toJsonSchema(FlowIconNodeSchema),
72
- htmlNode: toJsonSchema(FlowHtmlNodeSchema),
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
- "imageNode.data.path must start with 'nodes/<id>/'.",
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
  },