@tuongaz/seeflow 0.1.41 → 0.1.47
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/README.md +2 -15
- package/dist/web/assets/{index-C029S3KL.js → index-BYeYJkCQ.js} +1541 -1541
- package/dist/web/assets/{index-BwdVgB2y.css → index-DSfixlbD.css} +1 -1
- package/dist/web/assets/{index.es-Ylk3HlXb.js → index.es-CqkMwhBu.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-Bf66gPs3.js → jspdf.es.min-DLHTB6Rk.js} +3 -3
- package/dist/web/index.html +2 -2
- package/examples/ecommerce-platform/.seeflow/flow.json +47 -47
- package/examples/ecommerce-platform/.seeflow/style.json +10 -10
- package/examples/order-pipeline/.seeflow/flow.json +17 -17
- package/examples/order-pipeline/.seeflow/style.json +4 -4
- package/package.json +2 -1
- package/src/api.ts +101 -14
- package/src/atomic-write.ts +16 -0
- package/src/cli-e2e.ts +424 -0
- package/src/cli-helpers.ts +65 -0
- package/src/cli.ts +371 -17
- package/src/file-ref.ts +27 -16
- package/src/mcp.ts +116 -23
- package/src/merge.ts +1 -1
- package/src/node-files.ts +48 -0
- package/src/operations.ts +325 -105
- package/src/proxy.ts +35 -6
- package/src/registry.ts +2 -1
- package/src/schema.ts +31 -25
- package/src/short-id.ts +24 -0
- package/src/status-runner.ts +9 -8
- package/src/watcher.ts +14 -14
- /package/examples/ecommerce-platform/.seeflow/{details/auth-service.md → nodes/node-3zFtHg6ENc/detail.md} +0 -0
- /package/examples/ecommerce-platform/.seeflow/{details/cart-service.md → nodes/node-5F424NWbEu/detail.md} +0 -0
- /package/examples/ecommerce-platform/.seeflow/{details/api-gateway.md → nodes/node-CbwYqb7NfB/detail.md} +0 -0
- /package/examples/ecommerce-platform/.seeflow/{scripts/platform-health.html → nodes/node-XwygzfKPZ5/view.html} +0 -0
- /package/examples/ecommerce-platform/.seeflow/{details/notification-service.md → nodes/node-fkptXw7uvs/detail.md} +0 -0
- /package/examples/ecommerce-platform/.seeflow/{details/product-service.md → nodes/node-kwBY8YPmYM/detail.md} +0 -0
- /package/examples/ecommerce-platform/.seeflow/{details/payment-service.md → nodes/node-mPqan8rFYN/detail.md} +0 -0
- /package/examples/ecommerce-platform/.seeflow/{details/order-service.md → nodes/node-yKrg9DV5fJ/detail.md} +0 -0
- /package/examples/order-pipeline/.seeflow/{details/inventory-service.md → nodes/node-GXTKUcE3ye/detail.md} +0 -0
- /package/examples/order-pipeline/.seeflow/{details/post-orders.md → nodes/node-XKIyds0TDg/detail.md} +0 -0
- /package/examples/order-pipeline/.seeflow/{details/payment-service.md → nodes/node-YOYiHJpY0i/detail.md} +0 -0
- /package/examples/order-pipeline/.seeflow/{details/fulfillment-service.md → nodes/node-zUIH7WFnhK/detail.md} +0 -0
package/src/registry.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { seeflowHome } from './paths.ts';
|
|
4
|
+
import { shortId } from './short-id.ts';
|
|
4
5
|
|
|
5
6
|
export interface FlowEntry {
|
|
6
7
|
id: string;
|
|
@@ -121,7 +122,7 @@ export function createRegistry(options: { path?: string } = {}): Registry {
|
|
|
121
122
|
persist();
|
|
122
123
|
return updated;
|
|
123
124
|
}
|
|
124
|
-
const id =
|
|
125
|
+
const id = shortId();
|
|
125
126
|
const slug = uniqueSlug(slugify(input.name));
|
|
126
127
|
const entry: FlowEntry = {
|
|
127
128
|
id,
|
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
|
|
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
|
|
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
|
|
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 —
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/src/short-id.ts
ADDED
|
@@ -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
|
+
}
|
package/src/status-runner.ts
CHANGED
|
@@ -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
|
|
20
|
-
* spawn arbitrary scripts outside the
|
|
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
|
|
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(
|
|
69
|
+
realRoot = realpathSync(nodeRoot);
|
|
69
70
|
} catch {
|
|
70
71
|
return { ok: false };
|
|
71
72
|
}
|
|
72
|
-
const target = resolve(
|
|
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 =
|
|
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.
|
|
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
|
-
* (
|
|
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
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
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 {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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:
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/examples/order-pipeline/.seeflow/{details/post-orders.md → nodes/node-XKIyds0TDg/detail.md}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|