@field/cascade 0.10.0 → 0.12.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 (229) hide show
  1. package/AGENTS.md +25 -5
  2. package/ARCHITECTURE.md +5 -4
  3. package/CHANGELOG.md +382 -0
  4. package/README.md +1 -2
  5. package/dist/assets/AgentPanel-1JugFjE4.css +1 -0
  6. package/dist/assets/AgentPanel-BDL5MddA.js +5 -0
  7. package/dist/assets/{CodeEditor-DSZBpzdy.js → CodeEditor-C-xYG-Wn.js} +120 -120
  8. package/dist/assets/CodePanel-CtXd50m6.js +2 -0
  9. package/dist/assets/CookInfoPanel-B3NMhKJF.css +1 -0
  10. package/dist/assets/CookInfoPanel-DG4_TRsX.js +2 -0
  11. package/dist/assets/CustomNodeDialog-CHfZUs-r.js +1 -0
  12. package/dist/assets/DefinitionNode-CaRTXQ_b.js +1 -0
  13. package/dist/assets/DefinitionPanel-C3CxZY9r.js +1 -0
  14. package/dist/assets/DefinitionPanel-VNLnAiQD.css +1 -0
  15. package/dist/assets/OpenGraphDialog-BLdr6k_q.js +1 -0
  16. package/dist/assets/ProjectSettings-ZnvmIAir.js +2 -0
  17. package/dist/assets/{SettingsDialog-p25h3tj0.js → SettingsDialog-CBX3Wa87.js} +1 -1
  18. package/dist/assets/TimelinePanel-CDeR0arY.css +1 -0
  19. package/dist/assets/TimelinePanel-CWgnFH1e.js +1 -0
  20. package/dist/assets/{VersionHistoryDialog-DzFLUotB.js → VersionHistoryDialog-BOSoW1R-.js} +1 -1
  21. package/dist/assets/assert-BO2_erPD.js +1 -0
  22. package/dist/assets/assert-hvykGCnP.js +1 -0
  23. package/dist/assets/builder-BmBKcDix.js +1 -0
  24. package/dist/assets/camera-DFF7YA3F.js +1 -0
  25. package/dist/assets/checks-DAvyO9Gf.js +1 -0
  26. package/dist/assets/contactSheet-N1NcaWxw.js +2 -0
  27. package/dist/assets/{core-BICrEt0y.js → core-DB-MkGjj.js} +3 -4
  28. package/dist/assets/coreTypes-DtnDATZ7.js +1 -0
  29. package/dist/assets/{cssMode-D7TXw57I.js → cssMode-DPVckSOc.js} +1 -1
  30. package/dist/assets/definitions-B227e7f_.js +1 -0
  31. package/dist/assets/definitions-D0WOUJd7.js +1 -0
  32. package/dist/assets/{editor.api-DvkCO3ko.js → editor.api-Dkutg_Z1.js} +1 -1
  33. package/dist/assets/expressions-X-Z-GrgW.js +5 -0
  34. package/dist/assets/file-DAL6IfjV.js +1 -0
  35. package/dist/assets/filters-D3k1LRMa.js +1 -0
  36. package/dist/assets/{freemarker2-BA2lxsF1.js → freemarker2-CLiLKk3B.js} +1 -1
  37. package/dist/assets/generators-BhDxRAFC.js +1 -0
  38. package/dist/assets/geo-BsMImJIJ.js +1 -0
  39. package/dist/assets/geo-C4YngDob.js +2 -0
  40. package/dist/assets/{handlebars-BcDiyPd8.js → handlebars-CcD3aMeL.js} +1 -1
  41. package/dist/assets/{html-C7O4TyhX.js → html-DvZWGfqb.js} +1 -1
  42. package/dist/assets/{htmlMode-WaDGF3Mj.js → htmlMode-DUD3VaoN.js} +1 -1
  43. package/dist/assets/image-0SfH-snh.js +1 -0
  44. package/dist/assets/image-Ck-RdGU4.js +2 -0
  45. package/dist/assets/index-BWnrmIB9.js +137 -0
  46. package/dist/assets/index-DIaxEONI.css +1 -0
  47. package/dist/assets/io-BGmt2g0D.js +1 -0
  48. package/dist/assets/{javascript-CpIEK7tz.js → javascript-Cap2ahgh.js} +1 -1
  49. package/dist/assets/{jsonMode-B2FltW0f.js → jsonMode-B0uLjp9C.js} +1 -1
  50. package/dist/assets/{liquid-CN6FFHZE.js → liquid-CnxwhfLn.js} +1 -1
  51. package/dist/assets/{lspLanguageFeatures-QhV9XtLp.js → lspLanguageFeatures-Dn9hlKX6.js} +1 -1
  52. package/dist/assets/math-GeULS-sk.js +1 -0
  53. package/dist/assets/{mdx-DE89pafV.js → mdx-BV3jbrVa.js} +1 -1
  54. package/dist/assets/pop-KMF_qloi.js +1 -0
  55. package/dist/assets/preload-helper-Czpn1I53.js +1 -0
  56. package/dist/assets/{python-ByTllTB9.js → python-CGyFk4XK.js} +1 -1
  57. package/dist/assets/{razor-USYlRRCE.js → razor-CQfzX3-N.js} +1 -1
  58. package/dist/assets/remap-5SjXQU-q.js +1 -0
  59. package/dist/assets/renderMarkdown-ork0Oir7.js +67 -0
  60. package/dist/assets/studioImages-CAyH-0Pl.js +1 -0
  61. package/dist/assets/text-ZpX7qNkx.js +2 -0
  62. package/dist/assets/time-P_OiBhcf.js +1 -0
  63. package/dist/assets/{tsMode-BXpN8iUz.js → tsMode-BOeXzO9H.js} +1 -1
  64. package/dist/assets/{typescript-DInBjGQQ.js → typescript-J1SnE_Tu.js} +1 -1
  65. package/dist/assets/{workers-C7Ye5B7D.js → workers-CYwwTiVF.js} +1 -1
  66. package/dist/assets/{xml-U0Uocqyo.js → xml-DnPRNzK9.js} +1 -1
  67. package/dist/assets/{yaml-CBw_PVYf.js → yaml-BmmYBNa9.js} +1 -1
  68. package/dist/cli/DefinitionNode-P7AA4TAU.js +15 -0
  69. package/dist/cli/Graph-OB6EWSGL.js +2975 -0
  70. package/dist/cli/assert-GROGQNCQ.js +25 -0
  71. package/dist/cli/checks-3WB44XBL.js +189 -0
  72. package/dist/cli/chunk-2JEU3MGB.js +1125 -0
  73. package/dist/cli/chunk-2KXB47FP.js +115 -0
  74. package/dist/cli/chunk-3RXL5GHB.js +816 -0
  75. package/dist/cli/chunk-53LGFPWX.js +138 -0
  76. package/dist/cli/chunk-5ORUQ6ES.js +267 -0
  77. package/dist/cli/chunk-73S4JZXK.js +336 -0
  78. package/dist/cli/chunk-7QE2ZNSN.js +121 -0
  79. package/dist/cli/chunk-BP7UKF36.js +376 -0
  80. package/dist/cli/chunk-BPODRH52.js +201 -0
  81. package/dist/cli/chunk-BZ63XQ3I.js +1301 -0
  82. package/dist/cli/chunk-CKS2FQWR.js +89 -0
  83. package/dist/cli/chunk-CO22XHNG.js +593 -0
  84. package/dist/cli/chunk-CTPLRSWF.js +2197 -0
  85. package/dist/cli/chunk-DEQSEFFH.js +153 -0
  86. package/dist/cli/chunk-EU4ZFZ7Y.js +122 -0
  87. package/dist/cli/chunk-EZRWJ4G2.js +104 -0
  88. package/dist/cli/chunk-FWMSGKEH.js +71 -0
  89. package/dist/cli/chunk-G23A2BZZ.js +705 -0
  90. package/dist/cli/chunk-GRIN2DDP.js +1465 -0
  91. package/dist/cli/chunk-HFHWYL55.js +139 -0
  92. package/dist/cli/chunk-HSNOBIVT.js +263 -0
  93. package/dist/cli/chunk-ICH7N5PN.js +26 -0
  94. package/dist/cli/chunk-J7VK7MNF.js +418 -0
  95. package/dist/cli/chunk-JVOCK5W4.js +83 -0
  96. package/dist/cli/chunk-KBGUO27Q.js +15 -0
  97. package/dist/cli/chunk-KH4MUBKF.js +1551 -0
  98. package/dist/cli/chunk-L6B6FUQP.js +330 -0
  99. package/dist/cli/chunk-LTXWBXOM.js +438 -0
  100. package/dist/cli/chunk-MI5JLQYA.js +220 -0
  101. package/dist/cli/chunk-MP653A5O.js +3766 -0
  102. package/dist/cli/chunk-NJFMDXHR.js +320 -0
  103. package/dist/cli/chunk-OM6BEME6.js +267 -0
  104. package/dist/cli/chunk-OWWBYLHC.js +136 -0
  105. package/dist/cli/chunk-QQPA2ZHH.js +273 -0
  106. package/dist/cli/chunk-RUU3YWXU.js +99 -0
  107. package/dist/cli/chunk-RXDIMXN5.js +98 -0
  108. package/dist/cli/chunk-ST6LROUG.js +267 -0
  109. package/dist/cli/chunk-TJW67QSI.js +225 -0
  110. package/dist/cli/chunk-WD62JIP6.js +118 -0
  111. package/dist/cli/chunk-WGKPJZDY.js +364 -0
  112. package/dist/cli/chunk-X4WKAOQP.js +634 -0
  113. package/dist/cli/chunk-X566FWBV.js +156 -0
  114. package/dist/cli/chunk-YH637PDL.js +317 -0
  115. package/dist/cli/chunk-ZL4JTG5Y.js +37 -0
  116. package/dist/cli/chunk-ZSCUZE6I.js +112 -0
  117. package/dist/cli/cliCommands-JJN5Z3ZC.js +3470 -0
  118. package/dist/cli/commands-PHYFYA3F.js +1374 -0
  119. package/dist/cli/contactSheet-2PQVRUFW.js +11 -0
  120. package/dist/cli/definitions-BPOPW52L.js +32 -0
  121. package/dist/cli/doctorCommand-ORWQRNYP.js +106 -0
  122. package/dist/cli/edit-S5KEQ5RA.js +28 -0
  123. package/dist/cli/editorCommands-NFR32JOY.js +41 -0
  124. package/dist/cli/encode-ICPEPMH5.js +17 -0
  125. package/dist/cli/file-3HTQHPYQ.js +41 -0
  126. package/dist/cli/filters-6ODFWYK4.js +379 -0
  127. package/dist/cli/frames-LSAUBZ7P.js +16 -0
  128. package/dist/cli/generators-YU7GB4MD.js +210 -0
  129. package/dist/cli/geo-6ZPGKZRU.js +43 -0
  130. package/dist/cli/graphCommands-EOUOBOIJ.js +184 -0
  131. package/dist/cli/graphFile-YTGJB5F5.js +292 -0
  132. package/dist/cli/headlessCanvas-ENRHC7GS.js +16 -0
  133. package/dist/cli/helpCommand-PULH6ZYB.js +157 -0
  134. package/dist/cli/image-TXELZZK5.js +55 -0
  135. package/dist/cli/index.js +100 -30756
  136. package/dist/cli/math-RSYRQ3C6.js +16 -0
  137. package/dist/cli/mcp-JVZFQOEX.js +164 -0
  138. package/dist/cli/parity-X6DEH2DV.js +334 -0
  139. package/dist/cli/pop-B7SIIYKN.js +39 -0
  140. package/dist/cli/project-VYGIA7JX.js +12 -0
  141. package/dist/cli/projectDefinition-F3D7UIHJ.js +27 -0
  142. package/dist/cli/projectRuntime-2Q4JKEH3.js +55 -0
  143. package/dist/cli/registerStandardNodes-BXJCB6E4.js +744 -0
  144. package/dist/cli/render-PT4MWI6U.js +438 -0
  145. package/dist/cli/runCommand-HNPQVZMS.js +198 -0
  146. package/dist/cli/runner-5XPXVW7J.js +43 -0
  147. package/dist/cli/runtime/image.ts +302 -0
  148. package/dist/cli/runtime/io.ts +78 -0
  149. package/dist/cli/runtime/stage.ts +4 -1
  150. package/dist/cli/share-MHF6TNRK.js +166 -0
  151. package/dist/cli/stageCache-TY2BFZJR.js +15 -0
  152. package/dist/cli/stageRunner-KY2EJP7X.js +10 -0
  153. package/dist/cli/text-MB3EKR4H.js +126 -0
  154. package/dist/cli/verbs-PI2JH5FI.js +19 -0
  155. package/dist/cli/webBuild-WPPMGGW7.js +44 -0
  156. package/dist/contracts/capabilities.d.ts +18 -1
  157. package/dist/contracts/color.d.ts +8 -0
  158. package/dist/contracts/color.js +23 -0
  159. package/dist/contracts/definition.d.ts +10 -0
  160. package/dist/contracts/documentFormat.d.ts +18 -0
  161. package/dist/contracts/documentFormat.js +195 -0
  162. package/dist/contracts/editor.d.ts +17 -0
  163. package/dist/contracts/index.d.ts +1 -0
  164. package/dist/contracts/index.js +1 -0
  165. package/dist/contracts/schema.d.ts +1 -1
  166. package/dist/contracts/schema.js +1 -0
  167. package/dist/contracts/validate.js +11 -1
  168. package/dist/contracts/values.d.ts +24 -0
  169. package/dist/image.d.ts +156 -0
  170. package/dist/image.js +161 -0
  171. package/dist/index.html +8 -2
  172. package/dist/io.d.ts +29 -0
  173. package/dist/io.js +40 -0
  174. package/dist/player/app.js +3 -2
  175. package/dist/player/editor.js +35 -9
  176. package/dist/player/editorEngine.js +3 -2
  177. package/dist/runtime/builtins/assert/checks.d.ts +18 -0
  178. package/dist/runtime/builtins/assert/checks.js +207 -0
  179. package/dist/runtime/builtins/assert/definitions.d.ts +130 -0
  180. package/dist/runtime/builtins/assert/definitions.js +54 -0
  181. package/dist/runtime/builtins/assert/index.d.ts +152 -0
  182. package/dist/runtime/builtins/assert/index.js +36 -0
  183. package/dist/runtime/builtins/image/contactSheet.d.ts +64 -0
  184. package/dist/runtime/builtins/image/contactSheet.js +150 -0
  185. package/dist/runtime/builtins/image/definitions.d.ts +950 -0
  186. package/dist/runtime/builtins/image/definitions.js +343 -0
  187. package/dist/runtime/builtins/image/file.d.ts +8 -0
  188. package/dist/runtime/builtins/image/file.js +37 -0
  189. package/dist/runtime/builtins/image/filters.d.ts +8 -0
  190. package/dist/runtime/builtins/image/filters.js +394 -0
  191. package/dist/runtime/builtins/image/generators.d.ts +7 -0
  192. package/dist/runtime/builtins/image/generators.js +144 -0
  193. package/dist/runtime/builtins/image/index.d.ts +952 -0
  194. package/dist/runtime/builtins/image/index.js +44 -0
  195. package/dist/runtime/builtins/image/io.d.ts +100 -0
  196. package/dist/runtime/builtins/image/io.js +142 -0
  197. package/dist/runtime/builtins/image/planar.d.ts +55 -0
  198. package/dist/runtime/builtins/image/planar.js +274 -0
  199. package/dist/runtime/builtins/image/simplex.d.ts +22 -0
  200. package/dist/runtime/builtins/image/simplex.js +92 -0
  201. package/dist/runtime/builtins/image/text.d.ts +4 -0
  202. package/dist/runtime/builtins/image/text.js +141 -0
  203. package/dist/runtime/builtins/index.d.ts +3 -0
  204. package/dist/runtime/builtins/index.js +7 -0
  205. package/dist/runtime/gpu.d.ts +25 -0
  206. package/dist/runtime/gpu.js +40 -0
  207. package/dist/runtime/runtime.d.ts +2 -0
  208. package/dist/runtime/runtime.js +268 -44
  209. package/dist/runtime/types.d.ts +30 -0
  210. package/dist/stage.js +4 -1
  211. package/doc/CLI_REFERENCE.md +91 -2
  212. package/doc/EDITOR_EXPORT.md +37 -2
  213. package/doc/MCP.md +18 -1
  214. package/doc/NODE_REFERENCE.md +424 -10
  215. package/doc/PROJECT_AUTHORING.md +222 -7
  216. package/package.json +19 -4
  217. package/dist/assets/CodePanel-sY6sZa7B.js +0 -2
  218. package/dist/assets/CustomNodeDialog-BRKnqZhp.js +0 -1
  219. package/dist/assets/ExportDialog-C7dVGua4.js +0 -358
  220. package/dist/assets/ExportDialog-DOq0flMV.css +0 -1
  221. package/dist/assets/OpenGraphDialog-ZsgL-iwr.js +0 -1
  222. package/dist/assets/ProjectSettings-Cw17nNe3.js +0 -2
  223. package/dist/assets/definitions-DUww2Rt-.js +0 -1
  224. package/dist/assets/geo-C2RHOVHC.js +0 -1
  225. package/dist/assets/image-DR4oeyJB.js +0 -2062
  226. package/dist/assets/index-7vae6YFC.css +0 -1
  227. package/dist/assets/index-DyvDC6pS.js +0 -219
  228. package/dist/assets/math-DaDEgWKt.js +0 -1
  229. package/dist/assets/pop-pLH7L0PW.js +0 -1
@@ -0,0 +1,1125 @@
1
+ import {
2
+ validateNodeDefinition
3
+ } from "./chunk-NJFMDXHR.js";
4
+
5
+ // server/src/project.ts
6
+ import fs5 from "fs/promises";
7
+ import fssync from "fs";
8
+ import path5 from "path";
9
+
10
+ // server/src/pathSafety.ts
11
+ import path from "path";
12
+ import fs from "node:fs";
13
+ var PathSafetyError = class extends Error {
14
+ };
15
+ function resolveWithinRoot(root, ...segments) {
16
+ for (const seg of segments) {
17
+ if (path.isAbsolute(seg)) {
18
+ throw new PathSafetyError(`absolute path segment not allowed: ${seg}`);
19
+ }
20
+ }
21
+ const full = path.resolve(root, ...segments);
22
+ const rel = path.relative(root, full);
23
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
24
+ throw new PathSafetyError(`path escapes root: ${segments.join("/")}`);
25
+ }
26
+ const canonicalRoot = canonical(root, `project root is not accessible: ${root}`);
27
+ let existing = full;
28
+ while (!existsIncludingSymlink(existing)) {
29
+ const parent = path.dirname(existing);
30
+ if (parent === existing) {
31
+ throw new PathSafetyError(`path has no accessible parent: ${segments.join("/")}`);
32
+ }
33
+ existing = parent;
34
+ }
35
+ const canonicalExisting = canonical(existing, `path is not accessible: ${segments.join("/")}`);
36
+ const canonicalRel = path.relative(canonicalRoot, canonicalExisting);
37
+ if (canonicalRel.startsWith("..") || path.isAbsolute(canonicalRel)) {
38
+ throw new PathSafetyError(`path escapes root through a symbolic link: ${segments.join("/")}`);
39
+ }
40
+ return full;
41
+ }
42
+ function existsIncludingSymlink(candidate) {
43
+ try {
44
+ fs.lstatSync(candidate);
45
+ return true;
46
+ } catch (error) {
47
+ if (error.code === "ENOENT") return false;
48
+ throw error;
49
+ }
50
+ }
51
+ function canonical(candidate, message) {
52
+ try {
53
+ return fs.realpathSync(candidate);
54
+ } catch {
55
+ throw new PathSafetyError(message);
56
+ }
57
+ }
58
+
59
+ // server/src/shell/service.ts
60
+ import fs4 from "node:fs";
61
+ import os2 from "node:os";
62
+ import path4 from "node:path";
63
+ import { execFile, spawn } from "node:child_process";
64
+ import { randomUUID } from "node:crypto";
65
+
66
+ // server/src/credentials.ts
67
+ import fs2 from "node:fs";
68
+ import os from "node:os";
69
+ import path2 from "node:path";
70
+ var CredentialStore = class {
71
+ constructor(file = process.env.CASCADE_CREDENTIALS || path2.join(os.homedir(), ".cascade", "credentials.yaml")) {
72
+ this.file = file;
73
+ }
74
+ has(name) {
75
+ const definition = this.read().get(name);
76
+ return !!definition && Object.keys(definition.fields).length > 0;
77
+ }
78
+ get(name) {
79
+ return this.read().get(name) ?? null;
80
+ }
81
+ environment(names) {
82
+ const definitions = this.read();
83
+ const env = {};
84
+ for (const name of names) {
85
+ const definition = definitions.get(name);
86
+ if (!definition) continue;
87
+ const normalizedName = envName(name);
88
+ for (const [field, value] of Object.entries(definition.fields)) {
89
+ env[`CASCADE_CREDENTIAL_${normalizedName}_${envName(field)}`] = value;
90
+ }
91
+ const secret = secretValue(definition);
92
+ if (secret) env[safeEnvName(definition.env) ?? `CASCADE_CREDENTIAL_${normalizedName}`] = secret;
93
+ }
94
+ return env;
95
+ }
96
+ read() {
97
+ let source;
98
+ try {
99
+ source = fs2.readFileSync(this.file, "utf8");
100
+ } catch (error) {
101
+ if (error.code === "ENOENT") return /* @__PURE__ */ new Map();
102
+ throw error;
103
+ }
104
+ return parseCredentials(source);
105
+ }
106
+ };
107
+ function secretValue(definition) {
108
+ const key = definition.field;
109
+ if (key && definition.fields[key]) return definition.fields[key];
110
+ for (const candidate of ["value", "api_key", "apiKey", "access_token", "token", "key"]) {
111
+ if (definition.fields[candidate]) return definition.fields[candidate];
112
+ }
113
+ return null;
114
+ }
115
+ function parseCredentials(source) {
116
+ const json = source.trim().startsWith("{") ? JSON.parse(source) : null;
117
+ const raw = json && typeof json === "object" ? json : parseSimpleYaml(source);
118
+ const result = /* @__PURE__ */ new Map();
119
+ for (const [name, value] of Object.entries(raw)) {
120
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(name) || !value || typeof value !== "object" || Array.isArray(value)) continue;
121
+ const record = value;
122
+ const fields = Object.fromEntries(Object.entries(record).filter(([key, item]) => typeof item === "string" && !["header", "scheme", "field", "env"].includes(key)));
123
+ const hosts = Array.isArray(record.hosts) ? record.hosts.filter((item) => typeof item === "string").map(normalizeHost).filter(Boolean) : [];
124
+ result.set(name, Object.freeze({
125
+ name,
126
+ fields: Object.freeze(fields),
127
+ hosts: Object.freeze(hosts),
128
+ ...typeof record.header === "string" ? { header: record.header } : {},
129
+ ...typeof record.scheme === "string" ? { scheme: record.scheme } : {},
130
+ ...typeof record.field === "string" ? { field: record.field } : {},
131
+ ...typeof record.env === "string" ? { env: record.env } : {}
132
+ }));
133
+ }
134
+ return result;
135
+ }
136
+ function parseSimpleYaml(source) {
137
+ const result = {};
138
+ let current = null;
139
+ for (const rawLine of source.split(/\r?\n/)) {
140
+ const line = rawLine.replace(/\s+#.*$/, "");
141
+ if (!line.trim()) continue;
142
+ const top = line.match(/^([A-Za-z0-9][A-Za-z0-9._-]*):\s*$/);
143
+ if (top) {
144
+ current = {};
145
+ result[top[1]] = current;
146
+ continue;
147
+ }
148
+ const field = line.match(/^\s{2,}([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
149
+ if (!current || !field) throw new Error(`Unsupported credentials.yaml syntax near ${JSON.stringify(rawLine)}`);
150
+ const value = unquote(field[2]);
151
+ current[field[1]] = value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1).split(",").map((item) => unquote(item.trim())).filter(Boolean) : value;
152
+ }
153
+ return result;
154
+ }
155
+ function unquote(value) {
156
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
157
+ return value;
158
+ }
159
+ function normalizeHost(value) {
160
+ try {
161
+ return value.includes("://") ? new URL(value).hostname.toLowerCase() : value.toLowerCase();
162
+ } catch {
163
+ return "";
164
+ }
165
+ }
166
+ function envName(value) {
167
+ return value.replace(/[^A-Za-z0-9]/g, "_").toUpperCase();
168
+ }
169
+ function safeEnvName(value) {
170
+ return value && /^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(value) && !/^(?:PATH|NODE_OPTIONS|PYTHONPATH|LD_.*|DYLD_.*)$/i.test(value) ? value : null;
171
+ }
172
+
173
+ // server/src/projectConfig.ts
174
+ import fs3 from "node:fs";
175
+ import path3 from "node:path";
176
+ var CREDENTIAL_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
177
+ var SECRET_KEY = /^(?:api[_-]?key|apiKey|secret|password|access[_-]?token|refresh[_-]?token|credential|credentials)$/i;
178
+ function readProjectManifest(root) {
179
+ const raw = readRaw(root);
180
+ return normalizeManifest(raw);
181
+ }
182
+ function writeProjectManifest(root, input) {
183
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("project manifest must be an object");
184
+ const value = input;
185
+ const current = readRaw(root);
186
+ const next = {
187
+ ...current,
188
+ name: typeof value.name === "string" ? value.name.trim() : "",
189
+ commands: stringRecord(value.commands, "commands"),
190
+ credentials: credentialNames(value.credentials),
191
+ settings: settingsRecord(value.settings)
192
+ };
193
+ fs3.writeFileSync(path3.join(root, "cascade.json"), `${JSON.stringify(next, null, 2)}
194
+ `, "utf8");
195
+ return normalizeManifest(next);
196
+ }
197
+ function readRaw(root) {
198
+ try {
199
+ const value = JSON.parse(fs3.readFileSync(path3.join(root, "cascade.json"), "utf8"));
200
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("root must be an object");
201
+ return value;
202
+ } catch (error) {
203
+ if (error.code === "ENOENT") return {};
204
+ throw new Error(`Cannot read cascade.json: ${error instanceof Error ? error.message : String(error)}`);
205
+ }
206
+ }
207
+ function normalizeManifest(raw) {
208
+ return Object.freeze({
209
+ name: typeof raw.name === "string" ? raw.name : "",
210
+ commands: Object.freeze(stringRecord(raw.commands, "commands")),
211
+ credentials: Object.freeze(credentialNames(raw.credentials)),
212
+ settings: Object.freeze(settingsRecord(raw.settings))
213
+ });
214
+ }
215
+ function stringRecord(input, label) {
216
+ if (input === void 0) return {};
217
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error(`${label} must be an object of strings`);
218
+ const entries = Object.entries(input);
219
+ if (entries.some(([key, value]) => !CREDENTIAL_NAME.test(key) || typeof value !== "string" || value.includes("\0"))) {
220
+ throw new Error(`${label} must contain safe names and string values`);
221
+ }
222
+ return Object.fromEntries(entries);
223
+ }
224
+ function credentialNames(input) {
225
+ if (input === void 0) return [];
226
+ if (!Array.isArray(input) || input.some((name) => typeof name !== "string" || !CREDENTIAL_NAME.test(name))) {
227
+ throw new Error("credentials must be an array of safe names");
228
+ }
229
+ return [...new Set(input)];
230
+ }
231
+ function settingsRecord(input) {
232
+ if (input === void 0) return {};
233
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("settings must be an object");
234
+ assertSafeSettings(input, 0);
235
+ return structuredClone(input);
236
+ }
237
+ function assertSafeSettings(value, depth) {
238
+ if (depth > 12) throw new Error("settings nesting is too deep");
239
+ if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) return;
240
+ if (Array.isArray(value)) return value.forEach((item) => assertSafeSettings(item, depth + 1));
241
+ if (!value || typeof value !== "object") throw new Error("settings must contain JSON values");
242
+ for (const [key, item] of Object.entries(value)) {
243
+ if (key === "__proto__" || key === "constructor" || key === "prototype" || SECRET_KEY.test(key)) {
244
+ throw new Error(`settings key ${JSON.stringify(key)} looks secret or unsafe; credentials belong outside the project`);
245
+ }
246
+ assertSafeSettings(item, depth + 1);
247
+ }
248
+ }
249
+
250
+ // server/src/shell/types.ts
251
+ var ShellProcessError = class extends Error {
252
+ constructor(kind, result, message) {
253
+ super(message ?? `Shell process failed: ${kind}`);
254
+ this.kind = kind;
255
+ this.result = result;
256
+ this.name = "ShellProcessError";
257
+ }
258
+ };
259
+ var ShellRequestError = class extends Error {
260
+ constructor(code, message) {
261
+ super(message);
262
+ this.code = code;
263
+ }
264
+ };
265
+
266
+ // server/src/shell/service.ts
267
+ var MAX_BUFFER = 64 * 1024 * 1024;
268
+ var MAX_ARGS = 4096;
269
+ var MAX_ARG_BYTES = 1024 * 1024;
270
+ var MAX_STDIN_BYTES = 16 * 1024 * 1024;
271
+ var MAX_ENV_BYTES = 1024 * 1024;
272
+ var MAX_ENV_KEYS = 128;
273
+ var ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
274
+ var ALIAS = /^[A-Za-z0-9._-]+$/;
275
+ var BLOCKED_ENV = /^(?:PATH|NODE_OPTIONS|PYTHONPATH|RUBYOPT|PERL5OPT|BASH_ENV|ENV|SHELLOPTS|IFS|CDPATH|GLOBIGNORE|LD_.*|DYLD_.*)$/i;
276
+ var ShellService = class {
277
+ constructor(project) {
278
+ this.project = project;
279
+ this.executables = /* @__PURE__ */ new Map();
280
+ this.auditSink = (record) => console.info("[cascade-shell]", JSON.stringify(record));
281
+ this.config = loadConfig(project.root);
282
+ this.configRevision = readConfigRevision(project.root);
283
+ for (const [alias, target] of Object.entries(this.config.commands)) {
284
+ if (!ALIAS.test(alias)) {
285
+ console.warn(`[cascade-shell] ignoring invalid command alias ${JSON.stringify(alias)}`);
286
+ continue;
287
+ }
288
+ const executable = resolveExecutable(project.root, target);
289
+ this.executables.set(alias, executable);
290
+ if (!executable) console.warn(`[cascade-shell] command ${JSON.stringify(alias)} could not be resolved`);
291
+ }
292
+ }
293
+ setAuditSink(sink) {
294
+ this.auditSink = sink;
295
+ }
296
+ async run(command, args = [], options = {}) {
297
+ const started = Date.now();
298
+ const id = randomUUID();
299
+ let cwd = ".";
300
+ let result = emptyResult();
301
+ let errorKind;
302
+ try {
303
+ this.reloadConfig();
304
+ const executable = this.resolveCommand(command);
305
+ const validated = this.validate(args, options);
306
+ cwd = validated.relativeCwd;
307
+ result = await execute(executable, validated.args, {
308
+ cwd: validated.cwd,
309
+ timeout: validated.timeout,
310
+ stdin: validated.stdin,
311
+ env: validated.env,
312
+ signal: options.signal
313
+ });
314
+ return result;
315
+ } catch (error) {
316
+ if (error instanceof ShellProcessError) {
317
+ result = error.result;
318
+ errorKind = error.kind;
319
+ } else if (error instanceof ShellRequestError) {
320
+ errorKind = error.code;
321
+ }
322
+ throw error;
323
+ } finally {
324
+ this.auditSink({
325
+ id,
326
+ command,
327
+ cwd,
328
+ durationMs: Date.now() - started,
329
+ code: result.code,
330
+ errorKind,
331
+ timedOut: result.timedOut,
332
+ cancelled: result.cancelled,
333
+ outputLimited: result.outputLimited
334
+ });
335
+ }
336
+ }
337
+ reloadConfig() {
338
+ const revision = readConfigRevision(this.project.root);
339
+ if (revision === this.configRevision) return;
340
+ this.config = loadConfig(this.project.root);
341
+ this.configRevision = revision;
342
+ this.executables.clear();
343
+ for (const [alias, target] of Object.entries(this.config.commands)) {
344
+ if (!ALIAS.test(alias)) continue;
345
+ this.executables.set(alias, resolveExecutable(this.project.root, target));
346
+ }
347
+ }
348
+ async runJson(command, args = [], options = {}) {
349
+ const result = await this.run(command, args, options);
350
+ if (result.code !== 0) throw new ShellProcessError("nonzero", result, result.stderr || `Command exited with code ${result.code}`);
351
+ const lines = result.stdout.split(/\r?\n/).filter((value) => value.trim());
352
+ const line = lines[lines.length - 1] ?? "";
353
+ try {
354
+ return JSON.parse(line);
355
+ } catch {
356
+ throw new ShellProcessError("invalid-json", result, "Command output was not valid JSON");
357
+ }
358
+ }
359
+ resolveCommand(command) {
360
+ if (!ALIAS.test(command)) throw new ShellRequestError("INVALID_REQUEST", "command must be an allowlist alias");
361
+ if (!this.executables.has(command)) {
362
+ throw new ShellRequestError(
363
+ "COMMAND_NOT_ALLOWED",
364
+ `Command "${command}" is not allowed. Add this under "commands" in ${this.project.root}/cascade.json: "${command}": "<path>"`
365
+ );
366
+ }
367
+ const executable = this.executables.get(command);
368
+ if (!executable) throw new ShellRequestError("MISSING_EXECUTABLE", `Configured command "${command}" is not available`);
369
+ return executable;
370
+ }
371
+ validate(args, options) {
372
+ if (!Array.isArray(args) || args.length > MAX_ARGS || args.some((arg) => typeof arg !== "string" || Buffer.byteLength(arg) > MAX_ARG_BYTES || arg.includes("\0"))) {
373
+ throw new ShellRequestError("INVALID_REQUEST", "args must be an array of bounded strings");
374
+ }
375
+ if (options.stdin !== void 0 && (typeof options.stdin !== "string" || Buffer.byteLength(options.stdin) > MAX_STDIN_BYTES || options.stdin.includes("\0"))) {
376
+ throw new ShellRequestError("INVALID_REQUEST", "stdin exceeds the 16 MiB limit or is invalid");
377
+ }
378
+ const { cwd, relativeCwd } = resolveCwd(this.project.root, options.cwd ?? ".");
379
+ const requestEnv = options.env ?? {};
380
+ const entries = Object.entries(requestEnv);
381
+ if (entries.length > MAX_ENV_KEYS || entries.reduce((sum, [key, value]) => sum + Buffer.byteLength(key) + Buffer.byteLength(String(value)), 0) > MAX_ENV_BYTES) {
382
+ throw new ShellRequestError("INVALID_REQUEST", "request environment exceeds its limit");
383
+ }
384
+ for (const [key, value] of entries) {
385
+ if (!ENV_NAME.test(key) || typeof value !== "string" || value.length > 65536 || value.includes("\0") || BLOCKED_ENV.test(key) || !this.config.shell.allowedRequestEnv.has(key)) {
386
+ throw new ShellRequestError("INVALID_REQUEST", `request environment variable ${JSON.stringify(key)} is not allowed`);
387
+ }
388
+ }
389
+ if (options.timeout !== void 0 && (typeof options.timeout !== "number" || !Number.isFinite(options.timeout))) {
390
+ throw new ShellRequestError("INVALID_REQUEST", "timeout must be a finite number");
391
+ }
392
+ const timeout = Math.min(6e5, Math.max(1, Math.trunc(options.timeout ?? this.config.shell.timeout)));
393
+ const credentialEnv = new CredentialStore().environment(readProjectManifest(this.project.root).credentials);
394
+ return {
395
+ args: [...args],
396
+ cwd,
397
+ relativeCwd,
398
+ timeout,
399
+ stdin: options.stdin,
400
+ env: { ...process.env, ...this.config.shell.env, ...requestEnv, ...credentialEnv }
401
+ };
402
+ }
403
+ };
404
+ function readConfigRevision(root) {
405
+ try {
406
+ return fs4.readFileSync(path4.join(root, "cascade.json"), "utf8");
407
+ } catch (error) {
408
+ if (error.code === "ENOENT") return "";
409
+ throw error;
410
+ }
411
+ }
412
+ function loadConfig(root) {
413
+ let raw = {};
414
+ try {
415
+ raw = JSON.parse(fs4.readFileSync(path4.join(root, "cascade.json"), "utf8"));
416
+ } catch (error) {
417
+ if (error.code !== "ENOENT") throw error;
418
+ }
419
+ return {
420
+ commands: raw?.commands && typeof raw.commands === "object" ? raw.commands : {},
421
+ shell: {
422
+ timeout: Number.isFinite(raw?.shell?.timeout) ? Math.min(6e5, Math.max(1, Math.trunc(raw.shell.timeout))) : 12e4,
423
+ env: stringRecord2(raw?.shell?.env),
424
+ allowedRequestEnv: new Set(Array.isArray(raw?.shell?.allowedRequestEnv) ? raw.shell.allowedRequestEnv.filter((v) => typeof v === "string") : [])
425
+ }
426
+ };
427
+ }
428
+ function stringRecord2(value) {
429
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
430
+ return Object.fromEntries(Object.entries(value).filter(([key, item]) => ENV_NAME.test(key) && !BLOCKED_ENV.test(key) && typeof item === "string" && item.length <= 65536 && !item.includes("\0")));
431
+ }
432
+ function resolveExecutable(root, target) {
433
+ if (typeof target !== "string" || !target || target.includes("\0")) return null;
434
+ const expanded = target.replace(/^~(?=\/|$)/, os2.homedir());
435
+ if (!path4.isAbsolute(expanded) && !/[\\/]/.test(expanded)) {
436
+ for (const dir of (process.env.PATH ?? "").split(path4.delimiter)) {
437
+ const found = executableFile(path4.join(dir, expanded));
438
+ if (found) return found;
439
+ }
440
+ return null;
441
+ }
442
+ const candidate = path4.isAbsolute(expanded) ? expanded : path4.resolve(root, expanded);
443
+ if (!path4.isAbsolute(expanded) && !within(fs4.realpathSync(root), safeRealpath(candidate))) return null;
444
+ return executableFile(candidate);
445
+ }
446
+ function executableFile(candidate) {
447
+ try {
448
+ fs4.accessSync(candidate, fs4.constants.X_OK);
449
+ return fs4.statSync(candidate).isFile() ? fs4.realpathSync(candidate) : null;
450
+ } catch {
451
+ return null;
452
+ }
453
+ }
454
+ function safeRealpath(candidate) {
455
+ try {
456
+ return fs4.realpathSync(candidate);
457
+ } catch {
458
+ return candidate;
459
+ }
460
+ }
461
+ function resolveCwd(root, requested) {
462
+ if (typeof requested !== "string" || path4.isAbsolute(requested) || requested.includes("\0")) throw new ShellRequestError("INVALID_REQUEST", "cwd must be project-relative");
463
+ const lexical = path4.resolve(root, requested);
464
+ if (!within(root, lexical)) throw new ShellRequestError("INVALID_REQUEST", "cwd escapes project root");
465
+ let real;
466
+ try {
467
+ real = fs4.realpathSync(lexical);
468
+ } catch {
469
+ throw new ShellRequestError("INVALID_REQUEST", "cwd does not exist");
470
+ }
471
+ const realRoot = fs4.realpathSync(root);
472
+ if (!within(realRoot, real)) throw new ShellRequestError("INVALID_REQUEST", "cwd escapes project root through a symlink");
473
+ return { cwd: real, relativeCwd: path4.relative(realRoot, real) || "." };
474
+ }
475
+ function within(root, candidate) {
476
+ const relative = path4.relative(root, candidate);
477
+ return relative === "" || !relative.startsWith("..") && !path4.isAbsolute(relative);
478
+ }
479
+ function emptyResult() {
480
+ return { stdout: "", stderr: "", code: null, timedOut: false, cancelled: false, outputLimited: false };
481
+ }
482
+ function execute(executable, args, options) {
483
+ return new Promise((resolve, reject) => {
484
+ let terminal = null;
485
+ let spawnError;
486
+ let termination;
487
+ const stdout = [];
488
+ const stderr = [];
489
+ let stdoutBytes = 0;
490
+ let stderrBytes = 0;
491
+ const child = spawn(executable, args, {
492
+ cwd: options.cwd,
493
+ env: options.env,
494
+ detached: process.platform !== "win32",
495
+ shell: false,
496
+ stdio: ["pipe", "pipe", "pipe"]
497
+ });
498
+ const finishCause = (cause) => {
499
+ if (terminal) return;
500
+ terminal = cause;
501
+ if (cause === "timeout" || cause === "cancelled" || cause === "output-limit") termination = terminateTree(child);
502
+ };
503
+ const timer = setTimeout(() => finishCause("timeout"), options.timeout);
504
+ const abort = () => finishCause("cancelled");
505
+ options.signal?.addEventListener("abort", abort, { once: true });
506
+ child.stdout.on("data", (chunk) => {
507
+ stdoutBytes += chunk.length;
508
+ if (stdoutBytes <= MAX_BUFFER) stdout.push(chunk);
509
+ else finishCause("output-limit");
510
+ });
511
+ child.stderr.on("data", (chunk) => {
512
+ stderrBytes += chunk.length;
513
+ if (stderrBytes <= MAX_BUFFER) stderr.push(chunk);
514
+ else finishCause("output-limit");
515
+ });
516
+ child.once("error", (error) => {
517
+ spawnError = error;
518
+ finishCause("spawn");
519
+ });
520
+ child.once("exit", (code, signal) => finishCause(code === null || signal ? "signal" : "exit"));
521
+ child.once("close", async (code, signal) => {
522
+ clearTimeout(timer);
523
+ options.signal?.removeEventListener("abort", abort);
524
+ await termination?.beforeSettle();
525
+ const output = { stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8") };
526
+ if (terminal === "timeout" || terminal === "cancelled" || terminal === "output-limit") {
527
+ const result = { ...output, code: null, timedOut: terminal === "timeout", cancelled: terminal === "cancelled", outputLimited: terminal === "output-limit" };
528
+ reject(new ShellProcessError(terminal, result));
529
+ } else if (terminal === "spawn" || terminal === "signal" || spawnError || code === null || signal) {
530
+ const reason = spawnError ? "Unable to spawn configured command" : `Command terminated by signal ${signal ?? "unknown"}`;
531
+ reject(new ShellProcessError("spawn", { ...emptyResult(), ...output }, reason));
532
+ } else {
533
+ resolve({ ...output, code, timedOut: false, cancelled: false, outputLimited: false });
534
+ }
535
+ });
536
+ if (options.stdin !== void 0) child.stdin.end(options.stdin);
537
+ else child.stdin.end();
538
+ if (options.signal?.aborted) abort();
539
+ });
540
+ }
541
+ function terminateTree(child) {
542
+ if (!child.pid) return { beforeSettle: async () => void 0 };
543
+ if (process.platform === "win32") {
544
+ const taskkill = new Promise((resolve) => {
545
+ execFile("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], () => resolve());
546
+ });
547
+ return { beforeSettle: () => taskkill };
548
+ }
549
+ try {
550
+ process.kill(-child.pid, "SIGTERM");
551
+ } catch {
552
+ child.kill("SIGTERM");
553
+ }
554
+ const killTimer = setTimeout(() => {
555
+ forceKillTree(child);
556
+ }, 2e3);
557
+ killTimer.unref();
558
+ return {
559
+ beforeSettle: async () => {
560
+ clearTimeout(killTimer);
561
+ forceKillTree(child);
562
+ }
563
+ };
564
+ }
565
+ function forceKillTree(child) {
566
+ if (!child.pid) return;
567
+ if (process.platform === "win32") return;
568
+ try {
569
+ process.kill(-child.pid, "SIGKILL");
570
+ } catch {
571
+ child.kill("SIGKILL");
572
+ }
573
+ }
574
+
575
+ // server/src/project.ts
576
+ import * as ts from "typescript";
577
+
578
+ // packages/runtime/src/definition/extract.ts
579
+ var MAX_DEPTH = 32;
580
+ var MAX_NODES = 1e4;
581
+ function extractNodeDefinition(source, fileName, ts2) {
582
+ const file = ts2.createSourceFile(
583
+ fileName,
584
+ source,
585
+ ts2.ScriptTarget.Latest,
586
+ true,
587
+ ts2.ScriptKind.TS
588
+ );
589
+ const syntaxDiagnostics = file.parseDiagnostics;
590
+ if (syntaxDiagnostics?.length) {
591
+ const syntax = syntaxDiagnostics[0];
592
+ const start = syntax.start ?? 0;
593
+ const point = file.getLineAndCharacterOfPosition(start);
594
+ return {
595
+ ok: false,
596
+ diagnostics: [
597
+ {
598
+ phase: "definition",
599
+ code: "definition/syntax",
600
+ message: ts2.flattenDiagnosticMessageText(syntax.messageText, "\n"),
601
+ file: fileName,
602
+ line: point.line + 1,
603
+ column: point.character + 1
604
+ }
605
+ ]
606
+ };
607
+ }
608
+ const declarations = [];
609
+ for (const statement of file.statements) {
610
+ if (!ts2.isVariableStatement(statement) || !statement.modifiers?.some(
611
+ (modifier) => modifier.kind === ts2.SyntaxKind.ExportKeyword
612
+ ))
613
+ continue;
614
+ if (!(statement.declarationList.flags & ts2.NodeFlags.Const)) continue;
615
+ for (const declaration2 of statement.declarationList.declarations) {
616
+ if (ts2.isIdentifier(declaration2.name) && declaration2.name.text === "definition")
617
+ declarations.push(declaration2);
618
+ }
619
+ }
620
+ if (declarations.length !== 1)
621
+ return failure(
622
+ declarations[1] ?? declarations[0] ?? file,
623
+ declarations.length ? "definition/duplicate-export" : "definition/missing-export",
624
+ declarations.length ? "Exactly one definition export is allowed" : "Missing exported const definition"
625
+ );
626
+ const declaration = declarations[0];
627
+ const initializer = declaration.initializer;
628
+ if (!initializer)
629
+ return failure(
630
+ declaration,
631
+ "definition/non-literal",
632
+ "definition requires a literal initializer"
633
+ );
634
+ let visited = 0;
635
+ try {
636
+ const definition = read(initializer, "$", 0);
637
+ const diagnostics = validateNodeDefinition(definition).map((item) => ({
638
+ ...item,
639
+ file: fileName,
640
+ ...location(initializer)
641
+ }));
642
+ return diagnostics.length ? { ok: false, diagnostics } : { ok: true, definition, diagnostics: [] };
643
+ } catch (error) {
644
+ return {
645
+ ok: false,
646
+ diagnostics: [error.diagnostic]
647
+ };
648
+ }
649
+ function read(node, path6, depth) {
650
+ if (++visited > MAX_NODES)
651
+ throw extraction(
652
+ node,
653
+ "definition/size-limit",
654
+ "Definition exceeds 10,000 syntax nodes",
655
+ path6
656
+ );
657
+ if (depth > MAX_DEPTH)
658
+ throw extraction(
659
+ node,
660
+ "definition/depth-limit",
661
+ "Definition exceeds 32 levels",
662
+ path6
663
+ );
664
+ if (ts2.isParenthesizedExpression(node) || ts2.isSatisfiesExpression(node))
665
+ return read(node.expression, path6, depth + 1);
666
+ if (ts2.isAsExpression(node)) {
667
+ if (node.type.getText(file) !== "const")
668
+ throw extraction(
669
+ node.type,
670
+ "definition/non-literal",
671
+ "Only 'as const' assertions are allowed",
672
+ path6
673
+ );
674
+ return read(node.expression, path6, depth + 1);
675
+ }
676
+ if (ts2.isStringLiteral(node) || ts2.isNoSubstitutionTemplateLiteral(node))
677
+ return node.text;
678
+ if (ts2.isNumericLiteral(node)) return Number(node.text);
679
+ if (node.kind === ts2.SyntaxKind.TrueKeyword) return true;
680
+ if (node.kind === ts2.SyntaxKind.FalseKeyword) return false;
681
+ if (node.kind === ts2.SyntaxKind.NullKeyword) return null;
682
+ if (ts2.isPrefixUnaryExpression(node) && node.operator === ts2.SyntaxKind.MinusToken && ts2.isNumericLiteral(node.operand))
683
+ return -Number(node.operand.text);
684
+ if (ts2.isArrayLiteralExpression(node))
685
+ return node.elements.map((element, index) => {
686
+ if (ts2.isSpreadElement(element))
687
+ throw extraction(
688
+ element,
689
+ "definition/spread",
690
+ "Spreads are not allowed",
691
+ `${path6}.${index}`
692
+ );
693
+ return read(element, `${path6}.${index}`, depth + 1);
694
+ });
695
+ if (ts2.isObjectLiteralExpression(node)) {
696
+ const object = /* @__PURE__ */ Object.create(null);
697
+ const keys = /* @__PURE__ */ new Set();
698
+ for (const property of node.properties) {
699
+ if (ts2.isSpreadAssignment(property))
700
+ throw extraction(
701
+ property,
702
+ "definition/spread",
703
+ "Spreads are not allowed",
704
+ path6
705
+ );
706
+ if (!ts2.isPropertyAssignment(property) || property.name && ts2.isComputedPropertyName(property.name))
707
+ throw extraction(
708
+ property,
709
+ "definition/non-literal",
710
+ "Only explicit properties are allowed",
711
+ path6
712
+ );
713
+ const key = ts2.isIdentifier(property.name) || ts2.isStringLiteral(property.name) || ts2.isNumericLiteral(property.name) ? property.name.text : void 0;
714
+ if (key === void 0)
715
+ throw extraction(
716
+ property,
717
+ "definition/non-literal",
718
+ "Property name must be literal",
719
+ path6
720
+ );
721
+ if (keys.has(key))
722
+ throw extraction(
723
+ property.name,
724
+ "definition/duplicate-key",
725
+ `Duplicate key ${key}`,
726
+ `${path6}.${key}`
727
+ );
728
+ keys.add(key);
729
+ object[key] = read(property.initializer, `${path6}.${key}`, depth + 1);
730
+ }
731
+ return object;
732
+ }
733
+ throw extraction(
734
+ node,
735
+ "definition/non-literal",
736
+ `Unsupported ${ts2.SyntaxKind[node.kind]}`,
737
+ path6
738
+ );
739
+ }
740
+ function location(node) {
741
+ const point = file.getLineAndCharacterOfPosition(node.getStart(file));
742
+ return { line: point.line + 1, column: point.character + 1 };
743
+ }
744
+ function extraction(node, code, message, path6) {
745
+ return new ExtractionFailure({
746
+ phase: "definition",
747
+ code,
748
+ message,
749
+ path: path6,
750
+ file: fileName,
751
+ ...location(node)
752
+ });
753
+ }
754
+ function failure(node, code, message) {
755
+ return {
756
+ ok: false,
757
+ diagnostics: [extraction(node, code, message).diagnostic]
758
+ };
759
+ }
760
+ }
761
+ var ExtractionFailure = class extends Error {
762
+ constructor(diagnostic) {
763
+ super(diagnostic.message);
764
+ this.diagnostic = diagnostic;
765
+ }
766
+ };
767
+
768
+ // server/src/project.ts
769
+ var BROWSER_ONLY_IDENTIFIERS = /* @__PURE__ */ new Set([
770
+ "document",
771
+ "window",
772
+ "navigator",
773
+ "localStorage",
774
+ "sessionStorage",
775
+ "HTMLCanvasElement",
776
+ "HTMLImageElement",
777
+ "HTMLElement",
778
+ "WebGLRenderingContext",
779
+ "WebGL2RenderingContext",
780
+ "requestAnimationFrame"
781
+ ]);
782
+ function usesBrowserOnlyApi(source, fileName) {
783
+ const file = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
784
+ let found = false;
785
+ const visit = (node) => {
786
+ if (found) return;
787
+ if (ts.isIdentifier(node) && BROWSER_ONLY_IDENTIFIERS.has(node.text)) {
788
+ const parent = node.parent;
789
+ const isPropertyName = ts.isPropertyAccessExpression(parent) && parent.name === node || ts.isPropertyAssignment(parent) && parent.name === node || ts.isPropertyDeclaration(parent) && parent.name === node || ts.isMethodDeclaration(parent) && parent.name === node;
790
+ if (!isPropertyName) {
791
+ found = true;
792
+ return;
793
+ }
794
+ }
795
+ ts.forEachChild(node, visit);
796
+ };
797
+ visit(file);
798
+ return found;
799
+ }
800
+ var ProjectRoot = class _ProjectRoot {
801
+ constructor(root, explicitGraph = null) {
802
+ this.root = path5.resolve(root);
803
+ if (!fssync.existsSync(this.root) || !fssync.statSync(this.root).isDirectory()) {
804
+ throw new Error(`Project root does not exist or is not a directory: ${this.root}`);
805
+ }
806
+ this.explicitGraph = explicitGraph;
807
+ this.shell = new ShellService(this);
808
+ }
809
+ /** The graph the client should open on startup, or null when there is
810
+ * nothing obvious to open. Order: the file named on the command line,
811
+ * then `index.cascade` by convention, then the sole `.cascade` file if
812
+ * there is exactly one. With several files and no `index.cascade` this
813
+ * returns null rather than guessing — the app opens empty and File >
814
+ * Open picks. */
815
+ async resolveDefaultGraph() {
816
+ if (this.explicitGraph) return this.explicitGraph;
817
+ const graphs = await this.listGraphFiles();
818
+ if (graphs.includes("index.cascade")) return "index.cascade";
819
+ if (graphs.length === 1) return graphs[0];
820
+ return null;
821
+ }
822
+ /** Split a `cascade <arg>` argument into a root directory and, when the
823
+ * argument named a `.cascade` file, the graph inside it. */
824
+ static fromArg(arg) {
825
+ const resolved = path5.resolve(arg);
826
+ const isFile = fssync.existsSync(resolved) && fssync.statSync(resolved).isFile();
827
+ if (isFile || resolved.endsWith(".cascade")) {
828
+ if (!resolved.endsWith(".cascade")) {
829
+ throw new Error(`Not a .cascade file: ${resolved}`);
830
+ }
831
+ if (!isFile) {
832
+ throw new Error(`Graph file does not exist: ${resolved}`);
833
+ }
834
+ return new _ProjectRoot(path5.dirname(resolved), path5.basename(resolved));
835
+ }
836
+ return new _ProjectRoot(resolved);
837
+ }
838
+ resolve(relativePath = "") {
839
+ return resolveWithinRoot(this.root, relativePath);
840
+ }
841
+ /** Resolve browser-readable project media. `shared/` is the one intentional
842
+ * external root: projects use it for reusable nodes, panels, and cached source
843
+ * assets. Its canonical target remains independently confined, while every
844
+ * other symlink escape is rejected by resolve(). */
845
+ resolveMedia(relativePath) {
846
+ const normalized = relativePath.split("\\").join("/");
847
+ if (normalized === "shared" || normalized.startsWith("shared/")) {
848
+ return this.resolveShared(...normalized.split("/").slice(1));
849
+ }
850
+ return this.resolve(relativePath);
851
+ }
852
+ /**
853
+ * Whether this project is under version control, walking **up** rather than
854
+ * looking only at the root.
855
+ *
856
+ * It checked `<root>/.git` alone, which is right for a sketch that is its own
857
+ * repository and wrong for one inside a repository of sketches — every
858
+ * example in `cascade-sketches` reported *"not a git repo yet"* while sitting
859
+ * in a perfectly good repo. Found 2026-09-09 by an agent that mentioned it in
860
+ * passing while launching a server, which is the sort of aside worth reading.
861
+ *
862
+ * A worktree's `.git` is a file rather than a directory, so the test is
863
+ * existence and not `isDirectory()`. And the walk stops at the filesystem
864
+ * root, where `dirname` stops changing — a `while (true)` on `dirname` is an
865
+ * infinite loop on any path that never contains one.
866
+ */
867
+ get isGitRepo() {
868
+ let directory = this.root;
869
+ for (; ; ) {
870
+ if (fssync.existsSync(path5.join(directory, ".git"))) return true;
871
+ const parent = path5.dirname(directory);
872
+ if (parent === directory) return false;
873
+ directory = parent;
874
+ }
875
+ }
876
+ /** Sorted graph filenames directly in the project root. The caller chooses
877
+ * the active document when a project contains several graphs. */
878
+ async listGraphFiles() {
879
+ const entries = await fs5.readdir(this.root, { withFileTypes: true });
880
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".cascade")).map((e) => e.name).sort();
881
+ }
882
+ async readGraph(filename) {
883
+ const full = this.resolve(filename);
884
+ if (!filename.endsWith(".cascade")) {
885
+ throw new PathSafetyError(`not a .cascade file: ${filename}`);
886
+ }
887
+ return fs5.readFile(full, "utf-8");
888
+ }
889
+ async writeGraph(filename, content) {
890
+ const full = this.resolve(filename);
891
+ if (!filename.endsWith(".cascade")) {
892
+ throw new PathSafetyError(`not a .cascade file: ${filename}`);
893
+ }
894
+ await fs5.writeFile(full, content, "utf-8");
895
+ }
896
+ /** `nodes/<moduleName>/` folders — one per custom node type. */
897
+ async listNodeModules() {
898
+ const nodesDir = this.resolve("nodes");
899
+ if (!fssync.existsSync(nodesDir)) return [];
900
+ const entries = await fs5.readdir(nodesDir, { withFileTypes: true });
901
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
902
+ }
903
+ /** Project-owned browser panels. A project-local panel overrides a panel
904
+ * with the same name provided by `shared/panels`. */
905
+ async listPanels() {
906
+ const [local, shared] = await Promise.all([
907
+ this.listPanelDirectory(this.resolve("panels")),
908
+ this.listPanelDirectory(this.resolveShared("panels"))
909
+ ]);
910
+ return [.../* @__PURE__ */ new Set([...local, ...shared])].sort();
911
+ }
912
+ /** Statically declared panel metadata. Reading this never imports or
913
+ * executes project code. */
914
+ async panelMeta(name) {
915
+ const source = await this.readPanelModuleFile(name);
916
+ const rendererTypes = exportedStringArray(source, "rendererTypes");
917
+ return {
918
+ name,
919
+ title: exportedString(source, "title") ?? name,
920
+ icon: exportedString(source, "icon"),
921
+ ...rendererTypes === null ? {} : { rendererTypes }
922
+ };
923
+ }
924
+ /** Resolve the selected panel entry, preferring the project-local version.
925
+ * Kept public for the compiler; callers must not use it for arbitrary files. */
926
+ async resolvePanelEntry(name) {
927
+ this.assertExtensionName(name, "panel");
928
+ const entries = [
929
+ this.resolve(path5.join("panels", name, "index.ts")),
930
+ this.resolveShared("panels", name, "index.ts")
931
+ ];
932
+ for (const entry of entries) {
933
+ try {
934
+ const stat = await fs5.stat(entry);
935
+ if (stat.isFile()) return entry;
936
+ } catch (err) {
937
+ if (err.code !== "ENOENT") throw err;
938
+ }
939
+ }
940
+ await fs5.access(entries[0]);
941
+ return entries[0];
942
+ }
943
+ /**
944
+ * Where a node module's work actually happens.
945
+ *
946
+ * A declaration normally wins. `cascade/shell` is the exception: it is a
947
+ * server-only effect, so declaring that import as browser code is rejected
948
+ * before compilation or loading. Otherwise the environment is inferred,
949
+ * distinction is already legible in the code — a node whose work happens
950
+ * server-side is exactly one that calls the exec bridge, since a module
951
+ * compiled into the browser cannot spawn a subprocess itself. That fallback
952
+ * is what makes every existing Python-backed node report correctly without
953
+ * being edited.
954
+ *
955
+ * Both kinds are peers: they exchange images as project-relative paths, which
956
+ * the browser reads back through /api/media.
957
+ * Undeclared legacy modules infer a host only from explicit host APIs;
958
+ * otherwise they remain portable. A literal runsOn declaration always wins.
959
+ */
960
+ async moduleRunsOn(moduleName) {
961
+ let source;
962
+ try {
963
+ source = await this.readNodeModuleFile(moduleName);
964
+ } catch {
965
+ return "browser";
966
+ }
967
+ const fromDefinition = this.definitionOf(moduleName, source);
968
+ if (fromDefinition?.runsOn) return fromDefinition.runsOn;
969
+ const declared = source.match(/export\s+const\s+runsOn\s*[:=][^'"`]*['"`](portable|server|browser)['"`]/);
970
+ const importsShell = /(?:from\s+|import\s*(?:\(\s*)?|require\s*\(\s*)['"`]cascade\/shell['"`]/.test(source);
971
+ if (importsShell && declared && declared[1] !== "server") {
972
+ throw new Error(`Node module "${moduleName}" imports cascade/shell but declares runsOn = '${declared?.[1]}'; shell nodes must run on the server`);
973
+ }
974
+ if (declared) return declared[1];
975
+ if (/\/api\/exec|runStage\s*(?:<[^>]*>)?\s*\(/.test(source) || importsShell) return "server";
976
+ return usesBrowserOnlyApi(source, `nodes/${moduleName}/index.ts`) ? "browser" : "portable";
977
+ }
978
+ /**
979
+ * A node module's declared icon, if it names one: `export const icon = 'Cloud'`.
980
+ *
981
+ * A project's nodes all fell back to the same cog, because the icon table is a
982
+ * hardcoded list of Cascade's own built-ins and knows nothing about a project.
983
+ * Letting a module say what it is turns a wall of identical boxes into
984
+ * something you can read at a glance.
985
+ */
986
+ async moduleIcon(moduleName) {
987
+ try {
988
+ const source = await this.readNodeModuleFile(moduleName);
989
+ return this.definitionOf(moduleName, source)?.icon ?? source.match(/export\s+const\s+icon\s*[:=][^'"`]*['"`]([A-Za-z0-9]+)['"`]/)?.[1] ?? null;
990
+ } catch {
991
+ return null;
992
+ }
993
+ }
994
+ /**
995
+ * A node module's definition-v1 `definition`, read statically.
996
+ *
997
+ * Static on purpose: classification happens before compilation, so the module
998
+ * cannot be imported to be asked. `extractNodeDefinition` walks the AST, which
999
+ * is the same thing `cascade check` does, so Studio and the CLI agree.
1000
+ * Returns null for a legacy dynamic module, which has no definition to find.
1001
+ */
1002
+ definitionOf(moduleName, source) {
1003
+ try {
1004
+ const result = extractNodeDefinition(source, `nodes/${moduleName}/index.ts`, ts);
1005
+ return result.ok ? result.definition : null;
1006
+ } catch {
1007
+ return null;
1008
+ }
1009
+ }
1010
+ async readPanelModuleFile(name) {
1011
+ return fs5.readFile(await this.resolvePanelEntry(name), "utf-8");
1012
+ }
1013
+ async listPanelDirectory(directory) {
1014
+ let entries;
1015
+ try {
1016
+ entries = await fs5.readdir(directory, { withFileTypes: true });
1017
+ } catch (err) {
1018
+ if (err.code === "ENOENT") return [];
1019
+ throw err;
1020
+ }
1021
+ const panels = await Promise.all(entries.filter((entry) => !entry.name.startsWith(".") && (entry.isDirectory() || entry.isSymbolicLink())).map(async (entry) => {
1022
+ try {
1023
+ return (await fs5.stat(path5.join(directory, entry.name, "index.ts"))).isFile() ? entry.name : null;
1024
+ } catch (err) {
1025
+ if (err.code === "ENOENT") return null;
1026
+ throw err;
1027
+ }
1028
+ }));
1029
+ return panels.filter((name) => name !== null);
1030
+ }
1031
+ /** `shared/` may intentionally link to a sibling library. Its canonical
1032
+ * target is a second confined root for shared project extensions only. */
1033
+ resolveShared(...segments) {
1034
+ const sharedLink = path5.join(this.root, "shared");
1035
+ let sharedRoot = sharedLink;
1036
+ try {
1037
+ sharedRoot = fssync.realpathSync(sharedLink);
1038
+ } catch (error) {
1039
+ if (error.code === "ENOENT") return path5.join(sharedLink, ...segments);
1040
+ throw error;
1041
+ }
1042
+ return resolveWithinRoot(sharedRoot, ...segments);
1043
+ }
1044
+ assertExtensionName(name, kind) {
1045
+ if (!name || name === "." || name === ".." || name.startsWith(".") || name !== path5.basename(name)) {
1046
+ throw new PathSafetyError(`invalid ${kind} name: ${name}`);
1047
+ }
1048
+ }
1049
+ async readNodeModuleFile(moduleName, relativeFile = "index.ts") {
1050
+ const full = this.resolve(path5.join("nodes", moduleName, relativeFile));
1051
+ return fs5.readFile(full, "utf-8");
1052
+ }
1053
+ async writeNodeModuleFile(moduleName, relativeFile, content) {
1054
+ const full = this.resolve(path5.join("nodes", moduleName, relativeFile));
1055
+ await fs5.mkdir(path5.dirname(full), { recursive: true });
1056
+ await fs5.writeFile(full, content, "utf-8");
1057
+ }
1058
+ async ensureNodeModuleScaffold(moduleName) {
1059
+ const dir = this.resolve(path5.join("nodes", moduleName));
1060
+ await fs5.mkdir(dir, { recursive: true });
1061
+ const indexPath = path5.join(dir, "index.ts");
1062
+ if (!fssync.existsSync(indexPath)) {
1063
+ await fs5.writeFile(
1064
+ indexPath,
1065
+ `// ${moduleName} \u2014 scaffolded by Cascade. Replace with real node logic.
1066
+ // Every node module exports execute(node, graph); real imports work here \u2014
1067
+ // relative files in this project, npm deps, whatever the code needs.
1068
+
1069
+ export function execute(node, graph) {
1070
+ // node.in('name', defaultValue) / node.out('name').setValue(value)
1071
+ }
1072
+ `,
1073
+ "utf-8"
1074
+ );
1075
+ }
1076
+ }
1077
+ };
1078
+ function exportedString(source, exportName) {
1079
+ const match = source.match(new RegExp(
1080
+ `export\\s+const\\s+${exportName}\\s*(?::[^=]+)?=\\s*([\\'"\`])((?:\\\\.|(?!\\1)[^\\\\])*)\\1`
1081
+ ));
1082
+ if (!match || match[1] === "`" && match[2].includes("${")) return null;
1083
+ if (match[1] === '"') {
1084
+ try {
1085
+ return JSON.parse(`"${match[2]}"`);
1086
+ } catch {
1087
+ return null;
1088
+ }
1089
+ }
1090
+ return match[2].replace(/\\([\\'"`])/g, "$1");
1091
+ }
1092
+ function exportedStringArray(source, exportName) {
1093
+ const declaration = new RegExp(`export\\s+const\\s+${exportName}\\s*(?::[^=]+)?=\\s*\\[`).exec(source);
1094
+ if (!declaration) return null;
1095
+ const values = [];
1096
+ const start = declaration.index + declaration[0].length;
1097
+ const end = source.indexOf("]", start);
1098
+ if (end < 0) return null;
1099
+ const contents = source.slice(start, end);
1100
+ const literal = /(['"`])([^'"`]*)\1/g;
1101
+ let cursor = 0;
1102
+ for (const match of contents.matchAll(literal)) {
1103
+ if (!/^[\s,]*$/.test(contents.slice(cursor, match.index))) return null;
1104
+ if (match[1] === "`" && match[2].includes("${")) return null;
1105
+ const decoded = exportedString(`export const value = ${match[0]}`, "value");
1106
+ if (decoded === null) return null;
1107
+ values.push(decoded);
1108
+ cursor = (match.index ?? 0) + match[0].length;
1109
+ }
1110
+ return /^[\s,]*$/.test(contents.slice(cursor)) ? values : null;
1111
+ }
1112
+
1113
+ export {
1114
+ PathSafetyError,
1115
+ resolveWithinRoot,
1116
+ readProjectManifest,
1117
+ writeProjectManifest,
1118
+ CredentialStore,
1119
+ secretValue,
1120
+ ShellProcessError,
1121
+ ShellRequestError,
1122
+ extractNodeDefinition,
1123
+ ProjectRoot
1124
+ };
1125
+ //# sourceMappingURL=chunk-2JEU3MGB.js.map