@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
|
@@ -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
|
@@ -40,6 +40,7 @@ import type { Registry } from './registry.ts';
|
|
|
40
40
|
import { getSchemaCategory, listSchemaCategories, schemaCategoryNames } from './schema-catalog.ts';
|
|
41
41
|
import { FlowSchema, ResolvedFlowSchema } from './schema.ts';
|
|
42
42
|
import { type Spawner, defaultSpawner } from './shellout.ts';
|
|
43
|
+
import { ID_TYPES, MAX_ID_COUNT, generateIds, isIdType } from './short-id.ts';
|
|
43
44
|
import type { StatusRunner } from './status-runner.ts';
|
|
44
45
|
import { readMergedFlow } from './watcher.ts';
|
|
45
46
|
import type { FlowWatcher } from './watcher.ts';
|
|
@@ -77,14 +78,14 @@ const EMIT_STATUS_TO_EVENT = {
|
|
|
77
78
|
const FilePathBodySchema = z.object({ path: z.string() });
|
|
78
79
|
|
|
79
80
|
type ResolvedProjectFile =
|
|
80
|
-
| { kind: 'ok'; absPath: string;
|
|
81
|
+
| { kind: 'ok'; absPath: string; projectRoot: string }
|
|
81
82
|
| { kind: 'unknownProject' }
|
|
82
83
|
| { kind: 'invalidPath'; reason: string }
|
|
83
84
|
| { kind: 'fileMissing'; absPath: string };
|
|
84
85
|
|
|
85
86
|
// Shared path-safety + filesystem resolution for project-scoped file routes.
|
|
86
87
|
// Performs textual rejection of absolute paths / `..` traversal, then layered
|
|
87
|
-
// realpath verification that the resolved file stays inside
|
|
88
|
+
// realpath verification that the resolved file stays inside the project root
|
|
88
89
|
// (defense against symlink escapes). Returns the realpath of an existing file
|
|
89
90
|
// on success, or `fileMissing` with the would-be absolute path so callers can
|
|
90
91
|
// soft-fail with that path included for clipboard fallback.
|
|
@@ -99,15 +100,15 @@ function resolveProjectFile(
|
|
|
99
100
|
const guard = validateRelativePath(relPath);
|
|
100
101
|
if (guard.kind === 'invalid') return { kind: 'invalidPath', reason: guard.reason };
|
|
101
102
|
|
|
102
|
-
const
|
|
103
|
+
const projectRoot = entry.repoPath;
|
|
103
104
|
let realRoot: string;
|
|
104
105
|
try {
|
|
105
|
-
realRoot = realpathSync(
|
|
106
|
+
realRoot = realpathSync(projectRoot);
|
|
106
107
|
} catch {
|
|
107
|
-
return { kind: 'fileMissing', absPath: resolve(
|
|
108
|
+
return { kind: 'fileMissing', absPath: resolve(projectRoot, relPath) };
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
const target = resolve(
|
|
111
|
+
const target = resolve(projectRoot, relPath);
|
|
111
112
|
let realTarget: string;
|
|
112
113
|
try {
|
|
113
114
|
realTarget = realpathSync(target);
|
|
@@ -120,7 +121,7 @@ function resolveProjectFile(
|
|
|
120
121
|
return { kind: 'invalidPath', reason: 'path escapes project root' };
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
return { kind: 'ok', absPath: realTarget,
|
|
124
|
+
return { kind: 'ok', absPath: realTarget, projectRoot: realRoot };
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
// Allowed extensions for /nodes/:nodeId/files/upload. Lowercased; matched after dropping the
|
|
@@ -175,8 +176,6 @@ export interface ApiOptions {
|
|
|
175
176
|
* Tests use this to record call order across runPlay / runReset /
|
|
176
177
|
* stopAllPlays and to drive each in isolation. */
|
|
177
178
|
proxy?: ProxyFacade;
|
|
178
|
-
/** Override base directory for new projects. Defaults to ~/.seeflow. Tests inject a tmp dir. */
|
|
179
|
-
projectBaseDir?: string;
|
|
180
179
|
}
|
|
181
180
|
|
|
182
181
|
/**
|
|
@@ -204,8 +203,7 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
204
203
|
const platform = options.platform ?? process.platform;
|
|
205
204
|
const processSpawner = options.processSpawner;
|
|
206
205
|
const proxy = options.proxy ?? defaultProxyFacade;
|
|
207
|
-
const
|
|
208
|
-
const ops = createOperations({ registry, watcher, projectBaseDir });
|
|
206
|
+
const ops = createOperations({ registry, watcher });
|
|
209
207
|
const api = new Hono();
|
|
210
208
|
|
|
211
209
|
api.post('/flows/register', async (c) => {
|
|
@@ -231,15 +229,6 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
231
229
|
return c.json({ error: 'Flow file is not valid JSON', detail: result.detail }, 400);
|
|
232
230
|
case 'badSchema':
|
|
233
231
|
return c.json({ error: 'Flow file failed schema validation', issues: result.issues }, 400);
|
|
234
|
-
case 'sdkWriteFailed':
|
|
235
|
-
return c.json(
|
|
236
|
-
{
|
|
237
|
-
error: `Failed to write SDK helper: ${result.message}`,
|
|
238
|
-
id: result.id,
|
|
239
|
-
slug: result.slug,
|
|
240
|
-
},
|
|
241
|
-
500,
|
|
242
|
-
);
|
|
243
232
|
}
|
|
244
233
|
});
|
|
245
234
|
|
|
@@ -299,8 +288,8 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
299
288
|
// POST /api/diagram/assemble — Phase 7a. The skill POSTs wiring + layout
|
|
300
289
|
// and gets back the assembled demo (IDs normalized, dupes dropped, dangling
|
|
301
290
|
// connectors removed, positions snapped to a 24px grid). Pure compute; the
|
|
302
|
-
// skill writes the response to $TARGET
|
|
303
|
-
//
|
|
291
|
+
// skill writes the response to $TARGET/flow.json. No schema validation
|
|
292
|
+
// here — call /demos/validate for that.
|
|
304
293
|
api.post('/diagram/assemble', async (c) => {
|
|
305
294
|
let body: unknown;
|
|
306
295
|
try {
|
|
@@ -414,17 +403,11 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
414
403
|
return c.json({ error: 'Body must be { flow, options? } or { nodes, edges, options? }' }, 400);
|
|
415
404
|
});
|
|
416
405
|
|
|
417
|
-
// POST /api/projects — UI-driven "Create new project" flow (US-020).
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
// becomes the registry display name; the on-disk demo's `name` is
|
|
423
|
-
// preserved on disk.
|
|
424
|
-
// 2. Fresh scaffold: mkdir -p the folder + .seeflow/, write a default
|
|
425
|
-
// scaffold flow.json keyed off `name`, and run the same SDK-emit
|
|
426
|
-
// helper write the CLI register flow uses (a no-op for an empty
|
|
427
|
-
// 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.
|
|
428
411
|
api.post('/projects', async (c) => {
|
|
429
412
|
let body: unknown;
|
|
430
413
|
try {
|
|
@@ -442,17 +425,10 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
442
425
|
switch (result.kind) {
|
|
443
426
|
case 'ok':
|
|
444
427
|
return c.json(result.data);
|
|
445
|
-
case '
|
|
446
|
-
return c.json({ error: `
|
|
447
|
-
case 'badSchema':
|
|
448
|
-
return c.json(
|
|
449
|
-
{ error: 'Existing demo file failed schema validation', issues: result.issues },
|
|
450
|
-
400,
|
|
451
|
-
);
|
|
428
|
+
case 'alreadyExists':
|
|
429
|
+
return c.json({ error: `Project already exists at ${result.path}` }, 409);
|
|
452
430
|
case 'scaffoldFailed':
|
|
453
431
|
return c.json({ error: `Failed to scaffold project: ${result.message}` }, 500);
|
|
454
|
-
case 'sdkWriteFailed':
|
|
455
|
-
return c.json({ error: `Failed to write SDK helper: ${result.message}` }, 500);
|
|
456
432
|
}
|
|
457
433
|
});
|
|
458
434
|
|
|
@@ -473,6 +449,41 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
473
449
|
return c.json({ ok: true as const, name, schemas: payload.schemas, notes: payload.notes });
|
|
474
450
|
});
|
|
475
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
|
+
|
|
476
487
|
api.get('/flows', (c) => {
|
|
477
488
|
const result = ops.listFlows();
|
|
478
489
|
return c.json(result.data);
|
|
@@ -535,9 +546,9 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
535
546
|
});
|
|
536
547
|
|
|
537
548
|
// GET /api/projects/:id/files/<path> — stream a project-scoped file from
|
|
538
|
-
// <repoPath
|
|
539
|
-
//
|
|
540
|
-
//
|
|
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).
|
|
541
552
|
api.get('/projects/:id/files/:path{.+}', async (c) => {
|
|
542
553
|
const rawPath = c.req.param('path');
|
|
543
554
|
let relPath: string;
|
|
@@ -661,11 +672,11 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
661
672
|
});
|
|
662
673
|
|
|
663
674
|
// POST /api/projects/:id/nodes/:nodeId/files/upload — accept a multipart
|
|
664
|
-
// image upload and persist it under `<project
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
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.
|
|
669
680
|
api.post('/projects/:id/nodes/:nodeId/files/upload', async (c) => {
|
|
670
681
|
const projectId = c.req.param('id');
|
|
671
682
|
const nodeId = c.req.param('nodeId');
|
|
@@ -700,7 +711,7 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
700
711
|
return c.json({ error: 'invalid filename or extension' }, 400);
|
|
701
712
|
}
|
|
702
713
|
|
|
703
|
-
const nodeDir = join(entry.repoPath, '
|
|
714
|
+
const nodeDir = join(entry.repoPath, 'nodes', nodeId);
|
|
704
715
|
try {
|
|
705
716
|
mkdirSync(nodeDir, { recursive: true });
|
|
706
717
|
} catch (err) {
|
|
@@ -917,10 +928,9 @@ export function createApi(options: ApiOptions): Hono {
|
|
|
917
928
|
return c.json({ ok: true, calledResetAction });
|
|
918
929
|
});
|
|
919
930
|
|
|
920
|
-
// PATCH a single node's position back into the on-disk flow.json.
|
|
921
|
-
//
|
|
922
|
-
//
|
|
923
|
-
// + 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.
|
|
924
934
|
api.patch('/flows/:id/nodes/:nodeId/position', async (c) => {
|
|
925
935
|
const id = c.req.param('id');
|
|
926
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
|
|