@tuongaz/seeflow 0.1.61 → 0.1.64
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 +3 -3
- package/dist/web/assets/{index-BXYHeBKM.js → index-BAEA18IR.js} +354 -354
- package/dist/web/assets/{index.es-BzG6d4Ro.js → index.es-DZEdTXNJ.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-CcOxqEhi.js → jspdf.es.min-DT1Li8zz.js} +3 -3
- package/dist/web/index.html +1 -1
- package/examples/ecommerce-platform/{.seeflow/flow.json → flow.json} +3 -25
- package/examples/ecommerce-platform/{.seeflow/scripts → scripts}/play.ts +1 -1
- package/examples/order-pipeline/{.seeflow/flow.json → flow.json} +1 -10
- package/package.json +1 -1
- package/src/api.ts +65 -55
- package/src/cli-helpers.ts +6 -5
- package/src/cli-manifest.ts +103 -15
- package/src/cli.ts +85 -13
- package/src/diagram.ts +0 -1
- package/src/file-ref.ts +16 -15
- package/src/mcp.ts +58 -16
- package/src/merge.ts +0 -1
- package/src/node-files.ts +5 -5
- package/src/operations.ts +40 -101
- package/src/paths.ts +16 -0
- package/src/proxy.ts +13 -13
- package/src/schema-catalog.ts +3 -9
- package/src/schema.ts +36 -96
- package/src/server.ts +0 -4
- package/src/short-id.ts +24 -0
- package/src/status-runner.ts +3 -3
- package/src/watcher.ts +15 -27
- package/src/sdk-template.ts +0 -37
- package/src/sdk-writer.ts +0 -37
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-3zFtHg6ENc/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-5F424NWbEu/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-CbwYqb7NfB/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-XwygzfKPZ5/view.html +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-fkptXw7uvs/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-kwBY8YPmYM/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-mPqan8rFYN/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/nodes → nodes}/node-yKrg9DV5fJ/detail.md +0 -0
- /package/examples/ecommerce-platform/{.seeflow/style.json → style.json} +0 -0
- /package/examples/order-pipeline/{.seeflow/nodes → nodes}/node-GXTKUcE3ye/detail.md +0 -0
- /package/examples/order-pipeline/{.seeflow/nodes → nodes}/node-XKIyds0TDg/detail.md +0 -0
- /package/examples/order-pipeline/{.seeflow/nodes → nodes}/node-YOYiHJpY0i/detail.md +0 -0
- /package/examples/order-pipeline/{.seeflow/nodes → nodes}/node-zUIH7WFnhK/detail.md +0 -0
- /package/examples/order-pipeline/{.seeflow/scripts → scripts}/play.ts +0 -0
- /package/examples/order-pipeline/{.seeflow/style.json → style.json} +0 -0
package/src/schema.ts
CHANGED
|
@@ -8,8 +8,8 @@ const PositionSchema = z.object({
|
|
|
8
8
|
// US-008: HttpAction was the original shape for both playAction and resetAction
|
|
9
9
|
// in pre-script-action releases. After US-001 cut playAction to script-only and
|
|
10
10
|
// US-008 cut resetAction the same way, no schema in this file uses HttpAction
|
|
11
|
-
// anymore.
|
|
12
|
-
//
|
|
11
|
+
// anymore. `HttpMethodSchema` is still used for the optional `method` field on
|
|
12
|
+
// connectors (documentation metadata).
|
|
13
13
|
const HttpMethodSchema = z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
|
|
14
14
|
|
|
15
15
|
// Curated palette tokens. Stored on disk as readable names; the frontend maps
|
|
@@ -62,10 +62,12 @@ const isCleanRelativePath = (s: string): boolean => {
|
|
|
62
62
|
};
|
|
63
63
|
|
|
64
64
|
// Script-based action: the studio spawns `<interpreter> [...args] <scriptPath>`
|
|
65
|
-
// from the project's repoPath. `scriptPath` is a relative path under
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
// `
|
|
65
|
+
// from the project's repoPath. `scriptPath` is a relative path under the
|
|
66
|
+
// project root (typically `nodes/<id>/scripts/<name>` for play/status, or any
|
|
67
|
+
// project-root-relative path for reset); `args` (optional) prepend to the
|
|
68
|
+
// interpreter; `input` (optional) gets JSON-serialized and written to the
|
|
69
|
+
// child's stdin then closed; `timeoutMs` caps execution (default applied at
|
|
70
|
+
// the spawn layer, not here).
|
|
69
71
|
const ScriptActionSchema = z.object({
|
|
70
72
|
kind: z.literal('script'),
|
|
71
73
|
interpreter: z.string().min(1),
|
|
@@ -117,7 +119,6 @@ export const StateSourceSchema = z.discriminatedUnion('kind', [
|
|
|
117
119
|
|
|
118
120
|
const NodeDataBaseSchema = z.object({
|
|
119
121
|
name: z.string().min(1),
|
|
120
|
-
kind: z.string().min(1),
|
|
121
122
|
stateSource: StateSourceSchema,
|
|
122
123
|
// Reserved for v2: a module path resolved by future skills runtime.
|
|
123
124
|
// Schema-only at v1 — never read at runtime.
|
|
@@ -188,14 +189,14 @@ const ShapeNodeSchema = z.object({
|
|
|
188
189
|
data: ShapeNodeDataSchema,
|
|
189
190
|
});
|
|
190
191
|
|
|
191
|
-
// Decorative image node — references a file under
|
|
192
|
+
// Decorative image node — references a file under the project root by
|
|
192
193
|
// relative path (US-004 hard-cut from base64 data URLs to path-backed files).
|
|
193
|
-
// `path` is a relative path under
|
|
194
|
-
//
|
|
194
|
+
// `path` is a relative path under the project root for imageNode uploads:
|
|
195
|
+
// no leading slash, no `..` segments. The renderer fetches via
|
|
195
196
|
// `GET /api/projects/:id/files/:path`.
|
|
196
197
|
const ImageNodeDataSchema = z.object({
|
|
197
198
|
path: z.string().min(1).refine(isCleanRelativePath, {
|
|
198
|
-
message: 'path must be a relative path under
|
|
199
|
+
message: 'path must be a relative path under the project root (no absolute / traversal)',
|
|
199
200
|
}),
|
|
200
201
|
alt: z.string().optional(),
|
|
201
202
|
...NodeVisualBaseShape,
|
|
@@ -212,8 +213,8 @@ const ImageNodeSchema = z.object({
|
|
|
212
213
|
// US-011 (illustrative-shapes-htmlnode): htmlNode is the escape-hatch node type
|
|
213
214
|
// for content the curated nodes don't cover — carries author-written HTML
|
|
214
215
|
// inline via `data.html`. The studio externalizes the content to
|
|
215
|
-
// `<project
|
|
216
|
-
//
|
|
216
|
+
// `<project>/nodes/<id>/view.html` and stores a `file://` ref in flow.json;
|
|
217
|
+
// the resolver inlines the content back on read so consumers see
|
|
217
218
|
// the resolved HTML string. The renderer sanitizes before injection
|
|
218
219
|
// (US-013/US-014). Spreads NodeVisualBaseShape so authors can theme the
|
|
219
220
|
// wrapper (border / background / radius / font) with the same fields
|
|
@@ -273,17 +274,13 @@ const NodeSchema = z.discriminatedUnion('type', [
|
|
|
273
274
|
HtmlNodeSchema,
|
|
274
275
|
]);
|
|
275
276
|
|
|
276
|
-
// Connector is the
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
// (id/source/target are reused; `label` becomes the edge label; visual style
|
|
284
|
-
// is picked from `kind`, but per-connector `style`/`color` overrides it). v1
|
|
285
|
-
// has no separate `edges[]` array — connectors are the sole source of truth
|
|
286
|
-
// for inter-node connections.
|
|
277
|
+
// Connector is the edge between two nodes. The frontend derives a React Flow
|
|
278
|
+
// Edge from each connector at render time (id/source/target are reused;
|
|
279
|
+
// `label` becomes the edge label; visual style comes from optional
|
|
280
|
+
// `style`/`color` fields). v1 has no separate `edges[]` array — connectors
|
|
281
|
+
// are the sole source of truth for inter-node connections. Optional
|
|
282
|
+
// metadata fields (`method`/`url`/`eventName`/`queueName`) may be present
|
|
283
|
+
// for documentation purposes; the renderer does not branch on them.
|
|
287
284
|
const ConnectorStyleSchema = z.enum(['solid', 'dashed', 'dotted']);
|
|
288
285
|
const ConnectorDirectionSchema = z.enum(['forward', 'backward', 'both', 'none']);
|
|
289
286
|
// Path geometry — orthogonal to `style` (which means the dash pattern). Absent
|
|
@@ -291,7 +288,7 @@ const ConnectorDirectionSchema = z.enum(['forward', 'backward', 'both', 'none'])
|
|
|
291
288
|
// (right-angle / zigzag) path. (US-017)
|
|
292
289
|
const ConnectorPathSchema = z.enum(['curve', 'step']);
|
|
293
290
|
|
|
294
|
-
// Visual fields shared by every connector
|
|
291
|
+
// Visual fields shared by every connector. All optional — existing
|
|
295
292
|
// demo files predate them and must continue to parse. `direction` defaults
|
|
296
293
|
// to 'forward' when absent (the historical behavior).
|
|
297
294
|
const ConnectorVisualBaseShape = {
|
|
@@ -305,7 +302,7 @@ const ConnectorVisualBaseShape = {
|
|
|
305
302
|
fontSize: z.number().positive().optional(),
|
|
306
303
|
};
|
|
307
304
|
|
|
308
|
-
// Handle ids — every node
|
|
305
|
+
// Handle ids — every node type in this codebase uses the same four-handle
|
|
309
306
|
// layout: target-only on top + left, source-only on right + bottom (US-013).
|
|
310
307
|
// `sourceHandle` MUST be a source-side id and `targetHandle` MUST be a
|
|
311
308
|
// target-side id; sending the wrong role leaves a stranded endpoint at render
|
|
@@ -350,37 +347,14 @@ const ConnectorBaseShape = {
|
|
|
350
347
|
...ConnectorVisualBaseShape,
|
|
351
348
|
};
|
|
352
349
|
|
|
353
|
-
const
|
|
350
|
+
const ConnectorSchema = z.object({
|
|
354
351
|
...ConnectorBaseShape,
|
|
355
|
-
kind: z.literal('http'),
|
|
356
352
|
method: HttpMethodSchema.optional(),
|
|
357
353
|
url: z.string().min(1).optional(),
|
|
354
|
+
eventName: z.string().min(1).optional(),
|
|
355
|
+
queueName: z.string().min(1).optional(),
|
|
358
356
|
});
|
|
359
357
|
|
|
360
|
-
const EventConnectorSchema = z.object({
|
|
361
|
-
...ConnectorBaseShape,
|
|
362
|
-
kind: z.literal('event'),
|
|
363
|
-
eventName: z.string().min(1),
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
const QueueConnectorSchema = z.object({
|
|
367
|
-
...ConnectorBaseShape,
|
|
368
|
-
kind: z.literal('queue'),
|
|
369
|
-
queueName: z.string().min(1),
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
const DefaultConnectorSchema = z.object({
|
|
373
|
-
...ConnectorBaseShape,
|
|
374
|
-
kind: z.literal('default'),
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
const ConnectorSchema = z.discriminatedUnion('kind', [
|
|
378
|
-
HttpConnectorSchema,
|
|
379
|
-
EventConnectorSchema,
|
|
380
|
-
QueueConnectorSchema,
|
|
381
|
-
DefaultConnectorSchema,
|
|
382
|
-
]);
|
|
383
|
-
|
|
384
358
|
export const ResolvedFlowSchema = z
|
|
385
359
|
.object({
|
|
386
360
|
version: z.literal(2),
|
|
@@ -439,10 +413,6 @@ export type HtmlNodeData = z.infer<typeof HtmlNodeDataSchema>;
|
|
|
439
413
|
export type ShapeKind = z.infer<typeof ShapeKindSchema>;
|
|
440
414
|
export type ColorToken = z.infer<typeof ColorTokenSchema>;
|
|
441
415
|
export type Connector = z.infer<typeof ConnectorSchema>;
|
|
442
|
-
export type HttpConnector = z.infer<typeof HttpConnectorSchema>;
|
|
443
|
-
export type EventConnector = z.infer<typeof EventConnectorSchema>;
|
|
444
|
-
export type QueueConnector = z.infer<typeof QueueConnectorSchema>;
|
|
445
|
-
export type DefaultConnector = z.infer<typeof DefaultConnectorSchema>;
|
|
446
416
|
export type ConnectorStyle = z.infer<typeof ConnectorStyleSchema>;
|
|
447
417
|
export type ConnectorDirection = z.infer<typeof ConnectorDirectionSchema>;
|
|
448
418
|
export type ConnectorPath = z.infer<typeof ConnectorPathSchema>;
|
|
@@ -456,12 +426,11 @@ export type StateSource = z.infer<typeof StateSourceSchema>;
|
|
|
456
426
|
|
|
457
427
|
// =============================================================================
|
|
458
428
|
// Flow schema — pure semantic data, every visual/layout field stripped.
|
|
459
|
-
// What lives on disk in <project
|
|
429
|
+
// What lives on disk in <project>/flow.json after the split.
|
|
460
430
|
// =============================================================================
|
|
461
431
|
|
|
462
432
|
const FlowNodeDataBaseShape = {
|
|
463
433
|
name: z.string().min(1),
|
|
464
|
-
kind: z.string().min(1),
|
|
465
434
|
stateSource: StateSourceSchema,
|
|
466
435
|
handlerModule: z.string().optional(),
|
|
467
436
|
icon: z.string().optional(),
|
|
@@ -495,7 +464,7 @@ const FlowShapeNodeDataSchema = z
|
|
|
495
464
|
const FlowImageNodeDataSchema = z
|
|
496
465
|
.object({
|
|
497
466
|
path: z.string().min(1).refine(isCleanRelativePath, {
|
|
498
|
-
message: 'path must be a relative path under
|
|
467
|
+
message: 'path must be a relative path under the project root (no absolute / traversal)',
|
|
499
468
|
}),
|
|
500
469
|
alt: z.string().optional(),
|
|
501
470
|
...NodeDescriptionBaseShape,
|
|
@@ -588,45 +557,16 @@ const FlowConnectorBaseShape = {
|
|
|
588
557
|
label: z.string().optional(),
|
|
589
558
|
};
|
|
590
559
|
|
|
591
|
-
export const
|
|
560
|
+
export const FlowConnectorSchema = z
|
|
592
561
|
.object({
|
|
593
562
|
...FlowConnectorBaseShape,
|
|
594
|
-
kind: z.literal('http'),
|
|
595
563
|
method: HttpMethodSchema.optional(),
|
|
596
564
|
url: z.string().min(1).optional(),
|
|
565
|
+
eventName: z.string().min(1).optional(),
|
|
566
|
+
queueName: z.string().min(1).optional(),
|
|
597
567
|
})
|
|
598
568
|
.strict();
|
|
599
569
|
|
|
600
|
-
export const FlowEventConnectorSchema = z
|
|
601
|
-
.object({
|
|
602
|
-
...FlowConnectorBaseShape,
|
|
603
|
-
kind: z.literal('event'),
|
|
604
|
-
eventName: z.string().min(1),
|
|
605
|
-
})
|
|
606
|
-
.strict();
|
|
607
|
-
|
|
608
|
-
export const FlowQueueConnectorSchema = z
|
|
609
|
-
.object({
|
|
610
|
-
...FlowConnectorBaseShape,
|
|
611
|
-
kind: z.literal('queue'),
|
|
612
|
-
queueName: z.string().min(1),
|
|
613
|
-
})
|
|
614
|
-
.strict();
|
|
615
|
-
|
|
616
|
-
export const FlowDefaultConnectorSchema = z
|
|
617
|
-
.object({
|
|
618
|
-
...FlowConnectorBaseShape,
|
|
619
|
-
kind: z.literal('default'),
|
|
620
|
-
})
|
|
621
|
-
.strict();
|
|
622
|
-
|
|
623
|
-
const FlowConnectorSchema = z.discriminatedUnion('kind', [
|
|
624
|
-
FlowHttpConnectorSchema,
|
|
625
|
-
FlowEventConnectorSchema,
|
|
626
|
-
FlowQueueConnectorSchema,
|
|
627
|
-
FlowDefaultConnectorSchema,
|
|
628
|
-
]);
|
|
629
|
-
|
|
630
570
|
export const FlowSchema = z
|
|
631
571
|
.object({
|
|
632
572
|
version: z.literal(2),
|
|
@@ -663,10 +603,10 @@ export type FlowConnector = z.infer<typeof FlowConnectorSchema>;
|
|
|
663
603
|
|
|
664
604
|
// Envelope-only flow shape for the `seeflow schema flow` surface. The full
|
|
665
605
|
// FlowSchema validates the whole graph; this companion schema describes the
|
|
666
|
-
// top-level shape without inlining every node
|
|
667
|
-
// runtime-introspectable JSON Schema stays compact. Authors
|
|
668
|
-
// `seeflow schema node` / `seeflow schema connector` for the
|
|
669
|
-
// shapes. Not used for validation — only the catalog reads it.
|
|
606
|
+
// top-level shape without inlining every node variant or the connector
|
|
607
|
+
// shape, so the runtime-introspectable JSON Schema stays compact. Authors
|
|
608
|
+
// drill into `seeflow schema node` / `seeflow schema connector` for the
|
|
609
|
+
// detailed shapes. Not used for validation — only the catalog reads it.
|
|
670
610
|
export const FlowEnvelopeSchema = z
|
|
671
611
|
.object({
|
|
672
612
|
version: z.literal(2),
|
|
@@ -680,7 +620,7 @@ export const FlowEnvelopeSchema = z
|
|
|
680
620
|
|
|
681
621
|
// =============================================================================
|
|
682
622
|
// Style schema — keyed map of presentation overrides, side-table by id.
|
|
683
|
-
// What lives on disk in <project
|
|
623
|
+
// What lives on disk in <project>/style.json (optional file).
|
|
684
624
|
// =============================================================================
|
|
685
625
|
|
|
686
626
|
const NodeStyleSchema = z
|
package/src/server.ts
CHANGED
|
@@ -54,8 +54,6 @@ export interface CreateAppOptions {
|
|
|
54
54
|
/** Inject a ProxyFacade — tests use this to short-circuit runPlay /
|
|
55
55
|
* runReset / stopAllPlays and assert call order. */
|
|
56
56
|
proxy?: ProxyFacade;
|
|
57
|
-
/** Override base directory for new projects. Defaults to ~/.seeflow. Tests inject a tmp dir. */
|
|
58
|
-
projectBaseDir?: string;
|
|
59
57
|
}
|
|
60
58
|
|
|
61
59
|
const DEFAULT_VITE_DEV_URL = 'http://localhost:5173';
|
|
@@ -139,7 +137,6 @@ export function createApp(options: CreateAppOptions = {}): Hono {
|
|
|
139
137
|
statusRunner,
|
|
140
138
|
processSpawner: options.processSpawner,
|
|
141
139
|
proxy: options.proxy,
|
|
142
|
-
projectBaseDir: options.projectBaseDir,
|
|
143
140
|
}),
|
|
144
141
|
);
|
|
145
142
|
|
|
@@ -158,7 +155,6 @@ export function createApp(options: CreateAppOptions = {}): Hono {
|
|
|
158
155
|
const mcpServer = createMcpServer({
|
|
159
156
|
registry,
|
|
160
157
|
watcher,
|
|
161
|
-
projectBaseDir: options.projectBaseDir,
|
|
162
158
|
});
|
|
163
159
|
await mcpServer.connect(transport);
|
|
164
160
|
try {
|
package/src/short-id.ts
CHANGED
|
@@ -22,3 +22,27 @@ export function shortId(len = 10): string {
|
|
|
22
22
|
}
|
|
23
23
|
return out;
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
// Public id-type vocabulary shared by the CLI (`seeflow ids`), the REST API
|
|
27
|
+
// (`GET /api/ids/:type/:count`), and the MCP tool (`seeflow_ids`). Internal
|
|
28
|
+
// prefix `conn-` stays canonical (operations.ts mints connectors as
|
|
29
|
+
// `conn-…`); the friendlier word `connector` is what callers pass.
|
|
30
|
+
export const ID_TYPES = ['node', 'connector'] as const;
|
|
31
|
+
export type IdType = (typeof ID_TYPES)[number];
|
|
32
|
+
|
|
33
|
+
export const ID_PREFIX_BY_TYPE: Record<IdType, string> = {
|
|
34
|
+
node: 'node-',
|
|
35
|
+
connector: 'conn-',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const MAX_ID_COUNT = 100;
|
|
39
|
+
|
|
40
|
+
export const isIdType = (v: unknown): v is IdType =>
|
|
41
|
+
typeof v === 'string' && (ID_TYPES as readonly string[]).includes(v);
|
|
42
|
+
|
|
43
|
+
export function generateIds(type: IdType, count: number): string[] {
|
|
44
|
+
const prefix = ID_PREFIX_BY_TYPE[type];
|
|
45
|
+
const out: string[] = new Array(count);
|
|
46
|
+
for (let i = 0; i < count; i++) out[i] = `${prefix}${shortId()}`;
|
|
47
|
+
return out;
|
|
48
|
+
}
|
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
|
|
20
|
-
*
|
|
19
|
+
* the resolved file against `<repoPath>/nodes/<nodeId>/` so a symlink-escape
|
|
20
|
+
* can't spawn arbitrary scripts outside the node folder.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { existsSync, realpathSync } from 'node:fs';
|
|
@@ -63,7 +63,7 @@ interface TrackedHandle {
|
|
|
63
63
|
type ResolvedScript = { ok: true; absPath: string } | { ok: false };
|
|
64
64
|
|
|
65
65
|
function resolveScript(repoPath: string, nodeId: string, scriptPath: string): ResolvedScript {
|
|
66
|
-
const nodeRoot = join(repoPath, '
|
|
66
|
+
const nodeRoot = join(repoPath, 'nodes', nodeId);
|
|
67
67
|
let realRoot: string;
|
|
68
68
|
try {
|
|
69
69
|
realRoot = realpathSync(nodeRoot);
|
package/src/watcher.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { type FSWatcher, existsSync, readFileSync, statSync, watch } from 'node:fs';
|
|
3
3
|
import { basename, dirname, isAbsolute, join } from 'node:path';
|
|
4
|
+
// `file://` refs in flow.json resolve against the project root (the dir that
|
|
5
|
+
// contains flow.json). The studio is path-agnostic — whatever path the caller
|
|
6
|
+
// supplied is the project root.
|
|
7
|
+
const projectRootForFlow = (flowPath: string): string => dirname(flowPath);
|
|
4
8
|
import type { EventBus } from './events.ts';
|
|
5
9
|
import { resolveFileRefs } from './file-ref.ts';
|
|
6
10
|
import { mergeFlowAndStyle } from './merge.ts';
|
|
@@ -70,17 +74,17 @@ export interface FlowWatcher {
|
|
|
70
74
|
styleContent: string,
|
|
71
75
|
): void;
|
|
72
76
|
/**
|
|
73
|
-
* Relative paths (under
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
+
* Relative paths (under the project root) currently being watched because
|
|
78
|
+
* they're referenced by a node's `data.path` (imageNode). htmlNode content
|
|
79
|
+
* rides on the file:// resolver via `data.html`, not this list. Sorted for
|
|
80
|
+
* stable assertion order. Used by tests.
|
|
77
81
|
*/
|
|
78
82
|
referencedPaths(flowId: string): string[];
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
interface FileWatchEntry {
|
|
82
86
|
fsWatcher: FSWatcher;
|
|
83
|
-
/** basename → relative path (rooted at
|
|
87
|
+
/** basename → relative path (rooted at the project root) */
|
|
84
88
|
files: Map<string, string>;
|
|
85
89
|
/** basename → pending debounce timer for the next broadcast */
|
|
86
90
|
timers: Map<string, ReturnType<typeof setTimeout>>;
|
|
@@ -101,22 +105,6 @@ interface WatchHandle {
|
|
|
101
105
|
const resolveFilePath = (repoPath: string, flowPath: string): string =>
|
|
102
106
|
isAbsolute(flowPath) ? flowPath : join(repoPath, flowPath);
|
|
103
107
|
|
|
104
|
-
// `file://` refs in flow.json resolve against `<project>/.seeflow/` per the
|
|
105
|
-
// skill spec — not against the flow file's own directory. Walk up from the
|
|
106
|
-
// flow's parent looking for an ancestor named `.seeflow`. Fallback to the
|
|
107
|
-
// flow's parent for flows registered outside the `.seeflow/` convention.
|
|
108
|
-
const computeSeeflowRoot = (flowPath: string): string => {
|
|
109
|
-
const flowDir = dirname(flowPath);
|
|
110
|
-
let current = flowDir;
|
|
111
|
-
while (true) {
|
|
112
|
-
if (basename(current) === '.seeflow') return current;
|
|
113
|
-
const parent = dirname(current);
|
|
114
|
-
if (parent === current) break;
|
|
115
|
-
current = parent;
|
|
116
|
-
}
|
|
117
|
-
return flowDir;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
108
|
const isCleanRelativePath = (p: string): boolean => {
|
|
121
109
|
if (!p) return false;
|
|
122
110
|
// Reject data URLs early — the pre-launch hard-cut (US-004) replaces
|
|
@@ -162,7 +150,7 @@ export interface ReadMergedFlowResult {
|
|
|
162
150
|
flow: ResolvedFlow | null;
|
|
163
151
|
valid: boolean;
|
|
164
152
|
error: string | null;
|
|
165
|
-
/** Sorted relative paths under
|
|
153
|
+
/** Sorted relative paths under the project root resolved via file://. */
|
|
166
154
|
fileRefs: string[];
|
|
167
155
|
/** Flow file paths referenced via imageNode.path. */
|
|
168
156
|
staticRefs: string[];
|
|
@@ -181,7 +169,7 @@ export function readMergedFlow(flowPath: string): ReadMergedFlowResult {
|
|
|
181
169
|
}
|
|
182
170
|
|
|
183
171
|
const flowDir = dirname(flowPath);
|
|
184
|
-
const
|
|
172
|
+
const projectRoot = projectRootForFlow(flowPath);
|
|
185
173
|
const stylePath = join(flowDir, 'style.json');
|
|
186
174
|
|
|
187
175
|
let rawFlow: unknown;
|
|
@@ -194,7 +182,7 @@ export function readMergedFlow(flowPath: string): ReadMergedFlowResult {
|
|
|
194
182
|
};
|
|
195
183
|
}
|
|
196
184
|
|
|
197
|
-
const { resolved, refs } = resolveFileRefs(rawFlow,
|
|
185
|
+
const { resolved, refs } = resolveFileRefs(rawFlow, projectRoot);
|
|
198
186
|
const staticRefs = collectReferencedPaths(rawFlow);
|
|
199
187
|
|
|
200
188
|
const flowParse = FlowSchema.safeParse(resolved);
|
|
@@ -306,12 +294,12 @@ export function createWatcher(deps: WatcherDeps): FlowWatcher {
|
|
|
306
294
|
const reconcileFileWatchers = (
|
|
307
295
|
flowId: string,
|
|
308
296
|
handle: WatchHandle,
|
|
309
|
-
|
|
297
|
+
projectRoot: string,
|
|
310
298
|
refs: string[],
|
|
311
299
|
): void => {
|
|
312
300
|
const desired = new Map<string, Map<string, string>>();
|
|
313
301
|
for (const relPath of refs) {
|
|
314
|
-
const abs = join(
|
|
302
|
+
const abs = join(projectRoot, relPath);
|
|
315
303
|
const dir = dirname(abs);
|
|
316
304
|
const base = basename(abs);
|
|
317
305
|
let dirMap = desired.get(dir);
|
|
@@ -417,7 +405,7 @@ export function createWatcher(deps: WatcherDeps): FlowWatcher {
|
|
|
417
405
|
const handle = handles.get(flowId);
|
|
418
406
|
if (handle) {
|
|
419
407
|
const allRefs = [...result.fileRefs, ...result.staticRefs];
|
|
420
|
-
reconcileFileWatchers(flowId, handle,
|
|
408
|
+
reconcileFileWatchers(flowId, handle, projectRootForFlow(filePath), allRefs);
|
|
421
409
|
}
|
|
422
410
|
|
|
423
411
|
return next;
|
package/src/sdk-template.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Self-contained `emit()` helper copied verbatim into a target repo's
|
|
3
|
-
* `.seeflow/sdk/emit.ts` by `seeflow register` when the demo declares any
|
|
4
|
-
* event-bound state node. Kept dependency-free so the user's app can call it
|
|
5
|
-
* without installing `@seeflow/sdk`.
|
|
6
|
-
*/
|
|
7
|
-
export const EMIT_TEMPLATE = `// Auto-generated by seeflow register. Safe to commit; safe to delete.
|
|
8
|
-
// Re-running register will not overwrite this file.
|
|
9
|
-
export type EmitStatus = 'running' | 'done' | 'error';
|
|
10
|
-
|
|
11
|
-
export interface EmitOptions {
|
|
12
|
-
runId?: string;
|
|
13
|
-
payload?: unknown;
|
|
14
|
-
studioUrl?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const readEnv = (key: string): string | undefined => {
|
|
18
|
-
const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;
|
|
19
|
-
return proc?.env?.[key];
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export async function emit(
|
|
23
|
-
flowId: string,
|
|
24
|
-
nodeId: string,
|
|
25
|
-
status: EmitStatus,
|
|
26
|
-
opts: EmitOptions = {},
|
|
27
|
-
): Promise<void> {
|
|
28
|
-
const base = (
|
|
29
|
-
opts.studioUrl ?? readEnv('SEEFLOW_STUDIO_URL') ?? 'http://localhost:4321'
|
|
30
|
-
).replace(/\\/+$/, '');
|
|
31
|
-
await fetch(\`\${base}/api/emit\`, {
|
|
32
|
-
method: 'POST',
|
|
33
|
-
headers: { 'content-type': 'application/json' },
|
|
34
|
-
body: JSON.stringify({ flowId, nodeId, status, runId: opts.runId, payload: opts.payload }),
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
`;
|
package/src/sdk-writer.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import type { Flow } from './schema.ts';
|
|
4
|
-
import { EMIT_TEMPLATE } from './sdk-template.ts';
|
|
5
|
-
|
|
6
|
-
export type SdkWriteOutcome = 'skipped' | 'written' | 'present';
|
|
7
|
-
|
|
8
|
-
export interface SdkWriteResult {
|
|
9
|
-
outcome: SdkWriteOutcome;
|
|
10
|
-
/** Absolute path of the SDK file (written or pre-existing). Null on `skipped`. */
|
|
11
|
-
filePath: string | null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Writes `.seeflow/sdk/emit.ts` into a target repo iff the demo declares any
|
|
16
|
-
* node with `stateSource.kind === 'event'`. Idempotent: existing files are
|
|
17
|
-
* never overwritten. The only place M1's CLI mutates a user repo.
|
|
18
|
-
*/
|
|
19
|
-
export function writeSdkEmitIfNeeded(repoPath: string, demo: Flow): SdkWriteResult {
|
|
20
|
-
const hasEventState = demo.nodes.some(
|
|
21
|
-
(n) =>
|
|
22
|
-
n.type !== 'shapeNode' &&
|
|
23
|
-
n.type !== 'imageNode' &&
|
|
24
|
-
n.type !== 'iconNode' &&
|
|
25
|
-
n.type !== 'htmlNode' &&
|
|
26
|
-
n.data.stateSource.kind === 'event',
|
|
27
|
-
);
|
|
28
|
-
if (!hasEventState) return { outcome: 'skipped', filePath: null };
|
|
29
|
-
|
|
30
|
-
const sdkDir = join(repoPath, '.seeflow', 'sdk');
|
|
31
|
-
const filePath = join(sdkDir, 'emit.ts');
|
|
32
|
-
if (existsSync(filePath)) return { outcome: 'present', filePath };
|
|
33
|
-
|
|
34
|
-
mkdirSync(sdkDir, { recursive: true });
|
|
35
|
-
writeFileSync(filePath, EMIT_TEMPLATE);
|
|
36
|
-
return { outcome: 'written', filePath };
|
|
37
|
-
}
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|