@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.
Files changed (45) hide show
  1. package/bin/seeflow +0 -0
  2. package/bin/seeflow-mcp +0 -0
  3. package/dist/web/assets/{index-BMaMEi2a.js → index--9KvdiJU.js} +1584 -1584
  4. package/dist/web/assets/index-XIY68z9O.css +1 -0
  5. package/dist/web/assets/{index.es-M1iBDKG6.js → index.es-CehYgUiq.js} +1 -1
  6. package/dist/web/assets/{jspdf.es.min-xZpq8bcn.js → jspdf.es.min-CaFlbpn0.js} +3 -3
  7. package/dist/web/index.html +2 -2
  8. package/examples/ecommerce-platform/.seeflow/{seeflow.json → architecture.json} +22 -77
  9. package/examples/ecommerce-platform/.seeflow/details/api-gateway.md +14 -0
  10. package/examples/ecommerce-platform/.seeflow/details/auth-service.md +9 -0
  11. package/examples/ecommerce-platform/.seeflow/details/cart-service.md +10 -0
  12. package/examples/ecommerce-platform/.seeflow/details/notification-service.md +13 -0
  13. package/examples/ecommerce-platform/.seeflow/details/order-service.md +16 -0
  14. package/examples/ecommerce-platform/.seeflow/details/payment-service.md +16 -0
  15. package/examples/ecommerce-platform/.seeflow/details/product-service.md +10 -0
  16. package/examples/ecommerce-platform/.seeflow/scripts/platform-health.html +42 -0
  17. package/examples/ecommerce-platform/.seeflow/style.json +98 -0
  18. package/examples/order-pipeline/.seeflow/architecture.json +93 -0
  19. package/examples/order-pipeline/.seeflow/details/fulfillment-service.md +21 -0
  20. package/examples/order-pipeline/.seeflow/details/inventory-service.md +23 -0
  21. package/examples/order-pipeline/.seeflow/details/payment-service.md +23 -0
  22. package/examples/order-pipeline/.seeflow/details/post-orders.md +19 -0
  23. package/examples/order-pipeline/.seeflow/scripts/play.ts +2 -2
  24. package/examples/order-pipeline/.seeflow/style.json +42 -0
  25. package/package.json +4 -2
  26. package/public/runtime/tailwind.js +931 -24378
  27. package/src/api.ts +118 -118
  28. package/src/cli.ts +13 -13
  29. package/src/demo.ts +6 -6
  30. package/src/diagram.ts +4 -4
  31. package/src/events.ts +14 -14
  32. package/src/file-ref.ts +79 -0
  33. package/src/mcp.ts +117 -89
  34. package/src/merge.ts +190 -0
  35. package/src/operations.ts +415 -416
  36. package/src/proxy.ts +31 -31
  37. package/src/registry.ts +32 -20
  38. package/src/schema.ts +252 -8
  39. package/src/sdk-template.ts +2 -2
  40. package/src/sdk-writer.ts +2 -2
  41. package/src/server.ts +14 -7
  42. package/src/status-runner.ts +34 -38
  43. package/src/watcher.ts +165 -114
  44. package/dist/web/assets/index-BotEftAD.css +0 -1
  45. 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 { DemoSchema } from './schema.ts';
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 DEFAULT_DEMO_PATH = '.seeflow/seeflow.json';
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/seeflow.json)
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 demoPath = '.seeflow/seeflow.json';
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.getByRepoPathAndDemoPath(destDir, demoPath)) return;
151
+ if (registry.getByRepoPathAndArchitecturePath(destDir, architecturePath)) return;
152
152
 
153
- const flowFile = join(destDir, demoPath);
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 = DemoSchema.safeParse(demo);
163
+ const parsed = ArchitectureSchema.safeParse(demo);
164
164
  if (!parsed.success) return;
165
165
 
166
- registry.upsert({ name: parsed.data.name, repoPath: destDir, demoPath });
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') ?? DEFAULT_DEMO_PATH;
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 ${DEFAULT_DEMO_PATH} in your repo, or pass --flow <path>.`);
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 = DemoSchema.safeParse(demo);
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/demos/register`, {
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
- demoPath: demoPathArg,
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
- demoId: string,
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
- demoId,
22
+ flowId,
23
23
  payload: { nodeId, runId, ...payload },
24
24
  });
25
25
  }
26
26
 
27
- async function runOrderPipeline(events: EventBus, demoId: string, runId: string, orderId: string) {
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, demoId, runId, nodeId, status, payload);
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 demoId = c.req.header('x-seeflow-demo-id') ?? '';
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, demoId, runId, orderId);
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 { DemoSchema } from './schema.ts';
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: 1;
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: 1,
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 = DemoSchema.safeParse(req.demo);
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 demoId. Subscribers receive every event published
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
- | 'demo:reload'
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
- demoId: string;
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(demoId: string, fn: Subscriber): () => void;
30
- /** Broadcast an event to all subscribers of `demoId`. */
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(demoId: string): number;
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(demoId, fn) {
41
- let set = subs.get(demoId);
40
+ subscribe(flowId, fn) {
41
+ let set = subs.get(flowId);
42
42
  if (!set) {
43
43
  set = new Set();
44
- subs.set(demoId, set);
44
+ subs.set(flowId, set);
45
45
  }
46
46
  set.add(fn);
47
47
  return () => {
48
- const current = subs.get(demoId);
48
+ const current = subs.get(flowId);
49
49
  if (!current) return;
50
50
  current.delete(fn);
51
- if (current.size === 0) subs.delete(demoId);
51
+ if (current.size === 0) subs.delete(flowId);
52
52
  };
53
53
  },
54
54
  broadcast(event) {
55
- const set = subs.get(event.demoId);
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(demoId) {
67
- return subs.get(demoId)?.size ?? 0;
66
+ subscriberCount(flowId) {
67
+ return subs.get(flowId)?.size ?? 0;
68
68
  },
69
69
  };
70
70
  }
@@ -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
+ }