@tuongaz/seeflow 0.1.61 → 0.1.63
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-DAP_yx-l.js} +354 -354
- package/dist/web/assets/{index.es-BzG6d4Ro.js → index.es-2bA-nRVD.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-CcOxqEhi.js → jspdf.es.min-C7u0-VKd.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/operations.ts
CHANGED
|
@@ -21,8 +21,7 @@ import {
|
|
|
21
21
|
removeNodeDir,
|
|
22
22
|
writeNodeFile,
|
|
23
23
|
} from './node-files.ts';
|
|
24
|
-
import {
|
|
25
|
-
import { type Registry, slugify } from './registry.ts';
|
|
24
|
+
import type { Registry } from './registry.ts';
|
|
26
25
|
import {
|
|
27
26
|
ColorTokenSchema,
|
|
28
27
|
EdgePinSchema,
|
|
@@ -38,11 +37,13 @@ import {
|
|
|
38
37
|
StyleSchema,
|
|
39
38
|
TargetHandleIdSchema,
|
|
40
39
|
} from './schema.ts';
|
|
41
|
-
import { writeSdkEmitIfNeeded } from './sdk-writer.ts';
|
|
42
40
|
import { shortId } from './short-id.ts';
|
|
43
41
|
import { type FlowSnapshot, type FlowWatcher, readMergedFlow } from './watcher.ts';
|
|
44
42
|
|
|
45
|
-
|
|
43
|
+
// Both projects:create and flows:register write/read flow.json at the project
|
|
44
|
+
// root. The studio never assumes a `.seeflow/` subdirectory — whatever path
|
|
45
|
+
// the caller supplies is treated as the seeflow project root.
|
|
46
|
+
const PROJECT_FLOW_RELATIVE_PATH = 'flow.json';
|
|
46
47
|
|
|
47
48
|
export const RegisterBodySchema = z.object({
|
|
48
49
|
name: z.string().min(1).optional(),
|
|
@@ -52,7 +53,9 @@ export const RegisterBodySchema = z.object({
|
|
|
52
53
|
export type RegisterBody = z.infer<typeof RegisterBodySchema>;
|
|
53
54
|
|
|
54
55
|
export const CreateProjectBodySchema = z.object({
|
|
56
|
+
path: z.string().min(1),
|
|
55
57
|
name: z.string().min(1),
|
|
58
|
+
description: z.string().min(1).optional(),
|
|
56
59
|
});
|
|
57
60
|
export type CreateProjectBody = z.infer<typeof CreateProjectBodySchema>;
|
|
58
61
|
|
|
@@ -96,8 +99,8 @@ export const NodePatchBodySchema = z
|
|
|
96
99
|
// preserved, and the post-merge ResolvedFlowSchema reparse enforces the
|
|
97
100
|
// new type's required fields (e.g. stateNode → playNode without a
|
|
98
101
|
// playAction in the same body surfaces as `badSchema`). The per-node
|
|
99
|
-
// folder under
|
|
100
|
-
//
|
|
102
|
+
// folder under `nodes/<id>/` is keyed by id, so retype keeps scripts,
|
|
103
|
+
// detail.md, and view.html attached.
|
|
101
104
|
type: NodeTypeSchema.optional(),
|
|
102
105
|
position: PositionBodySchema.optional(),
|
|
103
106
|
name: z.string().optional(),
|
|
@@ -136,8 +139,8 @@ export const NodePatchBodySchema = z
|
|
|
136
139
|
description: z.string().optional(),
|
|
137
140
|
detail: z.string().optional(),
|
|
138
141
|
// htmlNode-only: inline HTML content. Externalized to
|
|
139
|
-
// `<project
|
|
140
|
-
//
|
|
142
|
+
// `<project>/nodes/<id>/view.html` by patchNodeImpl; the file:// ref on
|
|
143
|
+
// the node persists. Empty string empties the file but keeps the ref.
|
|
141
144
|
html: z.string().optional(),
|
|
142
145
|
// P5 overlay attach: lets the skill (or any consumer) wire executable
|
|
143
146
|
// behaviour onto a previously-created node without re-issuing it. Final
|
|
@@ -283,10 +286,10 @@ export const mergeNodeUpdates = (node: Record<string, unknown>, updates: NodePat
|
|
|
283
286
|
// (they route to style.json on write); any semantic data key not allowed
|
|
284
287
|
// by the new type's FlowDataSchema is stripped so the post-merge reparse
|
|
285
288
|
// doesn't reject lingering fields. The per-node folder under
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
289
|
+
// `nodes/<id>/` is keyed by id (unchanged), so scripts and externalized
|
|
290
|
+
// files stay attached. Missing required fields on the new type (e.g.
|
|
291
|
+
// stateNode → playNode without a playAction in the same patch) surface as
|
|
292
|
+
// `badSchema` from the ResolvedFlowSchema reparse.
|
|
290
293
|
if (updates.type !== undefined && updates.type !== node.type) {
|
|
291
294
|
node.type = updates.type;
|
|
292
295
|
const allowedSemantic = SEMANTIC_KEYS_BY_TYPE[updates.type];
|
|
@@ -335,12 +338,6 @@ export const mergeNodeUpdates = (node: Record<string, unknown>, updates: NodePat
|
|
|
335
338
|
export interface OperationsDeps {
|
|
336
339
|
registry: Registry;
|
|
337
340
|
watcher?: FlowWatcher;
|
|
338
|
-
/**
|
|
339
|
-
* Override the base directory for new projects. Defaults to seeflowHome()
|
|
340
|
-
* — `${SEEFLOW_WORKSPACE}/.seeflow` inside Docker, `~/.seeflow` locally.
|
|
341
|
-
* Tests inject a tmp dir.
|
|
342
|
-
*/
|
|
343
|
-
projectBaseDir?: string;
|
|
344
341
|
}
|
|
345
342
|
|
|
346
343
|
export interface FlowListItem {
|
|
@@ -365,13 +362,11 @@ export interface FlowGetResponse {
|
|
|
365
362
|
export interface RegisterFlowSuccess {
|
|
366
363
|
id: string;
|
|
367
364
|
slug: string;
|
|
368
|
-
sdk: { outcome: 'written' | 'present' | 'skipped'; filePath: string | null };
|
|
369
365
|
}
|
|
370
366
|
|
|
371
367
|
export interface CreateProjectSuccess {
|
|
372
368
|
id: string;
|
|
373
369
|
slug: string;
|
|
374
|
-
scaffolded: boolean;
|
|
375
370
|
}
|
|
376
371
|
|
|
377
372
|
export type ListFlowsOutcome = { kind: 'ok'; data: FlowListItem[] };
|
|
@@ -432,17 +427,14 @@ export type RegisterFlowOutcome =
|
|
|
432
427
|
| { kind: 'ok'; data: RegisterFlowSuccess }
|
|
433
428
|
| { kind: 'fileNotFound'; path: string }
|
|
434
429
|
| { kind: 'badJson'; detail: string }
|
|
435
|
-
| { kind: 'badSchema'; issues: ZodIssue[] }
|
|
436
|
-
| { kind: 'sdkWriteFailed'; id: string; slug: string; message: string };
|
|
430
|
+
| { kind: 'badSchema'; issues: ZodIssue[] };
|
|
437
431
|
|
|
438
432
|
export type DeleteFlowOutcome = { kind: 'ok' } | { kind: 'notFound' };
|
|
439
433
|
|
|
440
434
|
export type CreateProjectOutcome =
|
|
441
435
|
| { kind: 'ok'; data: CreateProjectSuccess }
|
|
442
|
-
| { kind: '
|
|
443
|
-
| { kind: '
|
|
444
|
-
| { kind: 'scaffoldFailed'; message: string }
|
|
445
|
-
| { kind: 'sdkWriteFailed'; message: string };
|
|
436
|
+
| { kind: 'alreadyExists'; path: string }
|
|
437
|
+
| { kind: 'scaffoldFailed'; message: string };
|
|
446
438
|
|
|
447
439
|
// Outcomes for the four node-lifecycle helpers. Every variant lines up with
|
|
448
440
|
// an existing REST error response so api.ts can translate them back to the
|
|
@@ -517,9 +509,8 @@ export type PatchNodeOutcome =
|
|
|
517
509
|
| { kind: 'writeFailed'; message: string };
|
|
518
510
|
|
|
519
511
|
// Partial connector update body. Strict at the top level so client typos
|
|
520
|
-
// surface as 400.
|
|
521
|
-
//
|
|
522
|
-
const ConnectorKindSchema = z.enum(['http', 'event', 'queue', 'default']);
|
|
512
|
+
// surface as 400. Field shape invariants are enforced post-merge by
|
|
513
|
+
// re-parsing the whole demo through ResolvedFlowSchema.
|
|
523
514
|
export const ConnectorPatchBodySchema = z
|
|
524
515
|
.object({
|
|
525
516
|
label: z.string().optional(),
|
|
@@ -530,7 +521,6 @@ export const ConnectorPatchBodySchema = z
|
|
|
530
521
|
path: z.enum(['curve', 'step']).optional(),
|
|
531
522
|
// US-018: per-connector label font size (mirrors NodeVisualBaseShape.fontSize).
|
|
532
523
|
fontSize: z.number().positive().optional(),
|
|
533
|
-
kind: ConnectorKindSchema.optional(),
|
|
534
524
|
eventName: z.string().optional(),
|
|
535
525
|
queueName: z.string().optional(),
|
|
536
526
|
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).optional(),
|
|
@@ -567,21 +557,10 @@ export const ConnectorPatchBodySchema = z
|
|
|
567
557
|
.strict();
|
|
568
558
|
export type ConnectorPatchBody = z.infer<typeof ConnectorPatchBodySchema>;
|
|
569
559
|
|
|
570
|
-
// Kind-specific connector fields. When `kind` changes via PATCH, these are
|
|
571
|
-
// dropped first so the resulting connector doesn't carry phantom payloads
|
|
572
|
-
// from the previous kind (e.g. an event→default change leaving eventName
|
|
573
|
-
// behind, which ResolvedFlowSchema would silently strip on parse but leave on disk).
|
|
574
|
-
const CONNECTOR_KIND_FIELDS = ['method', 'url', 'eventName', 'queueName'] as const;
|
|
575
|
-
|
|
576
560
|
export const mergeConnectorUpdates = (
|
|
577
561
|
conn: Record<string, unknown>,
|
|
578
562
|
updates: ConnectorPatchBody,
|
|
579
563
|
): void => {
|
|
580
|
-
if (updates.kind !== undefined && updates.kind !== conn.kind) {
|
|
581
|
-
for (const key of CONNECTOR_KIND_FIELDS) {
|
|
582
|
-
delete conn[key];
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
564
|
for (const [key, value] of Object.entries(updates)) {
|
|
586
565
|
if (value === undefined) continue;
|
|
587
566
|
// US-025: explicit null in the patch body means "clear this field on
|
|
@@ -1119,20 +1098,11 @@ export async function registerFlowImpl(
|
|
|
1119
1098
|
|
|
1120
1099
|
watcher?.watch(entry.id);
|
|
1121
1100
|
|
|
1122
|
-
let sdkResult: { outcome: 'written' | 'present' | 'skipped'; filePath: string | null };
|
|
1123
|
-
try {
|
|
1124
|
-
sdkResult = writeSdkEmitIfNeeded(repoPath, merged.flow);
|
|
1125
|
-
} catch (err) {
|
|
1126
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1127
|
-
return { kind: 'sdkWriteFailed', id: entry.id, slug: entry.slug, message };
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
1101
|
return {
|
|
1131
1102
|
kind: 'ok',
|
|
1132
1103
|
data: {
|
|
1133
1104
|
id: entry.id,
|
|
1134
1105
|
slug: entry.slug,
|
|
1135
|
-
sdk: { outcome: sdkResult.outcome, filePath: sdkResult.filePath },
|
|
1136
1106
|
},
|
|
1137
1107
|
};
|
|
1138
1108
|
}
|
|
@@ -1151,63 +1121,41 @@ export async function createProjectImpl(
|
|
|
1151
1121
|
body: CreateProjectBody,
|
|
1152
1122
|
): Promise<CreateProjectOutcome> {
|
|
1153
1123
|
const { registry, watcher } = deps;
|
|
1154
|
-
const { name } = body;
|
|
1155
|
-
const baseDir = deps.projectBaseDir ?? seeflowHome();
|
|
1156
|
-
const folderPath = join(baseDir, slugify(name));
|
|
1124
|
+
const { path: folderPath, name, description } = body;
|
|
1157
1125
|
|
|
1158
|
-
const demoFullPath = join(folderPath,
|
|
1126
|
+
const demoFullPath = join(folderPath, PROJECT_FLOW_RELATIVE_PATH);
|
|
1159
1127
|
|
|
1160
1128
|
if (existsSync(demoFullPath)) {
|
|
1161
|
-
|
|
1162
|
-
try {
|
|
1163
|
-
raw = await Bun.file(demoFullPath).json();
|
|
1164
|
-
} catch (err) {
|
|
1165
|
-
return { kind: 'badJson', detail: err instanceof Error ? err.message : String(err) };
|
|
1166
|
-
}
|
|
1167
|
-
const flowParse = FlowSchema.safeParse(raw);
|
|
1168
|
-
if (!flowParse.success) return { kind: 'badSchema', issues: flowParse.error.issues };
|
|
1169
|
-
|
|
1170
|
-
const lastModified = statSync(demoFullPath).mtimeMs;
|
|
1171
|
-
const entry = registry.upsert({
|
|
1172
|
-
name,
|
|
1173
|
-
repoPath: folderPath,
|
|
1174
|
-
flowPath: DEFAULT_FLOW_RELATIVE_PATH,
|
|
1175
|
-
valid: true,
|
|
1176
|
-
lastModified,
|
|
1177
|
-
});
|
|
1178
|
-
watcher?.watch(entry.id);
|
|
1179
|
-
return { kind: 'ok', data: { id: entry.id, slug: entry.slug, scaffolded: false } };
|
|
1129
|
+
return { kind: 'alreadyExists', path: folderPath };
|
|
1180
1130
|
}
|
|
1181
1131
|
|
|
1182
1132
|
// Flow-only scaffold: empty nodes/connectors, no style.json needed.
|
|
1183
|
-
const scaffold: Flow = {
|
|
1133
|
+
const scaffold: Flow = {
|
|
1134
|
+
version: 2,
|
|
1135
|
+
name,
|
|
1136
|
+
...(description !== undefined ? { description } : {}),
|
|
1137
|
+
nodes: [],
|
|
1138
|
+
connectors: [],
|
|
1139
|
+
};
|
|
1184
1140
|
|
|
1185
1141
|
try {
|
|
1186
|
-
mkdirSync(
|
|
1142
|
+
mkdirSync(folderPath, { recursive: true });
|
|
1187
1143
|
writeFileSync(demoFullPath, `${JSON.stringify(scaffold, null, 2)}\n`);
|
|
1188
1144
|
} catch (err) {
|
|
1189
1145
|
return { kind: 'scaffoldFailed', message: err instanceof Error ? err.message : String(err) };
|
|
1190
1146
|
}
|
|
1191
1147
|
|
|
1192
|
-
// Same SDK-emit path as the CLI register flow. For a fresh scaffold with no
|
|
1193
|
-
// event-bound state nodes this returns 'skipped' and writes nothing —
|
|
1194
|
-
// retained for parity with `seeflow register`.
|
|
1195
|
-
try {
|
|
1196
|
-
writeSdkEmitIfNeeded(folderPath, scaffold);
|
|
1197
|
-
} catch (err) {
|
|
1198
|
-
return { kind: 'sdkWriteFailed', message: err instanceof Error ? err.message : String(err) };
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
1148
|
const lastModified = statSync(demoFullPath).mtimeMs;
|
|
1202
1149
|
const entry = registry.upsert({
|
|
1203
1150
|
name,
|
|
1151
|
+
description,
|
|
1204
1152
|
repoPath: folderPath,
|
|
1205
|
-
flowPath:
|
|
1153
|
+
flowPath: PROJECT_FLOW_RELATIVE_PATH,
|
|
1206
1154
|
valid: true,
|
|
1207
1155
|
lastModified,
|
|
1208
1156
|
});
|
|
1209
1157
|
watcher?.watch(entry.id);
|
|
1210
|
-
return { kind: 'ok', data: { id: entry.id, slug: entry.slug
|
|
1158
|
+
return { kind: 'ok', data: { id: entry.id, slug: entry.slug } };
|
|
1211
1159
|
}
|
|
1212
1160
|
|
|
1213
1161
|
// Append a new node to the demo. Auto-generates an id when absent; ResolvedFlowSchema
|
|
@@ -1346,9 +1294,6 @@ export async function addFlowBulkImpl(
|
|
|
1346
1294
|
if (typeof newConn.id !== 'string' || newConn.id.length === 0) {
|
|
1347
1295
|
newConn.id = `conn-${shortId()}`;
|
|
1348
1296
|
}
|
|
1349
|
-
if (typeof newConn.kind !== 'string' || newConn.kind.length === 0) {
|
|
1350
|
-
newConn.kind = 'default';
|
|
1351
|
-
}
|
|
1352
1297
|
const newId = newConn.id as string;
|
|
1353
1298
|
if (connIdsInBatch.has(newId)) {
|
|
1354
1299
|
return { kind: 'duplicateIdInBatch', collection: 'connectors', id: newId };
|
|
@@ -1432,8 +1377,8 @@ export async function addFlowBulkImpl(
|
|
|
1432
1377
|
// atomic write. Final ResolvedFlowSchema parse stays in place so a pre-existing
|
|
1433
1378
|
// schema violation surfaces honestly instead of being silently papered over.
|
|
1434
1379
|
// After the flow.json write, `removeNodeDir` cascades the node's whole
|
|
1435
|
-
// `<project
|
|
1436
|
-
//
|
|
1380
|
+
// `<project>/nodes/<id>/` folder — covering detail.md, view.html, and any
|
|
1381
|
+
// imageNode upload that lived there.
|
|
1437
1382
|
export async function deleteNodeImpl(
|
|
1438
1383
|
deps: OperationsDeps,
|
|
1439
1384
|
flowId: string,
|
|
@@ -1617,9 +1562,6 @@ export async function addConnectorImpl(
|
|
|
1617
1562
|
if (typeof newConn.id !== 'string' || newConn.id.length === 0) {
|
|
1618
1563
|
newConn.id = `conn-${shortId()}`;
|
|
1619
1564
|
}
|
|
1620
|
-
if (typeof newConn.kind !== 'string' || newConn.kind.length === 0) {
|
|
1621
|
-
newConn.kind = 'default';
|
|
1622
|
-
}
|
|
1623
1565
|
const newId = newConn.id as string;
|
|
1624
1566
|
|
|
1625
1567
|
const fullPath = resolveFilePath(entry.repoPath, entry.flowPath);
|
|
@@ -1636,13 +1578,10 @@ export async function addConnectorImpl(
|
|
|
1636
1578
|
|
|
1637
1579
|
// Apply a partial PATCH body to a single connector. Mutation runs against
|
|
1638
1580
|
// the raw parsed JSON (so unknown forward-compat fields survive a round-trip).
|
|
1639
|
-
//
|
|
1640
|
-
//
|
|
1641
|
-
//
|
|
1642
|
-
//
|
|
1643
|
-
// commit so the discriminated union catches missing-required-fields
|
|
1644
|
-
// (e.g. kind='event' without eventName) and the superRefine gates
|
|
1645
|
-
// source/target referential integrity + handle role invariants.
|
|
1581
|
+
// Explicit `null` in the patch clears the field on disk (used by
|
|
1582
|
+
// reconnect-to-body to drop a pinned handle id). The whole demo is
|
|
1583
|
+
// re-validated through ResolvedFlowSchema before commit so the superRefine
|
|
1584
|
+
// gates source/target referential integrity + handle role invariants.
|
|
1646
1585
|
export async function patchConnectorImpl(
|
|
1647
1586
|
deps: OperationsDeps,
|
|
1648
1587
|
flowId: string,
|
package/src/paths.ts
CHANGED
|
@@ -11,3 +11,19 @@ export function seeflowHome(): string {
|
|
|
11
11
|
if (workspace && workspace.length > 0) return join(workspace, '.seeflow');
|
|
12
12
|
return join(homedir(), '.seeflow');
|
|
13
13
|
}
|
|
14
|
+
|
|
15
|
+
// Per-project layout: everything lives at the project root. The studio never
|
|
16
|
+
// assumes a `.seeflow/` subdirectory — whatever path the CLI / API was handed
|
|
17
|
+
// is treated as the seeflow project root. The `/seeflow` skill creates a
|
|
18
|
+
// `<host>/.seeflow/<flow-name>/` container per flow and passes that as the
|
|
19
|
+
// project path, but that's a skill convention, not a studio rule.
|
|
20
|
+
export const PROJECT_FLOW_FILENAME = 'flow.json';
|
|
21
|
+
|
|
22
|
+
export const projectFlowPath = (repoPath: string): string => join(repoPath, PROJECT_FLOW_FILENAME);
|
|
23
|
+
|
|
24
|
+
export const projectNodesRoot = (repoPath: string): string => join(repoPath, 'nodes');
|
|
25
|
+
|
|
26
|
+
export const projectNodeDir = (repoPath: string, nodeId: string): string =>
|
|
27
|
+
join(repoPath, 'nodes', nodeId);
|
|
28
|
+
|
|
29
|
+
export const projectSdkDir = (repoPath: string): string => join(repoPath, 'sdk');
|
package/src/proxy.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Defense-in-depth on scriptPath: schema.ts already rejects absolute paths
|
|
10
10
|
* and `..` traversal textually. Here we additionally realpath-resolve the
|
|
11
|
-
* script under
|
|
11
|
+
* script under the project root and reject anything that escapes that root —
|
|
12
12
|
* symlink-escape defense in line with `resolveProjectFile` in api.ts.
|
|
13
13
|
*/
|
|
14
14
|
|
|
@@ -34,7 +34,7 @@ export interface RunPlayOptions {
|
|
|
34
34
|
events: EventBus;
|
|
35
35
|
flowId: string;
|
|
36
36
|
nodeId: string;
|
|
37
|
-
/** Project root (`<repoPath>`). Script resolves under `<cwd
|
|
37
|
+
/** Project root (`<repoPath>`). Script resolves under `<cwd>/nodes/<nodeId>/`. */
|
|
38
38
|
cwd: string;
|
|
39
39
|
action: PlayAction;
|
|
40
40
|
/** Injectable for tests; defaults to `defaultProcessSpawner`. */
|
|
@@ -48,11 +48,11 @@ const SCRIPT_PATH_ESCAPE = 'scriptPath escapes project root';
|
|
|
48
48
|
|
|
49
49
|
type Resolved = { ok: true; absPath: string } | { ok: false };
|
|
50
50
|
|
|
51
|
-
// Resolve `<cwd
|
|
52
|
-
//
|
|
51
|
+
// Resolve `<cwd>/nodes/<nodeId>/<scriptPath>` and verify via realpath it
|
|
52
|
+
// stays inside the node folder. The per-node anchor means scriptPath is
|
|
53
53
|
// "scripts/play.ts" — no node id leaks into its own path.
|
|
54
54
|
function resolveScript(cwd: string, nodeId: string, scriptPath: string): Resolved {
|
|
55
|
-
const nodeRoot = join(cwd, '
|
|
55
|
+
const nodeRoot = join(cwd, 'nodes', nodeId);
|
|
56
56
|
let realRoot: string;
|
|
57
57
|
try {
|
|
58
58
|
realRoot = realpathSync(nodeRoot);
|
|
@@ -74,17 +74,17 @@ function resolveScript(cwd: string, nodeId: string, scriptPath: string): Resolve
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// Legacy anchor for resetAction (kept until resetAction gets its own design
|
|
77
|
-
// round). Same realpath escape check as resolveScript, but rooted at
|
|
78
|
-
//
|
|
77
|
+
// round). Same realpath escape check as resolveScript, but rooted at the
|
|
78
|
+
// project root rather than a per-node folder.
|
|
79
79
|
function resolveResetScript(cwd: string, scriptPath: string): Resolved {
|
|
80
|
-
const
|
|
80
|
+
const projectRoot = cwd;
|
|
81
81
|
let realRoot: string;
|
|
82
82
|
try {
|
|
83
|
-
realRoot = realpathSync(
|
|
83
|
+
realRoot = realpathSync(projectRoot);
|
|
84
84
|
} catch {
|
|
85
85
|
return { ok: false };
|
|
86
86
|
}
|
|
87
|
-
const target = resolve(
|
|
87
|
+
const target = resolve(projectRoot, scriptPath);
|
|
88
88
|
let realTarget: string;
|
|
89
89
|
try {
|
|
90
90
|
realTarget = realpathSync(target);
|
|
@@ -307,7 +307,7 @@ export async function runPlay(options: RunPlayOptions): Promise<PlayResult> {
|
|
|
307
307
|
export interface RunResetOptions {
|
|
308
308
|
events: EventBus;
|
|
309
309
|
flowId: string;
|
|
310
|
-
/** Project root (`<repoPath>`). Script resolves under `<cwd
|
|
310
|
+
/** Project root (`<repoPath>`). Script resolves under `<cwd>/`. */
|
|
311
311
|
cwd: string;
|
|
312
312
|
action: ResetAction;
|
|
313
313
|
/** Injectable for tests; defaults to `defaultProcessSpawner`. */
|
|
@@ -330,8 +330,8 @@ export async function runReset(options: RunResetOptions): Promise<ResetResult> {
|
|
|
330
330
|
const { events, flowId, cwd, action } = options;
|
|
331
331
|
const spawner = options.spawner ?? defaultProcessSpawner;
|
|
332
332
|
|
|
333
|
-
// resetAction
|
|
334
|
-
// resetAction to a later round (decision #7).
|
|
333
|
+
// resetAction is anchored at the project root — design defers per-node
|
|
334
|
+
// resetAction to a later round (decision #7).
|
|
335
335
|
const resolved = resolveResetScript(cwd, action.scriptPath);
|
|
336
336
|
if (!resolved.ok) {
|
|
337
337
|
events.broadcast({
|
package/src/schema-catalog.ts
CHANGED
|
@@ -8,15 +8,12 @@
|
|
|
8
8
|
import type { ZodTypeAny } from 'zod';
|
|
9
9
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
10
10
|
import {
|
|
11
|
-
|
|
11
|
+
FlowConnectorSchema,
|
|
12
12
|
FlowEnvelopeSchema,
|
|
13
|
-
FlowEventConnectorSchema,
|
|
14
13
|
FlowHtmlNodeSchema,
|
|
15
|
-
FlowHttpConnectorSchema,
|
|
16
14
|
FlowIconNodeSchema,
|
|
17
15
|
FlowImageNodeSchema,
|
|
18
16
|
FlowPlayNodeSchema,
|
|
19
|
-
FlowQueueConnectorSchema,
|
|
20
17
|
FlowShapeNodeSchema,
|
|
21
18
|
FlowStateNodeSchema,
|
|
22
19
|
PlayActionSchema,
|
|
@@ -51,7 +48,7 @@ const CATEGORIES: SchemaCategory[] = [
|
|
|
51
48
|
},
|
|
52
49
|
{
|
|
53
50
|
name: 'connector',
|
|
54
|
-
description: '
|
|
51
|
+
description: 'Edge between two nodes (id/source/target + optional label/style/metadata).',
|
|
55
52
|
},
|
|
56
53
|
{
|
|
57
54
|
name: 'action',
|
|
@@ -81,10 +78,7 @@ const PAYLOADS: Record<string, SchemaPayload> = {
|
|
|
81
78
|
},
|
|
82
79
|
connector: {
|
|
83
80
|
schemas: {
|
|
84
|
-
|
|
85
|
-
event: toJsonSchema(FlowEventConnectorSchema),
|
|
86
|
-
queue: toJsonSchema(FlowQueueConnectorSchema),
|
|
87
|
-
default: toJsonSchema(FlowDefaultConnectorSchema),
|
|
81
|
+
connector: toJsonSchema(FlowConnectorSchema),
|
|
88
82
|
},
|
|
89
83
|
notes: [],
|
|
90
84
|
},
|