@tuongaz/seeflow 0.1.41 → 0.1.42

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 (38) hide show
  1. package/README.md +2 -15
  2. package/dist/web/assets/{index-C029S3KL.js → index-BPUoNIBm.js} +1541 -1541
  3. package/dist/web/assets/{index-BwdVgB2y.css → index-BlkUOp7f.css} +1 -1
  4. package/dist/web/assets/{index.es-Ylk3HlXb.js → index.es-mje3R_63.js} +1 -1
  5. package/dist/web/assets/{jspdf.es.min-Bf66gPs3.js → jspdf.es.min-DX3imOs2.js} +3 -3
  6. package/dist/web/index.html +2 -2
  7. package/examples/ecommerce-platform/.seeflow/flow.json +47 -47
  8. package/examples/ecommerce-platform/.seeflow/style.json +10 -10
  9. package/examples/order-pipeline/.seeflow/flow.json +17 -17
  10. package/examples/order-pipeline/.seeflow/style.json +4 -4
  11. package/package.json +1 -1
  12. package/src/api.ts +101 -14
  13. package/src/atomic-write.ts +16 -0
  14. package/src/cli-e2e.ts +420 -0
  15. package/src/cli-helpers.ts +65 -0
  16. package/src/cli.ts +371 -17
  17. package/src/mcp.ts +116 -23
  18. package/src/merge.ts +1 -1
  19. package/src/node-files.ts +45 -0
  20. package/src/operations.ts +304 -98
  21. package/src/proxy.ts +35 -6
  22. package/src/registry.ts +2 -1
  23. package/src/schema.ts +31 -25
  24. package/src/short-id.ts +24 -0
  25. package/src/status-runner.ts +9 -8
  26. package/src/watcher.ts +14 -14
  27. /package/examples/ecommerce-platform/.seeflow/{details/auth-service.md → nodes/node-3zFtHg6ENc/detail.md} +0 -0
  28. /package/examples/ecommerce-platform/.seeflow/{details/cart-service.md → nodes/node-5F424NWbEu/detail.md} +0 -0
  29. /package/examples/ecommerce-platform/.seeflow/{details/api-gateway.md → nodes/node-CbwYqb7NfB/detail.md} +0 -0
  30. /package/examples/ecommerce-platform/.seeflow/{scripts/platform-health.html → nodes/node-XwygzfKPZ5/view.html} +0 -0
  31. /package/examples/ecommerce-platform/.seeflow/{details/notification-service.md → nodes/node-fkptXw7uvs/detail.md} +0 -0
  32. /package/examples/ecommerce-platform/.seeflow/{details/product-service.md → nodes/node-kwBY8YPmYM/detail.md} +0 -0
  33. /package/examples/ecommerce-platform/.seeflow/{details/payment-service.md → nodes/node-mPqan8rFYN/detail.md} +0 -0
  34. /package/examples/ecommerce-platform/.seeflow/{details/order-service.md → nodes/node-yKrg9DV5fJ/detail.md} +0 -0
  35. /package/examples/order-pipeline/.seeflow/{details/inventory-service.md → nodes/node-GXTKUcE3ye/detail.md} +0 -0
  36. /package/examples/order-pipeline/.seeflow/{details/post-orders.md → nodes/node-XKIyds0TDg/detail.md} +0 -0
  37. /package/examples/order-pipeline/.seeflow/{details/payment-service.md → nodes/node-YOYiHJpY0i/detail.md} +0 -0
  38. /package/examples/order-pipeline/.seeflow/{details/fulfillment-service.md → nodes/node-zUIH7WFnhK/detail.md} +0 -0
package/src/schema.ts CHANGED
@@ -71,13 +71,13 @@ const ScriptActionSchema = z.object({
71
71
  interpreter: z.string().min(1),
72
72
  args: z.array(z.string()).optional(),
73
73
  scriptPath: z.string().min(1).refine(isCleanRelativePath, {
74
- message: 'scriptPath must be a relative path under .seeflow/ (no absolute / traversal)',
74
+ message: 'scriptPath must be a relative path under the node folder (no absolute / traversal)',
75
75
  }),
76
76
  input: z.unknown().optional(),
77
77
  timeoutMs: z.number().int().positive().max(600_000).optional(),
78
78
  });
79
79
 
80
- const PlayActionSchema = ScriptActionSchema;
80
+ export const PlayActionSchema = ScriptActionSchema;
81
81
 
82
82
  // US-008: resetAction is a one-shot script action — same shape as a play
83
83
  // script (interpreter + args + scriptPath + optional input/timeoutMs) but
@@ -89,12 +89,12 @@ const ResetActionSchema = ScriptActionSchema;
89
89
  // Long-running status script. Same spawn shape as ScriptAction (interpreter +
90
90
  // args + scriptPath) but no stdin payload and a much longer max lifetime since
91
91
  // these processes tick continuously and stream StatusReports to stdout.
92
- const StatusActionSchema = z.object({
92
+ export const StatusActionSchema = z.object({
93
93
  kind: z.literal('script'),
94
94
  interpreter: z.string().min(1),
95
95
  args: z.array(z.string()).optional(),
96
96
  scriptPath: z.string().min(1).refine(isCleanRelativePath, {
97
- message: 'scriptPath must be a relative path under .seeflow/ (no absolute / traversal)',
97
+ message: 'scriptPath must be a relative path under the node folder (no absolute / traversal)',
98
98
  }),
99
99
  maxLifetimeMs: z.number().int().positive().max(3_600_000).optional(),
100
100
  });
@@ -110,7 +110,7 @@ export const StatusReportSchema = z.object({
110
110
  ts: z.number().int().positive().optional(),
111
111
  });
112
112
 
113
- const StateSourceSchema = z.discriminatedUnion('kind', [
113
+ export const StateSourceSchema = z.discriminatedUnion('kind', [
114
114
  z.object({ kind: z.literal('request') }),
115
115
  z.object({ kind: z.literal('event') }),
116
116
  ]);
@@ -163,7 +163,7 @@ const StateNodeSchema = z.object({
163
163
  // via inline SVG inside shape-node.tsx). Illustrative shapes share the same
164
164
  // shapeNode wrapper and color/border fields but own their own visuals via a
165
165
  // per-shape component under `apps/web/src/components/nodes/shapes/`.
166
- const ShapeKindSchema = z.enum([
166
+ export const ShapeKindSchema = z.enum([
167
167
  'rectangle',
168
168
  'ellipse',
169
169
  'sticky',
@@ -190,7 +190,7 @@ const ShapeNodeSchema = z.object({
190
190
 
191
191
  // Decorative image node — references a file under `<project>/.seeflow/` by
192
192
  // relative path (US-004 hard-cut from base64 data URLs to path-backed files).
193
- // `path` is the same kind of relative path as `htmlPath` on htmlNode: rooted
193
+ // `path` is a relative path under `<project>/.seeflow/` for imageNode uploads: rooted
194
194
  // at `.seeflow/`, no leading slash, no `..` segments. The renderer fetches via
195
195
  // `GET /api/projects/:id/files/:path`.
196
196
  const ImageNodeDataSchema = z.object({
@@ -210,23 +210,16 @@ const ImageNodeSchema = z.object({
210
210
  });
211
211
 
212
212
  // US-011 (illustrative-shapes-htmlnode): htmlNode is the escape-hatch node type
213
- // for content the curated nodes don't cover — references author-written HTML at
214
- // `<project>/.seeflow/<htmlPath>`. The renderer fetches via the file-serving
215
- // endpoint and sanitizes before injecting (US-013/US-014). `htmlPath` uses the
216
- // same path-safety refine as imageNode.path: relative under `.seeflow/`, no
217
- // absolute root, no `..` traversal. Spreads NodeVisualBaseShape so authors can
218
- // theme the wrapper (border / background / radius / font) with the same fields
213
+ // for content the curated nodes don't cover — carries author-written HTML
214
+ // inline via `data.html`. The studio externalizes the content to
215
+ // `<project>/.seeflow/nodes/<id>/view.html` and stores a `file://` ref in
216
+ // flow.json; the resolver inlines the content back on read so consumers see
217
+ // the resolved HTML string. The renderer sanitizes before injection
218
+ // (US-013/US-014). Spreads NodeVisualBaseShape so authors can theme the
219
+ // wrapper (border / background / radius / font) with the same fields
219
220
  // available on every other visual node.
220
- //
221
- // File existence is INTENTIONALLY not validated at the schema level. Missing
222
- // files are a normal authoring state (author drops a node, file hasn't been
223
- // written yet) and would otherwise reject the whole demo. The US-014 renderer
224
- // renders a `PlaceholderCard` instead — so a missing htmlPath WARNS (via the
225
- // placeholder visual) without ERRORING (without failing demo parse).
226
221
  export const HtmlNodeDataSchema = z.object({
227
- htmlPath: z.string().min(1).refine(isCleanRelativePath, {
228
- message: 'htmlPath must be a relative path under .seeflow/ (no absolute / traversal)',
229
- }),
222
+ html: z.string().optional(),
230
223
  name: z.string().optional(),
231
224
  // Decorative caption glyph. Lucide icon name (kebab-case) resolved by the
232
225
  // canvas <Icon> primitive; rendered inline with the caption when set.
@@ -418,6 +411,21 @@ export const ResolvedFlowSchema = z
418
411
  });
419
412
  }
420
413
  });
414
+ // imageNode upload paths must live under the node's own
415
+ // `nodes/<id>/` folder so delete_node's removeNodeDir cascade is the
416
+ // single source of cleanup.
417
+ resolved.nodes.forEach((node, idx) => {
418
+ if (node.type !== 'imageNode') return;
419
+ const path = (node.data as { path?: string }).path;
420
+ const expected = `nodes/${node.id}/`;
421
+ if (typeof path === 'string' && !path.startsWith(expected)) {
422
+ ctx.addIssue({
423
+ code: z.ZodIssueCode.custom,
424
+ path: ['nodes', idx, 'data', 'path'],
425
+ message: `imageNode path must start with "${expected}"`,
426
+ });
427
+ }
428
+ });
421
429
  });
422
430
 
423
431
  export type ResolvedFlow = z.infer<typeof ResolvedFlowSchema>;
@@ -504,9 +512,7 @@ const FlowIconNodeDataSchema = z
504
512
 
505
513
  const FlowHtmlNodeDataSchema = z
506
514
  .object({
507
- htmlPath: z.string().min(1).refine(isCleanRelativePath, {
508
- message: 'htmlPath must be a relative path under .seeflow/ (no absolute / traversal)',
509
- }),
515
+ html: z.string().optional(),
510
516
  name: z.string().optional(),
511
517
  icon: z.string().optional(),
512
518
  ...NodeDescriptionBaseShape,
@@ -0,0 +1,24 @@
1
+ // Short unique identifier for nodes, connectors, flow registry entries, and
2
+ // runIds. 10 base62 chars (62^10 ≈ 8.4e17 combos) is plenty for our scale and
3
+ // keeps URLs / file paths (e.g. `blocks/<id>.html`) readable.
4
+ //
5
+ // Rejection sampling avoids the modulo bias of `byte % 62`: 256 % 62 = 8, so
6
+ // bytes 0..247 map evenly across the 62-char alphabet and 248..255 are
7
+ // re-rolled. The oversample factor (×2) makes a second round almost never
8
+ // needed in practice.
9
+
10
+ const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
11
+ const UNBIASED_MAX = 248; // floor(256 / 62) * 62
12
+
13
+ export function shortId(len = 10): string {
14
+ let out = '';
15
+ const buf = new Uint8Array(len * 2);
16
+ while (out.length < len) {
17
+ crypto.getRandomValues(buf);
18
+ for (let i = 0; i < buf.length && out.length < len; i++) {
19
+ const b = buf[i] as number;
20
+ if (b < UNBIASED_MAX) out += ALPHABET[b % 62];
21
+ }
22
+ }
23
+ return out;
24
+ }
@@ -16,8 +16,8 @@
16
16
  * report. A solicited kill (restart / stop / maxLifetimeMs) is silent on exit.
17
17
  *
18
18
  * Defense-in-depth on scriptPath mirrors proxy.ts:`resolveScript` — realpath
19
- * the resolved file against `<repoPath>/.seeflow/` so a symlink-escape can't
20
- * spawn arbitrary scripts outside the project.
19
+ * the resolved file against `<repoPath>/.seeflow/nodes/<nodeId>/` so a
20
+ * symlink-escape can't spawn arbitrary scripts outside the node folder.
21
21
  */
22
22
 
23
23
  import { existsSync, realpathSync } from 'node:fs';
@@ -26,6 +26,7 @@ import type { EventBus } from './events.ts';
26
26
  import { type ProcessSpawner, type SpawnHandle, defaultProcessSpawner } from './process-spawner.ts';
27
27
  import type { FlowEntry, Registry } from './registry.ts';
28
28
  import { type ResolvedFlow, type StatusAction, StatusReportSchema } from './schema.ts';
29
+ import { shortId } from './short-id.ts';
29
30
  import { readMergedFlow } from './watcher.ts';
30
31
 
31
32
  export interface StatusRunner {
@@ -61,15 +62,15 @@ interface TrackedHandle {
61
62
 
62
63
  type ResolvedScript = { ok: true; absPath: string } | { ok: false };
63
64
 
64
- function resolveScript(repoPath: string, scriptPath: string): ResolvedScript {
65
- const seeflowRoot = join(repoPath, '.seeflow');
65
+ function resolveScript(repoPath: string, nodeId: string, scriptPath: string): ResolvedScript {
66
+ const nodeRoot = join(repoPath, '.seeflow', 'nodes', nodeId);
66
67
  let realRoot: string;
67
68
  try {
68
- realRoot = realpathSync(seeflowRoot);
69
+ realRoot = realpathSync(nodeRoot);
69
70
  } catch {
70
71
  return { ok: false };
71
72
  }
72
- const target = resolve(seeflowRoot, scriptPath);
73
+ const target = resolve(nodeRoot, scriptPath);
73
74
  let realTarget: string;
74
75
  try {
75
76
  realTarget = realpathSync(target);
@@ -178,7 +179,7 @@ export function createStatusRunner(options: CreateStatusRunnerOptions): StatusRu
178
179
  ): TrackedHandle | undefined {
179
180
  const { nodeId, action } = sn;
180
181
 
181
- const resolved = resolveScript(repoPath, action.scriptPath);
182
+ const resolved = resolveScript(repoPath, nodeId, action.scriptPath);
182
183
  if (!resolved.ok) {
183
184
  events.broadcast({
184
185
  type: 'node:status',
@@ -193,7 +194,7 @@ export function createStatusRunner(options: CreateStatusRunnerOptions): StatusRu
193
194
  return undefined;
194
195
  }
195
196
 
196
- const runId = crypto.randomUUID();
197
+ const runId = shortId();
197
198
  const env = buildChildEnv({
198
199
  SEEFLOW_DEMO_ID: flowId,
199
200
  SEEFLOW_NODE_ID: nodeId,
package/src/watcher.ts CHANGED
@@ -71,7 +71,8 @@ export interface FlowWatcher {
71
71
  ): void;
72
72
  /**
73
73
  * Relative paths (under `<project>/.seeflow/`) currently being watched
74
- * because they're referenced by a node's `data.htmlPath` or `data.path`.
74
+ * because they're referenced by a node's `data.path` (imageNode). htmlNode
75
+ * content rides on the file:// resolver via `data.html`, not this list.
75
76
  * Sorted for stable assertion order. Used by tests.
76
77
  */
77
78
  referencedPaths(flowId: string): string[];
@@ -91,7 +92,7 @@ interface WatchHandle {
91
92
  filePath: string;
92
93
  /**
93
94
  * Per-directory file watchers for files referenced by node data
94
- * (`htmlPath`, imageNode `path`). Each directory watcher dispatches to
95
+ * (imageNode `path`). Each directory watcher dispatches to
95
96
  * specific basenames in its `files` map.
96
97
  */
97
98
  fileWatchers: Map<string, FileWatchEntry>;
@@ -129,10 +130,11 @@ const isCleanRelativePath = (p: string): boolean => {
129
130
  };
130
131
 
131
132
  /**
132
- * Walk raw flow JSON (pre-schema-parse) collecting referenced file
133
- * paths: `nodes[].data.htmlPath` (htmlNode) and `nodes[].data.path`
134
- * (imageNode). Operates on the raw JSON so the watcher works before those
135
- * fields are formally validated.
133
+ * Walk raw flow JSON (pre-schema-parse) collecting referenced file paths:
134
+ * `nodes[].data.path` (imageNode). htmlNode content now flows through the
135
+ * `file://nodes/<id>/view.html` ref handled by the file-ref resolver, so it
136
+ * does NOT need a separate fs.watch entry here. Operates on the raw JSON so
137
+ * the watcher works before those fields are formally validated.
136
138
  */
137
139
  const collectReferencedPaths = (raw: unknown): string[] => {
138
140
  if (!raw || typeof raw !== 'object') return [];
@@ -143,12 +145,10 @@ const collectReferencedPaths = (raw: unknown): string[] => {
143
145
  if (!node || typeof node !== 'object') continue;
144
146
  const data = (node as { data?: unknown }).data;
145
147
  if (!data || typeof data !== 'object') continue;
146
- const d = data as { htmlPath?: unknown; path?: unknown };
147
- for (const candidate of [d.htmlPath, d.path]) {
148
- if (typeof candidate !== 'string') continue;
149
- if (!isCleanRelativePath(candidate)) continue;
150
- out.add(candidate);
151
- }
148
+ const d = data as { path?: unknown };
149
+ if (typeof d.path !== 'string') continue;
150
+ if (!isCleanRelativePath(d.path)) continue;
151
+ out.add(d.path);
152
152
  }
153
153
  return [...out];
154
154
  };
@@ -164,7 +164,7 @@ export interface ReadMergedFlowResult {
164
164
  error: string | null;
165
165
  /** Sorted relative paths under `<seeflowRoot>` resolved via file://. */
166
166
  fileRefs: string[];
167
- /** Flow file paths referenced via htmlPath / imageNode.path. */
167
+ /** Flow file paths referenced via imageNode.path. */
168
168
  staticRefs: string[];
169
169
  }
170
170
 
@@ -394,7 +394,7 @@ export function createWatcher(deps: WatcherDeps): FlowWatcher {
394
394
 
395
395
  snapshots.set(flowId, next);
396
396
 
397
- // Reconcile the referenced-file watch set: htmlPath/imageNode.path from
397
+ // Reconcile the referenced-file watch set: imageNode.path from
398
398
  // flow + any file:// targets that resolved cleanly. Schema errors
399
399
  // shouldn't drop the watch set — the user is mid-edit and the referenced
400
400
  // files are still valid targets, so this reconciles whenever the JSON