@tuongaz/seeflow 0.1.26 → 0.1.28
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/bin/seeflow +0 -0
- package/bin/seeflow-mcp +0 -0
- package/dist/web/assets/{index-BMaMEi2a.js → index--9KvdiJU.js} +1584 -1584
- package/dist/web/assets/index-XIY68z9O.css +1 -0
- package/dist/web/assets/{index.es-M1iBDKG6.js → index.es-CehYgUiq.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-xZpq8bcn.js → jspdf.es.min-CaFlbpn0.js} +3 -3
- package/dist/web/index.html +2 -2
- package/examples/ecommerce-platform/.seeflow/{seeflow.json → architecture.json} +22 -77
- package/examples/ecommerce-platform/.seeflow/details/api-gateway.md +14 -0
- package/examples/ecommerce-platform/.seeflow/details/auth-service.md +9 -0
- package/examples/ecommerce-platform/.seeflow/details/cart-service.md +10 -0
- package/examples/ecommerce-platform/.seeflow/details/notification-service.md +13 -0
- package/examples/ecommerce-platform/.seeflow/details/order-service.md +16 -0
- package/examples/ecommerce-platform/.seeflow/details/payment-service.md +16 -0
- package/examples/ecommerce-platform/.seeflow/details/product-service.md +10 -0
- package/examples/ecommerce-platform/.seeflow/scripts/platform-health.html +42 -0
- package/examples/ecommerce-platform/.seeflow/style.json +98 -0
- package/examples/order-pipeline/.seeflow/architecture.json +93 -0
- package/examples/order-pipeline/.seeflow/details/fulfillment-service.md +21 -0
- package/examples/order-pipeline/.seeflow/details/inventory-service.md +23 -0
- package/examples/order-pipeline/.seeflow/details/payment-service.md +23 -0
- package/examples/order-pipeline/.seeflow/details/post-orders.md +19 -0
- package/examples/order-pipeline/.seeflow/scripts/play.ts +2 -2
- package/examples/order-pipeline/.seeflow/style.json +42 -0
- package/package.json +4 -2
- package/public/runtime/tailwind.js +931 -24378
- package/src/api.ts +118 -118
- package/src/cli.ts +13 -13
- package/src/demo.ts +6 -6
- package/src/diagram.ts +4 -4
- package/src/events.ts +14 -14
- package/src/file-ref.ts +79 -0
- package/src/mcp.ts +117 -89
- package/src/merge.ts +190 -0
- package/src/operations.ts +415 -416
- package/src/proxy.ts +31 -31
- package/src/registry.ts +32 -20
- package/src/schema.ts +252 -8
- package/src/sdk-template.ts +2 -2
- package/src/sdk-writer.ts +2 -2
- package/src/server.ts +14 -7
- package/src/status-runner.ts +34 -38
- package/src/watcher.ts +165 -114
- package/dist/web/assets/index-BotEftAD.css +0 -1
- package/examples/order-pipeline/.seeflow/seeflow.json +0 -123
package/src/proxy.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface PlayResult {
|
|
|
31
31
|
|
|
32
32
|
export interface RunPlayOptions {
|
|
33
33
|
events: EventBus;
|
|
34
|
-
|
|
34
|
+
flowId: string;
|
|
35
35
|
nodeId: string;
|
|
36
36
|
/** Project root (`<repoPath>`). Script resolves under `<cwd>/.seeflow/`. */
|
|
37
37
|
cwd: string;
|
|
@@ -103,24 +103,24 @@ async function writeStdinPayload(handle: SpawnHandle, input: unknown): Promise<v
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
// Live play-script handles indexed by
|
|
106
|
+
// Live play-script handles indexed by flowId. Populated by runPlay() on spawn;
|
|
107
107
|
// entries are removed when each handle's `exited` promise resolves (success
|
|
108
|
-
// AND error paths). `stopAllPlays(
|
|
108
|
+
// AND error paths). `stopAllPlays(flowId)` consults this map to terminate
|
|
109
109
|
// every in-flight play for a demo on /reset.
|
|
110
110
|
const livePlayHandles = new Map<string, Set<SpawnHandle>>();
|
|
111
111
|
|
|
112
|
-
function registerLiveHandle(
|
|
113
|
-
let set = livePlayHandles.get(
|
|
112
|
+
function registerLiveHandle(flowId: string, handle: SpawnHandle): void {
|
|
113
|
+
let set = livePlayHandles.get(flowId);
|
|
114
114
|
if (!set) {
|
|
115
115
|
set = new Set();
|
|
116
|
-
livePlayHandles.set(
|
|
116
|
+
livePlayHandles.set(flowId, set);
|
|
117
117
|
}
|
|
118
118
|
set.add(handle);
|
|
119
119
|
handle.exited.finally(() => {
|
|
120
|
-
const current = livePlayHandles.get(
|
|
120
|
+
const current = livePlayHandles.get(flowId);
|
|
121
121
|
if (!current) return;
|
|
122
122
|
current.delete(handle);
|
|
123
|
-
if (current.size === 0) livePlayHandles.delete(
|
|
123
|
+
if (current.size === 0) livePlayHandles.delete(flowId);
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -138,21 +138,21 @@ async function killWithGrace(handle: SpawnHandle): Promise<void> {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
// Kill every live play-script for `
|
|
142
|
-
// parallel) and wait for each to exit. Idempotent on an unknown
|
|
143
|
-
// map is keyed by
|
|
144
|
-
export async function stopAllPlays(
|
|
145
|
-
const set = livePlayHandles.get(
|
|
141
|
+
// Kill every live play-script for `flowId` (SIGTERM → 2s grace → SIGKILL in
|
|
142
|
+
// parallel) and wait for each to exit. Idempotent on an unknown flowId. The
|
|
143
|
+
// map is keyed by flowId so a stop on demo A never touches demo B.
|
|
144
|
+
export async function stopAllPlays(flowId: string): Promise<void> {
|
|
145
|
+
const set = livePlayHandles.get(flowId);
|
|
146
146
|
if (!set || set.size === 0) return;
|
|
147
147
|
const handles = [...set];
|
|
148
148
|
// Clear eagerly so a parallel runPlay can't double-count an entry we're
|
|
149
149
|
// about to await on. The exited.finally() will no-op the second delete.
|
|
150
|
-
livePlayHandles.delete(
|
|
150
|
+
livePlayHandles.delete(flowId);
|
|
151
151
|
await Promise.all(handles.map((h) => killWithGrace(h)));
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
|
|
155
|
-
const { events,
|
|
155
|
+
const { events, flowId, nodeId, cwd, action } = options;
|
|
156
156
|
const spawner = options.spawner ?? defaultProcessSpawner;
|
|
157
157
|
const runId = crypto.randomUUID();
|
|
158
158
|
|
|
@@ -160,7 +160,7 @@ export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
|
|
|
160
160
|
if (!resolved.ok) {
|
|
161
161
|
events.broadcast({
|
|
162
162
|
type: 'node:error',
|
|
163
|
-
|
|
163
|
+
flowId,
|
|
164
164
|
payload: { nodeId, runId, message: SCRIPT_PATH_ESCAPE },
|
|
165
165
|
});
|
|
166
166
|
return { runId, error: SCRIPT_PATH_ESCAPE };
|
|
@@ -168,7 +168,7 @@ export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
|
|
|
168
168
|
|
|
169
169
|
events.broadcast({
|
|
170
170
|
type: 'node:running',
|
|
171
|
-
|
|
171
|
+
flowId,
|
|
172
172
|
payload: {
|
|
173
173
|
nodeId,
|
|
174
174
|
runId,
|
|
@@ -179,7 +179,7 @@ export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
|
|
|
179
179
|
|
|
180
180
|
const wantsStdin = action.input !== undefined;
|
|
181
181
|
const env = buildChildEnv({
|
|
182
|
-
SEEFLOW_DEMO_ID:
|
|
182
|
+
SEEFLOW_DEMO_ID: flowId,
|
|
183
183
|
SEEFLOW_NODE_ID: nodeId,
|
|
184
184
|
SEEFLOW_RUN_ID: runId,
|
|
185
185
|
});
|
|
@@ -196,13 +196,13 @@ export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
|
|
|
196
196
|
const message = err instanceof Error ? err.message : String(err);
|
|
197
197
|
events.broadcast({
|
|
198
198
|
type: 'node:error',
|
|
199
|
-
|
|
199
|
+
flowId,
|
|
200
200
|
payload: { nodeId, runId, message },
|
|
201
201
|
});
|
|
202
202
|
return { runId, error: message };
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
registerLiveHandle(
|
|
205
|
+
registerLiveHandle(flowId, handle);
|
|
206
206
|
|
|
207
207
|
// Drain stdout AND stderr CONCURRENTLY with the process running so OS pipe
|
|
208
208
|
// buffers (~64 KB) don't fill up and deadlock the child.
|
|
@@ -242,7 +242,7 @@ export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
|
|
|
242
242
|
const message = `script timed out after ${timeoutMs}ms`;
|
|
243
243
|
events.broadcast({
|
|
244
244
|
type: 'node:error',
|
|
245
|
-
|
|
245
|
+
flowId,
|
|
246
246
|
payload: { nodeId, runId, message },
|
|
247
247
|
});
|
|
248
248
|
return { runId, error: message };
|
|
@@ -260,7 +260,7 @@ export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
|
|
|
260
260
|
}
|
|
261
261
|
events.broadcast({
|
|
262
262
|
type: 'node:done',
|
|
263
|
-
|
|
263
|
+
flowId,
|
|
264
264
|
payload: { nodeId, runId, status: 200, body },
|
|
265
265
|
});
|
|
266
266
|
return { runId, status: 200, body };
|
|
@@ -271,7 +271,7 @@ export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
|
|
|
271
271
|
const message = truncated.length > 0 ? truncated : `script exited with code ${code}`;
|
|
272
272
|
events.broadcast({
|
|
273
273
|
type: 'node:error',
|
|
274
|
-
|
|
274
|
+
flowId,
|
|
275
275
|
payload: { nodeId, runId, message },
|
|
276
276
|
});
|
|
277
277
|
return { runId, error: message };
|
|
@@ -279,7 +279,7 @@ export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
|
|
|
279
279
|
|
|
280
280
|
export interface RunResetOptions {
|
|
281
281
|
events: EventBus;
|
|
282
|
-
|
|
282
|
+
flowId: string;
|
|
283
283
|
/** Project root (`<repoPath>`). Script resolves under `<cwd>/.seeflow/`. */
|
|
284
284
|
cwd: string;
|
|
285
285
|
action: ResetAction;
|
|
@@ -300,21 +300,21 @@ export interface ResetResult {
|
|
|
300
300
|
// returned shape. Callers (the /reset endpoint) decide what HTTP status to
|
|
301
301
|
// surface; this returns `{ ok }` plus body/error so the endpoint can map.
|
|
302
302
|
export async function runReset(options: RunResetOptions): Promise<ResetResult> {
|
|
303
|
-
const { events,
|
|
303
|
+
const { events, flowId, cwd, action } = options;
|
|
304
304
|
const spawner = options.spawner ?? defaultProcessSpawner;
|
|
305
305
|
|
|
306
306
|
const resolved = resolveScript(cwd, action.scriptPath);
|
|
307
307
|
if (!resolved.ok) {
|
|
308
308
|
events.broadcast({
|
|
309
309
|
type: 'demo:reset',
|
|
310
|
-
|
|
310
|
+
flowId,
|
|
311
311
|
payload: { ok: false, error: SCRIPT_PATH_ESCAPE },
|
|
312
312
|
});
|
|
313
313
|
return { ok: false, error: SCRIPT_PATH_ESCAPE };
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
const wantsStdin = action.input !== undefined;
|
|
317
|
-
const env = buildChildEnv({ SEEFLOW_DEMO_ID:
|
|
317
|
+
const env = buildChildEnv({ SEEFLOW_DEMO_ID: flowId });
|
|
318
318
|
|
|
319
319
|
let handle: SpawnHandle;
|
|
320
320
|
try {
|
|
@@ -328,7 +328,7 @@ export async function runReset(options: RunResetOptions): Promise<ResetResult> {
|
|
|
328
328
|
const message = err instanceof Error ? err.message : String(err);
|
|
329
329
|
events.broadcast({
|
|
330
330
|
type: 'demo:reset',
|
|
331
|
-
|
|
331
|
+
flowId,
|
|
332
332
|
payload: { ok: false, error: message },
|
|
333
333
|
});
|
|
334
334
|
return { ok: false, error: message };
|
|
@@ -357,7 +357,7 @@ export async function runReset(options: RunResetOptions): Promise<ResetResult> {
|
|
|
357
357
|
const message = `reset script timed out after ${timeoutMs}ms`;
|
|
358
358
|
events.broadcast({
|
|
359
359
|
type: 'demo:reset',
|
|
360
|
-
|
|
360
|
+
flowId,
|
|
361
361
|
payload: { ok: false, error: message },
|
|
362
362
|
});
|
|
363
363
|
return { ok: false, error: message };
|
|
@@ -375,7 +375,7 @@ export async function runReset(options: RunResetOptions): Promise<ResetResult> {
|
|
|
375
375
|
}
|
|
376
376
|
events.broadcast({
|
|
377
377
|
type: 'demo:reset',
|
|
378
|
-
|
|
378
|
+
flowId,
|
|
379
379
|
payload: { ok: true, body },
|
|
380
380
|
});
|
|
381
381
|
return { ok: true, body };
|
|
@@ -386,7 +386,7 @@ export async function runReset(options: RunResetOptions): Promise<ResetResult> {
|
|
|
386
386
|
const message = truncated.length > 0 ? truncated : `reset script exited with code ${code}`;
|
|
387
387
|
events.broadcast({
|
|
388
388
|
type: 'demo:reset',
|
|
389
|
-
|
|
389
|
+
flowId,
|
|
390
390
|
payload: { ok: false, error: message },
|
|
391
391
|
});
|
|
392
392
|
return { ok: false, error: message };
|
package/src/registry.ts
CHANGED
|
@@ -2,12 +2,12 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { seeflowHome } from './paths.ts';
|
|
4
4
|
|
|
5
|
-
export interface
|
|
5
|
+
export interface FlowEntry {
|
|
6
6
|
id: string;
|
|
7
7
|
slug: string;
|
|
8
8
|
name: string;
|
|
9
9
|
repoPath: string;
|
|
10
|
-
|
|
10
|
+
architecturePath: string;
|
|
11
11
|
lastModified: number;
|
|
12
12
|
valid: boolean;
|
|
13
13
|
}
|
|
@@ -15,18 +15,21 @@ export interface DemoEntry {
|
|
|
15
15
|
export interface RegisterInput {
|
|
16
16
|
name: string;
|
|
17
17
|
repoPath: string;
|
|
18
|
-
|
|
18
|
+
architecturePath: string;
|
|
19
19
|
valid?: boolean;
|
|
20
20
|
lastModified?: number;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface Registry {
|
|
24
|
-
list():
|
|
25
|
-
getById(id: string):
|
|
26
|
-
getBySlug(slug: string):
|
|
27
|
-
getByRepoPath(repoPath: string):
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
list(): FlowEntry[];
|
|
25
|
+
getById(id: string): FlowEntry | undefined;
|
|
26
|
+
getBySlug(slug: string): FlowEntry | undefined;
|
|
27
|
+
getByRepoPath(repoPath: string): FlowEntry | undefined;
|
|
28
|
+
getByRepoPathAndArchitecturePath(
|
|
29
|
+
repoPath: string,
|
|
30
|
+
architecturePath: string,
|
|
31
|
+
): FlowEntry | undefined;
|
|
32
|
+
upsert(input: RegisterInput): FlowEntry;
|
|
30
33
|
remove(id: string): boolean;
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -44,7 +47,7 @@ export function slugify(name: string): string {
|
|
|
44
47
|
|
|
45
48
|
export function createRegistry(options: { path?: string } = {}): Registry {
|
|
46
49
|
const path = options.path ?? defaultRegistryPath();
|
|
47
|
-
const entries = new Map<string,
|
|
50
|
+
const entries = new Map<string, FlowEntry>();
|
|
48
51
|
|
|
49
52
|
if (existsSync(path)) {
|
|
50
53
|
try {
|
|
@@ -57,7 +60,13 @@ export function createRegistry(options: { path?: string } = {}): Registry {
|
|
|
57
60
|
typeof e.slug === 'string' &&
|
|
58
61
|
typeof e.repoPath === 'string'
|
|
59
62
|
) {
|
|
60
|
-
|
|
63
|
+
if (typeof e.architecturePath !== 'string') {
|
|
64
|
+
console.warn(
|
|
65
|
+
`[registry] ignoring legacy entry ${e.id} (${e.slug}) — pre-split format, please re-register`,
|
|
66
|
+
);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
entries.set(e.id, e as FlowEntry);
|
|
61
70
|
}
|
|
62
71
|
}
|
|
63
72
|
}
|
|
@@ -71,16 +80,19 @@ export function createRegistry(options: { path?: string } = {}): Registry {
|
|
|
71
80
|
writeFileSync(path, JSON.stringify([...entries.values()], null, 2));
|
|
72
81
|
};
|
|
73
82
|
|
|
74
|
-
const findByRepoPath = (repoPath: string):
|
|
83
|
+
const findByRepoPath = (repoPath: string): FlowEntry | undefined => {
|
|
75
84
|
for (const e of entries.values()) {
|
|
76
85
|
if (e.repoPath === repoPath) return e;
|
|
77
86
|
}
|
|
78
87
|
return undefined;
|
|
79
88
|
};
|
|
80
89
|
|
|
81
|
-
const
|
|
90
|
+
const findByRepoPathAndArchitecturePath = (
|
|
91
|
+
repoPath: string,
|
|
92
|
+
architecturePath: string,
|
|
93
|
+
): FlowEntry | undefined => {
|
|
82
94
|
for (const e of entries.values()) {
|
|
83
|
-
if (e.repoPath === repoPath && e.
|
|
95
|
+
if (e.repoPath === repoPath && e.architecturePath === architecturePath) return e;
|
|
84
96
|
}
|
|
85
97
|
return undefined;
|
|
86
98
|
};
|
|
@@ -98,16 +110,16 @@ export function createRegistry(options: { path?: string } = {}): Registry {
|
|
|
98
110
|
getById: (id) => entries.get(id),
|
|
99
111
|
getBySlug: (slug) => [...entries.values()].find((e) => e.slug === slug),
|
|
100
112
|
getByRepoPath: findByRepoPath,
|
|
101
|
-
|
|
113
|
+
getByRepoPathAndArchitecturePath: findByRepoPathAndArchitecturePath,
|
|
102
114
|
upsert(input) {
|
|
103
115
|
const lastModified = input.lastModified ?? Date.now();
|
|
104
116
|
const valid = input.valid ?? true;
|
|
105
|
-
const existing =
|
|
117
|
+
const existing = findByRepoPathAndArchitecturePath(input.repoPath, input.architecturePath);
|
|
106
118
|
if (existing) {
|
|
107
|
-
const updated:
|
|
119
|
+
const updated: FlowEntry = {
|
|
108
120
|
...existing,
|
|
109
121
|
name: input.name,
|
|
110
|
-
|
|
122
|
+
architecturePath: input.architecturePath,
|
|
111
123
|
lastModified,
|
|
112
124
|
valid,
|
|
113
125
|
};
|
|
@@ -117,12 +129,12 @@ export function createRegistry(options: { path?: string } = {}): Registry {
|
|
|
117
129
|
}
|
|
118
130
|
const id = crypto.randomUUID();
|
|
119
131
|
const slug = uniqueSlug(slugify(input.name));
|
|
120
|
-
const entry:
|
|
132
|
+
const entry: FlowEntry = {
|
|
121
133
|
id,
|
|
122
134
|
slug,
|
|
123
135
|
name: input.name,
|
|
124
136
|
repoPath: input.repoPath,
|
|
125
|
-
|
|
137
|
+
architecturePath: input.architecturePath,
|
|
126
138
|
lastModified,
|
|
127
139
|
valid,
|
|
128
140
|
};
|
package/src/schema.ts
CHANGED
|
@@ -396,21 +396,21 @@ const ConnectorSchema = z.discriminatedUnion('kind', [
|
|
|
396
396
|
DefaultConnectorSchema,
|
|
397
397
|
]);
|
|
398
398
|
|
|
399
|
-
export const
|
|
399
|
+
export const FlowSchema = z
|
|
400
400
|
.object({
|
|
401
|
-
version: z.literal(
|
|
401
|
+
version: z.literal(2),
|
|
402
402
|
name: z.string().min(1),
|
|
403
403
|
nodes: z.array(NodeSchema),
|
|
404
404
|
connectors: z.array(ConnectorSchema),
|
|
405
405
|
// Optional one-shot script the studio runs when the user clicks Restart.
|
|
406
406
|
// Lets the running app wipe its own in-memory state. The studio kills
|
|
407
|
-
// every live play + status script for the
|
|
407
|
+
// every live play + status script for the flow BEFORE invoking this
|
|
408
408
|
// script (US-008), so the script sees no stragglers.
|
|
409
409
|
resetAction: ResetActionSchema.optional(),
|
|
410
410
|
})
|
|
411
|
-
.superRefine((
|
|
412
|
-
const nodeIds = new Set(
|
|
413
|
-
|
|
411
|
+
.superRefine((flow, ctx) => {
|
|
412
|
+
const nodeIds = new Set(flow.nodes.map((n) => n.id));
|
|
413
|
+
flow.connectors.forEach((c, idx) => {
|
|
414
414
|
if (!nodeIds.has(c.source)) {
|
|
415
415
|
ctx.addIssue({
|
|
416
416
|
code: z.ZodIssueCode.custom,
|
|
@@ -428,8 +428,8 @@ export const DemoSchema = z
|
|
|
428
428
|
});
|
|
429
429
|
});
|
|
430
430
|
|
|
431
|
-
export type
|
|
432
|
-
export type
|
|
431
|
+
export type Flow = z.infer<typeof FlowSchema>;
|
|
432
|
+
export type FlowNode = z.infer<typeof NodeSchema>;
|
|
433
433
|
export type ShapeNode = z.infer<typeof ShapeNodeSchema>;
|
|
434
434
|
export type ImageNode = z.infer<typeof ImageNodeSchema>;
|
|
435
435
|
export type IconNode = z.infer<typeof IconNodeSchema>;
|
|
@@ -452,3 +452,247 @@ export type StatusAction = z.infer<typeof StatusActionSchema>;
|
|
|
452
452
|
export type StatusReport = z.infer<typeof StatusReportSchema>;
|
|
453
453
|
export type ResetAction = z.infer<typeof ResetActionSchema>;
|
|
454
454
|
export type StateSource = z.infer<typeof StateSourceSchema>;
|
|
455
|
+
|
|
456
|
+
// =============================================================================
|
|
457
|
+
// Architecture schema — pure semantic data, every visual/layout field stripped.
|
|
458
|
+
// What lives on disk in <project>/.seeflow/architecture.json after the split.
|
|
459
|
+
// =============================================================================
|
|
460
|
+
|
|
461
|
+
const ArchitectureNodeDataBaseShape = {
|
|
462
|
+
name: z.string().min(1),
|
|
463
|
+
kind: z.string().min(1),
|
|
464
|
+
stateSource: StateSourceSchema,
|
|
465
|
+
handlerModule: z.string().optional(),
|
|
466
|
+
icon: z.string().optional(),
|
|
467
|
+
...NodeDescriptionBaseShape,
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const ArchitecturePlayNodeDataSchema = z
|
|
471
|
+
.object({
|
|
472
|
+
...ArchitectureNodeDataBaseShape,
|
|
473
|
+
playAction: PlayActionSchema,
|
|
474
|
+
statusAction: StatusActionSchema.optional(),
|
|
475
|
+
})
|
|
476
|
+
.strict();
|
|
477
|
+
|
|
478
|
+
const ArchitectureStateNodeDataSchema = z
|
|
479
|
+
.object({
|
|
480
|
+
...ArchitectureNodeDataBaseShape,
|
|
481
|
+
playAction: PlayActionSchema.optional(),
|
|
482
|
+
statusAction: StatusActionSchema.optional(),
|
|
483
|
+
})
|
|
484
|
+
.strict();
|
|
485
|
+
|
|
486
|
+
const ArchitectureShapeNodeDataSchema = z
|
|
487
|
+
.object({
|
|
488
|
+
shape: ShapeKindSchema,
|
|
489
|
+
name: z.string().optional(),
|
|
490
|
+
...NodeDescriptionBaseShape,
|
|
491
|
+
})
|
|
492
|
+
.strict();
|
|
493
|
+
|
|
494
|
+
const ArchitectureImageNodeDataSchema = z
|
|
495
|
+
.object({
|
|
496
|
+
path: z.string().min(1).refine(isCleanRelativePath, {
|
|
497
|
+
message: 'path must be a relative path under .seeflow/ (no absolute / traversal)',
|
|
498
|
+
}),
|
|
499
|
+
alt: z.string().optional(),
|
|
500
|
+
...NodeDescriptionBaseShape,
|
|
501
|
+
})
|
|
502
|
+
.strict();
|
|
503
|
+
|
|
504
|
+
const ArchitectureIconNodeDataSchema = z
|
|
505
|
+
.object({
|
|
506
|
+
icon: z.string().min(1),
|
|
507
|
+
alt: z.string().optional(),
|
|
508
|
+
name: z.string().optional(),
|
|
509
|
+
...NodeDescriptionBaseShape,
|
|
510
|
+
})
|
|
511
|
+
.strict();
|
|
512
|
+
|
|
513
|
+
const ArchitectureHtmlNodeDataSchema = z
|
|
514
|
+
.object({
|
|
515
|
+
htmlPath: z.string().min(1).refine(isCleanRelativePath, {
|
|
516
|
+
message: 'htmlPath must be a relative path under .seeflow/ (no absolute / traversal)',
|
|
517
|
+
}),
|
|
518
|
+
name: z.string().optional(),
|
|
519
|
+
icon: z.string().optional(),
|
|
520
|
+
...NodeDescriptionBaseShape,
|
|
521
|
+
})
|
|
522
|
+
.strict();
|
|
523
|
+
|
|
524
|
+
const ArchitectureNodeBaseShape = {
|
|
525
|
+
id: z.string().min(1),
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const ArchitectureNodeSchema = z.discriminatedUnion('type', [
|
|
529
|
+
z
|
|
530
|
+
.object({
|
|
531
|
+
...ArchitectureNodeBaseShape,
|
|
532
|
+
type: z.literal('playNode'),
|
|
533
|
+
data: ArchitecturePlayNodeDataSchema,
|
|
534
|
+
})
|
|
535
|
+
.strict(),
|
|
536
|
+
z
|
|
537
|
+
.object({
|
|
538
|
+
...ArchitectureNodeBaseShape,
|
|
539
|
+
type: z.literal('stateNode'),
|
|
540
|
+
data: ArchitectureStateNodeDataSchema,
|
|
541
|
+
})
|
|
542
|
+
.strict(),
|
|
543
|
+
z
|
|
544
|
+
.object({
|
|
545
|
+
...ArchitectureNodeBaseShape,
|
|
546
|
+
type: z.literal('shapeNode'),
|
|
547
|
+
data: ArchitectureShapeNodeDataSchema,
|
|
548
|
+
})
|
|
549
|
+
.strict(),
|
|
550
|
+
z
|
|
551
|
+
.object({
|
|
552
|
+
...ArchitectureNodeBaseShape,
|
|
553
|
+
type: z.literal('imageNode'),
|
|
554
|
+
data: ArchitectureImageNodeDataSchema,
|
|
555
|
+
})
|
|
556
|
+
.strict(),
|
|
557
|
+
z
|
|
558
|
+
.object({
|
|
559
|
+
...ArchitectureNodeBaseShape,
|
|
560
|
+
type: z.literal('iconNode'),
|
|
561
|
+
data: ArchitectureIconNodeDataSchema,
|
|
562
|
+
})
|
|
563
|
+
.strict(),
|
|
564
|
+
z
|
|
565
|
+
.object({
|
|
566
|
+
...ArchitectureNodeBaseShape,
|
|
567
|
+
type: z.literal('htmlNode'),
|
|
568
|
+
data: ArchitectureHtmlNodeDataSchema,
|
|
569
|
+
})
|
|
570
|
+
.strict(),
|
|
571
|
+
]);
|
|
572
|
+
|
|
573
|
+
const ArchitectureConnectorBaseShape = {
|
|
574
|
+
id: z.string().min(1),
|
|
575
|
+
source: z.string().min(1),
|
|
576
|
+
target: z.string().min(1),
|
|
577
|
+
label: z.string().optional(),
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const ArchitectureConnectorSchema = z.discriminatedUnion('kind', [
|
|
581
|
+
z
|
|
582
|
+
.object({
|
|
583
|
+
...ArchitectureConnectorBaseShape,
|
|
584
|
+
kind: z.literal('http'),
|
|
585
|
+
method: HttpMethodSchema.optional(),
|
|
586
|
+
url: z.string().min(1).optional(),
|
|
587
|
+
})
|
|
588
|
+
.strict(),
|
|
589
|
+
z
|
|
590
|
+
.object({
|
|
591
|
+
...ArchitectureConnectorBaseShape,
|
|
592
|
+
kind: z.literal('event'),
|
|
593
|
+
eventName: z.string().min(1),
|
|
594
|
+
})
|
|
595
|
+
.strict(),
|
|
596
|
+
z
|
|
597
|
+
.object({
|
|
598
|
+
...ArchitectureConnectorBaseShape,
|
|
599
|
+
kind: z.literal('queue'),
|
|
600
|
+
queueName: z.string().min(1),
|
|
601
|
+
})
|
|
602
|
+
.strict(),
|
|
603
|
+
z
|
|
604
|
+
.object({
|
|
605
|
+
...ArchitectureConnectorBaseShape,
|
|
606
|
+
kind: z.literal('default'),
|
|
607
|
+
})
|
|
608
|
+
.strict(),
|
|
609
|
+
]);
|
|
610
|
+
|
|
611
|
+
export const ArchitectureSchema = z
|
|
612
|
+
.object({
|
|
613
|
+
version: z.literal(2),
|
|
614
|
+
name: z.string().min(1),
|
|
615
|
+
resetAction: ResetActionSchema.optional(),
|
|
616
|
+
nodes: z.array(ArchitectureNodeSchema),
|
|
617
|
+
connectors: z.array(ArchitectureConnectorSchema),
|
|
618
|
+
})
|
|
619
|
+
.strict()
|
|
620
|
+
.superRefine((arch, ctx) => {
|
|
621
|
+
const ids = new Set(arch.nodes.map((n) => n.id));
|
|
622
|
+
arch.connectors.forEach((c, idx) => {
|
|
623
|
+
if (!ids.has(c.source)) {
|
|
624
|
+
ctx.addIssue({
|
|
625
|
+
code: z.ZodIssueCode.custom,
|
|
626
|
+
path: ['connectors', idx, 'source'],
|
|
627
|
+
message: `Connector ${c.id} references unknown source node: ${c.source}`,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
if (!ids.has(c.target)) {
|
|
631
|
+
ctx.addIssue({
|
|
632
|
+
code: z.ZodIssueCode.custom,
|
|
633
|
+
path: ['connectors', idx, 'target'],
|
|
634
|
+
message: `Connector ${c.id} references unknown target node: ${c.target}`,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
export type Architecture = z.infer<typeof ArchitectureSchema>;
|
|
641
|
+
export type ArchitectureNode = z.infer<typeof ArchitectureNodeSchema>;
|
|
642
|
+
export type ArchitectureConnector = z.infer<typeof ArchitectureConnectorSchema>;
|
|
643
|
+
|
|
644
|
+
// =============================================================================
|
|
645
|
+
// Style schema — keyed map of presentation overrides, side-table by id.
|
|
646
|
+
// What lives on disk in <project>/.seeflow/style.json (optional file).
|
|
647
|
+
// =============================================================================
|
|
648
|
+
|
|
649
|
+
const NodeStyleSchema = z
|
|
650
|
+
.object({
|
|
651
|
+
position: PositionSchema.optional(),
|
|
652
|
+
width: z.number().positive().optional(),
|
|
653
|
+
height: z.number().positive().optional(),
|
|
654
|
+
borderColor: ColorTokenSchema.optional(),
|
|
655
|
+
backgroundColor: ColorTokenSchema.optional(),
|
|
656
|
+
borderSize: z.number().positive().optional(),
|
|
657
|
+
borderStyle: z.enum(['solid', 'dashed', 'dotted']).optional(),
|
|
658
|
+
fontSize: z.number().positive().optional(),
|
|
659
|
+
textColor: ColorTokenSchema.optional(),
|
|
660
|
+
cornerRadius: z.number().min(0).optional(),
|
|
661
|
+
locked: z.boolean().optional(),
|
|
662
|
+
// imageNode-specific
|
|
663
|
+
borderWidth: z.number().min(1).max(8).optional(),
|
|
664
|
+
// iconNode-specific
|
|
665
|
+
color: ColorTokenSchema.optional(),
|
|
666
|
+
strokeWidth: z.number().min(0.5).max(4).optional(),
|
|
667
|
+
// htmlNode-specific
|
|
668
|
+
autoSize: z.boolean().optional(),
|
|
669
|
+
})
|
|
670
|
+
.strict();
|
|
671
|
+
|
|
672
|
+
const ConnectorStyleEntrySchema = z
|
|
673
|
+
.object({
|
|
674
|
+
sourceHandle: SourceHandleIdSchema.optional(),
|
|
675
|
+
targetHandle: TargetHandleIdSchema.optional(),
|
|
676
|
+
sourceHandleAutoPicked: z.boolean().optional(),
|
|
677
|
+
targetHandleAutoPicked: z.boolean().optional(),
|
|
678
|
+
sourcePin: EdgePinSchema.optional(),
|
|
679
|
+
targetPin: EdgePinSchema.optional(),
|
|
680
|
+
style: ConnectorStyleSchema.optional(),
|
|
681
|
+
color: ColorTokenSchema.optional(),
|
|
682
|
+
direction: ConnectorDirectionSchema.optional(),
|
|
683
|
+
borderSize: z.number().positive().optional(),
|
|
684
|
+
path: ConnectorPathSchema.optional(),
|
|
685
|
+
fontSize: z.number().positive().optional(),
|
|
686
|
+
})
|
|
687
|
+
.strict();
|
|
688
|
+
|
|
689
|
+
export const StyleSchema = z
|
|
690
|
+
.object({
|
|
691
|
+
nodes: z.record(z.string(), NodeStyleSchema).optional(),
|
|
692
|
+
connectors: z.record(z.string(), ConnectorStyleEntrySchema).optional(),
|
|
693
|
+
})
|
|
694
|
+
.strict();
|
|
695
|
+
|
|
696
|
+
export type Style = z.infer<typeof StyleSchema>;
|
|
697
|
+
export type NodeStyle = z.infer<typeof NodeStyleSchema>;
|
|
698
|
+
export type ConnectorStyleEntry = z.infer<typeof ConnectorStyleEntrySchema>;
|
package/src/sdk-template.ts
CHANGED
|
@@ -20,7 +20,7 @@ const readEnv = (key: string): string | undefined => {
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
export async function emit(
|
|
23
|
-
|
|
23
|
+
flowId: string,
|
|
24
24
|
nodeId: string,
|
|
25
25
|
status: EmitStatus,
|
|
26
26
|
opts: EmitOptions = {},
|
|
@@ -31,7 +31,7 @@ export async function emit(
|
|
|
31
31
|
await fetch(\`\${base}/api/emit\`, {
|
|
32
32
|
method: 'POST',
|
|
33
33
|
headers: { 'content-type': 'application/json' },
|
|
34
|
-
body: JSON.stringify({
|
|
34
|
+
body: JSON.stringify({ flowId, nodeId, status, runId: opts.runId, payload: opts.payload }),
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
`;
|
package/src/sdk-writer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import type {
|
|
3
|
+
import type { Flow } from './schema.ts';
|
|
4
4
|
import { EMIT_TEMPLATE } from './sdk-template.ts';
|
|
5
5
|
|
|
6
6
|
export type SdkWriteOutcome = 'skipped' | 'written' | 'present';
|
|
@@ -16,7 +16,7 @@ export interface SdkWriteResult {
|
|
|
16
16
|
* node with `stateSource.kind === 'event'`. Idempotent: existing files are
|
|
17
17
|
* never overwritten. The only place M1's CLI mutates a user repo.
|
|
18
18
|
*/
|
|
19
|
-
export function writeSdkEmitIfNeeded(repoPath: string, demo:
|
|
19
|
+
export function writeSdkEmitIfNeeded(repoPath: string, demo: Flow): SdkWriteResult {
|
|
20
20
|
const hasEventState = demo.nodes.some(
|
|
21
21
|
(n) =>
|
|
22
22
|
n.type !== 'shapeNode' &&
|