@field/cascade 0.7.0 → 0.8.0

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/assets/{CodeEditor-DkpxJFRs.js → CodeEditor-BEk-kIBd.js} +29 -29
  3. package/dist/assets/CodePanel-CeSdjXkg.js +2 -0
  4. package/dist/assets/CustomNodeDialog-BQbEhIwL.js +1 -0
  5. package/dist/assets/{ExportDialog-BJu2Bpvv.js → ExportDialog-CjTbbTZJ.js} +2 -2
  6. package/dist/assets/OpenGraphDialog-CsMNY3C6.css +1 -0
  7. package/dist/assets/OpenGraphDialog-DCwH9itM.js +1 -0
  8. package/dist/assets/ProjectSettings-Dg7M5xt9.js +2 -0
  9. package/dist/assets/SettingsDialog-e7GgsjB9.js +1 -0
  10. package/dist/assets/VersionHistoryDialog-D6VBzSQ0.js +3 -0
  11. package/dist/assets/{core-DwPLRUzm.js → core-BInLG4gw.js} +1 -1
  12. package/dist/assets/{cssMode-C1bjAPYj.js → cssMode-DfR2qmYG.js} +1 -1
  13. package/dist/assets/definitions-DZpOjWKR.js +1 -0
  14. package/dist/assets/{editor.api-wr5lElMs.js → editor.api-Ck8_vgQV.js} +1 -1
  15. package/dist/assets/{freemarker2-DQHTaSDb.js → freemarker2-fVn10Wn3.js} +1 -1
  16. package/dist/assets/geo-D2VPjAdt.js +1 -0
  17. package/dist/assets/{handlebars-DOiZYGbp.js → handlebars-BCSchLT2.js} +1 -1
  18. package/dist/assets/{html-B1u4CWMY.js → html-CVpSv_k9.js} +1 -1
  19. package/dist/assets/{htmlMode-YNKJ8Hq1.js → htmlMode-Da4ESTv0.js} +1 -1
  20. package/dist/assets/{image-DmeeS7Tl.js → image-BLn--oSF.js} +1 -1
  21. package/dist/assets/index-CMiBdEuT.js +212 -0
  22. package/dist/assets/index-DMIuml9a.css +1 -0
  23. package/dist/assets/index-client-BI2H82FJ.js +1 -0
  24. package/dist/assets/{javascript-DVujl3OI.js → javascript-BpX0Gvcf.js} +1 -1
  25. package/dist/assets/{jsonMode-Bp_Ujoae.js → jsonMode-CsLqaou7.js} +1 -1
  26. package/dist/assets/{legacy-CAj2_7QE.js → legacy-ZA7textG.js} +1 -1
  27. package/dist/assets/{liquid-C0wcDX3X.js → liquid-efUz51Wf.js} +1 -1
  28. package/dist/assets/{lspLanguageFeatures-C2XtKL_c.js → lspLanguageFeatures-DCi5-K0n.js} +1 -1
  29. package/dist/assets/math-B6hpxPJO.js +1 -0
  30. package/dist/assets/{mdx-Dz4Hb1ZN.js → mdx-DP_sjGVy.js} +1 -1
  31. package/dist/assets/pop-CYUIjT6x.js +1 -0
  32. package/dist/assets/{python-Y4eTKlAp.js → python-aAuP3LUi.js} +1 -1
  33. package/dist/assets/{razor-BmpH_QN_.js → razor-7M6sMmC5.js} +1 -1
  34. package/dist/assets/{tsMode-mOQwydYU.js → tsMode-Bf4CnoYK.js} +1 -1
  35. package/dist/assets/{typescript-8ItD7azm.js → typescript-ddhYBGyT.js} +1 -1
  36. package/dist/assets/{workers-xhda_-L2.js → workers-X5la8DDT.js} +1 -1
  37. package/dist/assets/{xml-ydyZSquM.js → xml-BzbXa3Ps.js} +1 -1
  38. package/dist/assets/{yaml-BQjpTmW5.js → yaml-CXs-VzxL.js} +1 -1
  39. package/dist/cli/index.js +276 -214
  40. package/dist/index.html +6 -4
  41. package/package.json +1 -1
  42. package/dist/assets/CodePanel-O31InnQg.js +0 -2
  43. package/dist/assets/CustomNodeDialog-Cn3sT-M2.js +0 -1
  44. package/dist/assets/ProjectSettings-BzEmtsYc.js +0 -2
  45. package/dist/assets/SettingsDialog-zWct7T8j.js +0 -1
  46. package/dist/assets/VersionHistoryDialog-DCe6XQcO.js +0 -3
  47. package/dist/assets/definitions-Dh-oZAA5.js +0 -1
  48. package/dist/assets/geo-DLRNQg57.js +0 -1
  49. package/dist/assets/index-C35beBgj.js +0 -212
  50. package/dist/assets/index-client-BoJgsN9M.js +0 -1
  51. package/dist/assets/index-ilgbOu6G.css +0 -1
  52. package/dist/assets/math-D0hLbnR2.js +0 -1
  53. package/dist/assets/pop-OaOoEUwk.js +0 -1
package/dist/cli/index.js CHANGED
@@ -212,9 +212,9 @@ function levelCount(geometry, level) {
212
212
  if (level === "primitive") return geometry.primitiveCount;
213
213
  return 1;
214
214
  }
215
- function checkName(name, path24) {
215
+ function checkName(name, path25) {
216
216
  if (!NAME.test(name) || RESERVED_NAMES.has(name))
217
- fail("invalid-name", `${path24}.${name} is not a valid attribute name`);
217
+ fail("invalid-name", `${path25}.${name} is not a valid attribute name`);
218
218
  }
219
219
  function checkAttributeSet(set, count, level) {
220
220
  for (const [name, attribute] of Object.entries(set)) {
@@ -826,8 +826,8 @@ var init_values = __esm({
826
826
  });
827
827
 
828
828
  // packages/contracts/src/validate.ts
829
- function diagnostic(code, message2, path24) {
830
- return { phase: "definition", code: `definition/${code}`, message: message2, path: path24 };
829
+ function diagnostic(code, message2, path25) {
830
+ return { phase: "definition", code: `definition/${code}`, message: message2, path: path25 };
831
831
  }
832
832
  function isRecord2(value) {
833
833
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -842,12 +842,12 @@ function finiteJson(value) {
842
842
  if (Array.isArray(value)) return value.every(finiteJson);
843
843
  return isRecord2(value) && Object.values(value).every(finiteJson);
844
844
  }
845
- function unknownFields(value, allowed, path24) {
845
+ function unknownFields(value, allowed, path25) {
846
846
  return Object.keys(value).filter((key2) => !allowed.has(key2)).map(
847
847
  (key2) => diagnostic(
848
848
  "unknown-field",
849
- `${path24}.${key2} is not allowed`,
850
- `${path24}.${key2}`
849
+ `${path25}.${key2} is not allowed`,
850
+ `${path25}.${key2}`
851
851
  )
852
852
  );
853
853
  }
@@ -879,7 +879,7 @@ function validateNodeDefinition(value) {
879
879
  if (!isRecord2(value))
880
880
  return [diagnostic("not-object", "Definition must be an object")];
881
881
  const result = unknownFields(value, TOP_FIELDS, "definition");
882
- const add = (code, message2, path24) => result.push(diagnostic(code, message2, path24));
882
+ const add = (code, message2, path25) => result.push(diagnostic(code, message2, path25));
883
883
  const environment = value.runsOn;
884
884
  if (value.apiVersion !== 1)
885
885
  add("api-version", "apiVersion must be 1", "apiVersion");
@@ -896,15 +896,15 @@ function validateNodeDefinition(value) {
896
896
  );
897
897
  const seen = /* @__PURE__ */ new Set();
898
898
  for (const [index, capability] of (Array.isArray(value.capabilities) ? value.capabilities : []).entries()) {
899
- const path24 = `capabilities.${index}`;
899
+ const path25 = `capabilities.${index}`;
900
900
  if (!seen.has(capability) && !allowed.includes(capability))
901
901
  add(
902
902
  "capability-not-allowed",
903
903
  `${String(capability)} is not available on ${String(environment)}`,
904
- path24
904
+ path25
905
905
  );
906
906
  if (seen.has(capability))
907
- add("duplicate-capability", `${String(capability)} is duplicated`, path24);
907
+ add("duplicate-capability", `${String(capability)} is duplicated`, path25);
908
908
  seen.add(capability);
909
909
  }
910
910
  const inward = /* @__PURE__ */ new Map();
@@ -917,31 +917,31 @@ function validateNodeDefinition(value) {
917
917
  continue;
918
918
  }
919
919
  for (const [name, raw] of Object.entries(entries2)) {
920
- const path24 = `${section}.${name}`;
920
+ const path25 = `${section}.${name}`;
921
921
  if (!IDENTIFIER.test(name))
922
- add("invalid-name", `${name} is not a valid identifier`, path24);
923
- if (RESERVED.has(name)) add("reserved-name", `${name} is reserved`, path24);
922
+ add("invalid-name", `${name} is not a valid identifier`, path25);
923
+ if (RESERVED.has(name)) add("reserved-name", `${name} is reserved`, path25);
924
924
  const names = section === "outputs" ? outward : inward;
925
925
  const previous = names.get(name);
926
926
  if (previous)
927
927
  add(
928
928
  "name-collision",
929
929
  `${name} is declared in ${previous} and ${section}`,
930
- path24
930
+ path25
931
931
  );
932
932
  else names.set(name, section);
933
933
  if (!isRecord2(raw)) {
934
- add("invalid-entry", `${path24} must be an object`, path24);
934
+ add("invalid-entry", `${path25} must be an object`, path25);
935
935
  continue;
936
936
  }
937
937
  const fields2 = section === "props" ? PROP_FIELDS : section === "outputs" ? OUTPUT_FIELDS : DATA_FIELDS;
938
938
  if (section !== "props" && raw.kind !== "data" && raw.kind !== "trigger") {
939
939
  add(
940
940
  "invalid-kind",
941
- `${path24}.kind must be data or trigger`,
942
- `${path24}.kind`
941
+ `${path25}.kind must be data or trigger`,
942
+ `${path25}.kind`
943
943
  );
944
- result.push(...unknownFields(raw, fields2, path24));
944
+ result.push(...unknownFields(raw, fields2, path25));
945
945
  continue;
946
946
  }
947
947
  if (section !== "props" && raw.kind === "trigger") {
@@ -951,22 +951,22 @@ function validateNodeDefinition(value) {
951
951
  add(
952
952
  "trigger-extra",
953
953
  "Trigger ports allow only kind and description",
954
- path24
954
+ path25
955
955
  );
956
956
  continue;
957
957
  }
958
- result.push(...unknownFields(raw, fields2, path24));
958
+ result.push(...unknownFields(raw, fields2, path25));
959
959
  if ("control" in raw && (typeof raw.control !== "string" || !CONTROLS.has(raw.control)))
960
960
  add(
961
961
  "invalid-control",
962
962
  `${String(raw.control)} is not a Cascade control`,
963
- `${path24}.control`
963
+ `${path25}.control`
964
964
  );
965
965
  if ("options" in raw && raw.control !== "select")
966
966
  add(
967
967
  "options-without-select",
968
968
  "options require the select control",
969
- `${path24}.options`
969
+ `${path25}.options`
970
970
  );
971
971
  if (Array.isArray(raw.options))
972
972
  for (const [index, option] of raw.options.entries()) {
@@ -976,19 +976,19 @@ function validateNodeDefinition(value) {
976
976
  add(
977
977
  "invalid-option",
978
978
  "a labelled option needs a value",
979
- `${path24}.options[${index}]`
979
+ `${path25}.options[${index}]`
980
980
  );
981
981
  if (typeof entry.label !== "string" || !entry.label)
982
982
  add(
983
983
  "invalid-option",
984
984
  "a labelled option needs a non-empty label",
985
- `${path24}.options[${index}].label`
985
+ `${path25}.options[${index}].label`
986
986
  );
987
987
  if ("disabled" in entry && typeof entry.disabled !== "boolean")
988
988
  add(
989
989
  "invalid-option",
990
990
  "disabled must be a boolean",
991
- `${path24}.options[${index}].disabled`
991
+ `${path25}.options[${index}].disabled`
992
992
  );
993
993
  }
994
994
  const type = raw.type;
@@ -996,56 +996,56 @@ function validateNodeDefinition(value) {
996
996
  add(
997
997
  "invalid-type",
998
998
  `${String(type)} is not a Cascade type`,
999
- `${path24}.type`
999
+ `${path25}.type`
1000
1000
  );
1001
1001
  continue;
1002
1002
  }
1003
1003
  if (type === "texture" && environment !== "browser")
1004
- add("texture-not-portable", "texture is browser-only", `${path24}.type`);
1004
+ add("texture-not-portable", "texture is browser-only", `${path25}.type`);
1005
1005
  if (type === "texture" && "default" in raw)
1006
1006
  add(
1007
1007
  "texture-default",
1008
1008
  "texture cannot have a default",
1009
- `${path24}.default`
1009
+ `${path25}.default`
1010
1010
  );
1011
1011
  if (section === "props" && !("default" in raw))
1012
- add("missing-default", "Props require defaults", `${path24}.default`);
1012
+ add("missing-default", "Props require defaults", `${path25}.default`);
1013
1013
  if ("default" in raw && !finiteJson(raw.default))
1014
1014
  add(
1015
1015
  "invalid-default",
1016
1016
  "Default must be finite JSON",
1017
- `${path24}.default`
1017
+ `${path25}.default`
1018
1018
  );
1019
1019
  else if ("default" in raw && !defaultMatches(type, raw.default))
1020
1020
  add(
1021
1021
  "default-type",
1022
1022
  `Default does not match ${type}`,
1023
- `${path24}.default`
1023
+ `${path25}.default`
1024
1024
  );
1025
1025
  if (type === "int" && "default" in raw && (!Number.isFinite(raw.default) || !Number.isInteger(raw.default)))
1026
1026
  add(
1027
1027
  "non-integer",
1028
1028
  "Integer defaults must be finite integers",
1029
- `${path24}.default`
1029
+ `${path25}.default`
1030
1030
  );
1031
1031
  if ("step" in raw && (typeof raw.step !== "number" || !Number.isFinite(raw.step) || raw.step <= 0))
1032
- add("invalid-step", "step must be greater than zero", `${path24}.step`);
1032
+ add("invalid-step", "step must be greater than zero", `${path25}.step`);
1033
1033
  if (section === "props" && "expression" in raw && (typeof raw.expression !== "string" || raw.expression.length === 0))
1034
1034
  add(
1035
1035
  "invalid-expression",
1036
1036
  "expression must be a non-empty string",
1037
- `${path24}.expression`
1037
+ `${path25}.expression`
1038
1038
  );
1039
1039
  for (const field of ["min", "max"]) {
1040
1040
  if (field in raw && (typeof raw[field] !== "number" || !Number.isFinite(raw[field])))
1041
1041
  add(
1042
1042
  `invalid-${field}`,
1043
1043
  `${field} must be a finite number`,
1044
- `${path24}.${field}`
1044
+ `${path25}.${field}`
1045
1045
  );
1046
1046
  }
1047
1047
  if (typeof raw.min === "number" && typeof raw.max === "number" && raw.min > raw.max)
1048
- add("invalid-range", "min must not exceed max", path24);
1048
+ add("invalid-range", "min must not exceed max", path25);
1049
1049
  }
1050
1050
  }
1051
1051
  return result;
@@ -1741,10 +1741,10 @@ var init_evaluator = __esm({
1741
1741
  fframe: this.timeState.frameFraction,
1742
1742
  fps: this.timeState.fps,
1743
1743
  self: node,
1744
- ch: (path24) => this.ch(node, path24),
1745
- chs: (path24) => this.chs(node, path24),
1746
- chv: (path24) => this.chv(node, path24),
1747
- opexist: (path24) => this.scope?.nodeExists(node, path24) ? 1 : 0,
1744
+ ch: (path25) => this.ch(node, path25),
1745
+ chs: (path25) => this.chs(node, path25),
1746
+ chv: (path25) => this.chv(node, path25),
1747
+ opexist: (path25) => this.scope?.nodeExists(node, path25) ? 1 : 0,
1748
1748
  opinput: (index) => this.scope?.inputNode(node, index) ?? null,
1749
1749
  fit,
1750
1750
  fit01,
@@ -1761,8 +1761,8 @@ var init_evaluator = __esm({
1761
1761
  };
1762
1762
  }
1763
1763
  // ============ Channel functions ============
1764
- ch(node, path24) {
1765
- const resolved = this.resolveParam(node, path24);
1764
+ ch(node, path25) {
1765
+ const resolved = this.resolveParam(node, path25);
1766
1766
  if (!resolved) return 0;
1767
1767
  const value = resolved.value;
1768
1768
  if (typeof value === "number") return value;
@@ -1770,13 +1770,13 @@ var init_evaluator = __esm({
1770
1770
  if (typeof value === "boolean") return value ? 1 : 0;
1771
1771
  return 0;
1772
1772
  }
1773
- chs(node, path24) {
1774
- const resolved = this.resolveParam(node, path24);
1773
+ chs(node, path25) {
1774
+ const resolved = this.resolveParam(node, path25);
1775
1775
  if (!resolved) return "";
1776
1776
  return String(resolved.value ?? "");
1777
1777
  }
1778
- chv(node, path24) {
1779
- const resolved = this.resolveParam(node, path24);
1778
+ chv(node, path25) {
1779
+ const resolved = this.resolveParam(node, path25);
1780
1780
  if (!resolved) return [];
1781
1781
  const value = resolved.value;
1782
1782
  if (Array.isArray(value)) {
@@ -1785,12 +1785,12 @@ var init_evaluator = __esm({
1785
1785
  if (typeof value === "number") return [value];
1786
1786
  return [];
1787
1787
  }
1788
- resolveParam(node, path24) {
1789
- return this.scope?.resolveParam(node, path24) ?? null;
1788
+ resolveParam(node, path25) {
1789
+ return this.scope?.resolveParam(node, path25) ?? null;
1790
1790
  }
1791
1791
  /** True if a referenced path resolves from this node. */
1792
- validatePath(fromNode, path24) {
1793
- return this.resolveParam(fromNode, path24) !== null;
1792
+ validatePath(fromNode, path25) {
1793
+ return this.resolveParam(fromNode, path25) !== null;
1794
1794
  }
1795
1795
  /** Parameter paths referenced through ch/chs/chv. */
1796
1796
  extractDependencies(expression) {
@@ -1829,14 +1829,14 @@ function geometryError(code, message2) {
1829
1829
  function attributeSet(geometry, level) {
1830
1830
  return geometry[level];
1831
1831
  }
1832
- function numericAttribute(attribute, path24, sizes) {
1832
+ function numericAttribute(attribute, path25, sizes) {
1833
1833
  if (attribute === void 0) return void 0;
1834
1834
  if (isStringAttribute(attribute))
1835
- geometryError("attribute-storage", `${path24} must be numeric`);
1835
+ geometryError("attribute-storage", `${path25} must be numeric`);
1836
1836
  if (sizes !== void 0 && !sizes.includes(attribute.size))
1837
1837
  geometryError(
1838
1838
  "attribute-size",
1839
- `${path24} has size ${attribute.size}; expected ${sizes.join(" or ")}`
1839
+ `${path25} has size ${attribute.size}; expected ${sizes.join(" or ")}`
1840
1840
  );
1841
1841
  return attribute;
1842
1842
  }
@@ -2647,7 +2647,7 @@ function geometryToSvg(geometry, options2 = {}) {
2647
2647
  if (mask2[primitive] === 1 && assigned[primitive] === -1)
2648
2648
  assigned[primitive] = index;
2649
2649
  }
2650
- const path24 = (primitive) => {
2650
+ const path25 = (primitive) => {
2651
2651
  const attributes = [`d="${pathData(geometry, primitive, number)}"`];
2652
2652
  const paint = paintOf(colour, primitive, "primitive");
2653
2653
  if (paint !== void 0) {
@@ -2666,13 +2666,13 @@ function geometryToSvg(geometry, options2 = {}) {
2666
2666
  const body = [];
2667
2667
  if (names.length === 0) {
2668
2668
  for (let primitive = 0; primitive < geometry.primitiveCount; primitive += 1)
2669
- body.push(indent(path24(primitive), 2));
2669
+ body.push(indent(path25(primitive), 2));
2670
2670
  } else {
2671
2671
  for (let index = 0; index < names.length; index += 1) {
2672
2672
  const members = [];
2673
2673
  for (let primitive = 0; primitive < geometry.primitiveCount; primitive += 1)
2674
2674
  if (assigned[primitive] === index)
2675
- members.push(indent(path24(primitive), 3));
2675
+ members.push(indent(path25(primitive), 3));
2676
2676
  body.push(
2677
2677
  indent(`<g id="${escapeXml(names[index])}">`, 2),
2678
2678
  ...members,
@@ -2681,7 +2681,7 @@ function geometryToSvg(geometry, options2 = {}) {
2681
2681
  }
2682
2682
  const rest = [];
2683
2683
  for (let primitive = 0; primitive < geometry.primitiveCount; primitive += 1)
2684
- if (assigned[primitive] === -1) rest.push(indent(path24(primitive), 3));
2684
+ if (assigned[primitive] === -1) rest.push(indent(path25(primitive), 3));
2685
2685
  if (rest.length > 0)
2686
2686
  body.push(
2687
2687
  indent(`<g id="${UNGROUPED}">`, 2),
@@ -2788,11 +2788,11 @@ function validateOptions(options2) {
2788
2788
  if (!Number.isSafeInteger(precision) || precision < 0 || precision > 15)
2789
2789
  geometryError("svg-precision", "precision must be an integer from 0 to 15");
2790
2790
  }
2791
- function validateRange(attribute, path24, accepts, expectation) {
2791
+ function validateRange(attribute, path25, accepts, expectation) {
2792
2792
  if (attribute === void 0) return;
2793
2793
  for (const value of attribute.data)
2794
2794
  if (!accepts(value))
2795
- geometryError("attribute-value", `${path24} values must be ${expectation}`);
2795
+ geometryError("attribute-value", `${path25} values must be ${expectation}`);
2796
2796
  }
2797
2797
  function indent(line2, depth) {
2798
2798
  return `${" ".repeat(depth)}${line2}`;
@@ -3081,8 +3081,8 @@ var init_resolve = __esm({
3081
3081
  });
3082
3082
 
3083
3083
  // packages/runtime/src/params/animator.ts
3084
- function split(path24) {
3085
- return String(path24 ?? "").split("/").filter((segment) => segment.length > 0 && segment !== ".");
3084
+ function split(path25) {
3085
+ return String(path25 ?? "").split("/").filter((segment) => segment.length > 0 && segment !== ".");
3086
3086
  }
3087
3087
  var PropAnimator;
3088
3088
  var init_animator = __esm({
@@ -3150,8 +3150,8 @@ var init_animator = __esm({
3150
3150
  */
3151
3151
  scope() {
3152
3152
  return {
3153
- resolveParam: (from, path24) => {
3154
- const segments = split(path24);
3153
+ resolveParam: (from, path25) => {
3154
+ const segments = split(path25);
3155
3155
  if (!segments.length) return null;
3156
3156
  const name = segments[segments.length - 1];
3157
3157
  const owner = this.owner(from, segments.slice(0, -1));
@@ -3161,8 +3161,8 @@ var init_animator = __esm({
3161
3161
  if (name in owner.inputs) return { value: owner.inputs[name] };
3162
3162
  return null;
3163
3163
  },
3164
- nodeExists: (from, path24) => {
3165
- const segments = split(path24);
3164
+ nodeExists: (from, path25) => {
3165
+ const segments = split(path25);
3166
3166
  if (!segments.length) return false;
3167
3167
  const last = segments[segments.length - 1];
3168
3168
  if (last === "..") return true;
@@ -5479,12 +5479,12 @@ function buildTrails(history, current, options2 = {}) {
5479
5479
  for (const frame of frames) {
5480
5480
  for (let index = 0; index < frame.id.length; index += 1) {
5481
5481
  const id = frame.id[index];
5482
- const path24 = paths.get(id) ?? [];
5483
- path24.push(
5482
+ const path25 = paths.get(id) ?? [];
5483
+ path25.push(
5484
5484
  frame.position[index * frame.size],
5485
5485
  frame.position[index * frame.size + 1]
5486
5486
  );
5487
- paths.set(id, path24);
5487
+ paths.set(id, path25);
5488
5488
  }
5489
5489
  }
5490
5490
  if (result === "velocity") {
@@ -5492,11 +5492,11 @@ function buildTrails(history, current, options2 = {}) {
5492
5492
  }
5493
5493
  const builder = new GeometryBuilder({ positionSize: 2 });
5494
5494
  const trailIds = [];
5495
- for (const [id, path24] of paths) {
5496
- if (path24.length < 4) continue;
5495
+ for (const [id, path25] of paths) {
5496
+ if (path25.length < 4) continue;
5497
5497
  const points = [];
5498
- for (let at = 0; at < path24.length; at += 2) {
5499
- points.push(builder.addPoint(path24[at], path24[at + 1]));
5498
+ for (let at = 0; at < path25.length; at += 2) {
5499
+ points.push(builder.addPoint(path25[at], path25[at + 1]));
5500
5500
  }
5501
5501
  builder.addPrimitive(points, { closed: false, kind: "poly" });
5502
5502
  trailIds.push(id);
@@ -5541,11 +5541,11 @@ function withDerivedVelocity(current, paths, velocityscale) {
5541
5541
  const ids = current.point.id?.data;
5542
5542
  const velocity = new Float32Array(current.pointCount * size);
5543
5543
  for (let index = 0; index < current.pointCount; index += 1) {
5544
- const path24 = ids ? paths.get(Number(ids[index])) : void 0;
5545
- if (!path24 || path24.length < 4) continue;
5546
- const last = path24.length;
5547
- velocity[index * size] = (path24[last - 2] - path24[last - 4]) * velocityscale;
5548
- velocity[index * size + 1] = (path24[last - 1] - path24[last - 3]) * velocityscale;
5544
+ const path25 = ids ? paths.get(Number(ids[index])) : void 0;
5545
+ if (!path25 || path25.length < 4) continue;
5546
+ const last = path25.length;
5547
+ velocity[index * size] = (path25[last - 2] - path25[last - 4]) * velocityscale;
5548
+ velocity[index * size + 1] = (path25[last - 1] - path25[last - 3]) * velocityscale;
5549
5549
  }
5550
5550
  return {
5551
5551
  ...current,
@@ -7063,16 +7063,16 @@ function cloneValue(value, ancestors) {
7063
7063
  ancestors.delete(value);
7064
7064
  return Object.freeze(clone);
7065
7065
  }
7066
- function runtimeValue(value, type, allowUndefined, path24) {
7066
+ function runtimeValue(value, type, allowUndefined, path25) {
7067
7067
  if (value === void 0) {
7068
7068
  if (allowUndefined) return void 0;
7069
- misuse("runtime/value-type", `${path24} requires a ${type} value`);
7069
+ misuse("runtime/value-type", `${path25} requires a ${type} value`);
7070
7070
  }
7071
7071
  const tupleLength = TUPLE_LENGTHS[type];
7072
7072
  const valid = type === "float" && typeof value === "number" && Number.isFinite(value) || type === "int" && typeof value === "number" && Number.isInteger(value) || type === "bool" && typeof value === "boolean" || type === "string" && typeof value === "string" || tupleLength !== void 0 && Array.isArray(value) && value.length === tupleLength && value.every(
7073
7073
  (item) => type.endsWith("i") ? typeof item === "number" && Number.isInteger(item) : typeof item === "number" && Number.isFinite(item)
7074
7074
  ) || !tupleLength && type !== "float" && type !== "int" && type !== "bool" && type !== "string";
7075
- if (!valid) misuse("runtime/value-type", `${path24} is not a valid ${type}`);
7075
+ if (!valid) misuse("runtime/value-type", `${path25} is not a valid ${type}`);
7076
7076
  return snapshot(value);
7077
7077
  }
7078
7078
  var PREFLIGHT_WARNING_CODES, RUN_TOLERATED_CODES, MutableAbortSignal, Runtime, Graph, TUPLE_LENGTHS;
@@ -8352,32 +8352,32 @@ function extractNodeDefinition(source, fileName, ts4) {
8352
8352
  diagnostics: [error.diagnostic]
8353
8353
  };
8354
8354
  }
8355
- function read(node, path24, depth) {
8355
+ function read(node, path25, depth) {
8356
8356
  if (++visited > MAX_NODES)
8357
8357
  throw extraction(
8358
8358
  node,
8359
8359
  "definition/size-limit",
8360
8360
  "Definition exceeds 10,000 syntax nodes",
8361
- path24
8361
+ path25
8362
8362
  );
8363
8363
  if (depth > MAX_DEPTH)
8364
8364
  throw extraction(
8365
8365
  node,
8366
8366
  "definition/depth-limit",
8367
8367
  "Definition exceeds 32 levels",
8368
- path24
8368
+ path25
8369
8369
  );
8370
8370
  if (ts4.isParenthesizedExpression(node) || ts4.isSatisfiesExpression(node))
8371
- return read(node.expression, path24, depth + 1);
8371
+ return read(node.expression, path25, depth + 1);
8372
8372
  if (ts4.isAsExpression(node)) {
8373
8373
  if (node.type.getText(file) !== "const")
8374
8374
  throw extraction(
8375
8375
  node.type,
8376
8376
  "definition/non-literal",
8377
8377
  "Only 'as const' assertions are allowed",
8378
- path24
8378
+ path25
8379
8379
  );
8380
- return read(node.expression, path24, depth + 1);
8380
+ return read(node.expression, path25, depth + 1);
8381
8381
  }
8382
8382
  if (ts4.isStringLiteral(node) || ts4.isNoSubstitutionTemplateLiteral(node))
8383
8383
  return node.text;
@@ -8394,9 +8394,9 @@ function extractNodeDefinition(source, fileName, ts4) {
8394
8394
  element,
8395
8395
  "definition/spread",
8396
8396
  "Spreads are not allowed",
8397
- `${path24}.${index}`
8397
+ `${path25}.${index}`
8398
8398
  );
8399
- return read(element, `${path24}.${index}`, depth + 1);
8399
+ return read(element, `${path25}.${index}`, depth + 1);
8400
8400
  });
8401
8401
  if (ts4.isObjectLiteralExpression(node)) {
8402
8402
  const object = /* @__PURE__ */ Object.create(null);
@@ -8407,14 +8407,14 @@ function extractNodeDefinition(source, fileName, ts4) {
8407
8407
  property,
8408
8408
  "definition/spread",
8409
8409
  "Spreads are not allowed",
8410
- path24
8410
+ path25
8411
8411
  );
8412
8412
  if (!ts4.isPropertyAssignment(property) || property.name && ts4.isComputedPropertyName(property.name))
8413
8413
  throw extraction(
8414
8414
  property,
8415
8415
  "definition/non-literal",
8416
8416
  "Only explicit properties are allowed",
8417
- path24
8417
+ path25
8418
8418
  );
8419
8419
  const key2 = ts4.isIdentifier(property.name) || ts4.isStringLiteral(property.name) || ts4.isNumericLiteral(property.name) ? property.name.text : void 0;
8420
8420
  if (key2 === void 0)
@@ -8422,17 +8422,17 @@ function extractNodeDefinition(source, fileName, ts4) {
8422
8422
  property,
8423
8423
  "definition/non-literal",
8424
8424
  "Property name must be literal",
8425
- path24
8425
+ path25
8426
8426
  );
8427
8427
  if (keys.has(key2))
8428
8428
  throw extraction(
8429
8429
  property.name,
8430
8430
  "definition/duplicate-key",
8431
8431
  `Duplicate key ${key2}`,
8432
- `${path24}.${key2}`
8432
+ `${path25}.${key2}`
8433
8433
  );
8434
8434
  keys.add(key2);
8435
- object[key2] = read(property.initializer, `${path24}.${key2}`, depth + 1);
8435
+ object[key2] = read(property.initializer, `${path25}.${key2}`, depth + 1);
8436
8436
  }
8437
8437
  return object;
8438
8438
  }
@@ -8440,19 +8440,19 @@ function extractNodeDefinition(source, fileName, ts4) {
8440
8440
  node,
8441
8441
  "definition/non-literal",
8442
8442
  `Unsupported ${ts4.SyntaxKind[node.kind]}`,
8443
- path24
8443
+ path25
8444
8444
  );
8445
8445
  }
8446
8446
  function location(node) {
8447
8447
  const point = file.getLineAndCharacterOfPosition(node.getStart(file));
8448
8448
  return { line: point.line + 1, column: point.character + 1 };
8449
8449
  }
8450
- function extraction(node, code, message2, path24) {
8450
+ function extraction(node, code, message2, path25) {
8451
8451
  return new ExtractionFailure({
8452
8452
  phase: "definition",
8453
8453
  code,
8454
8454
  message: message2,
8455
- path: path24,
8455
+ path: path25,
8456
8456
  file: fileName,
8457
8457
  ...location(node)
8458
8458
  });
@@ -9568,13 +9568,14 @@ export function execute(node, graph) {
9568
9568
  import fs6 from "node:fs/promises";
9569
9569
  import path6 from "node:path";
9570
9570
  function createNodeFileCapability(project) {
9571
- const root = path6.resolve(project.resolve("."));
9572
9571
  const within3 = (relative, what) => {
9573
- const clean = relative.replace(/^\.?\//, "");
9574
- const full = path6.resolve(project.resolve(clean));
9575
- if (full !== root && !full.startsWith(`${root}${path6.sep}`))
9576
- throw new Error(`files: ${what} outside the project is refused \u2014 ${relative}`);
9577
- return full;
9572
+ try {
9573
+ return project.resolve(relative.replace(/^\.?\//, ""));
9574
+ } catch (error) {
9575
+ throw new Error(
9576
+ `files: ${what} outside the project is refused \u2014 ${relative} (${error instanceof Error ? error.message : String(error)})`
9577
+ );
9578
+ }
9578
9579
  };
9579
9580
  const cacheOnly = (relative) => {
9580
9581
  const clean = relative.replace(/^\.?\//, "");
@@ -9974,6 +9975,7 @@ async function renderDeterministicProjectFrames(file, document2, options2) {
9974
9975
  try {
9975
9976
  graph = await runtime.load(document2);
9976
9977
  reportPreflightWarnings(host, classifyPreflight(graph.preflight()).warnings);
9978
+ const targets = options2.nodes?.length ? options2.nodes : options2.entryNode ? [options2.entryNode] : [];
9977
9979
  const outDirectory = project.resolve(options2.out);
9978
9980
  await fs8.mkdir(outDirectory, { recursive: true });
9979
9981
  for (const frame of frameRange(options2.start, options2.end, options2.step ?? 1)) {
@@ -9984,17 +9986,24 @@ async function renderDeterministicProjectFrames(file, document2, options2) {
9984
9986
  const result = await graph.run({
9985
9987
  frame,
9986
9988
  ...options2.fps === void 0 ? {} : { fps: options2.fps },
9987
- ...options2.entryNode ? { target: { kind: "node", nodeId: options2.entryNode } } : {}
9989
+ /**
9990
+ * One named node narrows the COOK as well as the output, because the
9991
+ * runtime can walk back from a single target. Several cannot be
9992
+ * expressed as one target, so the graph cooks whole and only the saving
9993
+ * narrows — which is still the bulk of the win when the outputs are
9994
+ * 18 MB PNGs.
9995
+ */
9996
+ ...targets.length === 1 ? { target: { kind: "node", nodeId: targets[0] } } : {}
9988
9997
  });
9989
9998
  if (result.status !== "completed") {
9990
9999
  throw new Error(
9991
10000
  `frame ${frame} ${result.status} \u2014 ${result.diagnostics.map((item) => item.message).join("; ") || "no diagnostics"}`
9992
10001
  );
9993
10002
  }
9994
- const outputs = imageOutputs(graph, options2.entryNode);
10003
+ const outputs = imageOutputs(graph, targets);
9995
10004
  if (!outputs.length) {
9996
10005
  throw new Error(
9997
- "No image output to save: the graph has no unconsumed image port. Name the node to render with --entry-node."
10006
+ targets.length ? `No image output on ${targets.join(", ")} \u2014 check the node id, and that it has an image output.` : "No image output to save: the graph has no unconsumed image port. Name the node to render with --node."
9998
10007
  );
9999
10008
  }
10000
10009
  for (const { nodeId, value } of outputs) {
@@ -10023,13 +10032,14 @@ async function renderDeterministicProjectFrames(file, document2, options2) {
10023
10032
  async function disposeProjectGpu(host) {
10024
10033
  await host.capabilities.gpu?.dispose();
10025
10034
  }
10026
- function imageOutputs(graph, entryNode) {
10035
+ function imageOutputs(graph, nodes = []) {
10036
+ const wanted = new Set(nodes);
10027
10037
  const found = [];
10028
10038
  for (const node of graph.inspect().nodes) {
10029
- if (entryNode && node.id !== entryNode) continue;
10039
+ if (wanted.size && !wanted.has(node.id)) continue;
10030
10040
  for (const port of Object.values(node.outputs)) {
10031
10041
  if (port.type !== "image") continue;
10032
- if (!entryNode && port.connected) continue;
10042
+ if (!wanted.size && port.connected) continue;
10033
10043
  if (port.value === void 0) continue;
10034
10044
  found.push({ nodeId: node.id, value: port.value });
10035
10045
  }
@@ -11011,6 +11021,8 @@ var init_git = __esm({
11011
11021
 
11012
11022
  // server/src/routes/graph.ts
11013
11023
  import { Router } from "express";
11024
+ import fs14 from "node:fs/promises";
11025
+ import path17 from "node:path";
11014
11026
  function handleError(res, err) {
11015
11027
  if (err instanceof PathSafetyError) {
11016
11028
  res.status(400).json({ error: err.message });
@@ -11063,6 +11075,42 @@ function createGraphRouter(project) {
11063
11075
  handleError(res, err);
11064
11076
  }
11065
11077
  });
11078
+ router.get("/:filename/workspace", async (req, res) => {
11079
+ try {
11080
+ const target = workspacePath(project, req.params.filename);
11081
+ await fs14.access(project.resolve(req.params.filename));
11082
+ let content;
11083
+ try {
11084
+ content = await fs14.readFile(target, "utf-8");
11085
+ } catch (err) {
11086
+ if (err?.code === "ENOENT") {
11087
+ res.json(null);
11088
+ return;
11089
+ }
11090
+ throw err;
11091
+ }
11092
+ res.type("application/json").send(content);
11093
+ } catch (err) {
11094
+ handleError(res, err);
11095
+ }
11096
+ });
11097
+ router.put("/:filename/workspace", async (req, res) => {
11098
+ try {
11099
+ const body = req.body;
11100
+ if (!body || typeof body !== "object" || Array.isArray(body) || !body.layout || typeof body.layout !== "object") {
11101
+ res.status(400).json({ error: "a workspace is an object with a layout" });
11102
+ return;
11103
+ }
11104
+ const slot = req.query.slot === "failed" ? "failed" : null;
11105
+ const target = workspacePath(project, req.params.filename, slot);
11106
+ const temporary = `${target}.${process.pid}.tmp`;
11107
+ await fs14.writeFile(temporary, JSON.stringify(body, null, 1), "utf-8");
11108
+ await fs14.rename(temporary, target);
11109
+ res.json({ ok: true, file: path17.basename(target) });
11110
+ } catch (err) {
11111
+ handleError(res, err);
11112
+ }
11113
+ });
11066
11114
  router.get("/:filename/versions", async (req, res) => {
11067
11115
  try {
11068
11116
  const filename = req.params.filename;
@@ -11100,6 +11148,13 @@ function createGraphRouter(project) {
11100
11148
  });
11101
11149
  return router;
11102
11150
  }
11151
+ function workspacePath(project, filename, slot = null) {
11152
+ if (!filename.endsWith(".cascade") || filename !== path17.basename(filename)) {
11153
+ throw new PathSafetyError(`not a .cascade file: ${filename}`);
11154
+ }
11155
+ const stem = filename.slice(0, -".cascade".length);
11156
+ return project.resolve(`${stem}.workspace${slot ? `.${slot}` : ""}.json`);
11157
+ }
11103
11158
  function saveCommitMessage(filename, at = /* @__PURE__ */ new Date()) {
11104
11159
  const time = at.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", hour12: false });
11105
11160
  return `Saved ${filename} ${time}`;
@@ -11116,8 +11171,8 @@ var init_graph = __esm({
11116
11171
  // server/src/routes/assets.ts
11117
11172
  import { Router as Router2 } from "express";
11118
11173
  import multer from "multer";
11119
- import path17 from "path";
11120
- import fs14 from "fs/promises";
11174
+ import path18 from "path";
11175
+ import fs15 from "fs/promises";
11121
11176
  function handleError2(res, err) {
11122
11177
  if (err instanceof PathSafetyError) {
11123
11178
  res.status(400).json({ error: err.message });
@@ -11133,8 +11188,8 @@ function createAssetsRouter(project) {
11133
11188
  let assetType = "data";
11134
11189
  if (file.mimetype.startsWith("image/")) assetType = "images";
11135
11190
  else if (file.mimetype.startsWith("audio/")) assetType = "audio";
11136
- const dir = path17.join(assetsRoot, assetType);
11137
- await fs14.mkdir(dir, { recursive: true });
11191
+ const dir = path18.join(assetsRoot, assetType);
11192
+ await fs15.mkdir(dir, { recursive: true });
11138
11193
  cb(null, dir);
11139
11194
  },
11140
11195
  filename: (_req, file, cb) => cb(null, file.originalname)
@@ -11145,7 +11200,7 @@ function createAssetsRouter(project) {
11145
11200
  res.status(400).json({ error: "no file uploaded" });
11146
11201
  return;
11147
11202
  }
11148
- const relativePath = path17.relative(assetsRoot, req.file.path);
11203
+ const relativePath = path18.relative(assetsRoot, req.file.path);
11149
11204
  res.json({
11150
11205
  id: req.file.filename,
11151
11206
  path: `./assets/${relativePath}`,
@@ -11171,7 +11226,7 @@ function createAssetsRouter(project) {
11171
11226
  try {
11172
11227
  const rel = req.params.assetPath?.join("/") ?? "";
11173
11228
  const full = resolveWithinRoot(assetsRoot, rel);
11174
- await fs14.unlink(full);
11229
+ await fs15.unlink(full);
11175
11230
  res.json({ ok: true });
11176
11231
  } catch (err) {
11177
11232
  handleError2(res, err);
@@ -11187,10 +11242,10 @@ var init_assets = __esm({
11187
11242
  });
11188
11243
 
11189
11244
  // server/src/nodeWatch.ts
11190
- import fs15 from "node:fs";
11191
- import path18 from "node:path";
11245
+ import fs16 from "node:fs";
11246
+ import path19 from "node:path";
11192
11247
  function moduleNameFor(relative) {
11193
- const segments = relative.split(path18.sep).filter(Boolean);
11248
+ const segments = relative.split(path19.sep).filter(Boolean);
11194
11249
  if (segments.length < 2) return null;
11195
11250
  const name = segments[0];
11196
11251
  if (!name || name.startsWith(".")) return null;
@@ -11212,9 +11267,9 @@ function createNodeWatcher(project) {
11212
11267
  );
11213
11268
  }
11214
11269
  function watch(directory, scope) {
11215
- if (!fs15.existsSync(directory)) return;
11270
+ if (!fs16.existsSync(directory)) return;
11216
11271
  try {
11217
- const watcher = fs15.watch(directory, { recursive: true }, (_event, filename) => {
11272
+ const watcher = fs16.watch(directory, { recursive: true }, (_event, filename) => {
11218
11273
  if (!filename) return;
11219
11274
  if (scope === "shared") {
11220
11275
  emit(ALL_MODULES);
@@ -11231,8 +11286,8 @@ function createNodeWatcher(project) {
11231
11286
  }
11232
11287
  watch(project.resolve("nodes"), "project");
11233
11288
  try {
11234
- const sharedNodes = fs15.realpathSync(path18.join(project.root, "shared"));
11235
- watch(path18.join(sharedNodes, "nodes"), "shared");
11289
+ const sharedNodes = fs16.realpathSync(path19.join(project.root, "shared"));
11290
+ watch(path19.join(sharedNodes, "nodes"), "shared");
11236
11291
  } catch {
11237
11292
  }
11238
11293
  return {
@@ -11902,15 +11957,15 @@ var init_eventBuffer = __esm({
11902
11957
  });
11903
11958
 
11904
11959
  // server/src/agent/allowlist.ts
11905
- import fs16 from "node:fs";
11960
+ import fs17 from "node:fs";
11906
11961
  import os4 from "node:os";
11907
- import path19 from "node:path";
11962
+ import path20 from "node:path";
11908
11963
  function allowlistHint(root, alias) {
11909
11964
  return `Add this under "commands" in ${root}/cascade.json: "${alias}": "<path>"`;
11910
11965
  }
11911
11966
  function readManifest(root) {
11912
11967
  try {
11913
- return JSON.parse(fs16.readFileSync(path19.join(root, "cascade.json"), "utf8"));
11968
+ return JSON.parse(fs17.readFileSync(path20.join(root, "cascade.json"), "utf8"));
11914
11969
  } catch {
11915
11970
  return {};
11916
11971
  }
@@ -11930,8 +11985,8 @@ function agentExtraArgs(root, alias) {
11930
11985
  }
11931
11986
  function executableFile2(candidate) {
11932
11987
  try {
11933
- fs16.accessSync(candidate, fs16.constants.X_OK);
11934
- return fs16.statSync(candidate).isFile() ? fs16.realpathSync(candidate) : null;
11988
+ fs17.accessSync(candidate, fs17.constants.X_OK);
11989
+ return fs17.statSync(candidate).isFile() ? fs17.realpathSync(candidate) : null;
11935
11990
  } catch {
11936
11991
  return null;
11937
11992
  }
@@ -11939,10 +11994,10 @@ function executableFile2(candidate) {
11939
11994
  function resolveExecutable2(target) {
11940
11995
  if (typeof target !== "string" || !target || target.includes("\0")) return null;
11941
11996
  const expanded = target.replace(/^~(?=\/|$)/, os4.homedir());
11942
- if (path19.isAbsolute(expanded)) return executableFile2(expanded);
11997
+ if (path20.isAbsolute(expanded)) return executableFile2(expanded);
11943
11998
  if (!/[\\/]/.test(expanded)) {
11944
- for (const dir of (process.env.PATH ?? "").split(path19.delimiter)) {
11945
- const found = executableFile2(path19.join(dir, expanded));
11999
+ for (const dir of (process.env.PATH ?? "").split(path20.delimiter)) {
12000
+ const found = executableFile2(path20.join(dir, expanded));
11946
12001
  if (found) return found;
11947
12002
  }
11948
12003
  return null;
@@ -12393,7 +12448,7 @@ var init_agent = __esm({
12393
12448
  });
12394
12449
 
12395
12450
  // server/src/graphWatch.ts
12396
- import fs17 from "node:fs";
12451
+ import fs18 from "node:fs";
12397
12452
  function graphFileFor(relative) {
12398
12453
  const segments = String(relative).split(/[\\/]/).filter(Boolean);
12399
12454
  if (segments.length !== 1) return null;
@@ -12418,7 +12473,7 @@ function createGraphWatcher(project) {
12418
12473
  );
12419
12474
  }
12420
12475
  try {
12421
- watcher = fs17.watch(project.root, { recursive: false }, (_event, filename) => {
12476
+ watcher = fs18.watch(project.root, { recursive: false }, (_event, filename) => {
12422
12477
  if (!filename) return;
12423
12478
  const graph = graphFileFor(String(filename));
12424
12479
  if (graph) emit(graph);
@@ -12604,8 +12659,8 @@ var init_net = __esm({
12604
12659
  // server/src/routes/media.ts
12605
12660
  import express, { Router as Router11 } from "express";
12606
12661
  import crypto2 from "crypto";
12607
- import fs18 from "fs";
12608
- import path20 from "path";
12662
+ import fs19 from "fs";
12663
+ import path21 from "path";
12609
12664
  async function loadSharp() {
12610
12665
  if (sharpModule) return sharpModule;
12611
12666
  if (sharpError) throw new Error(sharpError);
@@ -12692,11 +12747,11 @@ function fieldToGrey(array) {
12692
12747
  }
12693
12748
  function contentAddressed2(rel, body) {
12694
12749
  const hash2 = crypto2.createHash("sha1").update(body).digest("hex").slice(0, 12);
12695
- const dir = path20.posix.dirname(rel);
12696
- const name = path20.posix.basename(rel);
12750
+ const dir = path21.posix.dirname(rel);
12751
+ const name = path21.posix.basename(rel);
12697
12752
  const [stem, ...rest] = name.split(".");
12698
12753
  const tail = rest.length ? `.${rest.join(".")}` : "";
12699
- return path20.posix.join(dir, `${stem}.${hash2}${tail}`);
12754
+ return path21.posix.join(dir, `${stem}.${hash2}${tail}`);
12700
12755
  }
12701
12756
  function clampInt(value, min, max) {
12702
12757
  const n = Number(value);
@@ -12718,8 +12773,8 @@ function createMediaRouter(project) {
12718
12773
  try {
12719
12774
  const target = req.query.exact === "1" ? rel : contentAddressed2(rel, req.body);
12720
12775
  const full = project.resolve(target);
12721
- await fs18.promises.mkdir(path20.dirname(full), { recursive: true });
12722
- await fs18.promises.writeFile(full, req.body);
12776
+ await fs19.promises.mkdir(path21.dirname(full), { recursive: true });
12777
+ await fs19.promises.writeFile(full, req.body);
12723
12778
  res.json({ ok: true, path: target, bytes: req.body.length });
12724
12779
  } catch (err) {
12725
12780
  res.status(err instanceof PathSafetyError ? 400 : 500).json({ error: String(err) });
@@ -12736,13 +12791,13 @@ function createMediaRouter(project) {
12736
12791
  }
12737
12792
  let stat;
12738
12793
  try {
12739
- stat = await fs18.promises.stat(full);
12794
+ stat = await fs19.promises.stat(full);
12740
12795
  if (!stat.isFile()) throw new Error("not a file");
12741
12796
  } catch {
12742
12797
  res.status(404).json({ error: `no such file: ${rel}` });
12743
12798
  return;
12744
12799
  }
12745
- const ext = path20.extname(full).toLowerCase();
12800
+ const ext = path21.extname(full).toLowerCase();
12746
12801
  const width = clampInt(req.query.w, 1, 2e4);
12747
12802
  const quality = clampInt(req.query.q, 1, 100) ?? 82;
12748
12803
  const requested = String(req.query.fmt ?? "").toLowerCase();
@@ -12756,7 +12811,7 @@ function createMediaRouter(project) {
12756
12811
  }
12757
12812
  if (!raw && ext === ".npy") {
12758
12813
  try {
12759
- const array = readNpy(await fs18.promises.readFile(full));
12814
+ const array = readNpy(await fs19.promises.readFile(full));
12760
12815
  const { pixels, min, max } = fieldToGrey(array);
12761
12816
  res.setHeader("X-Field-Shape", `${array.height}x${array.width}`);
12762
12817
  res.setHeader("X-Field-Range", `${min}:${max}`);
@@ -12787,7 +12842,7 @@ function createMediaRouter(project) {
12787
12842
  if (raw || !RASTER.has(ext)) {
12788
12843
  res.type(MIME[ext] ?? "application/octet-stream");
12789
12844
  res.setHeader("Content-Length", String(stat.size));
12790
- fs18.createReadStream(full).pipe(res);
12845
+ fs19.createReadStream(full).pipe(res);
12791
12846
  return;
12792
12847
  }
12793
12848
  let format2 = requested;
@@ -12795,7 +12850,7 @@ function createMediaRouter(project) {
12795
12850
  if (format2 === "gif") {
12796
12851
  res.type("image/gif");
12797
12852
  res.setHeader("Content-Length", String(stat.size));
12798
- fs18.createReadStream(full).pipe(res);
12853
+ fs19.createReadStream(full).pipe(res);
12799
12854
  return;
12800
12855
  }
12801
12856
  try {
@@ -12862,11 +12917,11 @@ var init_media = __esm({
12862
12917
  });
12863
12918
 
12864
12919
  // server/src/build.ts
12865
- import fs19 from "node:fs";
12920
+ import fs20 from "node:fs";
12866
12921
  import { createHash } from "node:crypto";
12867
12922
  function buildId(distDir) {
12868
12923
  try {
12869
- const contents = fs19.readFileSync(`${distDir}/index.html`);
12924
+ const contents = fs20.readFileSync(`${distDir}/index.html`);
12870
12925
  return createHash("sha256").update(contents).digest("hex").slice(0, 16);
12871
12926
  } catch {
12872
12927
  return null;
@@ -12881,8 +12936,8 @@ var init_build = __esm({
12881
12936
  // server/src/index.ts
12882
12937
  import express2 from "express";
12883
12938
  import { createServer } from "http";
12884
- import path21 from "path";
12885
- import fs20 from "fs";
12939
+ import path22 from "path";
12940
+ import fs21 from "fs";
12886
12941
  import { fileURLToPath as fileURLToPath3 } from "url";
12887
12942
  function startServer(project, opts = {}) {
12888
12943
  const app = express2();
@@ -12949,7 +13004,7 @@ function startServer(project, opts = {}) {
12949
13004
  app.get("/api/build", (_req, res) => {
12950
13005
  res.json({ id: buildId(CASCADE_DIST) });
12951
13006
  });
12952
- if (fs20.existsSync(CASCADE_DIST)) {
13007
+ if (fs21.existsSync(CASCADE_DIST)) {
12953
13008
  app.use(express2.static(CASCADE_DIST, {
12954
13009
  setHeaders(res, filePath) {
12955
13010
  res.setHeader(
@@ -12961,7 +13016,7 @@ function startServer(project, opts = {}) {
12961
13016
  app.get("/{*splat}", (req, res, next) => {
12962
13017
  if (req.path.startsWith("/api") || req.path === "/health") return next();
12963
13018
  res.setHeader("Cache-Control", "no-store");
12964
- res.sendFile(path21.join(CASCADE_DIST, "index.html"));
13019
+ res.sendFile(path22.join(CASCADE_DIST, "index.html"));
12965
13020
  });
12966
13021
  } else {
12967
13022
  console.warn(`[cascade-server] no build found at ${CASCADE_DIST} \u2014 run "npm run build" in the Cascade repo first. API-only for now.`);
@@ -12994,18 +13049,18 @@ var init_src3 = __esm({
12994
13049
  init_build();
12995
13050
  init_service();
12996
13051
  __filename = fileURLToPath3(import.meta.url);
12997
- __dirname = path21.dirname(__filename);
12998
- CASCADE_DIST = path21.resolve(__dirname, "..", "..", "dist");
13052
+ __dirname = path22.dirname(__filename);
13053
+ CASCADE_DIST = path22.resolve(__dirname, "..", "..", "dist");
12999
13054
  }
13000
13055
  });
13001
13056
 
13002
13057
  // server/src/projectTemplate.ts
13003
- import fs21 from "node:fs";
13004
- import path22 from "node:path";
13058
+ import fs22 from "node:fs";
13059
+ import path23 from "node:path";
13005
13060
  function cascadeVersionRange() {
13006
13061
  try {
13007
13062
  const pkg = JSON.parse(
13008
- fs21.readFileSync(new URL("../../package.json", import.meta.url), "utf8")
13063
+ fs22.readFileSync(new URL("../../package.json", import.meta.url), "utf8")
13009
13064
  );
13010
13065
  const match = /^(\d+)\.(\d+)\./.exec(pkg.version ?? "");
13011
13066
  return match ? `^${match[1]}.${match[2]}.0` : "*";
@@ -13017,14 +13072,14 @@ function createProject(projectsRoot, name) {
13017
13072
  if (!PROJECT_NAME.test(name)) {
13018
13073
  throw new Error("Project names use lowercase letters, numbers, and hyphens (maximum 64 characters).");
13019
13074
  }
13020
- const root = path22.resolve(projectsRoot);
13021
- const directory = path22.resolve(root, name);
13022
- if (path22.dirname(directory) !== root) throw new Error("Project path escapes the configured projects directory.");
13023
- if (fs21.existsSync(directory)) throw new Error(`Already exists: ${directory}`);
13024
- fs21.mkdirSync(path22.join(directory, "nodes"), { recursive: true });
13025
- fs21.mkdirSync(path22.join(directory, "assets"));
13026
- writeJson(path22.join(directory, "cascade.json"), { name });
13027
- writeJson(path22.join(directory, "index.cascade"), {
13075
+ const root = path23.resolve(projectsRoot);
13076
+ const directory = path23.resolve(root, name);
13077
+ if (path23.dirname(directory) !== root) throw new Error("Project path escapes the configured projects directory.");
13078
+ if (fs22.existsSync(directory)) throw new Error(`Already exists: ${directory}`);
13079
+ fs22.mkdirSync(path23.join(directory, "nodes"), { recursive: true });
13080
+ fs22.mkdirSync(path23.join(directory, "assets"));
13081
+ writeJson(path23.join(directory, "cascade.json"), { name });
13082
+ writeJson(path23.join(directory, "index.cascade"), {
13028
13083
  name,
13029
13084
  version: "0.2",
13030
13085
  viewport: { x: 0, y: 0, zoom: 1 },
@@ -13032,7 +13087,7 @@ function createProject(projectsRoot, name) {
13032
13087
  connections: [],
13033
13088
  annotations: []
13034
13089
  });
13035
- writeJson(path22.join(directory, "package.json"), {
13090
+ writeJson(path23.join(directory, "package.json"), {
13036
13091
  name,
13037
13092
  private: true,
13038
13093
  type: "module",
@@ -13052,7 +13107,7 @@ function createProject(projectsRoot, name) {
13052
13107
  typescript: "^6.0.0"
13053
13108
  }
13054
13109
  });
13055
- writeJson(path22.join(directory, "tsconfig.json"), {
13110
+ writeJson(path23.join(directory, "tsconfig.json"), {
13056
13111
  compilerOptions: {
13057
13112
  target: "ES2022",
13058
13113
  module: "ESNext",
@@ -13062,8 +13117,8 @@ function createProject(projectsRoot, name) {
13062
13117
  },
13063
13118
  include: ["nodes/**/*.ts"]
13064
13119
  });
13065
- fs21.writeFileSync(path22.join(directory, ".gitignore"), ".cascade-cache/\nrenders/\nnode_modules/\n");
13066
- fs21.writeFileSync(path22.join(directory, "AGENTS.md"), `# Cascade project
13120
+ fs22.writeFileSync(path23.join(directory, ".gitignore"), ".cascade-cache/\nrenders/\nnode_modules/\n");
13121
+ fs22.writeFileSync(path23.join(directory, "AGENTS.md"), `# Cascade project
13067
13122
 
13068
13123
  Read \`node_modules/cascade/AGENTS.md\` for the framework itself. What follows is what an agent working in *this* project needs.
13069
13124
 
@@ -13218,14 +13273,14 @@ function createNode(projectRoot, name) {
13218
13273
  if (!NODE_NAME.test(name)) {
13219
13274
  throw new Error("Node names start with an uppercase letter and contain only letters or numbers.");
13220
13275
  }
13221
- const directory = path22.resolve(projectRoot, "nodes", name);
13222
- if (fs21.existsSync(directory)) throw new Error(`Already exists: ${directory}`);
13223
- fs21.mkdirSync(directory, { recursive: true });
13224
- fs21.writeFileSync(path22.join(directory, "index.ts"), nodeSource(name));
13276
+ const directory = path23.resolve(projectRoot, "nodes", name);
13277
+ if (fs22.existsSync(directory)) throw new Error(`Already exists: ${directory}`);
13278
+ fs22.mkdirSync(directory, { recursive: true });
13279
+ fs22.writeFileSync(path23.join(directory, "index.ts"), nodeSource(name));
13225
13280
  return directory;
13226
13281
  }
13227
13282
  function writeJson(file, value) {
13228
- fs21.writeFileSync(file, `${JSON.stringify(value, null, 2)}
13283
+ fs22.writeFileSync(file, `${JSON.stringify(value, null, 2)}
13229
13284
  `);
13230
13285
  }
13231
13286
  function nodeSource(name) {
@@ -13263,8 +13318,8 @@ __export(cliCommands_exports, {
13263
13318
  parseStudioArgs: () => parseStudioArgs,
13264
13319
  runStudioCli: () => runStudioCli
13265
13320
  });
13266
- import fs22 from "node:fs";
13267
- import path23 from "node:path";
13321
+ import fs23 from "node:fs";
13322
+ import path24 from "node:path";
13268
13323
  import { execFile as execFile5 } from "node:child_process";
13269
13324
  function parseStudioArgs(args) {
13270
13325
  const positional = [];
@@ -13319,7 +13374,7 @@ async function runStudioCli(args) {
13319
13374
  const config = loadConfig2();
13320
13375
  if (positional[0] === "projects") {
13321
13376
  if (positional[1]) {
13322
- saveConfig({ projectsRoot: path23.resolve(positional[1].replace(/^~(?=\/|$)/, process.env.HOME ?? "~")) });
13377
+ saveConfig({ projectsRoot: path24.resolve(positional[1].replace(/^~(?=\/|$)/, process.env.HOME ?? "~")) });
13323
13378
  console.log(`[cascade] projects root set to ${loadConfig2().projectsRoot} (${configPath()})`);
13324
13379
  } else console.log(loadConfig2().projectsRoot);
13325
13380
  return;
@@ -13338,9 +13393,9 @@ async function runStudioCli(args) {
13338
13393
  return;
13339
13394
  }
13340
13395
  let target = positional[0] ?? ".";
13341
- if (!fs22.existsSync(path23.resolve(target))) {
13342
- const projectTarget = path23.resolve(config.projectsRoot, target);
13343
- if (fs22.existsSync(projectTarget)) target = projectTarget;
13396
+ if (!fs23.existsSync(path24.resolve(target))) {
13397
+ const projectTarget = path24.resolve(config.projectsRoot, target);
13398
+ if (fs23.existsSync(projectTarget)) target = projectTarget;
13344
13399
  }
13345
13400
  const project = ProjectRoot.fromArg(target);
13346
13401
  const server = startServer(project, { host, port, trustedHosts });
@@ -13486,8 +13541,8 @@ var ExpressionEngine = class extends ExpressionEvaluator {
13486
13541
  super();
13487
13542
  this.graph = null;
13488
13543
  const scope = {
13489
- resolveParam: (from, path24) => this.resolveNodePath(from, path24),
13490
- nodeExists: (from, path24) => from.node(path24) !== null,
13544
+ resolveParam: (from, path25) => this.resolveNodePath(from, path25),
13545
+ nodeExists: (from, path25) => from.node(path25) !== null,
13491
13546
  inputNode: (from, index) => this.resolveInput(from, index)
13492
13547
  };
13493
13548
  this.setScope(scope);
@@ -13501,15 +13556,15 @@ var ExpressionEngine = class extends ExpressionEvaluator {
13501
13556
  /**
13502
13557
  * Resolve a path to get the parameter value
13503
13558
  */
13504
- resolveNodePath(fromNode, path24) {
13505
- const lastSlash = path24.lastIndexOf("/");
13506
- if (lastSlash === -1 || path24.startsWith("./")) {
13507
- const paramName2 = path24.replace("./", "");
13559
+ resolveNodePath(fromNode, path25) {
13560
+ const lastSlash = path25.lastIndexOf("/");
13561
+ if (lastSlash === -1 || path25.startsWith("./")) {
13562
+ const paramName2 = path25.replace("./", "");
13508
13563
  const prop2 = fromNode.props[paramName2];
13509
13564
  return prop2 ? { value: prop2.value } : null;
13510
13565
  }
13511
- const nodePath = path24.substring(0, lastSlash) || ".";
13512
- const paramName = path24.substring(lastSlash + 1);
13566
+ const nodePath = path25.substring(0, lastSlash) || ".";
13567
+ const paramName = path25.substring(lastSlash + 1);
13513
13568
  const targetNode = fromNode.node(nodePath);
13514
13569
  if (!targetNode) return null;
13515
13570
  const prop = targetNode.props[paramName];
@@ -13536,14 +13591,14 @@ var ExpressionEngine = class extends ExpressionEvaluator {
13536
13591
  for (const [propKey, prop] of Object.entries(node.props)) {
13537
13592
  if (!prop.expression) continue;
13538
13593
  const paths = this.extractDependencies(prop.expression);
13539
- for (const path24 of paths) {
13540
- if (!this.validatePath(node, path24)) {
13594
+ for (const path25 of paths) {
13595
+ if (!this.validatePath(node, path25)) {
13541
13596
  warnings.push({
13542
13597
  nodeId: node.id,
13543
13598
  nodePath: node.path(),
13544
13599
  propName: propKey,
13545
13600
  expression: prop.expression,
13546
- brokenPath: path24
13601
+ brokenPath: path25
13547
13602
  });
13548
13603
  }
13549
13604
  }
@@ -13584,24 +13639,24 @@ var CascadeContext = class {
13584
13639
  /**
13585
13640
  * Find a node by path (absolute or relative to pwd)
13586
13641
  */
13587
- node(path24) {
13642
+ node(path25) {
13588
13643
  if (!this._graph) return null;
13589
- if (path24.startsWith("/")) {
13590
- return this.resolveAbsolutePath(path24);
13644
+ if (path25.startsWith("/")) {
13645
+ return this.resolveAbsolutePath(path25);
13591
13646
  }
13592
13647
  if (this._pwd) {
13593
- return this._pwd.node(path24);
13648
+ return this._pwd.node(path25);
13594
13649
  }
13595
- return this.resolveAbsolutePath("/" + path24);
13650
+ return this.resolveAbsolutePath("/" + path25);
13596
13651
  }
13597
13652
  /**
13598
13653
  * Find a parameter by path (e.g., "/node1/radius")
13599
13654
  */
13600
- parm(path24) {
13601
- const lastSlash = path24.lastIndexOf("/");
13655
+ parm(path25) {
13656
+ const lastSlash = path25.lastIndexOf("/");
13602
13657
  if (lastSlash === -1) return null;
13603
- const nodePath = path24.substring(0, lastSlash) || "/";
13604
- const parmName = path24.substring(lastSlash + 1);
13658
+ const nodePath = path25.substring(0, lastSlash) || "/";
13659
+ const parmName = path25.substring(lastSlash + 1);
13605
13660
  const node = this.node(nodePath);
13606
13661
  return node?.parm(parmName) ?? null;
13607
13662
  }
@@ -13627,12 +13682,12 @@ var CascadeContext = class {
13627
13682
  /**
13628
13683
  * Navigate to a path (like cd command)
13629
13684
  */
13630
- cd(path24) {
13631
- if (path24 === "/") {
13685
+ cd(path25) {
13686
+ if (path25 === "/") {
13632
13687
  this._pwd = null;
13633
13688
  return;
13634
13689
  }
13635
- const target = this.node(path24);
13690
+ const target = this.node(path25);
13636
13691
  if (target && target.isNetwork()) {
13637
13692
  this._pwd = target;
13638
13693
  }
@@ -13730,22 +13785,22 @@ var CascadeContext = class {
13730
13785
  /**
13731
13786
  * Get parameter value as number (shorthand)
13732
13787
  */
13733
- ch(path24) {
13734
- const p = this.parm(path24);
13788
+ ch(path25) {
13789
+ const p = this.parm(path25);
13735
13790
  return p?.evalAsFloat() ?? 0;
13736
13791
  }
13737
13792
  /**
13738
13793
  * Get parameter value as string (shorthand)
13739
13794
  */
13740
- chs(path24) {
13741
- const p = this.parm(path24);
13795
+ chs(path25) {
13796
+ const p = this.parm(path25);
13742
13797
  return p?.evalAsString() ?? "";
13743
13798
  }
13744
13799
  /**
13745
13800
  * Get parameter value as vector (shorthand)
13746
13801
  */
13747
- chv(path24) {
13748
- const p = this.parm(path24);
13802
+ chv(path25) {
13803
+ const p = this.parm(path25);
13749
13804
  const val = p?.eval();
13750
13805
  if (Array.isArray(val)) return val;
13751
13806
  if (typeof val === "number") return [val];
@@ -13782,9 +13837,9 @@ var CascadeContext = class {
13782
13837
  return this.fframe();
13783
13838
  }
13784
13839
  // ============ Internal Helpers ============
13785
- resolveAbsolutePath(path24) {
13840
+ resolveAbsolutePath(path25) {
13786
13841
  if (!this._graph) return null;
13787
- const segments = path24.replace(/^\//, "").split("/").filter((s) => s.length > 0);
13842
+ const segments = path25.replace(/^\//, "").split("/").filter((s) => s.length > 0);
13788
13843
  if (segments.length === 0) {
13789
13844
  return null;
13790
13845
  }
@@ -13820,7 +13875,7 @@ function parseFrameSpec(spec) {
13820
13875
 
13821
13876
  // src/cli/runner.ts
13822
13877
  async function runGraph(options2) {
13823
- const { file, entryNode, validateOnly, checkOnly, inspectOnly, strict, verbose, frames, fps, out } = options2;
13878
+ const { file, entryNode, nodes, validateOnly, checkOnly, inspectOnly, strict, verbose, frames, fps, out } = options2;
13824
13879
  if (verbose) {
13825
13880
  console.log(`Loading graph from: ${file}`);
13826
13881
  }
@@ -13914,6 +13969,7 @@ async function runGraph(options2) {
13914
13969
  ...fps === void 0 ? {} : { fps },
13915
13970
  out: out ?? "renders",
13916
13971
  ...entryNode ? { entryNode } : {},
13972
+ ...nodes?.length ? { nodes } : {},
13917
13973
  ...verbose ? { verbose } : {}
13918
13974
  });
13919
13975
  if (rendered) {
@@ -13923,7 +13979,7 @@ async function runGraph(options2) {
13923
13979
  return;
13924
13980
  }
13925
13981
  if (options2.json) throw new Error("--json rendering requires a fully definition-v1 graph");
13926
- } else if (await runDeterministicProjectGraph(file, graphData, entryNode)) {
13982
+ } else if (await runDeterministicProjectGraph(file, graphData, nodes?.[0] ?? entryNode)) {
13927
13983
  if (verbose) console.log("Graph execution completed");
13928
13984
  releaseHost();
13929
13985
  return;
@@ -13939,7 +13995,7 @@ async function runGraph(options2) {
13939
13995
  }
13940
13996
 
13941
13997
  // src/cli/index.ts
13942
- import * as fs23 from "fs/promises";
13998
+ import * as fs24 from "fs/promises";
13943
13999
  import { readFileSync } from "node:fs";
13944
14000
 
13945
14001
  // src/cli/supervise.ts
@@ -14056,7 +14112,8 @@ Options:
14056
14112
  --port <number> Bind Studio HTTP to an explicit port
14057
14113
  --trusted-host <name> Allow an exact remote browser hostname (repeatable)
14058
14114
  --no-open Do not open Studio in a browser
14059
- --entry-node <id> Execute from a specific entry node
14115
+ --node <id> Render only this node's output. Repeatable.
14116
+ --entry-node <id> Older spelling of --node, single valued
14060
14117
  --frames <range> Render a frame sequence: 1-100, 1-100x2 (step), or 42
14061
14118
  --fps <number> Frame rate to evaluate the range at
14062
14119
  --out <dir> Sequence directory, project-relative (default: renders)
@@ -14098,7 +14155,7 @@ image output nothing downstream consumes, or per output of --entry-node.
14098
14155
  process.exit(1);
14099
14156
  }
14100
14157
  try {
14101
- await fs23.access(fileArg);
14158
+ await fs24.access(fileArg);
14102
14159
  } catch {
14103
14160
  console.error(`Error: File not found: ${fileArg}`);
14104
14161
  process.exit(1);
@@ -14114,6 +14171,11 @@ image output nothing downstream consumes, or per output of --entry-node.
14114
14171
  if (verboseIndex !== -1) {
14115
14172
  options2.verbose = true;
14116
14173
  }
14174
+ const named = [];
14175
+ for (let index = 0; index < args.length; index += 1) {
14176
+ if (args[index] === "--node" && args[index + 1]) named.push(args[index + 1]);
14177
+ }
14178
+ if (named.length) options2.nodes = named;
14117
14179
  const entryNodeIndex = args.indexOf("--entry-node");
14118
14180
  if (entryNodeIndex !== -1 && args[entryNodeIndex + 1]) {
14119
14181
  options2.entryNode = args[entryNodeIndex + 1];