@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/mcp.ts
CHANGED
|
@@ -8,14 +8,18 @@ import { type ZodTypeAny, z } from 'zod';
|
|
|
8
8
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
9
9
|
import {
|
|
10
10
|
ConnectorPatchBodySchema,
|
|
11
|
+
ConnectorsBulkBodySchema,
|
|
11
12
|
CreateProjectBodySchema,
|
|
12
13
|
NodePatchBodySchema,
|
|
14
|
+
NodesBulkBodySchema,
|
|
13
15
|
type OperationsDeps,
|
|
14
16
|
PositionBodySchema,
|
|
15
17
|
RegisterBodySchema,
|
|
16
18
|
ReorderBodySchema,
|
|
17
19
|
addConnectorImpl,
|
|
20
|
+
addConnectorsBulkImpl,
|
|
18
21
|
addNodeImpl,
|
|
22
|
+
addNodesBulkImpl,
|
|
19
23
|
createProjectImpl,
|
|
20
24
|
deleteConnectorImpl,
|
|
21
25
|
deleteFlowImpl,
|
|
@@ -55,16 +59,25 @@ export interface McpTool {
|
|
|
55
59
|
// default. The MCP `tools/list` response carries `inputSchema` inline, so
|
|
56
60
|
// stripping the wrapper keeps the wire payload tidy without losing any of
|
|
57
61
|
// the actual shape constraints.
|
|
62
|
+
//
|
|
63
|
+
// MCP clients validate that every `inputSchema.type === "object"`. Plain
|
|
64
|
+
// `z.object(...)` schemas already produce that, but `z.discriminatedUnion`
|
|
65
|
+
// emits `{anyOf: [...]}` with no top-level `type` — so we force it on.
|
|
66
|
+
// Every tool argument is an object envelope, so this is always correct.
|
|
58
67
|
const inputSchemaFromZod = (schema: ZodTypeAny): Record<string, unknown> => {
|
|
59
68
|
const json = zodToJsonSchema(schema, { $refStrategy: 'none' }) as Record<string, unknown>;
|
|
60
69
|
const { $schema: _$schema, ...rest } = json;
|
|
61
|
-
return rest;
|
|
70
|
+
return rest.type === 'object' ? rest : { type: 'object', ...rest };
|
|
62
71
|
};
|
|
63
72
|
|
|
64
73
|
const okResult = (value: unknown): CallToolResult => ({
|
|
65
74
|
content: [{ type: 'text', text: JSON.stringify(value) }],
|
|
66
75
|
});
|
|
67
76
|
|
|
77
|
+
// Error payloads (e.g. 'unknown demo', 'Failed to write demo file') still say
|
|
78
|
+
// "demo" so the strings match the REST handlers in api.ts byte-for-byte.
|
|
79
|
+
// Renaming requires updating api.ts + ~18 test assertions in lockstep — a
|
|
80
|
+
// separate refactor from this MCP review.
|
|
68
81
|
const errorResult = (text: string): CallToolResult => ({
|
|
69
82
|
isError: true,
|
|
70
83
|
content: [{ type: 'text', text }],
|
|
@@ -73,14 +86,14 @@ const errorResult = (text: string): CallToolResult => ({
|
|
|
73
86
|
// Most MCP tools take a single flowId argument. Defined inline as plain
|
|
74
87
|
// JSON Schema (rather than a one-off Zod schema) because there's no REST
|
|
75
88
|
// counterpart to share with.
|
|
76
|
-
const
|
|
89
|
+
const FLOW_ID_INPUT_SCHEMA = {
|
|
77
90
|
type: 'object',
|
|
78
91
|
properties: { flowId: { type: 'string', minLength: 1 } },
|
|
79
92
|
required: ['flowId'],
|
|
80
93
|
additionalProperties: false,
|
|
81
94
|
} as const;
|
|
82
95
|
|
|
83
|
-
const
|
|
96
|
+
const requireFlowId = (args: unknown): { flowId: string } | { error: string } => {
|
|
84
97
|
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
85
98
|
return { error: 'Invalid arguments: expected an object with flowId' };
|
|
86
99
|
}
|
|
@@ -92,7 +105,7 @@ const requireDemoId = (args: unknown): { flowId: string } | { error: string } =>
|
|
|
92
105
|
};
|
|
93
106
|
|
|
94
107
|
// {flowId, nodeId} body shape shared by move + reorder + delete inputs.
|
|
95
|
-
const
|
|
108
|
+
const FlowNodeIdBaseSchema = z.object({
|
|
96
109
|
flowId: z.string().min(1),
|
|
97
110
|
nodeId: z.string().min(1),
|
|
98
111
|
});
|
|
@@ -105,11 +118,19 @@ const AddNodeInputSchema = z.object({
|
|
|
105
118
|
node: z.record(z.unknown()),
|
|
106
119
|
});
|
|
107
120
|
|
|
108
|
-
|
|
121
|
+
// add_nodes input: { flowId, nodes: [...] }. Same loose per-item shape as
|
|
122
|
+
// add_node — ResolvedFlowSchema runs once over the whole batch server-side
|
|
123
|
+
// after the merge. Min/max bounds come from NodesBulkBodySchema so the
|
|
124
|
+
// 100-item cap shows up in the JSON Schema the agent introspects.
|
|
125
|
+
const AddNodesInputSchema = NodesBulkBodySchema.extend({
|
|
126
|
+
flowId: z.string().min(1),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const DeleteNodeInputSchema = FlowNodeIdBaseSchema;
|
|
109
130
|
|
|
110
131
|
// move_node input: { flowId, nodeId } extended with PositionBodySchema's
|
|
111
132
|
// { x, y } fields so agents see one flat schema.
|
|
112
|
-
const MoveNodeInputSchema =
|
|
133
|
+
const MoveNodeInputSchema = FlowNodeIdBaseSchema.extend({
|
|
113
134
|
x: PositionBodySchema.shape.x,
|
|
114
135
|
y: PositionBodySchema.shape.y,
|
|
115
136
|
});
|
|
@@ -118,11 +139,11 @@ const MoveNodeInputSchema = DemoNodeIdBaseSchema.extend({
|
|
|
118
139
|
// discriminated union extended with flowId/nodeId. Keeps the discriminator
|
|
119
140
|
// on `op` so the emitted JSON Schema is an oneOf the agent can introspect.
|
|
120
141
|
const ReorderNodeInputSchema = z.discriminatedUnion('op', [
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
142
|
+
FlowNodeIdBaseSchema.extend({ op: z.literal('forward') }),
|
|
143
|
+
FlowNodeIdBaseSchema.extend({ op: z.literal('backward') }),
|
|
144
|
+
FlowNodeIdBaseSchema.extend({ op: z.literal('toFront') }),
|
|
145
|
+
FlowNodeIdBaseSchema.extend({ op: z.literal('toBack') }),
|
|
146
|
+
FlowNodeIdBaseSchema.extend({
|
|
126
147
|
op: z.literal('toIndex'),
|
|
127
148
|
index: z.number().int().nonnegative(),
|
|
128
149
|
}),
|
|
@@ -147,6 +168,11 @@ const AddConnectorInputSchema = z.object({
|
|
|
147
168
|
connector: z.record(z.unknown()),
|
|
148
169
|
});
|
|
149
170
|
|
|
171
|
+
// add_connectors input: { flowId, connectors: [...] }. Mirrors add_nodes.
|
|
172
|
+
const AddConnectorsInputSchema = ConnectorsBulkBodySchema.extend({
|
|
173
|
+
flowId: z.string().min(1),
|
|
174
|
+
});
|
|
175
|
+
|
|
150
176
|
// patch_connector input: { flowId, connectorId } merged with the strict
|
|
151
177
|
// ConnectorPatchBodySchema. .extend() preserves strict mode so unknown
|
|
152
178
|
// top-level keys trip the Zod parse before any IO — matching the REST
|
|
@@ -164,7 +190,7 @@ const DeleteConnectorInputSchema = z.object({
|
|
|
164
190
|
const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
165
191
|
{
|
|
166
192
|
name: 'seeflow_list_flows',
|
|
167
|
-
description: 'List every
|
|
193
|
+
description: 'List every flow registered with the studio.',
|
|
168
194
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
169
195
|
handler: async () => {
|
|
170
196
|
const result = listDemosImpl(deps);
|
|
@@ -200,10 +226,10 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
200
226
|
},
|
|
201
227
|
{
|
|
202
228
|
name: 'seeflow_get_flow',
|
|
203
|
-
description: 'Get the full
|
|
204
|
-
inputSchema:
|
|
229
|
+
description: 'Get the full flow definition and on-disk state for a flowId.',
|
|
230
|
+
inputSchema: FLOW_ID_INPUT_SCHEMA,
|
|
205
231
|
handler: async (args) => {
|
|
206
|
-
const v =
|
|
232
|
+
const v = requireFlowId(args);
|
|
207
233
|
if ('error' in v) return errorResult(v.error);
|
|
208
234
|
const result = await getFlowImpl(deps, v.flowId);
|
|
209
235
|
switch (result.kind) {
|
|
@@ -218,7 +244,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
218
244
|
},
|
|
219
245
|
{
|
|
220
246
|
name: 'seeflow_register_flow',
|
|
221
|
-
description: 'Register an existing
|
|
247
|
+
description: 'Register an existing flow file on disk with the studio.',
|
|
222
248
|
inputSchema: inputSchemaFromZod(RegisterBodySchema),
|
|
223
249
|
handler: async (args) => {
|
|
224
250
|
const parsed = RegisterBodySchema.safeParse(args);
|
|
@@ -244,10 +270,10 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
244
270
|
},
|
|
245
271
|
{
|
|
246
272
|
name: 'seeflow_delete_flow',
|
|
247
|
-
description: 'Unregister a
|
|
248
|
-
inputSchema:
|
|
273
|
+
description: 'Unregister a flow from the studio (the on-disk file is left untouched).',
|
|
274
|
+
inputSchema: FLOW_ID_INPUT_SCHEMA,
|
|
249
275
|
handler: async (args) => {
|
|
250
|
-
const v =
|
|
276
|
+
const v = requireFlowId(args);
|
|
251
277
|
if ('error' in v) return errorResult(v.error);
|
|
252
278
|
const result = deleteFlowImpl(deps, v.flowId);
|
|
253
279
|
switch (result.kind) {
|
|
@@ -286,7 +312,8 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
286
312
|
},
|
|
287
313
|
{
|
|
288
314
|
name: 'seeflow_add_node',
|
|
289
|
-
description:
|
|
315
|
+
description:
|
|
316
|
+
'Append a new node to a flow (cascade-safe; id auto-generated when omitted). Text content fields (detail on every node; html on htmlNode) are auto-externalized to <project>/.seeflow/nodes/<id>/ and stored as file:// refs in flow.json; reads inline the resolved content transparently.',
|
|
290
317
|
inputSchema: inputSchemaFromZod(AddNodeInputSchema),
|
|
291
318
|
handler: async (args) => {
|
|
292
319
|
const parsed = AddNodeInputSchema.safeParse(args);
|
|
@@ -311,6 +338,38 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
311
338
|
}
|
|
312
339
|
},
|
|
313
340
|
},
|
|
341
|
+
{
|
|
342
|
+
name: 'seeflow_add_nodes',
|
|
343
|
+
description:
|
|
344
|
+
'Append 1–100 nodes to a flow in a single transactional write. Either every node lands or nothing does — if any item fails schema validation the whole batch is rejected. Use this instead of multiple seeflow_add_node calls when seeding a flow; it avoids per-item round-trips. Same per-item shape and externalization rules as seeflow_add_node.',
|
|
345
|
+
inputSchema: inputSchemaFromZod(AddNodesInputSchema),
|
|
346
|
+
handler: async (args) => {
|
|
347
|
+
const parsed = AddNodesInputSchema.safeParse(args);
|
|
348
|
+
if (!parsed.success) {
|
|
349
|
+
return errorResult(`Invalid add_nodes arguments: ${JSON.stringify(parsed.error.issues)}`);
|
|
350
|
+
}
|
|
351
|
+
const { flowId, nodes } = parsed.data;
|
|
352
|
+
const result = await addNodesBulkImpl(deps, flowId, { nodes });
|
|
353
|
+
switch (result.kind) {
|
|
354
|
+
case 'ok':
|
|
355
|
+
return okResult({ ok: true, nodes: result.data.nodes });
|
|
356
|
+
case 'flowNotFound':
|
|
357
|
+
return errorResult('unknown demo');
|
|
358
|
+
case 'fileNotFound':
|
|
359
|
+
return errorResult(`Flow file not found: ${result.path}`);
|
|
360
|
+
case 'badJson':
|
|
361
|
+
return errorResult(`Flow file is not valid JSON: ${result.message}`);
|
|
362
|
+
case 'badSchema':
|
|
363
|
+
return errorResult(`Flow failed schema validation: ${JSON.stringify(result.issues)}`);
|
|
364
|
+
case 'duplicateIdInBatch':
|
|
365
|
+
return errorResult(`Duplicate id in batch: ${result.id}`);
|
|
366
|
+
case 'idAlreadyExists':
|
|
367
|
+
return errorResult(`Node id already exists: ${result.id}`);
|
|
368
|
+
case 'writeFailed':
|
|
369
|
+
return errorResult(`Failed to write demo file: ${result.message}`);
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
},
|
|
314
373
|
{
|
|
315
374
|
name: 'seeflow_delete_node',
|
|
316
375
|
description: 'Delete a node and cascade-remove every connector touching it.',
|
|
@@ -372,7 +431,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
372
431
|
{
|
|
373
432
|
name: 'seeflow_patch_node',
|
|
374
433
|
description:
|
|
375
|
-
'Update fields on an existing node (position, name, description, detail, colors, border, font, shape, dimensions).',
|
|
434
|
+
'Update fields on an existing node (position, name, description, detail, icon, colors, border, font, shape, dimensions, autoSize, plus iconNode-only color/strokeWidth/alt). Setting detail (every node) or html (htmlNode) writes the content to <project>/.seeflow/nodes/<id>/{detail.md|view.html}; the file:// ref on the node persists. Empty-string detail empties the file but keeps the ref.',
|
|
376
435
|
inputSchema: inputSchemaFromZod(PatchNodeInputSchema),
|
|
377
436
|
handler: async (args) => {
|
|
378
437
|
const parsed = PatchNodeInputSchema.safeParse(args);
|
|
@@ -402,7 +461,7 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
402
461
|
{
|
|
403
462
|
name: 'seeflow_reorder_node',
|
|
404
463
|
description:
|
|
405
|
-
'Reorder a node within
|
|
464
|
+
'Reorder a node within flow.nodes[] (forward / backward / toFront / toBack / toIndex).',
|
|
406
465
|
inputSchema: inputSchemaFromZod(ReorderNodeInputSchema),
|
|
407
466
|
handler: async (args) => {
|
|
408
467
|
const parsed = ReorderNodeInputSchema.safeParse(args);
|
|
@@ -465,10 +524,44 @@ const buildTools = (deps: OperationsDeps): McpTool[] => [
|
|
|
465
524
|
}
|
|
466
525
|
},
|
|
467
526
|
},
|
|
527
|
+
{
|
|
528
|
+
name: 'seeflow_add_connectors',
|
|
529
|
+
description:
|
|
530
|
+
'Append 1–100 connectors to a flow in a single transactional write. Either every connector lands or nothing does — a dangling source/target on any item rolls back the whole batch. Use after seeflow_add_nodes when seeding a flow.',
|
|
531
|
+
inputSchema: inputSchemaFromZod(AddConnectorsInputSchema),
|
|
532
|
+
handler: async (args) => {
|
|
533
|
+
const parsed = AddConnectorsInputSchema.safeParse(args);
|
|
534
|
+
if (!parsed.success) {
|
|
535
|
+
return errorResult(
|
|
536
|
+
`Invalid add_connectors arguments: ${JSON.stringify(parsed.error.issues)}`,
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const { flowId, connectors } = parsed.data;
|
|
540
|
+
const result = await addConnectorsBulkImpl(deps, flowId, { connectors });
|
|
541
|
+
switch (result.kind) {
|
|
542
|
+
case 'ok':
|
|
543
|
+
return okResult({ ok: true, connectors: result.data.connectors });
|
|
544
|
+
case 'flowNotFound':
|
|
545
|
+
return errorResult('unknown demo');
|
|
546
|
+
case 'fileNotFound':
|
|
547
|
+
return errorResult(`Flow file not found: ${result.path}`);
|
|
548
|
+
case 'badJson':
|
|
549
|
+
return errorResult(`Flow file is not valid JSON: ${result.message}`);
|
|
550
|
+
case 'badSchema':
|
|
551
|
+
return errorResult(`Flow failed schema validation: ${JSON.stringify(result.issues)}`);
|
|
552
|
+
case 'duplicateIdInBatch':
|
|
553
|
+
return errorResult(`Duplicate id in batch: ${result.id}`);
|
|
554
|
+
case 'idAlreadyExists':
|
|
555
|
+
return errorResult(`Connector id already exists: ${result.id}`);
|
|
556
|
+
case 'writeFailed':
|
|
557
|
+
return errorResult(`Failed to write demo file: ${result.message}`);
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
},
|
|
468
561
|
{
|
|
469
562
|
name: 'seeflow_patch_connector',
|
|
470
563
|
description:
|
|
471
|
-
'Update fields on an existing connector (label, style, color, kind, per-kind payload, reconnect endpoints).',
|
|
564
|
+
'Update fields on an existing connector (label, style, color, direction, path, borderSize, fontSize, kind, per-kind payload, reconnect endpoints + handles + pins).',
|
|
472
565
|
inputSchema: inputSchemaFromZod(PatchConnectorInputSchema),
|
|
473
566
|
handler: async (args) => {
|
|
474
567
|
const parsed = PatchConnectorInputSchema.safeParse(args);
|
package/src/merge.ts
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { mkdirSync, rmSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { writeFileAtomic } from './atomic-write.ts';
|
|
4
|
+
|
|
5
|
+
// Spec for fields that the studio externalizes to disk under
|
|
6
|
+
// `<project>/.seeflow/nodes/<id>/<fileName>`. `nodeTypes` (when present)
|
|
7
|
+
// scopes the spec entry to specific node types; absent means "applies to
|
|
8
|
+
// every node type". Adding a future text field is one line.
|
|
9
|
+
export interface ExternalizedFieldSpec {
|
|
10
|
+
field: string;
|
|
11
|
+
fileName: string;
|
|
12
|
+
nodeTypes?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const EXTERNALIZED_NODE_FIELDS: readonly ExternalizedFieldSpec[] = [
|
|
16
|
+
{ field: 'detail', fileName: 'detail.md' },
|
|
17
|
+
{ field: 'html', fileName: 'view.html', nodeTypes: ['htmlNode'] },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export const externalizedFieldsForNodeType = (
|
|
21
|
+
nodeType: unknown,
|
|
22
|
+
): readonly ExternalizedFieldSpec[] => {
|
|
23
|
+
if (typeof nodeType !== 'string') return EXTERNALIZED_NODE_FIELDS.filter((e) => !e.nodeTypes);
|
|
24
|
+
return EXTERNALIZED_NODE_FIELDS.filter((e) => !e.nodeTypes || e.nodeTypes.includes(nodeType));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ExternalizedFieldName = (typeof EXTERNALIZED_NODE_FIELDS)[number]['field'];
|
|
28
|
+
|
|
29
|
+
export const nodeFileRelPath = (nodeId: string, fileName: string): string =>
|
|
30
|
+
`nodes/${nodeId}/${fileName}`;
|
|
31
|
+
|
|
32
|
+
// Node-relative ref: the resolver knows the enclosing node id from the flow.json
|
|
33
|
+
// shape (nodes[i].id), so the on-disk string only needs the filename. Kept as a
|
|
34
|
+
// 2-arg helper so call sites don't change shape and the spec stays explicit
|
|
35
|
+
// that the file lives under the given node.
|
|
36
|
+
export const nodeFileRef = (_nodeId: string, fileName: string): string => `file://${fileName}`;
|
|
37
|
+
|
|
38
|
+
export const nodeFileAbsPath = (repoPath: string, nodeId: string, fileName: string): string =>
|
|
39
|
+
join(repoPath, '.seeflow', nodeFileRelPath(nodeId, fileName));
|
|
40
|
+
|
|
41
|
+
export function writeNodeFile(absPath: string, content: string): void {
|
|
42
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
43
|
+
writeFileAtomic(absPath, content);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function removeNodeDir(repoPath: string, nodeId: string): void {
|
|
47
|
+
rmSync(join(repoPath, '.seeflow', 'nodes', nodeId), { recursive: true, force: true });
|
|
48
|
+
}
|