@tuongaz/seeflow 0.1.57 → 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-CPlccVLi.js → index-DAP_yx-l.js} +354 -354
- package/dist/web/assets/{index.es-CYTTDW0Q.js → index.es-2bA-nRVD.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-DOaPC0dc.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 +83 -55
- package/src/cli-helpers.ts +6 -5
- package/src/cli-manifest.ts +129 -15
- package/src/cli.ts +106 -13
- package/src/diagram.ts +0 -1
- package/src/file-ref.ts +16 -15
- package/src/mcp.ts +96 -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 +114 -0
- package/src/schema.ts +110 -133
- package/src/server.ts +3 -5
- 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
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
"type": "playNode",
|
|
17
17
|
"data": {
|
|
18
18
|
"name": "API Gateway",
|
|
19
|
-
"kind": "gateway",
|
|
20
19
|
"stateSource": {
|
|
21
20
|
"kind": "request"
|
|
22
21
|
},
|
|
@@ -25,9 +24,7 @@
|
|
|
25
24
|
"playAction": {
|
|
26
25
|
"kind": "script",
|
|
27
26
|
"interpreter": "bun",
|
|
28
|
-
"args": [
|
|
29
|
-
"run"
|
|
30
|
-
],
|
|
27
|
+
"args": ["run"],
|
|
31
28
|
"scriptPath": "scripts/play.ts"
|
|
32
29
|
}
|
|
33
30
|
}
|
|
@@ -37,7 +34,6 @@
|
|
|
37
34
|
"type": "stateNode",
|
|
38
35
|
"data": {
|
|
39
36
|
"name": "Auth Service",
|
|
40
|
-
"kind": "service",
|
|
41
37
|
"stateSource": {
|
|
42
38
|
"kind": "request"
|
|
43
39
|
},
|
|
@@ -50,7 +46,6 @@
|
|
|
50
46
|
"type": "stateNode",
|
|
51
47
|
"data": {
|
|
52
48
|
"name": "Product Catalog",
|
|
53
|
-
"kind": "service",
|
|
54
49
|
"stateSource": {
|
|
55
50
|
"kind": "request"
|
|
56
51
|
},
|
|
@@ -63,7 +58,6 @@
|
|
|
63
58
|
"type": "playNode",
|
|
64
59
|
"data": {
|
|
65
60
|
"name": "Cart Service",
|
|
66
|
-
"kind": "service",
|
|
67
61
|
"stateSource": {
|
|
68
62
|
"kind": "request"
|
|
69
63
|
},
|
|
@@ -72,9 +66,7 @@
|
|
|
72
66
|
"playAction": {
|
|
73
67
|
"kind": "script",
|
|
74
68
|
"interpreter": "bun",
|
|
75
|
-
"args": [
|
|
76
|
-
"run"
|
|
77
|
-
],
|
|
69
|
+
"args": ["run"],
|
|
78
70
|
"scriptPath": "scripts/play.ts"
|
|
79
71
|
}
|
|
80
72
|
}
|
|
@@ -84,7 +76,6 @@
|
|
|
84
76
|
"type": "playNode",
|
|
85
77
|
"data": {
|
|
86
78
|
"name": "Order Service",
|
|
87
|
-
"kind": "worker",
|
|
88
79
|
"stateSource": {
|
|
89
80
|
"kind": "event"
|
|
90
81
|
},
|
|
@@ -93,9 +84,7 @@
|
|
|
93
84
|
"playAction": {
|
|
94
85
|
"kind": "script",
|
|
95
86
|
"interpreter": "bun",
|
|
96
|
-
"args": [
|
|
97
|
-
"run"
|
|
98
|
-
],
|
|
87
|
+
"args": ["run"],
|
|
99
88
|
"scriptPath": "scripts/play.ts"
|
|
100
89
|
}
|
|
101
90
|
}
|
|
@@ -105,7 +94,6 @@
|
|
|
105
94
|
"type": "stateNode",
|
|
106
95
|
"data": {
|
|
107
96
|
"name": "Payment Service",
|
|
108
|
-
"kind": "worker",
|
|
109
97
|
"stateSource": {
|
|
110
98
|
"kind": "event"
|
|
111
99
|
},
|
|
@@ -118,7 +106,6 @@
|
|
|
118
106
|
"type": "stateNode",
|
|
119
107
|
"data": {
|
|
120
108
|
"name": "Notification Service",
|
|
121
|
-
"kind": "worker",
|
|
122
109
|
"stateSource": {
|
|
123
110
|
"kind": "event"
|
|
124
111
|
},
|
|
@@ -149,7 +136,6 @@
|
|
|
149
136
|
"id": "conn-4XKU3GcGPF",
|
|
150
137
|
"source": "node-cQOUPXanaX",
|
|
151
138
|
"target": "node-CbwYqb7NfB",
|
|
152
|
-
"kind": "http",
|
|
153
139
|
"method": "POST",
|
|
154
140
|
"label": "REST API"
|
|
155
141
|
},
|
|
@@ -157,7 +143,6 @@
|
|
|
157
143
|
"id": "conn-OxNUxp7qB3",
|
|
158
144
|
"source": "node-CbwYqb7NfB",
|
|
159
145
|
"target": "node-3zFtHg6ENc",
|
|
160
|
-
"kind": "http",
|
|
161
146
|
"method": "POST",
|
|
162
147
|
"label": "POST /auth"
|
|
163
148
|
},
|
|
@@ -165,7 +150,6 @@
|
|
|
165
150
|
"id": "conn-7HlJF6KVHx",
|
|
166
151
|
"source": "node-CbwYqb7NfB",
|
|
167
152
|
"target": "node-kwBY8YPmYM",
|
|
168
|
-
"kind": "http",
|
|
169
153
|
"method": "GET",
|
|
170
154
|
"label": "GET /products"
|
|
171
155
|
},
|
|
@@ -173,7 +157,6 @@
|
|
|
173
157
|
"id": "conn-ORfUTiooia",
|
|
174
158
|
"source": "node-CbwYqb7NfB",
|
|
175
159
|
"target": "node-5F424NWbEu",
|
|
176
|
-
"kind": "http",
|
|
177
160
|
"method": "POST",
|
|
178
161
|
"label": "POST /cart"
|
|
179
162
|
},
|
|
@@ -181,7 +164,6 @@
|
|
|
181
164
|
"id": "conn-EABXtQv89M",
|
|
182
165
|
"source": "node-5F424NWbEu",
|
|
183
166
|
"target": "node-yKrg9DV5fJ",
|
|
184
|
-
"kind": "event",
|
|
185
167
|
"eventName": "cart.checkout",
|
|
186
168
|
"label": "cart.checkout"
|
|
187
169
|
},
|
|
@@ -189,7 +171,6 @@
|
|
|
189
171
|
"id": "conn-kjyg3RDDvu",
|
|
190
172
|
"source": "node-yKrg9DV5fJ",
|
|
191
173
|
"target": "node-mPqan8rFYN",
|
|
192
|
-
"kind": "event",
|
|
193
174
|
"eventName": "order.created",
|
|
194
175
|
"label": "order.created"
|
|
195
176
|
},
|
|
@@ -197,7 +178,6 @@
|
|
|
197
178
|
"id": "conn-wqFq0shXO5",
|
|
198
179
|
"source": "node-mPqan8rFYN",
|
|
199
180
|
"target": "node-fkptXw7uvs",
|
|
200
|
-
"kind": "event",
|
|
201
181
|
"eventName": "payment.captured",
|
|
202
182
|
"label": "payment.captured"
|
|
203
183
|
},
|
|
@@ -205,7 +185,6 @@
|
|
|
205
185
|
"id": "conn-8ftFXZvD4r",
|
|
206
186
|
"source": "node-yKrg9DV5fJ",
|
|
207
187
|
"target": "node-5SDiw3Wz6s",
|
|
208
|
-
"kind": "http",
|
|
209
188
|
"method": "POST",
|
|
210
189
|
"label": "read/write"
|
|
211
190
|
},
|
|
@@ -213,7 +192,6 @@
|
|
|
213
192
|
"id": "conn-VTfjsOckF2",
|
|
214
193
|
"source": "node-mPqan8rFYN",
|
|
215
194
|
"target": "node-5SDiw3Wz6s",
|
|
216
|
-
"kind": "http",
|
|
217
195
|
"method": "POST",
|
|
218
196
|
"label": "read/write"
|
|
219
197
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Placeholder — wire up to your running app to make this demo playable.
|
|
2
|
-
console.log(
|
|
2
|
+
console.log('play triggered');
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
"type": "playNode",
|
|
8
8
|
"data": {
|
|
9
9
|
"name": "POST /orders",
|
|
10
|
-
"kind": "service",
|
|
11
10
|
"stateSource": {
|
|
12
11
|
"kind": "request"
|
|
13
12
|
},
|
|
@@ -16,9 +15,7 @@
|
|
|
16
15
|
"playAction": {
|
|
17
16
|
"kind": "script",
|
|
18
17
|
"interpreter": "bun",
|
|
19
|
-
"args": [
|
|
20
|
-
"run"
|
|
21
|
-
],
|
|
18
|
+
"args": ["run"],
|
|
22
19
|
"scriptPath": "scripts/play.ts"
|
|
23
20
|
}
|
|
24
21
|
}
|
|
@@ -28,7 +25,6 @@
|
|
|
28
25
|
"type": "stateNode",
|
|
29
26
|
"data": {
|
|
30
27
|
"name": "Inventory Service",
|
|
31
|
-
"kind": "worker",
|
|
32
28
|
"stateSource": {
|
|
33
29
|
"kind": "event"
|
|
34
30
|
},
|
|
@@ -42,7 +38,6 @@
|
|
|
42
38
|
"type": "stateNode",
|
|
43
39
|
"data": {
|
|
44
40
|
"name": "Payment Service",
|
|
45
|
-
"kind": "worker",
|
|
46
41
|
"stateSource": {
|
|
47
42
|
"kind": "event"
|
|
48
43
|
},
|
|
@@ -55,7 +50,6 @@
|
|
|
55
50
|
"type": "stateNode",
|
|
56
51
|
"data": {
|
|
57
52
|
"name": "Fulfillment Service",
|
|
58
|
-
"kind": "worker",
|
|
59
53
|
"stateSource": {
|
|
60
54
|
"kind": "event"
|
|
61
55
|
},
|
|
@@ -69,7 +63,6 @@
|
|
|
69
63
|
"id": "conn-jJjuWBfe3a",
|
|
70
64
|
"source": "node-XKIyds0TDg",
|
|
71
65
|
"target": "node-GXTKUcE3ye",
|
|
72
|
-
"kind": "event",
|
|
73
66
|
"eventName": "order.created",
|
|
74
67
|
"label": "order.created"
|
|
75
68
|
},
|
|
@@ -77,7 +70,6 @@
|
|
|
77
70
|
"id": "conn-8DkPOzrnYo",
|
|
78
71
|
"source": "node-GXTKUcE3ye",
|
|
79
72
|
"target": "node-YOYiHJpY0i",
|
|
80
|
-
"kind": "event",
|
|
81
73
|
"eventName": "stock.reserved",
|
|
82
74
|
"label": "stock.reserved"
|
|
83
75
|
},
|
|
@@ -85,7 +77,6 @@
|
|
|
85
77
|
"id": "conn-qp90Rd2cgw",
|
|
86
78
|
"source": "node-YOYiHJpY0i",
|
|
87
79
|
"target": "node-zUIH7WFnhK",
|
|
88
|
-
"kind": "event",
|
|
89
80
|
"eventName": "payment.captured",
|
|
90
81
|
"label": "payment.captured"
|
|
91
82
|
}
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -37,8 +37,10 @@ import {
|
|
|
37
37
|
stopAllPlays as defaultStopAllPlays,
|
|
38
38
|
} from './proxy.ts';
|
|
39
39
|
import type { Registry } from './registry.ts';
|
|
40
|
+
import { getSchemaCategory, listSchemaCategories, schemaCategoryNames } from './schema-catalog.ts';
|
|
40
41
|
import { FlowSchema, ResolvedFlowSchema } from './schema.ts';
|
|
41
42
|
import { type Spawner, defaultSpawner } from './shellout.ts';
|
|
43
|
+
import { ID_TYPES, MAX_ID_COUNT, generateIds, isIdType } from './short-id.ts';
|
|
42
44
|
import type { StatusRunner } from './status-runner.ts';
|
|
43
45
|
import { readMergedFlow } from './watcher.ts';
|
|
44
46
|
import type { FlowWatcher } from './watcher.ts';
|
|
@@ -76,14 +78,14 @@ const EMIT_STATUS_TO_EVENT = {
|
|
|
76
78
|
const FilePathBodySchema = z.object({ path: z.string() });
|
|
77
79
|
|
|
78
80
|
type ResolvedProjectFile =
|
|
79
|
-
| { kind: 'ok'; absPath: string;
|
|
81
|
+
| { kind: 'ok'; absPath: string; projectRoot: string }
|
|
80
82
|
| { kind: 'unknownProject' }
|
|
81
83
|
| { kind: 'invalidPath'; reason: string }
|
|
82
84
|
| { kind: 'fileMissing'; absPath: string };
|
|
83
85
|
|
|
84
86
|
// Shared path-safety + filesystem resolution for project-scoped file routes.
|
|
85
87
|
// Performs textual rejection of absolute paths / `..` traversal, then layered
|
|
86
|
-
// realpath verification that the resolved file stays inside
|
|
88
|
+
// realpath verification that the resolved file stays inside the project root
|
|
87
89
|
// (defense against symlink escapes). Returns the realpath of an existing file
|
|
88
90
|
// on success, or `fileMissing` with the would-be absolute path so callers can
|
|
89
91
|
// soft-fail with that path included for clipboard fallback.
|
|
@@ -98,15 +100,15 @@ function resolveProjectFile(
|
|
|
98
100
|
const guard = validateRelativePath(relPath);
|
|
99
101
|
if (guard.kind === 'invalid') return { kind: 'invalidPath', reason: guard.reason };
|
|
100
102
|
|
|
101
|
-
const
|
|
103
|
+
const projectRoot = entry.repoPath;
|
|
102
104
|
let realRoot: string;
|
|
103
105
|
try {
|
|
104
|
-
realRoot = realpathSync(
|
|
106
|
+
realRoot = realpathSync(projectRoot);
|
|
105
107
|
} catch {
|
|
106
|
-
return { kind: 'fileMissing', absPath: resolve(
|
|
108
|
+
return { kind: 'fileMissing', absPath: resolve(projectRoot, relPath) };
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
const target = resolve(
|
|
111
|
+
const target = resolve(projectRoot, relPath);
|
|
110
112
|
let realTarget: string;
|
|
111
113
|
try {
|
|
112
114
|
realTarget = realpathSync(target);
|
|
@@ -119,7 +121,7 @@ function resolveProjectFile(
|
|
|
119
121
|
return { kind: 'invalidPath', reason: 'path escapes project root' };
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
return { kind: 'ok', absPath: realTarget,
|
|
124
|
+
return { kind: 'ok', absPath: realTarget, projectRoot: realRoot };
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
// Allowed extensions for /nodes/:nodeId/files/upload. Lowercased; matched after dropping the
|
|
@@ -174,8 +176,6 @@ export interface ApiOptions {
|
|
|
174
176
|
* Tests use this to record call order across runPlay / runReset /
|
|
175
177
|
* stopAllPlays and to drive each in isolation. */
|
|
176
178
|
proxy?: ProxyFacade;
|
|
177
|
-
/** Override base directory for new projects. Defaults to ~/.seeflow. Tests inject a tmp dir. */
|
|
178
|
-
projectBaseDir?: string;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
/**
|
|
@@ -203,8 +203,7 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
203
203
|
const platform = options.platform ?? process.platform;
|
|
204
204
|
const processSpawner = options.processSpawner;
|
|
205
205
|
const proxy = options.proxy ?? defaultProxyFacade;
|
|
206
|
-
const
|
|
207
|
-
const ops = createOperations({ registry, watcher, projectBaseDir });
|
|
206
|
+
const ops = createOperations({ registry, watcher });
|
|
208
207
|
const api = new Hono();
|
|
209
208
|
|
|
210
209
|
api.post('/flows/register', async (c) => {
|
|
@@ -230,15 +229,6 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
230
229
|
return c.json({ error: 'Flow file is not valid JSON', detail: result.detail }, 400);
|
|
231
230
|
case 'badSchema':
|
|
232
231
|
return c.json({ error: 'Flow file failed schema validation', issues: result.issues }, 400);
|
|
233
|
-
case 'sdkWriteFailed':
|
|
234
|
-
return c.json(
|
|
235
|
-
{
|
|
236
|
-
error: `Failed to write SDK helper: ${result.message}`,
|
|
237
|
-
id: result.id,
|
|
238
|
-
slug: result.slug,
|
|
239
|
-
},
|
|
240
|
-
500,
|
|
241
|
-
);
|
|
242
232
|
}
|
|
243
233
|
});
|
|
244
234
|
|
|
@@ -298,8 +288,8 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
298
288
|
// POST /api/diagram/assemble — Phase 7a. The skill POSTs wiring + layout
|
|
299
289
|
// and gets back the assembled demo (IDs normalized, dupes dropped, dangling
|
|
300
290
|
// connectors removed, positions snapped to a 24px grid). Pure compute; the
|
|
301
|
-
// skill writes the response to $TARGET
|
|
302
|
-
//
|
|
291
|
+
// skill writes the response to $TARGET/flow.json. No schema validation
|
|
292
|
+
// here — call /demos/validate for that.
|
|
303
293
|
api.post('/diagram/assemble', async (c) => {
|
|
304
294
|
let body: unknown;
|
|
305
295
|
try {
|
|
@@ -413,17 +403,11 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
413
403
|
return c.json({ error: 'Body must be { flow, options? } or { nodes, edges, options? }' }, 400);
|
|
414
404
|
});
|
|
415
405
|
|
|
416
|
-
// POST /api/projects — UI-driven "Create new project" flow (US-020).
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
// becomes the registry display name; the on-disk demo's `name` is
|
|
422
|
-
// preserved on disk.
|
|
423
|
-
// 2. Fresh scaffold: mkdir -p the folder + .seeflow/, write a default
|
|
424
|
-
// scaffold flow.json keyed off `name`, and run the same SDK-emit
|
|
425
|
-
// helper write the CLI register flow uses (a no-op for an empty
|
|
426
|
-
// scaffold, but kept for parity).
|
|
406
|
+
// POST /api/projects — UI-driven "Create new project" flow (US-020).
|
|
407
|
+
// Scaffolds `<path>/flow.json` with the supplied name + optional
|
|
408
|
+
// description, then registers it. If the target already has a
|
|
409
|
+
// `flow.json`, returns 409 — callers should use POST /api/flows/register
|
|
410
|
+
// instead.
|
|
427
411
|
api.post('/projects', async (c) => {
|
|
428
412
|
let body: unknown;
|
|
429
413
|
try {
|
|
@@ -441,20 +425,65 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
441
425
|
switch (result.kind) {
|
|
442
426
|
case 'ok':
|
|
443
427
|
return c.json(result.data);
|
|
444
|
-
case '
|
|
445
|
-
return c.json({ error: `
|
|
446
|
-
case 'badSchema':
|
|
447
|
-
return c.json(
|
|
448
|
-
{ error: 'Existing demo file failed schema validation', issues: result.issues },
|
|
449
|
-
400,
|
|
450
|
-
);
|
|
428
|
+
case 'alreadyExists':
|
|
429
|
+
return c.json({ error: `Project already exists at ${result.path}` }, 409);
|
|
451
430
|
case 'scaffoldFailed':
|
|
452
431
|
return c.json({ error: `Failed to scaffold project: ${result.message}` }, 500);
|
|
453
|
-
case 'sdkWriteFailed':
|
|
454
|
-
return c.json({ error: `Failed to write SDK helper: ${result.message}` }, 500);
|
|
455
432
|
}
|
|
456
433
|
});
|
|
457
434
|
|
|
435
|
+
// GET /api/schema — index of categories the skill / agents can introspect.
|
|
436
|
+
// Mirrors `seeflow schema` and the `seeflow_schema` MCP tool. Drill in via
|
|
437
|
+
// GET /api/schema/:name for the full JSON Schema(s) + invariant notes.
|
|
438
|
+
api.get('/schema', (c) => c.json({ ok: true as const, categories: listSchemaCategories() }));
|
|
439
|
+
|
|
440
|
+
api.get('/schema/:name', (c) => {
|
|
441
|
+
const name = c.req.param('name');
|
|
442
|
+
const payload = getSchemaCategory(name);
|
|
443
|
+
if (!payload) {
|
|
444
|
+
return c.json(
|
|
445
|
+
{ error: `unknown schema category: ${name}`, available: schemaCategoryNames() },
|
|
446
|
+
404,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
return c.json({ ok: true as const, name, schemas: payload.schemas, notes: payload.notes });
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// GET /api/ids/:type/:count — batch-mint canonical short ids. Mirrors
|
|
453
|
+
// `seeflow ids <type> <count>` and the `seeflow_ids` MCP tool. Pure compute,
|
|
454
|
+
// no state read, no studio side effects. Same alphabet, length, and
|
|
455
|
+
// rejection-sampling as every other id producer (operations.ts, canvas,
|
|
456
|
+
// upload regex), so generated ids match wherever they're inserted.
|
|
457
|
+
api.get('/ids/:type/:count', (c) => {
|
|
458
|
+
const type = c.req.param('type');
|
|
459
|
+
if (!isIdType(type)) {
|
|
460
|
+
return c.json(
|
|
461
|
+
{
|
|
462
|
+
ok: false as const,
|
|
463
|
+
error: `invalid type: ${type} (expected one of: ${ID_TYPES.join(', ')})`,
|
|
464
|
+
},
|
|
465
|
+
400,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
const rawCount = c.req.param('count');
|
|
469
|
+
const count = Number.parseInt(rawCount, 10);
|
|
470
|
+
if (
|
|
471
|
+
!Number.isFinite(count) ||
|
|
472
|
+
String(count) !== rawCount ||
|
|
473
|
+
count < 1 ||
|
|
474
|
+
count > MAX_ID_COUNT
|
|
475
|
+
) {
|
|
476
|
+
return c.json(
|
|
477
|
+
{
|
|
478
|
+
ok: false as const,
|
|
479
|
+
error: `invalid count: ${rawCount} (expected an integer in [1, ${MAX_ID_COUNT}])`,
|
|
480
|
+
},
|
|
481
|
+
400,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
return c.json({ ok: true as const, ids: generateIds(type, count) });
|
|
485
|
+
});
|
|
486
|
+
|
|
458
487
|
api.get('/flows', (c) => {
|
|
459
488
|
const result = ops.listFlows();
|
|
460
489
|
return c.json(result.data);
|
|
@@ -517,9 +546,9 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
517
546
|
});
|
|
518
547
|
|
|
519
548
|
// GET /api/projects/:id/files/<path> — stream a project-scoped file from
|
|
520
|
-
// <repoPath
|
|
521
|
-
//
|
|
522
|
-
//
|
|
549
|
+
// <repoPath>/<path>. Path safety is layered: textual rejection (absolute /
|
|
550
|
+
// traversal), then realpath check that the resolved file stays inside the
|
|
551
|
+
// project root (defends against symlink escapes).
|
|
523
552
|
api.get('/projects/:id/files/:path{.+}', async (c) => {
|
|
524
553
|
const rawPath = c.req.param('path');
|
|
525
554
|
let relPath: string;
|
|
@@ -643,11 +672,11 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
643
672
|
});
|
|
644
673
|
|
|
645
674
|
// POST /api/projects/:id/nodes/:nodeId/files/upload — accept a multipart
|
|
646
|
-
// image upload and persist it under `<project
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
//
|
|
675
|
+
// image upload and persist it under `<project>/nodes/<nodeId>/`. Multipart
|
|
676
|
+
// shape: `file` (Blob) and optional `filename` (the original OS name).
|
|
677
|
+
// Allowlist + 5 MB cap guard against arbitrary uploads; the destination
|
|
678
|
+
// folder is scoped to the node, so delete_node's removeNodeDir cascade
|
|
679
|
+
// cleans up the asset along with the node row.
|
|
651
680
|
api.post('/projects/:id/nodes/:nodeId/files/upload', async (c) => {
|
|
652
681
|
const projectId = c.req.param('id');
|
|
653
682
|
const nodeId = c.req.param('nodeId');
|
|
@@ -682,7 +711,7 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
682
711
|
return c.json({ error: 'invalid filename or extension' }, 400);
|
|
683
712
|
}
|
|
684
713
|
|
|
685
|
-
const nodeDir = join(entry.repoPath, '
|
|
714
|
+
const nodeDir = join(entry.repoPath, 'nodes', nodeId);
|
|
686
715
|
try {
|
|
687
716
|
mkdirSync(nodeDir, { recursive: true });
|
|
688
717
|
} catch (err) {
|
|
@@ -899,10 +928,9 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
899
928
|
return c.json({ ok: true, calledResetAction });
|
|
900
929
|
});
|
|
901
930
|
|
|
902
|
-
// PATCH a single node's position back into the on-disk flow.json.
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
// + rename keeps editor diffs clean and avoids corruption mid-write.
|
|
931
|
+
// PATCH a single node's position back into the on-disk flow.json. Atomic
|
|
932
|
+
// write via tempfile + rename keeps editor diffs clean and avoids
|
|
933
|
+
// corruption mid-write.
|
|
906
934
|
api.patch('/flows/:id/nodes/:nodeId/position', async (c) => {
|
|
907
935
|
const id = c.req.param('id');
|
|
908
936
|
const nodeId = c.req.param('nodeId');
|
package/src/cli-helpers.ts
CHANGED
|
@@ -62,8 +62,9 @@ export function printError(message: string, opts: CliOutcomeOptions = {}): never
|
|
|
62
62
|
* - kind === 'badSchema'|'badJson' → exit 2
|
|
63
63
|
* - kind === 'notFound'|'flowNotFound'|'fileNotFound'|
|
|
64
64
|
* 'unknownNode'|'unknownConnector' → exit 3
|
|
65
|
-
* - kind === 'duplicateIdInBatch'|'idAlreadyExists'
|
|
66
|
-
*
|
|
65
|
+
* - kind === 'duplicateIdInBatch'|'idAlreadyExists'|
|
|
66
|
+
* 'alreadyExists' → exit 4
|
|
67
|
+
* - kind === 'writeFailed'|'scaffoldFailed' → exit 5
|
|
67
68
|
* - anything else → exit 1
|
|
68
69
|
*
|
|
69
70
|
* The error message mirrors the strings used by api.ts so the CLI's
|
|
@@ -121,10 +122,10 @@ function describeOutcome(outcome: { kind: string } & Record<string, unknown>): s
|
|
|
121
122
|
: 'Id already exists';
|
|
122
123
|
return `${prefix}: ${String(outcome.id ?? '')}`;
|
|
123
124
|
}
|
|
125
|
+
case 'alreadyExists':
|
|
126
|
+
return `Project already exists at ${String(outcome.path ?? '')}`;
|
|
124
127
|
case 'writeFailed':
|
|
125
128
|
return `Failed to write demo file: ${String(outcome.message ?? '')}`;
|
|
126
|
-
case 'sdkWriteFailed':
|
|
127
|
-
return `Failed to write SDK helper: ${String(outcome.message ?? '')}`;
|
|
128
129
|
case 'scaffoldFailed':
|
|
129
130
|
return `Failed to scaffold project: ${String(outcome.message ?? '')}`;
|
|
130
131
|
default:
|
|
@@ -142,8 +143,8 @@ export const EXIT_CODE_BY_KIND: Record<string, number> = {
|
|
|
142
143
|
unknownConnector: 3,
|
|
143
144
|
duplicateIdInBatch: 4,
|
|
144
145
|
idAlreadyExists: 4,
|
|
146
|
+
alreadyExists: 4,
|
|
145
147
|
writeFailed: 5,
|
|
146
|
-
sdkWriteFailed: 5,
|
|
147
148
|
scaffoldFailed: 5,
|
|
148
149
|
};
|
|
149
150
|
|