fluidcad 0.0.32 → 0.0.34

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 (279) hide show
  1. package/README.md +3 -2
  2. package/bin/commands/init.js +55 -0
  3. package/bin/commands/mcp.js +33 -0
  4. package/bin/commands/serve.js +77 -0
  5. package/bin/fluidcad.js +15 -107
  6. package/lib/dist/common/scene-object.d.ts +4 -1
  7. package/lib/dist/common/scene-object.js +9 -2
  8. package/lib/dist/common/solid.d.ts +4 -1
  9. package/lib/dist/common/solid.js +13 -0
  10. package/lib/dist/core/2d/tarc.d.ts +20 -2
  11. package/lib/dist/core/2d/tarc.js +24 -0
  12. package/lib/dist/core/index.d.ts +2 -1
  13. package/lib/dist/core/index.js +1 -0
  14. package/lib/dist/core/interfaces.d.ts +107 -2
  15. package/lib/dist/core/load.d.ts +2 -2
  16. package/lib/dist/core/repeat.js +62 -46
  17. package/lib/dist/core/rib.d.ts +18 -0
  18. package/lib/dist/core/rib.js +37 -0
  19. package/lib/dist/features/2d/arc.d.ts +8 -2
  20. package/lib/dist/features/2d/arc.js +94 -17
  21. package/lib/dist/features/2d/back.js +3 -2
  22. package/lib/dist/features/2d/sketch.d.ts +4 -0
  23. package/lib/dist/features/2d/sketch.js +21 -0
  24. package/lib/dist/features/2d/tarc-constrained.d.ts +2 -0
  25. package/lib/dist/features/2d/tarc-constrained.js +8 -0
  26. package/lib/dist/features/2d/tarc-radius-to-object.d.ts +16 -0
  27. package/lib/dist/features/2d/tarc-radius-to-object.js +58 -0
  28. package/lib/dist/features/2d/tarc-to-object.d.ts +18 -0
  29. package/lib/dist/features/2d/tarc-to-object.js +66 -0
  30. package/lib/dist/features/2d/tarc-to-point-tangent.d.ts +2 -0
  31. package/lib/dist/features/2d/tarc-to-point-tangent.js +3 -0
  32. package/lib/dist/features/2d/tarc-to-point.d.ts +2 -0
  33. package/lib/dist/features/2d/tarc-to-point.js +3 -0
  34. package/lib/dist/features/2d/tarc-with-tangent.d.ts +2 -0
  35. package/lib/dist/features/2d/tarc-with-tangent.js +3 -0
  36. package/lib/dist/features/2d/tarc.d.ts +2 -0
  37. package/lib/dist/features/2d/tarc.js +3 -0
  38. package/lib/dist/features/extrude-base.d.ts +9 -0
  39. package/lib/dist/features/extrude-base.js +22 -0
  40. package/lib/dist/features/extrude-to-face.js +1 -5
  41. package/lib/dist/features/extrude-two-distances.js +1 -2
  42. package/lib/dist/features/extrude.js +1 -2
  43. package/lib/dist/features/load.d.ts +6 -0
  44. package/lib/dist/features/load.js +53 -1
  45. package/lib/dist/features/mirror-feature.d.ts +3 -2
  46. package/lib/dist/features/mirror-feature.js +1 -1
  47. package/lib/dist/features/repeat-circular.d.ts +3 -3
  48. package/lib/dist/features/repeat-circular.js +8 -1
  49. package/lib/dist/features/repeat-linear.d.ts +4 -2
  50. package/lib/dist/features/repeat-linear.js +10 -1
  51. package/lib/dist/features/repeat-matrix.d.ts +3 -1
  52. package/lib/dist/features/repeat-matrix.js +7 -2
  53. package/lib/dist/features/rib.d.ts +31 -0
  54. package/lib/dist/features/rib.js +321 -0
  55. package/lib/dist/features/select.d.ts +1 -0
  56. package/lib/dist/features/select.js +81 -10
  57. package/lib/dist/features/shell.d.ts +4 -1
  58. package/lib/dist/features/shell.js +14 -3
  59. package/lib/dist/filters/edge/belongs-to-face.d.ts +12 -9
  60. package/lib/dist/filters/edge/belongs-to-face.js +64 -15
  61. package/lib/dist/filters/filter-builder-base.d.ts +25 -0
  62. package/lib/dist/filters/filter-builder-base.js +47 -0
  63. package/lib/dist/filters/filter.js +39 -14
  64. package/lib/dist/filters/from-object.d.ts +4 -0
  65. package/lib/dist/filters/from-object.js +10 -0
  66. package/lib/dist/helpers/clone-transform.d.ts +2 -1
  67. package/lib/dist/helpers/scene-helpers.d.ts +1 -1
  68. package/lib/dist/helpers/scene-helpers.js +146 -12
  69. package/lib/dist/index.d.ts +7 -1
  70. package/lib/dist/index.js +3 -3
  71. package/lib/dist/io/file-import.d.ts +5 -1
  72. package/lib/dist/io/file-import.js +29 -18
  73. package/lib/dist/math/lazy-matrix.d.ts +31 -0
  74. package/lib/dist/math/lazy-matrix.js +66 -0
  75. package/lib/dist/oc/color-transfer.d.ts +19 -8
  76. package/lib/dist/oc/color-transfer.js +70 -12
  77. package/lib/dist/oc/constraints/constraint-solver-adaptor.d.ts +5 -0
  78. package/lib/dist/oc/constraints/constraint-solver-adaptor.js +16 -0
  79. package/lib/dist/oc/constraints/constraint-solver.d.ts +4 -0
  80. package/lib/dist/oc/constraints/curve/curve-constraint-solver.d.ts +4 -0
  81. package/lib/dist/oc/constraints/curve/curve-constraint-solver.js +3 -0
  82. package/lib/dist/oc/constraints/geometric/geometric-constraint-solver.d.ts +6 -1
  83. package/lib/dist/oc/constraints/geometric/geometric-constraint-solver.js +4 -0
  84. package/lib/dist/oc/constraints/geometric/tangent-arc-from-point-tangent.d.ts +8 -0
  85. package/lib/dist/oc/constraints/geometric/tangent-arc-from-point-tangent.js +111 -0
  86. package/lib/dist/oc/constraints/geometric/tangent-arc-radius-to-object.d.ts +8 -0
  87. package/lib/dist/oc/constraints/geometric/tangent-arc-radius-to-object.js +161 -0
  88. package/lib/dist/oc/extrude-ops.d.ts +2 -1
  89. package/lib/dist/oc/extrude-ops.js +51 -2
  90. package/lib/dist/oc/mesh.d.ts +9 -4
  91. package/lib/dist/oc/mesh.js +14 -13
  92. package/lib/dist/oc/rib-ops.d.ts +35 -0
  93. package/lib/dist/oc/rib-ops.js +619 -0
  94. package/lib/dist/oc/shell-ops.d.ts +2 -1
  95. package/lib/dist/oc/shell-ops.js +5 -2
  96. package/lib/dist/oc/topology-index.d.ts +6 -0
  97. package/lib/dist/oc/topology-index.js +36 -0
  98. package/lib/dist/rendering/mesh-builder.d.ts +3 -0
  99. package/lib/dist/rendering/mesh-builder.js +8 -4
  100. package/lib/dist/rendering/render-edge.d.ts +2 -1
  101. package/lib/dist/rendering/render-edge.js +2 -2
  102. package/lib/dist/rendering/render-face.d.ts +2 -1
  103. package/lib/dist/rendering/render-face.js +2 -2
  104. package/lib/dist/rendering/render-solid.d.ts +2 -1
  105. package/lib/dist/rendering/render-solid.js +3 -5
  106. package/lib/dist/rendering/render-wire.d.ts +2 -1
  107. package/lib/dist/rendering/render-wire.js +2 -2
  108. package/lib/dist/rendering/render.d.ts +4 -0
  109. package/lib/dist/rendering/render.js +50 -2
  110. package/lib/dist/rendering/scene-compare.js +3 -0
  111. package/lib/dist/rendering/scene.d.ts +1 -0
  112. package/lib/dist/rendering/scene.js +4 -0
  113. package/lib/dist/scene-manager.d.ts +4 -2
  114. package/lib/dist/scene-manager.js +12 -4
  115. package/lib/dist/tests/features/2d/arc.test.js +64 -0
  116. package/lib/dist/tests/features/2d/back.test.js +17 -1
  117. package/lib/dist/tests/features/2d/tarc.test.js +157 -0
  118. package/lib/dist/tests/features/color-lineage.test.js +18 -0
  119. package/lib/dist/tests/features/filter-positional.test.d.ts +1 -0
  120. package/lib/dist/tests/features/filter-positional.test.js +129 -0
  121. package/lib/dist/tests/features/repeat-user-repro.test.d.ts +1 -0
  122. package/lib/dist/tests/features/repeat-user-repro.test.js +60 -0
  123. package/lib/dist/tests/features/rib.test.d.ts +1 -0
  124. package/lib/dist/tests/features/rib.test.js +598 -0
  125. package/lib/dist/tests/features/shell.test.js +36 -0
  126. package/lib/dist/tests/global-setup.js +2 -1
  127. package/lib/dist/tests/helpers/extract-blocks.d.ts +9 -0
  128. package/lib/dist/tests/helpers/extract-blocks.js +56 -0
  129. package/lib/dist/tests/llm-docs-examples.test.d.ts +1 -0
  130. package/lib/dist/tests/llm-docs-examples.test.js +62 -0
  131. package/lib/dist/tests/scene-compare.test.d.ts +1 -0
  132. package/lib/dist/tests/scene-compare.test.js +77 -0
  133. package/lib/dist/tests/setup.js +2 -1
  134. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  135. package/llm-docs/.coverage-allowlist.txt +9 -0
  136. package/llm-docs/api/arc.md +48 -0
  137. package/llm-docs/api/axis.md +42 -0
  138. package/llm-docs/api/bezier.md +42 -0
  139. package/llm-docs/api/booleans.md +44 -0
  140. package/llm-docs/api/chamfer.md +40 -0
  141. package/llm-docs/api/circle.md +36 -0
  142. package/llm-docs/api/color.md +34 -0
  143. package/llm-docs/api/connect.md +41 -0
  144. package/llm-docs/api/constraint-qualifiers.md +48 -0
  145. package/llm-docs/api/copy.md +63 -0
  146. package/llm-docs/api/cursor-lines.md +50 -0
  147. package/llm-docs/api/cursor-move.md +61 -0
  148. package/llm-docs/api/cut.md +55 -0
  149. package/llm-docs/api/draft.md +36 -0
  150. package/llm-docs/api/edge-filter.md +57 -0
  151. package/llm-docs/api/ellipse.md +34 -0
  152. package/llm-docs/api/extrude.md +74 -0
  153. package/llm-docs/api/face-filter.md +61 -0
  154. package/llm-docs/api/fillet.md +51 -0
  155. package/llm-docs/api/index.json +139 -0
  156. package/llm-docs/api/line.md +42 -0
  157. package/llm-docs/api/load.md +37 -0
  158. package/llm-docs/api/local.md +38 -0
  159. package/llm-docs/api/loft.md +37 -0
  160. package/llm-docs/api/mirror.md +44 -0
  161. package/llm-docs/api/offset.md +36 -0
  162. package/llm-docs/api/part.md +40 -0
  163. package/llm-docs/api/plane.md +44 -0
  164. package/llm-docs/api/polygon.md +37 -0
  165. package/llm-docs/api/primitive-solids.md +39 -0
  166. package/llm-docs/api/project-intersect.md +48 -0
  167. package/llm-docs/api/rect.md +48 -0
  168. package/llm-docs/api/remove.md +32 -0
  169. package/llm-docs/api/repeat.md +79 -0
  170. package/llm-docs/api/revolve.md +38 -0
  171. package/llm-docs/api/rib.md +40 -0
  172. package/llm-docs/api/rotate.md +37 -0
  173. package/llm-docs/api/select.md +41 -0
  174. package/llm-docs/api/shell.md +41 -0
  175. package/llm-docs/api/sketch.md +76 -0
  176. package/llm-docs/api/slot.md +36 -0
  177. package/llm-docs/api/split-trim.md +42 -0
  178. package/llm-docs/api/sweep.md +43 -0
  179. package/llm-docs/api/tarc.md +45 -0
  180. package/llm-docs/api/tcircle.md +38 -0
  181. package/llm-docs/api/tline.md +42 -0
  182. package/llm-docs/api/translate.md +40 -0
  183. package/llm-docs/api/types/aline.md +35 -0
  184. package/llm-docs/api/types/arc-angles.md +29 -0
  185. package/llm-docs/api/types/arc-points.md +48 -0
  186. package/llm-docs/api/types/axis-like.md +38 -0
  187. package/llm-docs/api/types/axis.md +21 -0
  188. package/llm-docs/api/types/boolean-operation.md +50 -0
  189. package/llm-docs/api/types/circular-repeat-options.md +31 -0
  190. package/llm-docs/api/types/common.md +32 -0
  191. package/llm-docs/api/types/cut.md +125 -0
  192. package/llm-docs/api/types/draft.md +21 -0
  193. package/llm-docs/api/types/extrudable-geometry.md +23 -0
  194. package/llm-docs/api/types/extrude.md +194 -0
  195. package/llm-docs/api/types/geometry.md +51 -0
  196. package/llm-docs/api/types/hline.md +35 -0
  197. package/llm-docs/api/types/linear-repeat-options.md +31 -0
  198. package/llm-docs/api/types/loft.md +154 -0
  199. package/llm-docs/api/types/mirror.md +35 -0
  200. package/llm-docs/api/types/offset.md +31 -0
  201. package/llm-docs/api/types/plane-like.md +35 -0
  202. package/llm-docs/api/types/plane-transform-options.md +29 -0
  203. package/llm-docs/api/types/plane.md +21 -0
  204. package/llm-docs/api/types/point-like.md +22 -0
  205. package/llm-docs/api/types/point2dlike.md +26 -0
  206. package/llm-docs/api/types/polygon.md +46 -0
  207. package/llm-docs/api/types/rect.md +128 -0
  208. package/llm-docs/api/types/revolve.md +102 -0
  209. package/llm-docs/api/types/rib.md +133 -0
  210. package/llm-docs/api/types/scene-object.md +33 -0
  211. package/llm-docs/api/types/select.md +21 -0
  212. package/llm-docs/api/types/shell.md +54 -0
  213. package/llm-docs/api/types/slot.md +43 -0
  214. package/llm-docs/api/types/sweep.md +189 -0
  215. package/llm-docs/api/types/tangent-arc-two-objects.md +46 -0
  216. package/llm-docs/api/types/transformable.md +93 -0
  217. package/llm-docs/api/types/trim.md +27 -0
  218. package/llm-docs/api/types/two-objects-tangent-line.md +46 -0
  219. package/llm-docs/api/types/vertex.md +17 -0
  220. package/llm-docs/api/types/vline.md +35 -0
  221. package/llm-docs/concepts/coordinate-system.md +45 -0
  222. package/llm-docs/concepts/history-and-rollback.md +40 -0
  223. package/llm-docs/concepts/last-selection.md +49 -0
  224. package/llm-docs/concepts/scene-graph.md +37 -0
  225. package/llm-docs/index.json +1750 -0
  226. package/mcp/dist/client.d.ts +64 -0
  227. package/mcp/dist/client.js +248 -0
  228. package/mcp/dist/discovery.d.ts +11 -0
  229. package/mcp/dist/discovery.js +78 -0
  230. package/mcp/dist/docs-index.d.ts +81 -0
  231. package/mcp/dist/docs-index.js +261 -0
  232. package/mcp/dist/resources.d.ts +4 -0
  233. package/mcp/dist/resources.js +115 -0
  234. package/mcp/dist/server.d.ts +12 -0
  235. package/mcp/dist/server.js +489 -0
  236. package/mcp/dist/tools/coordination.d.ts +9 -0
  237. package/mcp/dist/tools/coordination.js +46 -0
  238. package/mcp/dist/tools/docs.d.ts +66 -0
  239. package/mcp/dist/tools/docs.js +122 -0
  240. package/mcp/dist/tools/engine.d.ts +56 -0
  241. package/mcp/dist/tools/engine.js +145 -0
  242. package/mcp/dist/tools/inspection.d.ts +75 -0
  243. package/mcp/dist/tools/inspection.js +121 -0
  244. package/mcp/dist/tools/screenshot.d.ts +63 -0
  245. package/mcp/dist/tools/screenshot.js +263 -0
  246. package/mcp/dist/tools/source.d.ts +84 -0
  247. package/mcp/dist/tools/source.js +434 -0
  248. package/mcp/dist/tools/workspaces.d.ts +13 -0
  249. package/mcp/dist/tools/workspaces.js +33 -0
  250. package/mcp/dist/types.d.ts +18 -0
  251. package/mcp/dist/types.js +11 -0
  252. package/package.json +19 -5
  253. package/server/dist/code-editor.d.ts +36 -0
  254. package/server/dist/code-editor.js +8 -0
  255. package/server/dist/fluidcad-server.d.ts +50 -0
  256. package/server/dist/fluidcad-server.js +153 -1
  257. package/server/dist/global-registry.d.ts +30 -0
  258. package/server/dist/global-registry.js +126 -0
  259. package/server/dist/index.js +171 -26
  260. package/server/dist/instance-file.d.ts +31 -0
  261. package/server/dist/instance-file.js +73 -0
  262. package/server/dist/lint-fluid-js.d.ts +15 -0
  263. package/server/dist/lint-fluid-js.js +271 -0
  264. package/server/dist/routes/editor.d.ts +24 -0
  265. package/server/dist/routes/editor.js +44 -0
  266. package/server/dist/routes/export.d.ts +1 -1
  267. package/server/dist/routes/export.js +45 -8
  268. package/server/dist/routes/health.d.ts +7 -0
  269. package/server/dist/routes/health.js +14 -0
  270. package/server/dist/routes/lint.d.ts +10 -0
  271. package/server/dist/routes/lint.js +28 -0
  272. package/server/dist/routes/render.d.ts +33 -0
  273. package/server/dist/routes/render.js +34 -0
  274. package/server/dist/routes/scene.d.ts +5 -0
  275. package/server/dist/routes/scene.js +48 -0
  276. package/server/dist/routes/screenshot.js +68 -1
  277. package/server/dist/ws-protocol.d.ts +56 -2
  278. package/ui/dist/assets/{index-DMw0OYCF.js → index-BdqrMDRu.js} +30 -30
  279. package/ui/dist/index.html +1 -1
@@ -1,4 +1,5 @@
1
1
  import crypto from 'crypto';
2
+ import fs from 'fs';
2
3
  import http from 'http';
3
4
  import path from 'path';
4
5
  import express from 'express';
@@ -9,11 +10,34 @@ import { createActionsRouter } from "./routes/actions.js";
9
10
  import { createExportRouter } from "./routes/export.js";
10
11
  import { createScreenshotRouter } from "./routes/screenshot.js";
11
12
  import { createPreferencesRouter } from "./routes/preferences.js";
13
+ import { createHealthRouter } from "./routes/health.js";
14
+ import { createSceneRouter } from "./routes/scene.js";
15
+ import { createEditorRouter, DirtyBufferState } from "./routes/editor.js";
16
+ import { createRenderRouter } from "./routes/render.js";
17
+ import { createLintRouter } from "./routes/lint.js";
12
18
  import { normalizePath } from "./normalize-path.js";
19
+ import { writeInstanceFile, deleteInstanceFile } from "./instance-file.js";
20
+ import { addInstance, removeInstance } from "./global-registry.js";
13
21
  import { extractSourceLocation } from '../../lib/dist/index.js';
14
22
  const PORT = parseInt(process.env.FLUIDCAD_SERVER_PORT || '3100', 10);
15
23
  const WORKSPACE_PATH = normalizePath(process.env.FLUIDCAD_WORKSPACE_PATH || '');
16
24
  const UI_DIST = path.resolve(import.meta.dirname, '../../ui/dist');
25
+ function readPackageVersion() {
26
+ try {
27
+ const pkgPath = path.resolve(import.meta.dirname, '../../package.json');
28
+ const raw = fs.readFileSync(pkgPath, 'utf8');
29
+ const parsed = JSON.parse(raw);
30
+ if (typeof parsed.version === 'string') {
31
+ return parsed.version;
32
+ }
33
+ }
34
+ catch {
35
+ // Fall through to unknown.
36
+ }
37
+ return '0.0.0';
38
+ }
39
+ const PACKAGE_VERSION = readPackageVersion();
40
+ const STARTED_AT = new Date().toISOString();
17
41
  // ---------------------------------------------------------------------------
18
42
  // IPC helpers — communication with extension host process
19
43
  // ---------------------------------------------------------------------------
@@ -26,13 +50,23 @@ function sendToExtension(msg) {
26
50
  // Express app
27
51
  // ---------------------------------------------------------------------------
28
52
  const fluidCadServer = new FluidCadServer();
53
+ const dirtyBufferState = new DirtyBufferState();
29
54
  const app = express();
30
55
  app.use(express.json({ limit: '50mb' }));
56
+ app.use('/api', createHealthRouter({
57
+ version: PACKAGE_VERSION,
58
+ workspacePath: WORKSPACE_PATH,
59
+ startedAt: STARTED_AT,
60
+ }));
31
61
  app.use('/api', createPropertiesRouter(fluidCadServer));
32
62
  app.use('/api', createActionsRouter(fluidCadServer, sendToExtension, broadcastToUI, WORKSPACE_PATH));
33
- app.use('/api', createExportRouter(fluidCadServer));
63
+ app.use('/api', createExportRouter(fluidCadServer, WORKSPACE_PATH));
34
64
  app.use('/api', createScreenshotRouter(requestScreenshot));
35
65
  app.use('/api', createPreferencesRouter());
66
+ app.use('/api', createSceneRouter(fluidCadServer, () => lastCameraState));
67
+ app.use('/api', createEditorRouter(dirtyBufferState));
68
+ app.use('/api', createRenderRouter((fileName, code) => runLiveRender(fileName, code)));
69
+ app.use('/api', createLintRouter());
36
70
  // Static files — serve UI build, with SPA fallback
37
71
  app.use(express.static(UI_DIST, {
38
72
  setHeaders(res, filePath) {
@@ -53,6 +87,7 @@ const wss = new WebSocketServer({ server: httpServer });
53
87
  const uiClients = new Set();
54
88
  let lastSceneMessage = null;
55
89
  let initCompleteMessage = null;
90
+ let lastCameraState = null;
56
91
  function broadcastToUI(msg) {
57
92
  const data = JSON.stringify(msg);
58
93
  if (msg.type === 'scene-rendered') {
@@ -117,6 +152,21 @@ function handleUIMessage(raw) {
117
152
  else {
118
153
  pending.reject(new Error(msg.error || 'Screenshot failed.'));
119
154
  }
155
+ return;
156
+ }
157
+ if (msg.type === 'camera-state') {
158
+ // Trust UI structure — we own both ends. Just shape-check arrays.
159
+ if (Array.isArray(msg.position) && msg.position.length === 3 &&
160
+ Array.isArray(msg.target) && msg.target.length === 3 &&
161
+ Array.isArray(msg.up) && msg.up.length === 3) {
162
+ lastCameraState = {
163
+ type: 'camera-state',
164
+ position: msg.position,
165
+ target: msg.target,
166
+ up: msg.up,
167
+ projection: msg.projection === 'perspective' ? 'perspective' : 'orthographic',
168
+ };
169
+ }
120
170
  }
121
171
  }
122
172
  // ---------------------------------------------------------------------------
@@ -144,8 +194,9 @@ wss.on('connection', (ws) => {
144
194
  let currentFile = null;
145
195
  let renderVersion = 0;
146
196
  const lastSceneByFile = new Map();
147
- function emitSuccess(absPath, result, rollbackStop, breakpointHit) {
197
+ function emitSuccess(version, absPath, result, rollbackStop, breakpointHit) {
148
198
  lastSceneByFile.set(absPath, { result, rollbackStop });
199
+ fluidCadServer.setCompileError(null);
149
200
  sendToExtension({
150
201
  type: 'scene-rendered',
151
202
  absPath,
@@ -159,6 +210,7 @@ function emitSuccess(absPath, result, rollbackStop, breakpointHit) {
159
210
  rollbackStop,
160
211
  breakpointHit,
161
212
  });
213
+ broadcastToUI({ type: 'render-version', version, state: 'end', absPath });
162
214
  }
163
215
  function buildCompileError(filePath, err) {
164
216
  const message = err?.message || String(err);
@@ -178,12 +230,13 @@ function buildCompileError(filePath, err) {
178
230
  sourceLocation: sourceLocation ?? undefined,
179
231
  };
180
232
  }
181
- function emitCompileError(filePath, err) {
233
+ function emitCompileError(version, filePath, err) {
182
234
  const compileError = buildCompileError(filePath, err);
183
235
  const key = compileError.filePath ?? normalizePath(filePath).replace('virtual:live-render:', '');
184
236
  const prev = lastSceneByFile.get(key);
185
237
  const result = prev?.result ?? [];
186
238
  const rollbackStop = prev?.rollbackStop ?? -1;
239
+ fluidCadServer.setCompileError(compileError);
187
240
  sendToExtension({
188
241
  type: 'scene-rendered',
189
242
  absPath: key,
@@ -198,12 +251,59 @@ function emitCompileError(filePath, err) {
198
251
  rollbackStop,
199
252
  compileError,
200
253
  });
254
+ broadcastToUI({ type: 'render-version', version, state: 'error', absPath: key });
255
+ return compileError;
256
+ }
257
+ /**
258
+ * Render-orchestration chokepoint shared by the IPC `live-update` handler and
259
+ * the HTTP `/api/render` route. Bumps `renderVersion`, broadcasts the
260
+ * lifecycle pings, runs the dedupable `updateLiveCode`, and emits success /
261
+ * compile-error to the UI + extension. Returns a structured outcome so the
262
+ * HTTP caller (MCP) can hand it straight to the agent.
263
+ */
264
+ async function runLiveRender(fileName, code) {
265
+ const startedAt = Date.now();
266
+ const myVersion = ++renderVersion;
267
+ broadcastToUI({ type: 'render-version', version: myVersion, state: 'start' });
268
+ if (fileName !== currentFile) {
269
+ broadcastToUI({ type: 'processing-file' });
270
+ currentFile = fileName;
271
+ }
272
+ try {
273
+ const data = await fluidCadServer.updateLiveCode(fileName, code);
274
+ if (myVersion !== renderVersion) {
275
+ return { state: 'superseded', version: myVersion, durationMs: Date.now() - startedAt };
276
+ }
277
+ if (!data) {
278
+ return { state: 'no-scene-manager', version: myVersion, durationMs: Date.now() - startedAt };
279
+ }
280
+ emitSuccess(myVersion, data.absPath, data.result, data.rollbackStop, data.breakpointHit);
281
+ return {
282
+ state: 'rendered',
283
+ version: myVersion,
284
+ absPath: data.absPath,
285
+ durationMs: Date.now() - startedAt,
286
+ };
287
+ }
288
+ catch (err) {
289
+ if (myVersion !== renderVersion) {
290
+ return { state: 'superseded', version: myVersion, durationMs: Date.now() - startedAt };
291
+ }
292
+ const compileError = emitCompileError(myVersion, fileName, err);
293
+ return {
294
+ state: 'compile-error',
295
+ version: myVersion,
296
+ durationMs: Date.now() - startedAt,
297
+ compileError,
298
+ };
299
+ }
201
300
  }
202
301
  async function handleExtensionMessage(msg) {
203
302
  try {
204
303
  switch (msg.type) {
205
304
  case 'process-file': {
206
305
  const myVersion = ++renderVersion;
306
+ broadcastToUI({ type: 'render-version', version: myVersion, state: 'start' });
207
307
  broadcastToUI({ type: 'processing-file' });
208
308
  currentFile = msg.filePath;
209
309
  try {
@@ -212,48 +312,30 @@ async function handleExtensionMessage(msg) {
212
312
  return;
213
313
  }
214
314
  if (data) {
215
- emitSuccess(data.absPath, data.result, data.rollbackStop, data.breakpointHit);
315
+ emitSuccess(myVersion, data.absPath, data.result, data.rollbackStop, data.breakpointHit);
216
316
  }
217
317
  }
218
318
  catch (err) {
219
319
  if (myVersion !== renderVersion) {
220
320
  return;
221
321
  }
222
- emitCompileError(msg.filePath, err);
322
+ emitCompileError(myVersion, msg.filePath, err);
223
323
  }
224
324
  break;
225
325
  }
226
326
  case 'live-update': {
227
- const myVersion = ++renderVersion;
228
- if (msg.fileName !== currentFile) {
229
- broadcastToUI({ type: 'processing-file' });
230
- currentFile = msg.fileName;
231
- }
232
- try {
233
- const data = await fluidCadServer.updateLiveCode(msg.fileName, msg.code);
234
- if (myVersion !== renderVersion) {
235
- return;
236
- }
237
- if (data) {
238
- emitSuccess(data.absPath, data.result, data.rollbackStop, data.breakpointHit);
239
- }
240
- }
241
- catch (err) {
242
- if (myVersion !== renderVersion) {
243
- return;
244
- }
245
- emitCompileError(msg.fileName, err);
246
- }
327
+ await runLiveRender(msg.fileName, msg.code);
247
328
  break;
248
329
  }
249
330
  case 'rollback': {
250
331
  const myVersion = ++renderVersion;
332
+ broadcastToUI({ type: 'render-version', version: myVersion, state: 'start' });
251
333
  const data = await fluidCadServer.rollback(msg.fileName, msg.index);
252
334
  if (myVersion !== renderVersion) {
253
335
  return;
254
336
  }
255
337
  if (data) {
256
- emitSuccess(data.absPath, data.result, data.rollbackStop);
338
+ emitSuccess(myVersion, data.absPath, data.result, data.rollbackStop);
257
339
  }
258
340
  break;
259
341
  }
@@ -279,6 +361,13 @@ async function handleExtensionMessage(msg) {
279
361
  broadcastToUI({ type: 'show-shape-properties', shapeId: msg.shapeId });
280
362
  break;
281
363
  }
364
+ case 'editor-dirty-state': {
365
+ if (Array.isArray(msg.dirtyFiles)) {
366
+ const paths = msg.dirtyFiles.filter((p) => typeof p === 'string');
367
+ dirtyBufferState.setDirtyFiles(paths);
368
+ }
369
+ break;
370
+ }
282
371
  case 'export-scene': {
283
372
  try {
284
373
  const result = fluidCadServer.exportShapes(msg.shapeIds, msg.options);
@@ -321,6 +410,36 @@ process.on('message', (msg) => {
321
410
  httpServer.listen(PORT, () => {
322
411
  const url = `http://localhost:${PORT}`;
323
412
  console.log(`FluidCAD server listening on ${url}`);
413
+ // Publish this instance so a standalone MCP process can discover us.
414
+ // Discovery is best-effort: an MCP-less workflow must keep working even if
415
+ // we can't write the file (read-only FS, permissions, …).
416
+ if (WORKSPACE_PATH) {
417
+ try {
418
+ writeInstanceFile({
419
+ schemaVersion: 1,
420
+ port: PORT,
421
+ pid: process.pid,
422
+ workspacePath: WORKSPACE_PATH,
423
+ version: PACKAGE_VERSION,
424
+ startedAt: STARTED_AT,
425
+ });
426
+ }
427
+ catch (err) {
428
+ console.warn(`Failed to write instance file: ${err?.message ?? err}`);
429
+ }
430
+ try {
431
+ addInstance({
432
+ workspacePath: WORKSPACE_PATH,
433
+ port: PORT,
434
+ pid: process.pid,
435
+ version: PACKAGE_VERSION,
436
+ startedAt: STARTED_AT,
437
+ });
438
+ }
439
+ catch (err) {
440
+ console.warn(`Failed to update global registry: ${err?.message ?? err}`);
441
+ }
442
+ }
324
443
  // Signal ready immediately so extension can show the webview
325
444
  sendToExtension({ type: 'ready', port: PORT, url });
326
445
  // Initialize FluidCAD server in the background
@@ -333,3 +452,29 @@ httpServer.listen(PORT, () => {
333
452
  broadcastToUI({ type: 'init-complete', success: false, error });
334
453
  });
335
454
  });
455
+ // ---------------------------------------------------------------------------
456
+ // Shutdown — clean up the instance file and registry entry
457
+ // ---------------------------------------------------------------------------
458
+ let cleanedUp = false;
459
+ function cleanupDiscovery() {
460
+ if (cleanedUp || !WORKSPACE_PATH) {
461
+ return;
462
+ }
463
+ cleanedUp = true;
464
+ deleteInstanceFile(WORKSPACE_PATH, process.pid);
465
+ try {
466
+ removeInstance(WORKSPACE_PATH, process.pid);
467
+ }
468
+ catch {
469
+ // Registry cleanup is best-effort; stale entries are pruned by readers.
470
+ }
471
+ }
472
+ process.on('exit', cleanupDiscovery);
473
+ process.on('SIGINT', () => {
474
+ cleanupDiscovery();
475
+ process.exit(0);
476
+ });
477
+ process.on('SIGTERM', () => {
478
+ cleanupDiscovery();
479
+ process.exit(0);
480
+ });
@@ -0,0 +1,31 @@
1
+ export declare const INSTANCE_DIR_NAME = ".fluidcad";
2
+ export declare const INSTANCE_FILE_NAME = "instance.json";
3
+ export type InstanceFile = {
4
+ /** Schema version; bump when changing the shape. */
5
+ schemaVersion: 1;
6
+ /** Port the HTTP+WS server is listening on. */
7
+ port: number;
8
+ /** PID of the server process — used by MCP for liveness probes. */
9
+ pid: number;
10
+ /** Absolute path to the workspace this instance serves. */
11
+ workspacePath: string;
12
+ /** FluidCAD package version. */
13
+ version: string;
14
+ /** ISO 8601 timestamp captured when the server began listening. */
15
+ startedAt: string;
16
+ };
17
+ export declare function instanceFilePath(workspacePath: string): string;
18
+ /**
19
+ * Atomically write the instance file. Uses a PID-suffixed tmp file plus
20
+ * `renameSync` so a concurrent reader never observes a half-written file.
21
+ */
22
+ export declare function writeInstanceFile(entry: InstanceFile): void;
23
+ /**
24
+ * Best-effort delete; never throws. Called from SIGINT/SIGTERM and `exit`
25
+ * handlers, where we cannot afford to interrupt shutdown over an I/O error.
26
+ *
27
+ * `expectedPid` guards against deleting another process's file on race —
28
+ * e.g., a second window started for the same workspace after a crash.
29
+ */
30
+ export declare function deleteInstanceFile(workspacePath: string, expectedPid: number): void;
31
+ export declare function readInstanceFile(workspacePath: string): InstanceFile | null;
@@ -0,0 +1,73 @@
1
+ // Per-workspace instance discovery file at <workspace>/.fluidcad/instance.json.
2
+ //
3
+ // Written when the HTTP server is listening, deleted on graceful shutdown. A
4
+ // standalone MCP process reads this file to learn the workspace's random port
5
+ // without scanning. The on-disk format is intentionally small and stable.
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ export const INSTANCE_DIR_NAME = '.fluidcad';
9
+ export const INSTANCE_FILE_NAME = 'instance.json';
10
+ function instanceDir(workspacePath) {
11
+ return path.join(workspacePath, INSTANCE_DIR_NAME);
12
+ }
13
+ export function instanceFilePath(workspacePath) {
14
+ return path.join(instanceDir(workspacePath), INSTANCE_FILE_NAME);
15
+ }
16
+ /**
17
+ * Atomically write the instance file. Uses a PID-suffixed tmp file plus
18
+ * `renameSync` so a concurrent reader never observes a half-written file.
19
+ */
20
+ export function writeInstanceFile(entry) {
21
+ const dir = instanceDir(entry.workspacePath);
22
+ fs.mkdirSync(dir, { recursive: true });
23
+ const destination = path.join(dir, INSTANCE_FILE_NAME);
24
+ const tmp = path.join(dir, `${INSTANCE_FILE_NAME}.${entry.pid}.tmp`);
25
+ const payload = JSON.stringify(entry, null, 2) + '\n';
26
+ fs.writeFileSync(tmp, payload, { encoding: 'utf8', mode: 0o644 });
27
+ fs.renameSync(tmp, destination);
28
+ }
29
+ /**
30
+ * Best-effort delete; never throws. Called from SIGINT/SIGTERM and `exit`
31
+ * handlers, where we cannot afford to interrupt shutdown over an I/O error.
32
+ *
33
+ * `expectedPid` guards against deleting another process's file on race —
34
+ * e.g., a second window started for the same workspace after a crash.
35
+ */
36
+ export function deleteInstanceFile(workspacePath, expectedPid) {
37
+ const file = instanceFilePath(workspacePath);
38
+ try {
39
+ const existing = readInstanceFile(workspacePath);
40
+ if (existing && existing.pid !== expectedPid) {
41
+ return;
42
+ }
43
+ fs.unlinkSync(file);
44
+ }
45
+ catch {
46
+ // File missing, permissions, partial write — nothing useful to do during shutdown.
47
+ }
48
+ }
49
+ export function readInstanceFile(workspacePath) {
50
+ try {
51
+ const raw = fs.readFileSync(instanceFilePath(workspacePath), 'utf8');
52
+ const parsed = JSON.parse(raw);
53
+ if (!isInstanceFile(parsed)) {
54
+ return null;
55
+ }
56
+ return parsed;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ function isInstanceFile(value) {
63
+ if (typeof value !== 'object' || value === null) {
64
+ return false;
65
+ }
66
+ const v = value;
67
+ return (v.schemaVersion === 1 &&
68
+ typeof v.port === 'number' &&
69
+ typeof v.pid === 'number' &&
70
+ typeof v.workspacePath === 'string' &&
71
+ typeof v.version === 'string' &&
72
+ typeof v.startedAt === 'string');
73
+ }
@@ -0,0 +1,15 @@
1
+ export type MissingImport = {
2
+ symbol: string;
3
+ module: string;
4
+ /** Zero-based line where the unbound use first appears. */
5
+ line: number;
6
+ /** Zero-based UTF-16 column where the unbound use first appears. */
7
+ column: number;
8
+ };
9
+ export type LintFluidJsResult = {
10
+ /** Free-variable usages of known symbols that lack an import. */
11
+ missing: MissingImport[];
12
+ /** A copy-pasteable block of suggested import statements, grouped by module. */
13
+ suggestion: string;
14
+ };
15
+ export declare function lintFluidJs(code: string): Promise<LintFluidJsResult>;