@tuongaz/seeflow 0.1.91 → 0.1.97
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/dist/web/assets/{architectureDiagram-3BPJPVTR-CLQb7I2I.js → architectureDiagram-3BPJPVTR-XQzgHME4.js} +1 -1
- package/dist/web/assets/{blockDiagram-GPEHLZMM-DN0OjtOL.js → blockDiagram-GPEHLZMM-D79pgdno.js} +1 -1
- package/dist/web/assets/{c4Diagram-AAUBKEIU-DMVbVbvl.js → c4Diagram-AAUBKEIU-DdpMasob.js} +1 -1
- package/dist/web/assets/channel-l7nIO4lY.js +1 -0
- package/dist/web/assets/{chart-Bx3ReVE3.js → chart-BS3qBv6b.js} +1 -1
- package/dist/web/assets/{chunk-2J33WTMH-9vQ1xqy3.js → chunk-2J33WTMH-DMiLaW3V.js} +1 -1
- package/dist/web/assets/{chunk-4BX2VUAB-DRGxmqVG.js → chunk-4BX2VUAB-BxRQSTSU.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-DlboNFJr.js → chunk-55IACEB6-B8VO9ECP.js} +1 -1
- package/dist/web/assets/{chunk-727SXJPM-2Se8RGwW.js → chunk-727SXJPM-CtI4DnVU.js} +1 -1
- package/dist/web/assets/{chunk-AQP2D5EJ-DqZEBh23.js → chunk-AQP2D5EJ-BUDEtGcc.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-BJX_21R2.js → chunk-FMBD7UC4-XRBBZk8O.js} +1 -1
- package/dist/web/assets/{chunk-ND2GUHAM-DFBKXknR.js → chunk-ND2GUHAM-D0exlO6X.js} +1 -1
- package/dist/web/assets/{chunk-QZHKN3VN-CHnWLNTw.js → chunk-QZHKN3VN-CnyiTlpq.js} +1 -1
- package/dist/web/assets/classDiagram-4FO5ZUOK-BPFTU8oh.js +1 -0
- package/dist/web/assets/classDiagram-v2-Q7XG4LA2-BPFTU8oh.js +1 -0
- package/dist/web/assets/{code-block-IJZcqBQa.js → code-block-CLrCA7Xe.js} +1 -1
- package/dist/web/assets/{cose-bilkent-S5V4N54A-B0LjjLKu.js → cose-bilkent-S5V4N54A-B4D1urlH.js} +1 -1
- package/dist/web/assets/{dagre-BM42HDAG-mMg_Ia_X.js → dagre-BM42HDAG-CNe7Uulx.js} +1 -1
- package/dist/web/assets/{diagram-2AECGRRQ-BUy1Taew.js → diagram-2AECGRRQ--mgpxm9o.js} +1 -1
- package/dist/web/assets/{diagram-5GNKFQAL-DH_tg4Cb.js → diagram-5GNKFQAL-CGHMTFDB.js} +1 -1
- package/dist/web/assets/{diagram-KO2AKTUF-CE1rkBey.js → diagram-KO2AKTUF-D31GLzm7.js} +1 -1
- package/dist/web/assets/{diagram-LMA3HP47-DS7ee5II.js → diagram-LMA3HP47-Bs2BLtxH.js} +1 -1
- package/dist/web/assets/{diagram-OG6HWLK6-DzCf3KBM.js → diagram-OG6HWLK6-CXUZ873r.js} +1 -1
- package/dist/web/assets/{erDiagram-TEJ5UH35-Spog-6po.js → erDiagram-TEJ5UH35-DL-eedkW.js} +1 -1
- package/dist/web/assets/{flowDiagram-I6XJVG4X-0VAojPZO.js → flowDiagram-I6XJVG4X-BQCu7G6G.js} +1 -1
- package/dist/web/assets/{ganttDiagram-6RSMTGT7-DcjKkuMU.js → ganttDiagram-6RSMTGT7-CyBhrhQa.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-PVQCEYII-Bq9UIINL.js → gitGraphDiagram-PVQCEYII-D9OqlmQL.js} +1 -1
- package/dist/web/assets/index-BKZTnCOL.js +8624 -0
- package/dist/web/assets/{index-CWGdrwRY.css → index-I8_SAWCr.css} +1 -1
- package/dist/web/assets/{index.es-BUxcZ9iL.js → index.es-BpDX3yd0.js} +1 -1
- package/dist/web/assets/{infoDiagram-5YYISTIA-yxIJiF1X.js → infoDiagram-5YYISTIA-CMxwx_2B.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-YF4QCWOH-BkcKepSB.js → ishikawaDiagram-YF4QCWOH-DD1y6qVy.js} +1 -1
- package/dist/web/assets/{journeyDiagram-JHISSGLW-KfXgvDdv.js → journeyDiagram-JHISSGLW-CHo991VZ.js} +1 -1
- package/dist/web/assets/{jspdf.es.min-BPfxcczM.js → jspdf.es.min-C8_HZhlK.js} +3 -3
- package/dist/web/assets/{kanban-definition-UN3LZRKU-B2m52Unk.js → kanban-definition-UN3LZRKU-CoYkI8Ob.js} +1 -1
- package/dist/web/assets/{linear-D-pwAWPr.js → linear-CQGcGLyB.js} +1 -1
- package/dist/web/assets/{markdown-lr17R9FO.js → markdown-Bud9JO0j.js} +1 -1
- package/dist/web/assets/{mermaid.core-CRo4rzDL.js → mermaid.core-1G8gw6AK.js} +4 -4
- package/dist/web/assets/{mindmap-definition-RKZ34NQL-DUpVPXgC.js → mindmap-definition-RKZ34NQL-CJHnwtSU.js} +1 -1
- package/dist/web/assets/{pieDiagram-4H26LBE5-CatVLCYi.js → pieDiagram-4H26LBE5-CXrXwuPG.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-W4KKPZXB-TJ5_ZxiK.js → quadrantDiagram-W4KKPZXB-BVJKIfMF.js} +1 -1
- package/dist/web/assets/{requirementDiagram-4Y6WPE33-B7CTMFGC.js → requirementDiagram-4Y6WPE33-ZFgLHB2Y.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-5OEKKPKP-C56xsvrm.js → sankeyDiagram-5OEKKPKP-cP9rHdFK.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-3UESZ5HK-EPKxvTJ9.js → sequenceDiagram-3UESZ5HK-BbruCi6T.js} +1 -1
- package/dist/web/assets/{stateDiagram-AJRCARHV-Cma2F0T8.js → stateDiagram-AJRCARHV-CqGbTDXI.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-BHNVJYJU-e0EPpmUp.js +1 -0
- package/dist/web/assets/{time-D6UR1Qac.js → time-CXSgtiIX.js} +1 -1
- package/dist/web/assets/{timeline-definition-PNZ67QCA-iYUvK9JO.js → timeline-definition-PNZ67QCA-B4in6942.js} +1 -1
- package/dist/web/assets/{vennDiagram-CIIHVFJN-CkLxULtS.js → vennDiagram-CIIHVFJN-D3Esdgtc.js} +1 -1
- package/dist/web/assets/{wardley-L42UT6IY-DPcFqZu2.js → wardley-L42UT6IY-CqOLhiLD.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-YWT4CUSO-BKqSvb-r.js → wardleyDiagram-YWT4CUSO-DalbSLu7.js} +1 -1
- package/dist/web/assets/{xychartDiagram-2RQKCTM6-0L00Xzr6.js → xychartDiagram-2RQKCTM6-Bgnuf0j-.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/api.ts +63 -8
- package/src/cli-manifest.ts +50 -25
- package/src/cli.ts +34 -9
- package/src/mcp.ts +16 -2
- package/src/merge.ts +0 -1
- package/src/node-files.ts +39 -1
- package/src/operations.ts +81 -55
- package/src/schema-catalog.ts +167 -3
- package/src/schema.ts +11 -7
- package/src/server.ts +6 -1
- package/dist/web/assets/channel-CilAQwI4.js +0 -1
- package/dist/web/assets/classDiagram-4FO5ZUOK-fiT0MjXY.js +0 -1
- package/dist/web/assets/classDiagram-v2-Q7XG4LA2-fiT0MjXY.js +0 -1
- package/dist/web/assets/index-DFpY3RpV.js +0 -8624
- package/dist/web/assets/stateDiagram-v2-BHNVJYJU-DRP_iswg.js +0 -1
package/src/cli-manifest.ts
CHANGED
|
@@ -499,7 +499,7 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
499
499
|
],
|
|
500
500
|
body: { schemaRef: 'CreateProjectBody' },
|
|
501
501
|
outputs: {
|
|
502
|
-
okExample: { id: 'abc12345', slug: 'checkout' },
|
|
502
|
+
okExample: { id: 'abc12345', slug: 'checkout/main' },
|
|
503
503
|
errorKinds: ['alreadyExists', 'scaffoldFailed'],
|
|
504
504
|
},
|
|
505
505
|
requiresStudio: false,
|
|
@@ -872,26 +872,36 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
872
872
|
synopsis: 'seeflow schema [<category> [<subname>]] [--jq <filter>]',
|
|
873
873
|
description:
|
|
874
874
|
'Introspect the SeeFlow flow.json / style.json / spec.json schemas at ' +
|
|
875
|
-
'runtime.
|
|
876
|
-
'
|
|
877
|
-
'
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
'
|
|
881
|
-
'
|
|
882
|
-
'
|
|
883
|
-
|
|
884
|
-
'
|
|
885
|
-
'
|
|
886
|
-
'
|
|
887
|
-
|
|
888
|
-
'
|
|
889
|
-
'
|
|
890
|
-
'
|
|
891
|
-
'
|
|
892
|
-
|
|
893
|
-
'
|
|
894
|
-
'
|
|
875
|
+
'runtime. **Run this before designing or authoring any node** — the CLI ' +
|
|
876
|
+
'is the only source of truth for field shapes, never memorise them.\n\n' +
|
|
877
|
+
'Progressive workflow:\n' +
|
|
878
|
+
' 1. `seeflow schema` → index of the six categories (flow, node, ' +
|
|
879
|
+
'connector, action, componentSpec, style) with every drill `subname` ' +
|
|
880
|
+
'inlined under each `categories[].subnames`, plus a `usage` block with ' +
|
|
881
|
+
'copy-paste examples.\n' +
|
|
882
|
+
' 2. `seeflow schema <category>` → full JSON Schema(s) (Draft-07) for ' +
|
|
883
|
+
"every variant in the category, the cross-variant `notes`, the category's " +
|
|
884
|
+
'`subnames`, and a `jqHints` block listing concrete filter paths to try ' +
|
|
885
|
+
'next.\n' +
|
|
886
|
+
' 3. `seeflow schema <category> <subname>` → just one named schema ' +
|
|
887
|
+
"(e.g. `node rectangle`, `action playAction`). The response's " +
|
|
888
|
+
'`jqHints.dataFields` lists every `data.<field>` available on the variant, ' +
|
|
889
|
+
'and `jqHints.examples` gives ready-to-paste `--jq` paths pointing at ' +
|
|
890
|
+
'each one — drill straight to the field you care about without re-paying ' +
|
|
891
|
+
'for the full schema.\n\n' +
|
|
892
|
+
"The `node` payload includes all 13 flat variants (including type:'component', " +
|
|
893
|
+
'whose `spec` field lives in a sidecar — drill into `componentSpec` for ' +
|
|
894
|
+
'that shape). The category-level `notes` ride along unchanged on subname ' +
|
|
895
|
+
'lookups because the cross-variant invariants still apply.\n\n' +
|
|
896
|
+
'Pass `--jq <filter>` to extract a slice of the response with a jq path ' +
|
|
897
|
+
'expression. Supported subset: identity (`.`), field access (`.foo.bar`), ' +
|
|
898
|
+
'bracket access (`.["foo"]`, `.[3]`, negative indices allowed), iteration ' +
|
|
899
|
+
'(`.foo[]`), optional `?` (e.g. `.foo?` to suppress type errors), and pipe ' +
|
|
900
|
+
'(`|`). Single-output filters return the value under `{ result: <value> }`; ' +
|
|
901
|
+
'multi-output filters (from `[]` or `|`) return `{ result: [<v1>, <v2>, ...] }`. ' +
|
|
902
|
+
'Bad filters exit with code 2 and `code:"badJq"`. The fastest pattern is ' +
|
|
903
|
+
'`seeflow schema <category> <subname> --jq .schemas.<subname>.properties.data.properties.<field>` ' +
|
|
904
|
+
'— pulls one field shape and nothing else.',
|
|
895
905
|
category: 'meta',
|
|
896
906
|
args: [
|
|
897
907
|
{
|
|
@@ -915,13 +925,27 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
915
925
|
name: 'jq',
|
|
916
926
|
valuePlaceholder: '<filter>',
|
|
917
927
|
description:
|
|
918
|
-
'Apply a jq path-subset filter to the response payload.
|
|
919
|
-
'`.schemas
|
|
920
|
-
'`.schemas
|
|
928
|
+
'Apply a jq path-subset filter to the response payload. Common paths: ' +
|
|
929
|
+
'`.schemas.<subname>` (one variant), ' +
|
|
930
|
+
'`.schemas.<subname>.required` (required fields), ' +
|
|
931
|
+
'`.schemas.<subname>.properties.data.properties` (every data.* shape), ' +
|
|
932
|
+
'`.schemas.<subname>.properties.data.properties.<field>` (one data.* ' +
|
|
933
|
+
'shape — `jqHints.dataFields` enumerates `<field>` for you), ' +
|
|
934
|
+
'`.schemas[]` (iterate variants), `.notes[]` (iterate invariants). ' +
|
|
935
|
+
'Single-variant lookups surface `jqHints.examples` with these paths ' +
|
|
936
|
+
'pre-built for the chosen subname.',
|
|
921
937
|
},
|
|
922
938
|
],
|
|
923
939
|
outputs: {
|
|
924
|
-
okExample: {
|
|
940
|
+
okExample: {
|
|
941
|
+
categories: [
|
|
942
|
+
{ name: 'flow', description: 'Top-level flow.json envelope.', subnames: ['flow'] },
|
|
943
|
+
],
|
|
944
|
+
usage: {
|
|
945
|
+
drill: 'seeflow schema <category> [<subname>]',
|
|
946
|
+
filter: 'seeflow schema <category> [<subname>] --jq <jq-path>',
|
|
947
|
+
},
|
|
948
|
+
},
|
|
925
949
|
errorKinds: ['notFound', 'badJq'],
|
|
926
950
|
},
|
|
927
951
|
requiresStudio: false,
|
|
@@ -934,6 +958,7 @@ export const COMMAND_MANIFEST: CommandManifestEntry[] = [
|
|
|
934
958
|
'seeflow schema connector',
|
|
935
959
|
'seeflow schema componentSpec',
|
|
936
960
|
'seeflow schema node --jq .schemas.rectangle',
|
|
961
|
+
"seeflow schema node rectangle --jq '.schemas.rectangle.properties.data.properties.playAction'",
|
|
937
962
|
"seeflow schema node --jq '.schemas.image.properties.data.properties.path'",
|
|
938
963
|
"seeflow schema node --jq '.schemas[]'",
|
|
939
964
|
],
|
package/src/cli.ts
CHANGED
|
@@ -268,11 +268,18 @@ Commands (work without a running studio):
|
|
|
268
268
|
connectors:patch <connId> Patch a connector (--project <p> --flow <f>) [--json/--file/--stdin]
|
|
269
269
|
connectors:delete <connId> Delete a connector (--project <p> --flow <f>)
|
|
270
270
|
validate Schema-validate a flow.json (--file <file> [--style <file>])
|
|
271
|
-
schema [<category> [<subname>]]
|
|
272
|
-
Get the flow.json schema.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
271
|
+
schema [<category> [<subname>]] [--jq <path>]
|
|
272
|
+
Get the flow.json schema. Run this before designing /
|
|
273
|
+
authoring nodes. No arg → category index with subnames
|
|
274
|
+
inlined; category arg → full JSON Schema(s) + jqHints;
|
|
275
|
+
subname arg → one variant + jqHints.dataFields listing
|
|
276
|
+
every data.<field> available. The 'componentCatalog'
|
|
277
|
+
category lists every legal componentSpec.elements[].type
|
|
278
|
+
and its props (drill: 'schema componentCatalog <Name>').
|
|
279
|
+
Every response carries jqHints.rootPath (the jq prefix
|
|
280
|
+
for that level). Pair with --jq to slice (e.g. 'schema
|
|
281
|
+
node rectangle --jq
|
|
282
|
+
.schemas.rectangle.properties.data.properties.playAction').
|
|
276
283
|
ids <type> <count> Print <count> short ids of the given <type>, one per
|
|
277
284
|
line. <type> is 'node' (-> 'node-...') or 'connector'
|
|
278
285
|
(-> 'conn-...'). <count> is 1..100. Call once per type
|
|
@@ -1100,10 +1107,21 @@ async function runSchema() {
|
|
|
1100
1107
|
const category = argv[1] && !argv[1].startsWith('--') ? argv[1] : undefined;
|
|
1101
1108
|
const subname = argv[2] && !argv[2].startsWith('--') ? argv[2] : undefined;
|
|
1102
1109
|
const jqFilter = flagValue('jq');
|
|
1103
|
-
const {
|
|
1104
|
-
|
|
1110
|
+
const {
|
|
1111
|
+
listSchemaCategories,
|
|
1112
|
+
getSchemaCategory,
|
|
1113
|
+
getCategorySubschema,
|
|
1114
|
+
listCategorySubnames,
|
|
1115
|
+
buildJqHints,
|
|
1116
|
+
buildIndexJqHints,
|
|
1117
|
+
SCHEMA_INDEX_USAGE,
|
|
1118
|
+
} = await import('./schema-catalog.ts');
|
|
1105
1119
|
if (!category) {
|
|
1106
|
-
const base = {
|
|
1120
|
+
const base = {
|
|
1121
|
+
categories: listSchemaCategories(),
|
|
1122
|
+
usage: SCHEMA_INDEX_USAGE,
|
|
1123
|
+
jqHints: buildIndexJqHints(),
|
|
1124
|
+
};
|
|
1107
1125
|
if (jqFilter !== undefined) {
|
|
1108
1126
|
printOk({ result: applyJqOrDie(base, jqFilter) });
|
|
1109
1127
|
}
|
|
@@ -1141,6 +1159,7 @@ async function runSchema() {
|
|
|
1141
1159
|
subname,
|
|
1142
1160
|
schemas: single.schemas,
|
|
1143
1161
|
notes: single.notes,
|
|
1162
|
+
jqHints: buildJqHints(category as string, subname),
|
|
1144
1163
|
};
|
|
1145
1164
|
if (jqFilter !== undefined) {
|
|
1146
1165
|
printOk({ name: category, subname, result: applyJqOrDie(base, jqFilter) });
|
|
@@ -1154,7 +1173,13 @@ async function runSchema() {
|
|
|
1154
1173
|
process.stderr.write(`${JSON.stringify({ error: message, code: 'notFound', available })}\n`);
|
|
1155
1174
|
process.exit(3);
|
|
1156
1175
|
}
|
|
1157
|
-
const base = {
|
|
1176
|
+
const base = {
|
|
1177
|
+
name: category,
|
|
1178
|
+
schemas: payload.schemas,
|
|
1179
|
+
notes: payload.notes,
|
|
1180
|
+
subnames: listCategorySubnames(category as string) ?? [],
|
|
1181
|
+
jqHints: buildJqHints(category as string),
|
|
1182
|
+
};
|
|
1158
1183
|
if (jqFilter !== undefined) {
|
|
1159
1184
|
printOk({ name: category, result: applyJqOrDie(base, jqFilter) });
|
|
1160
1185
|
}
|
package/src/mcp.ts
CHANGED
|
@@ -30,6 +30,9 @@ import {
|
|
|
30
30
|
} from './operations.ts';
|
|
31
31
|
import type { Registry } from './registry.ts';
|
|
32
32
|
import {
|
|
33
|
+
SCHEMA_INDEX_USAGE,
|
|
34
|
+
buildIndexJqHints,
|
|
35
|
+
buildJqHints,
|
|
33
36
|
getCategorySubschema,
|
|
34
37
|
getSchemaCategory,
|
|
35
38
|
listCategorySubnames,
|
|
@@ -329,7 +332,11 @@ const buildTools = (ops: Operations, ctx: ToolContext): McpTool[] => [
|
|
|
329
332
|
if (subname !== undefined && subname !== null && subname !== '') {
|
|
330
333
|
return errorResult('Invalid arguments: `subname` requires `name` to be set');
|
|
331
334
|
}
|
|
332
|
-
return okResult({
|
|
335
|
+
return okResult({
|
|
336
|
+
categories: listSchemaCategories(),
|
|
337
|
+
usage: SCHEMA_INDEX_USAGE,
|
|
338
|
+
jqHints: buildIndexJqHints(),
|
|
339
|
+
});
|
|
333
340
|
}
|
|
334
341
|
if (typeof name !== 'string') {
|
|
335
342
|
return errorResult('Invalid arguments: `name` must be a string when present');
|
|
@@ -345,6 +352,7 @@ const buildTools = (ops: Operations, ctx: ToolContext): McpTool[] => [
|
|
|
345
352
|
subname,
|
|
346
353
|
schemas: single.schemas,
|
|
347
354
|
notes: single.notes,
|
|
355
|
+
jqHints: buildJqHints(name, subname),
|
|
348
356
|
});
|
|
349
357
|
}
|
|
350
358
|
const availableSubs = listCategorySubnames(name);
|
|
@@ -363,7 +371,13 @@ const buildTools = (ops: Operations, ctx: ToolContext): McpTool[] => [
|
|
|
363
371
|
`unknown schema category: ${name} (available: ${schemaCategoryNames().join(', ')})`,
|
|
364
372
|
);
|
|
365
373
|
}
|
|
366
|
-
return okResult({
|
|
374
|
+
return okResult({
|
|
375
|
+
name,
|
|
376
|
+
schemas: payload.schemas,
|
|
377
|
+
notes: payload.notes,
|
|
378
|
+
subnames: listCategorySubnames(name) ?? [],
|
|
379
|
+
jqHints: buildJqHints(name),
|
|
380
|
+
});
|
|
367
381
|
},
|
|
368
382
|
},
|
|
369
383
|
{
|
package/src/merge.ts
CHANGED
package/src/node-files.ts
CHANGED
|
@@ -8,16 +8,54 @@ import { writeFileAtomic } from './atomic-write.ts';
|
|
|
8
8
|
// `flows/<flow-id>/nodes/<id>/`; for legacy single-flow fixtures with
|
|
9
9
|
// `flowPath: 'flow.json'` it collapses to the project root. `nodeTypes` (when
|
|
10
10
|
// present) scopes the spec entry to specific node types; absent means
|
|
11
|
-
// "applies to every node type".
|
|
11
|
+
// "applies to every node type".
|
|
12
|
+
//
|
|
13
|
+
// Two flavors of externalization:
|
|
14
|
+
// - `kind: 'ref'` (default) — write the file, replace `data[field]` with
|
|
15
|
+
// `file://<fileName>`. The ref survives splitFlow (the field is in
|
|
16
|
+
// NODE_DATA_FLOW_KEYS in merge.ts). Used by string fields like `detail`
|
|
17
|
+
// and `html`.
|
|
18
|
+
// - `kind: 'sidecar'` — write the file, leave `data[field]` untouched on
|
|
19
|
+
// the in-memory node so the post-mutation parse still sees the original
|
|
20
|
+
// value. splitFlow drops the field from flow.json on write; the resolver
|
|
21
|
+
// inlines it from disk on read. Used by JSON fields like component `spec`.
|
|
22
|
+
//
|
|
23
|
+
// `serialize` turns the in-memory value into file contents. Returning `null`
|
|
24
|
+
// skips the write entirely — used by `spec` to no-op when the caller didn't
|
|
25
|
+
// supply one, instead of writing an empty file that would fail JSON parse
|
|
26
|
+
// on the next read.
|
|
12
27
|
export interface ExternalizedFieldSpec {
|
|
13
28
|
field: string;
|
|
14
29
|
fileName: string;
|
|
15
30
|
nodeTypes?: readonly string[];
|
|
31
|
+
kind?: 'ref' | 'sidecar';
|
|
32
|
+
serialize?: (value: unknown) => string | null;
|
|
16
33
|
}
|
|
17
34
|
|
|
35
|
+
// Default serializer: strings pass through; non-strings coerce to empty.
|
|
36
|
+
// Keeps the historical detail/html behavior — an absent detail still writes
|
|
37
|
+
// an empty detail.md so the file:// ref points somewhere.
|
|
38
|
+
export const defaultExternalizedSerializer = (value: unknown): string =>
|
|
39
|
+
typeof value === 'string' ? value : '';
|
|
40
|
+
|
|
41
|
+
// JSON serializer for sidecar fields: pretty-print plain objects with a
|
|
42
|
+
// trailing newline. Returns null for anything else so the loop can skip the
|
|
43
|
+
// write rather than emit an invalid sidecar.
|
|
44
|
+
const jsonExternalizedSerializer = (value: unknown): string | null =>
|
|
45
|
+
value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
46
|
+
? `${JSON.stringify(value, null, 2)}\n`
|
|
47
|
+
: null;
|
|
48
|
+
|
|
18
49
|
export const EXTERNALIZED_NODE_FIELDS: readonly ExternalizedFieldSpec[] = [
|
|
19
50
|
{ field: 'detail', fileName: 'detail.md' },
|
|
20
51
|
{ field: 'html', fileName: 'view.html', nodeTypes: ['html'] },
|
|
52
|
+
{
|
|
53
|
+
field: 'spec',
|
|
54
|
+
fileName: 'spec.json',
|
|
55
|
+
nodeTypes: ['component'],
|
|
56
|
+
kind: 'sidecar',
|
|
57
|
+
serialize: jsonExternalizedSerializer,
|
|
58
|
+
},
|
|
21
59
|
];
|
|
22
60
|
|
|
23
61
|
export const externalizedFieldsForNodeType = (
|
package/src/operations.ts
CHANGED
|
@@ -16,12 +16,14 @@ import { type LayoutOptions, computeLayout } from './layout.ts';
|
|
|
16
16
|
import { mergeFlowAndStyle, splitFlow } from './merge.ts';
|
|
17
17
|
import {
|
|
18
18
|
EXTERNALIZED_NODE_FIELDS,
|
|
19
|
+
defaultExternalizedSerializer,
|
|
19
20
|
externalizedFieldsForNodeType,
|
|
20
21
|
nodeFileAbsPath,
|
|
21
22
|
nodeFileRef,
|
|
22
23
|
removeNodeDir,
|
|
23
24
|
writeNodeFile,
|
|
24
25
|
} from './node-files.ts';
|
|
26
|
+
import { seeflowHome } from './paths.ts';
|
|
25
27
|
import { readProjectManifest, scanProject } from './project-scanner.ts';
|
|
26
28
|
import { type FlowEntry, type Registry, slugify } from './registry.ts';
|
|
27
29
|
import {
|
|
@@ -52,7 +54,9 @@ export const RegisterBodySchema = z.object({
|
|
|
52
54
|
export type RegisterBody = z.infer<typeof RegisterBodySchema>;
|
|
53
55
|
|
|
54
56
|
export const CreateProjectBodySchema = z.object({
|
|
55
|
-
|
|
57
|
+
// Optional: when omitted the project is scaffolded under
|
|
58
|
+
// <seeflowHome>/projects/<slug-of-name>. See createProjectImpl.
|
|
59
|
+
path: z.string().min(1).optional(),
|
|
56
60
|
name: z.string().min(1),
|
|
57
61
|
description: z.string().min(1).optional(),
|
|
58
62
|
});
|
|
@@ -94,16 +98,19 @@ export const NodePatchBodySchema = z
|
|
|
94
98
|
type: NodeTypeSchema.optional(),
|
|
95
99
|
position: PositionBodySchema.optional(),
|
|
96
100
|
name: z.string().optional(),
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
// Style/visual keys accept explicit `null` as the clear signal (mirrors
|
|
102
|
+
// `icon`): mergeNodeUpdates strips the key from disk, so undo can revert a
|
|
103
|
+
// style field back to its pre-edit "unset" default instead of being a
|
|
104
|
+
// no-op (a dropped-undefined PATCH leaves the field untouched).
|
|
105
|
+
borderColor: ColorTokenSchema.nullable().optional(),
|
|
106
|
+
backgroundColor: ColorTokenSchema.nullable().optional(),
|
|
107
|
+
borderSize: z.number().min(0).nullable().optional(),
|
|
108
|
+
borderWidth: z.number().min(0).max(8).nullable().optional(),
|
|
109
|
+
borderStyle: z.enum(['solid', 'dashed', 'dotted']).nullable().optional(),
|
|
110
|
+
fontSize: z.number().positive().nullable().optional(),
|
|
111
|
+
textAlign: z.enum(['left', 'center', 'right']).nullable().optional(),
|
|
112
|
+
cornerRadius: z.number().min(0).nullable().optional(),
|
|
113
|
+
shadow: z.number().int().min(0).max(5).nullable().optional(),
|
|
107
114
|
width: z.number().positive().optional(),
|
|
108
115
|
height: z.number().positive().optional(),
|
|
109
116
|
// type:'html'-only: when true, the renderer measures content and React Flow
|
|
@@ -112,11 +119,12 @@ export const NodePatchBodySchema = z
|
|
|
112
119
|
autoSize: z.boolean().optional(),
|
|
113
120
|
// type:'icon'-only: stroke color token. Lands at data.color; the
|
|
114
121
|
// post-merge ResolvedFlowSchema reparse gates that this is only valid on
|
|
115
|
-
// type:'icon'.
|
|
116
|
-
color: ColorTokenSchema.optional(),
|
|
122
|
+
// type:'icon'. Nullable: explicit null clears it (undo of an icon recolor).
|
|
123
|
+
color: ColorTokenSchema.nullable().optional(),
|
|
117
124
|
// type:'icon'-only: glyph stroke width. Lands at data.strokeWidth; the
|
|
118
|
-
// post-merge reparse gates the [0.5, 4] bound and arm validity.
|
|
119
|
-
|
|
125
|
+
// post-merge reparse gates the [0.5, 4] bound and arm validity. Nullable:
|
|
126
|
+
// explicit null clears it (undo back to the default stroke width).
|
|
127
|
+
strokeWidth: z.number().min(0.5).max(4).nullable().optional(),
|
|
120
128
|
// type:'icon'/type:'image'-only: accessible alt text. Lands at data.alt.
|
|
121
129
|
alt: z.string().optional(),
|
|
122
130
|
// kebab-case Lucide icon name. Lands at data.icon. The post-merge reparse
|
|
@@ -164,7 +172,6 @@ const NODE_DATA_PATCH_KEYS = [
|
|
|
164
172
|
'borderWidth',
|
|
165
173
|
'borderStyle',
|
|
166
174
|
'fontSize',
|
|
167
|
-
'textColor',
|
|
168
175
|
'textAlign',
|
|
169
176
|
'cornerRadius',
|
|
170
177
|
'shadow',
|
|
@@ -267,7 +274,6 @@ const NODE_VISUAL_KEYS = new Set([
|
|
|
267
274
|
'borderSize',
|
|
268
275
|
'borderStyle',
|
|
269
276
|
'fontSize',
|
|
270
|
-
'textColor',
|
|
271
277
|
'textAlign',
|
|
272
278
|
'cornerRadius',
|
|
273
279
|
'shadow',
|
|
@@ -304,10 +310,12 @@ export const mergeNodeUpdates = (node: Record<string, unknown>, updates: NodePat
|
|
|
304
310
|
}
|
|
305
311
|
continue;
|
|
306
312
|
}
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
|
|
313
|
+
// Explicit null is the clear signal for every nullable key (style/visual
|
|
314
|
+
// tokens + US-009's `icon`): strip the key from disk so a re-parsed demo
|
|
315
|
+
// doesn't reintroduce it, and so an undo can restore a field to its
|
|
316
|
+
// pre-edit "unset" default. Only keys declared `.nullable()` in
|
|
317
|
+
// NodePatchBodySchema can reach here carrying null.
|
|
318
|
+
if (updates[key] === null) {
|
|
311
319
|
if (key in data) {
|
|
312
320
|
delete data[key];
|
|
313
321
|
touchedData = true;
|
|
@@ -1241,7 +1249,15 @@ export async function createProjectImpl(
|
|
|
1241
1249
|
body: CreateProjectBody,
|
|
1242
1250
|
): Promise<CreateProjectOutcome> {
|
|
1243
1251
|
const { registry, watcher } = deps;
|
|
1244
|
-
const {
|
|
1252
|
+
const { name, description } = body;
|
|
1253
|
+
|
|
1254
|
+
// Path is optional: when the caller omits it, scaffold under the studio's
|
|
1255
|
+
// home at <seeflowHome>/projects/<slug-of-name> so projects created from the
|
|
1256
|
+
// UI without a path land in a predictable, writable location.
|
|
1257
|
+
const folderPath =
|
|
1258
|
+
body.path && body.path.trim().length > 0
|
|
1259
|
+
? body.path
|
|
1260
|
+
: join(seeflowHome(), 'projects', slugify(name));
|
|
1245
1261
|
|
|
1246
1262
|
// Manifest-driven layout (US-018): a project is the seeflow.json manifest
|
|
1247
1263
|
// plus one flow folder under flows/<id>/. The default flow id for a
|
|
@@ -1347,12 +1363,16 @@ export async function addNodeImpl(
|
|
|
1347
1363
|
? { ...(newNode.data as Record<string, unknown>) }
|
|
1348
1364
|
: {};
|
|
1349
1365
|
const flowDir = dirname(entry.flowPath);
|
|
1350
|
-
for (const
|
|
1351
|
-
const incoming = data[field];
|
|
1352
|
-
const
|
|
1353
|
-
|
|
1366
|
+
for (const spec of externalizedFieldsForNodeType(newNode.type)) {
|
|
1367
|
+
const incoming = data[spec.field];
|
|
1368
|
+
const serializer = spec.serialize ?? defaultExternalizedSerializer;
|
|
1369
|
+
const content = serializer(incoming);
|
|
1370
|
+
if (content === null) continue; // serializer opted out (e.g. spec absent)
|
|
1371
|
+
if ((spec.kind ?? 'ref') === 'ref') {
|
|
1372
|
+
data[spec.field] = nodeFileRef(newId, spec.fileName);
|
|
1373
|
+
}
|
|
1354
1374
|
externalized.push({
|
|
1355
|
-
absPath: nodeFileAbsPath(entry.repoPath, flowDir, newId, fileName),
|
|
1375
|
+
absPath: nodeFileAbsPath(entry.repoPath, flowDir, newId, spec.fileName),
|
|
1356
1376
|
content,
|
|
1357
1377
|
});
|
|
1358
1378
|
}
|
|
@@ -1431,12 +1451,16 @@ export async function addFlowBulkImpl(
|
|
|
1431
1451
|
const data: Record<string, unknown> = dataIsRecord
|
|
1432
1452
|
? { ...(newNode.data as Record<string, unknown>) }
|
|
1433
1453
|
: {};
|
|
1434
|
-
for (const
|
|
1435
|
-
const incoming = data[field];
|
|
1436
|
-
const
|
|
1437
|
-
|
|
1454
|
+
for (const spec of externalizedFieldsForNodeType(newNode.type)) {
|
|
1455
|
+
const incoming = data[spec.field];
|
|
1456
|
+
const serializer = spec.serialize ?? defaultExternalizedSerializer;
|
|
1457
|
+
const content = serializer(incoming);
|
|
1458
|
+
if (content === null) continue; // serializer opted out (e.g. spec absent)
|
|
1459
|
+
if ((spec.kind ?? 'ref') === 'ref') {
|
|
1460
|
+
data[spec.field] = nodeFileRef(newId, spec.fileName);
|
|
1461
|
+
}
|
|
1438
1462
|
externalized.push({
|
|
1439
|
-
absPath: nodeFileAbsPath(entry.repoPath, flowDir, newId, fileName),
|
|
1463
|
+
absPath: nodeFileAbsPath(entry.repoPath, flowDir, newId, spec.fileName),
|
|
1440
1464
|
content,
|
|
1441
1465
|
});
|
|
1442
1466
|
}
|
|
@@ -1637,15 +1661,27 @@ export async function patchNodeImpl(
|
|
|
1637
1661
|
ref: string;
|
|
1638
1662
|
field: string;
|
|
1639
1663
|
content: string;
|
|
1664
|
+
kind: 'ref' | 'sidecar';
|
|
1665
|
+
value: unknown;
|
|
1640
1666
|
}> = [];
|
|
1641
|
-
|
|
1642
|
-
|
|
1667
|
+
// Externalize against the node's *target* type: a retype-into-component
|
|
1668
|
+
// carrying `spec` in the same patch is still geometric at this point, so
|
|
1669
|
+
// keying off `node.type` would miss `spec` (only externalized for
|
|
1670
|
+
// 'component'), leaving spec.json unwritten and data.spec unpopulated.
|
|
1671
|
+
const targetType = updates.type ?? node.type;
|
|
1672
|
+
for (const spec of externalizedFieldsForNodeType(targetType)) {
|
|
1673
|
+
const incoming = (updates as Record<string, unknown>)[spec.field];
|
|
1643
1674
|
if (incoming === undefined) continue;
|
|
1675
|
+
const serializer = spec.serialize ?? defaultExternalizedSerializer;
|
|
1676
|
+
const content = serializer(incoming);
|
|
1677
|
+
if (content === null) continue; // serializer opted out
|
|
1644
1678
|
externalizedWrites.push({
|
|
1645
|
-
absPath: nodeFileAbsPath(entry.repoPath, flowDir, nodeId, fileName),
|
|
1646
|
-
ref: nodeFileRef(nodeId, fileName),
|
|
1647
|
-
field,
|
|
1648
|
-
content
|
|
1679
|
+
absPath: nodeFileAbsPath(entry.repoPath, flowDir, nodeId, spec.fileName),
|
|
1680
|
+
ref: nodeFileRef(nodeId, spec.fileName),
|
|
1681
|
+
field: spec.field,
|
|
1682
|
+
content,
|
|
1683
|
+
kind: spec.kind ?? 'ref',
|
|
1684
|
+
value: incoming,
|
|
1649
1685
|
});
|
|
1650
1686
|
}
|
|
1651
1687
|
mergeNodeUpdates(node, updates);
|
|
@@ -1664,26 +1700,16 @@ export async function patchNodeImpl(
|
|
|
1664
1700
|
message: err instanceof Error ? err.message : String(err),
|
|
1665
1701
|
};
|
|
1666
1702
|
}
|
|
1667
|
-
data[
|
|
1703
|
+
// 'ref' fields swap data[field] for a file:// pointer; 'sidecar'
|
|
1704
|
+
// fields (e.g. component spec) keep the incoming value in memory so the
|
|
1705
|
+
// post-mutation reparse + SSE broadcast see it — splitFlow drops it
|
|
1706
|
+
// from flow.json on write and the resolver inlines it back from disk on
|
|
1707
|
+
// read. Setting it here is required for a retype-into-component patch,
|
|
1708
|
+
// where there is no pre-inlined data.spec to fall back on.
|
|
1709
|
+
data[w.field] = w.kind === 'ref' ? w.ref : w.value;
|
|
1668
1710
|
}
|
|
1669
1711
|
node.data = data;
|
|
1670
1712
|
}
|
|
1671
|
-
// Component spec sidecar — write the pretty-printed JSON to
|
|
1672
|
-
// `<repoPath>/<flowDir>/nodes/<id>/spec.json` so the on-disk source of
|
|
1673
|
-
// truth stays in sync. mergeNodeUpdates already put data.spec on the
|
|
1674
|
-
// merged tree for the post-mutation ResolvedFlowSchema parse; splitFlow
|
|
1675
|
-
// strips it from flow.json so we don't double-store the spec.
|
|
1676
|
-
if (node.type === 'component' && updates.spec !== undefined) {
|
|
1677
|
-
const specAbs = nodeFileAbsPath(entry.repoPath, flowDir, nodeId, 'spec.json');
|
|
1678
|
-
try {
|
|
1679
|
-
writeNodeFile(specAbs, `${JSON.stringify(updates.spec, null, 2)}\n`);
|
|
1680
|
-
} catch (err) {
|
|
1681
|
-
return {
|
|
1682
|
-
kind: 'writeFailed',
|
|
1683
|
-
message: err instanceof Error ? err.message : String(err),
|
|
1684
|
-
};
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
1713
|
return { kind: 'ok' };
|
|
1688
1714
|
});
|
|
1689
1715
|
}
|