@tuongaz/seeflow 0.1.26 → 0.1.27
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-BMaMEi2a.js → index-CdNWAi1U.js} +4 -4
- package/dist/web/assets/{index.es-M1iBDKG6.js → index.es-CPyvUCV3.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-xZpq8bcn.js → jspdf.es.min-Dkq0NSxE.js} +3 -3
- package/dist/web/index.html +1 -1
- package/examples/ecommerce-platform/.seeflow/{seeflow.json → architecture.json} +14 -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/style.json +85 -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 +1 -1
- 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 +2 -2
- package/src/status-runner.ts +34 -38
- package/src/watcher.ts +165 -114
- package/examples/order-pipeline/.seeflow/seeflow.json +0 -123
package/src/cli.ts
CHANGED
|
@@ -15,11 +15,11 @@ import {
|
|
|
15
15
|
writeConfig,
|
|
16
16
|
writePid,
|
|
17
17
|
} from './runtime.ts';
|
|
18
|
-
import {
|
|
18
|
+
import { ArchitectureSchema, FlowSchema } from './schema.ts';
|
|
19
19
|
import { serve } from './server.ts';
|
|
20
20
|
import { createStatusRunner } from './status-runner.ts';
|
|
21
21
|
|
|
22
|
-
const
|
|
22
|
+
const DEFAULT_ARCHITECTURE_PATH = '.seeflow/architecture.json';
|
|
23
23
|
const HEALTH_TIMEOUT_MS = 10_000;
|
|
24
24
|
const HEALTH_POLL_INTERVAL_MS = 150;
|
|
25
25
|
|
|
@@ -75,7 +75,7 @@ Options (start):
|
|
|
75
75
|
Options (register):
|
|
76
76
|
--path <dir> Path to repo root (default: current directory)
|
|
77
77
|
--flow <file> Path to flow JSON, relative to repo root
|
|
78
|
-
(default: .seeflow/
|
|
78
|
+
(default: .seeflow/architecture.json)
|
|
79
79
|
--no-start Fail if studio is not already running
|
|
80
80
|
|
|
81
81
|
Examples:
|
|
@@ -140,7 +140,7 @@ async function seedExamples(registry: Registry) {
|
|
|
140
140
|
|
|
141
141
|
async function seedExample(registry: Registry, exampleName: string) {
|
|
142
142
|
const destDir = join(seeflowHome(), exampleName);
|
|
143
|
-
const
|
|
143
|
+
const architecturePath = '.seeflow/architecture.json';
|
|
144
144
|
|
|
145
145
|
// Always sync from source so that schema changes and example updates are
|
|
146
146
|
// reflected on every startup, even when the dest directory already exists.
|
|
@@ -148,9 +148,9 @@ async function seedExample(registry: Registry, exampleName: string) {
|
|
|
148
148
|
if (!existsSync(srcDir)) return;
|
|
149
149
|
cpSync(srcDir, destDir, { recursive: true });
|
|
150
150
|
|
|
151
|
-
if (registry.
|
|
151
|
+
if (registry.getByRepoPathAndArchitecturePath(destDir, architecturePath)) return;
|
|
152
152
|
|
|
153
|
-
const flowFile = join(destDir,
|
|
153
|
+
const flowFile = join(destDir, architecturePath);
|
|
154
154
|
if (!existsSync(flowFile)) return;
|
|
155
155
|
|
|
156
156
|
let demo: unknown;
|
|
@@ -160,10 +160,10 @@ async function seedExample(registry: Registry, exampleName: string) {
|
|
|
160
160
|
return;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
const parsed =
|
|
163
|
+
const parsed = ArchitectureSchema.safeParse(demo);
|
|
164
164
|
if (!parsed.success) return;
|
|
165
165
|
|
|
166
|
-
registry.upsert({ name: parsed.data.name, repoPath: destDir,
|
|
166
|
+
registry.upsert({ name: parsed.data.name, repoPath: destDir, architecturePath });
|
|
167
167
|
console.log(`Seeded example: ${parsed.data.name} → ${destDir}`);
|
|
168
168
|
}
|
|
169
169
|
|
|
@@ -217,7 +217,7 @@ function runStop() {
|
|
|
217
217
|
|
|
218
218
|
async function runRegister() {
|
|
219
219
|
const repoPath = resolve(flagValue('path') ?? '.');
|
|
220
|
-
const demoPathArg = flagValue('flow') ??
|
|
220
|
+
const demoPathArg = flagValue('flow') ?? DEFAULT_ARCHITECTURE_PATH;
|
|
221
221
|
const noStart = hasFlag('no-start');
|
|
222
222
|
const config = readConfig();
|
|
223
223
|
const overrideUrl = process.env.SEEFLOW_STUDIO_URL?.replace(/\/+$/, '');
|
|
@@ -226,7 +226,7 @@ async function runRegister() {
|
|
|
226
226
|
const fullPath = isAbsolute(demoPathArg) ? demoPathArg : join(repoPath, demoPathArg);
|
|
227
227
|
if (!existsSync(fullPath)) {
|
|
228
228
|
console.error(`No demo file at ${fullPath}`);
|
|
229
|
-
console.error(`Create ${
|
|
229
|
+
console.error(`Create ${DEFAULT_ARCHITECTURE_PATH} in your repo, or pass --flow <path>.`);
|
|
230
230
|
process.exit(1);
|
|
231
231
|
}
|
|
232
232
|
|
|
@@ -238,7 +238,7 @@ async function runRegister() {
|
|
|
238
238
|
process.exit(1);
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
const parsed =
|
|
241
|
+
const parsed = ArchitectureSchema.safeParse(demo);
|
|
242
242
|
if (!parsed.success) {
|
|
243
243
|
console.error(`${fullPath} failed schema validation:`);
|
|
244
244
|
for (const issue of parsed.error.issues) {
|
|
@@ -251,13 +251,13 @@ async function runRegister() {
|
|
|
251
251
|
|
|
252
252
|
let res: Response;
|
|
253
253
|
try {
|
|
254
|
-
res = await fetch(`${url}/api/
|
|
254
|
+
res = await fetch(`${url}/api/flows/register`, {
|
|
255
255
|
method: 'POST',
|
|
256
256
|
headers: { 'content-type': 'application/json' },
|
|
257
257
|
body: JSON.stringify({
|
|
258
258
|
name: parsed.data.name,
|
|
259
259
|
repoPath,
|
|
260
|
-
|
|
260
|
+
architecturePath: demoPathArg,
|
|
261
261
|
}),
|
|
262
262
|
});
|
|
263
263
|
} catch (err) {
|
package/src/demo.ts
CHANGED
|
@@ -11,7 +11,7 @@ const delay = (): Promise<void> => new Promise((r) => setTimeout(r, 200 + Math.r
|
|
|
11
11
|
|
|
12
12
|
function nodeEmit(
|
|
13
13
|
events: EventBus,
|
|
14
|
-
|
|
14
|
+
flowId: string,
|
|
15
15
|
runId: string,
|
|
16
16
|
nodeId: string,
|
|
17
17
|
status: keyof typeof STATUS_TO_EVENT,
|
|
@@ -19,17 +19,17 @@ function nodeEmit(
|
|
|
19
19
|
) {
|
|
20
20
|
events.broadcast({
|
|
21
21
|
type: STATUS_TO_EVENT[status],
|
|
22
|
-
|
|
22
|
+
flowId,
|
|
23
23
|
payload: { nodeId, runId, ...payload },
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
async function runOrderPipeline(events: EventBus,
|
|
27
|
+
async function runOrderPipeline(events: EventBus, flowId: string, runId: string, orderId: string) {
|
|
28
28
|
const emit = (
|
|
29
29
|
nodeId: string,
|
|
30
30
|
status: keyof typeof STATUS_TO_EVENT,
|
|
31
31
|
payload?: Record<string, unknown>,
|
|
32
|
-
) => nodeEmit(events,
|
|
32
|
+
) => nodeEmit(events, flowId, runId, nodeId, status, payload);
|
|
33
33
|
|
|
34
34
|
emit('post-orders', 'running');
|
|
35
35
|
await delay();
|
|
@@ -52,11 +52,11 @@ export function createDemoRouter(events: EventBus): Hono {
|
|
|
52
52
|
const app = new Hono();
|
|
53
53
|
|
|
54
54
|
app.post('/orders', async (c) => {
|
|
55
|
-
const
|
|
55
|
+
const flowId = c.req.header('x-seeflow-demo-id') ?? '';
|
|
56
56
|
const runId = c.req.header('x-seeflow-run-id') ?? '';
|
|
57
57
|
const orderId = `ord_${Date.now()}`;
|
|
58
58
|
|
|
59
|
-
void runOrderPipeline(events,
|
|
59
|
+
void runOrderPipeline(events, flowId, runId, orderId);
|
|
60
60
|
|
|
61
61
|
return c.json({ orderId });
|
|
62
62
|
});
|
package/src/diagram.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import dagre from 'dagre';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
3
|
+
import { FlowSchema } from './schema.ts';
|
|
4
4
|
|
|
5
5
|
// Pure-compute helpers backing the three diagram-pipeline endpoints. No file
|
|
6
6
|
// I/O lives here — the skill writes responses to disk on the user's machine.
|
|
@@ -137,7 +137,7 @@ export interface AssembleStats {
|
|
|
137
137
|
|
|
138
138
|
export interface AssembleResult {
|
|
139
139
|
demo: {
|
|
140
|
-
version:
|
|
140
|
+
version: 2;
|
|
141
141
|
name: string;
|
|
142
142
|
nodes: Array<Record<string, unknown>>;
|
|
143
143
|
connectors: Array<Record<string, unknown>>;
|
|
@@ -177,7 +177,7 @@ export const assembleDemo = (req: AssembleRequest): AssembleResult => {
|
|
|
177
177
|
|
|
178
178
|
return {
|
|
179
179
|
demo: {
|
|
180
|
-
version:
|
|
180
|
+
version: 2,
|
|
181
181
|
name: req.wiring.name ?? 'Untitled diagram',
|
|
182
182
|
nodes: positionedNodes,
|
|
183
183
|
connectors,
|
|
@@ -364,7 +364,7 @@ export const validateDemo = (req: ValidateRequest): ValidateReport => {
|
|
|
364
364
|
const issues: ValidateIssue[] = [];
|
|
365
365
|
const warnings: ValidateIssue[] = [];
|
|
366
366
|
|
|
367
|
-
const parsed =
|
|
367
|
+
const parsed = FlowSchema.safeParse(req.demo);
|
|
368
368
|
if (!parsed.success) {
|
|
369
369
|
for (const issue of parsed.error.issues) {
|
|
370
370
|
issues.push({
|
package/src/events.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* In-memory pub/sub keyed by
|
|
2
|
+
* In-memory pub/sub keyed by flowId. Subscribers receive every event published
|
|
3
3
|
* for that demo until they unsubscribe; subscribers for other demos are not
|
|
4
4
|
* notified.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export type StudioEventType =
|
|
8
|
-
| '
|
|
8
|
+
| 'flow:reload'
|
|
9
9
|
| 'demo:reset'
|
|
10
10
|
| 'node:running'
|
|
11
11
|
| 'node:done'
|
|
@@ -15,7 +15,7 @@ export type StudioEventType =
|
|
|
15
15
|
|
|
16
16
|
export interface StudioEvent {
|
|
17
17
|
type: StudioEventType;
|
|
18
|
-
|
|
18
|
+
flowId: string;
|
|
19
19
|
/** Arbitrary JSON-serializable payload. Shape depends on event type. */
|
|
20
20
|
payload: unknown;
|
|
21
21
|
/** Server-side timestamp (ms since epoch). */
|
|
@@ -26,33 +26,33 @@ export type Subscriber = (event: StudioEvent) => void;
|
|
|
26
26
|
|
|
27
27
|
export interface EventBus {
|
|
28
28
|
/** Subscribe to events for a specific demo. Returns an unsubscribe fn. */
|
|
29
|
-
subscribe(
|
|
30
|
-
/** Broadcast an event to all subscribers of `
|
|
29
|
+
subscribe(flowId: string, fn: Subscriber): () => void;
|
|
30
|
+
/** Broadcast an event to all subscribers of `flowId`. */
|
|
31
31
|
broadcast(event: Omit<StudioEvent, 'ts'> & { ts?: number }): void;
|
|
32
32
|
/** Number of active subscribers for a given demo (used in tests). */
|
|
33
|
-
subscriberCount(
|
|
33
|
+
subscriberCount(flowId: string): number;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function createEventBus(): EventBus {
|
|
37
37
|
const subs = new Map<string, Set<Subscriber>>();
|
|
38
38
|
|
|
39
39
|
return {
|
|
40
|
-
subscribe(
|
|
41
|
-
let set = subs.get(
|
|
40
|
+
subscribe(flowId, fn) {
|
|
41
|
+
let set = subs.get(flowId);
|
|
42
42
|
if (!set) {
|
|
43
43
|
set = new Set();
|
|
44
|
-
subs.set(
|
|
44
|
+
subs.set(flowId, set);
|
|
45
45
|
}
|
|
46
46
|
set.add(fn);
|
|
47
47
|
return () => {
|
|
48
|
-
const current = subs.get(
|
|
48
|
+
const current = subs.get(flowId);
|
|
49
49
|
if (!current) return;
|
|
50
50
|
current.delete(fn);
|
|
51
|
-
if (current.size === 0) subs.delete(
|
|
51
|
+
if (current.size === 0) subs.delete(flowId);
|
|
52
52
|
};
|
|
53
53
|
},
|
|
54
54
|
broadcast(event) {
|
|
55
|
-
const set = subs.get(event.
|
|
55
|
+
const set = subs.get(event.flowId);
|
|
56
56
|
if (!set) return;
|
|
57
57
|
const full: StudioEvent = { ...event, ts: event.ts ?? Date.now() };
|
|
58
58
|
for (const fn of set) {
|
|
@@ -63,8 +63,8 @@ export function createEventBus(): EventBus {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
|
-
subscriberCount(
|
|
67
|
-
return subs.get(
|
|
66
|
+
subscriberCount(flowId) {
|
|
67
|
+
return subs.get(flowId)?.size ?? 0;
|
|
68
68
|
},
|
|
69
69
|
};
|
|
70
70
|
}
|
package/src/file-ref.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join, relative } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const FILE_PREFIX = 'file://';
|
|
5
|
+
|
|
6
|
+
const isCleanRelativePath = (p: string): boolean => {
|
|
7
|
+
if (p.length === 0) return false;
|
|
8
|
+
if (p.startsWith('/') || p.startsWith('\\')) return false;
|
|
9
|
+
if (/^[A-Za-z]:[\\/]/.test(p)) return false;
|
|
10
|
+
const segments = p.split(/[\\/]/);
|
|
11
|
+
return !segments.some((seg) => seg === '..');
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const invalidMarker = (rawPath: string) => `[seeflow: invalid file:// path '${rawPath}']`;
|
|
15
|
+
const missingMarker = (rawPath: string) => `[seeflow: missing file '${rawPath}']`;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve every `file://<relative-path>` string in `raw` by reading the file
|
|
19
|
+
* under `<seeflowRoot>` and substituting its UTF-8 content. Missing or invalid
|
|
20
|
+
* paths are replaced with placeholder markers so schema parse still succeeds.
|
|
21
|
+
*
|
|
22
|
+
* Returns the mutated tree plus the sorted, de-duplicated list of relative
|
|
23
|
+
* paths that resolved cleanly (the watcher tracks these for live reload).
|
|
24
|
+
*/
|
|
25
|
+
export function resolveFileRefs(
|
|
26
|
+
raw: unknown,
|
|
27
|
+
seeflowRoot: string,
|
|
28
|
+
): { resolved: unknown; refs: string[] } {
|
|
29
|
+
const refs = new Set<string>();
|
|
30
|
+
let seeflowRealRoot: string;
|
|
31
|
+
try {
|
|
32
|
+
seeflowRealRoot = existsSync(seeflowRoot) ? realpathSync(seeflowRoot) : seeflowRoot;
|
|
33
|
+
} catch {
|
|
34
|
+
seeflowRealRoot = seeflowRoot;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const resolveString = (s: string): string => {
|
|
38
|
+
if (!s.startsWith(FILE_PREFIX)) return s;
|
|
39
|
+
const relPath = s.slice(FILE_PREFIX.length);
|
|
40
|
+
if (!isCleanRelativePath(relPath)) return invalidMarker(relPath);
|
|
41
|
+
|
|
42
|
+
const abs = join(seeflowRoot, relPath);
|
|
43
|
+
if (!existsSync(abs)) return missingMarker(relPath);
|
|
44
|
+
|
|
45
|
+
// Symlink-escape defense: resolve realpath and confirm it stays inside root.
|
|
46
|
+
let realAbs: string;
|
|
47
|
+
try {
|
|
48
|
+
realAbs = realpathSync(abs);
|
|
49
|
+
} catch {
|
|
50
|
+
return missingMarker(relPath);
|
|
51
|
+
}
|
|
52
|
+
const rel = relative(seeflowRealRoot, realAbs);
|
|
53
|
+
if (rel.startsWith('..') || isAbsolute(rel)) return invalidMarker(relPath);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const content = readFileSync(realAbs, 'utf8');
|
|
57
|
+
refs.add(relPath);
|
|
58
|
+
return content;
|
|
59
|
+
} catch {
|
|
60
|
+
return missingMarker(relPath);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const walk = (node: unknown): unknown => {
|
|
65
|
+
if (typeof node === 'string') return resolveString(node);
|
|
66
|
+
if (Array.isArray(node)) return node.map(walk);
|
|
67
|
+
if (node && typeof node === 'object') {
|
|
68
|
+
const out: Record<string, unknown> = {};
|
|
69
|
+
for (const [k, v] of Object.entries(node as Record<string, unknown>)) {
|
|
70
|
+
out[k] = walk(v);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
return node;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const resolved = walk(raw);
|
|
78
|
+
return { resolved, refs: [...refs].sort() };
|
|
79
|
+
}
|