fluidcad 0.0.33 → 0.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -2
- package/bin/commands/init.js +55 -0
- package/bin/commands/login.js +120 -0
- package/bin/commands/mcp.js +33 -0
- package/bin/commands/pack.js +49 -0
- package/bin/commands/publish.js +136 -0
- package/bin/commands/serve.js +77 -0
- package/bin/fluidcad.js +21 -107
- package/bin/lib/api-client.js +40 -0
- package/bin/lib/browser.js +16 -0
- package/bin/lib/config.js +39 -0
- package/bin/lib/model-config.js +38 -0
- package/bin/lib/workspace.js +57 -0
- package/lib/dist/common/scene-object.d.ts +2 -1
- package/lib/dist/common/scene-object.js +3 -2
- package/lib/dist/common/shape-factory.d.ts +2 -1
- package/lib/dist/common/shape-factory.js +4 -0
- package/lib/dist/common/transformable-primitive.d.ts +6 -5
- package/lib/dist/common/transformable-primitive.js +8 -7
- package/lib/dist/common/vertex.js +0 -1
- package/lib/dist/core/2d/aline.d.ts +4 -3
- package/lib/dist/core/2d/aline.js +3 -2
- package/lib/dist/core/2d/arc.d.ts +3 -2
- package/lib/dist/core/2d/arc.js +4 -3
- package/lib/dist/core/2d/bezier.d.ts +8 -6
- package/lib/dist/core/2d/circle.d.ts +4 -3
- package/lib/dist/core/2d/circle.js +3 -2
- package/lib/dist/core/2d/ellipse.d.ts +5 -4
- package/lib/dist/core/2d/ellipse.js +5 -4
- package/lib/dist/core/2d/hline.d.ts +4 -3
- package/lib/dist/core/2d/hline.js +5 -3
- package/lib/dist/core/2d/line.js +1 -0
- package/lib/dist/core/2d/offset.d.ts +3 -2
- package/lib/dist/core/2d/offset.js +6 -5
- package/lib/dist/core/2d/polygon.d.ts +5 -4
- package/lib/dist/core/2d/polygon.js +10 -9
- package/lib/dist/core/2d/rect.d.ts +4 -3
- package/lib/dist/core/2d/rect.js +10 -9
- package/lib/dist/core/2d/slot.d.ts +14 -6
- package/lib/dist/core/2d/slot.js +19 -8
- package/lib/dist/core/2d/tarc.d.ts +20 -2
- package/lib/dist/core/2d/tarc.js +24 -0
- package/lib/dist/core/2d/vline.d.ts +4 -3
- package/lib/dist/core/2d/vline.js +5 -3
- package/lib/dist/core/chamfer.d.ts +5 -4
- package/lib/dist/core/chamfer.js +7 -6
- package/lib/dist/core/color.d.ts +3 -2
- package/lib/dist/core/color.js +2 -1
- package/lib/dist/core/cut.d.ts +4 -3
- package/lib/dist/core/cut.js +5 -4
- package/lib/dist/core/cylinder.d.ts +2 -1
- package/lib/dist/core/cylinder.js +2 -1
- package/lib/dist/core/draft.d.ts +3 -2
- package/lib/dist/core/draft.js +3 -2
- package/lib/dist/core/extrude.d.ts +4 -3
- package/lib/dist/core/extrude.js +5 -4
- package/lib/dist/core/fillet.d.ts +5 -4
- package/lib/dist/core/fillet.js +6 -5
- package/lib/dist/core/index.d.ts +1 -0
- package/lib/dist/core/index.js +1 -0
- package/lib/dist/core/interfaces.d.ts +55 -25
- package/lib/dist/core/param.d.ts +74 -0
- package/lib/dist/core/param.js +147 -0
- package/lib/dist/core/repeat.d.ts +2 -1
- package/lib/dist/core/repeat.js +72 -54
- package/lib/dist/core/revolve.d.ts +2 -1
- package/lib/dist/core/revolve.js +3 -2
- package/lib/dist/core/rib.d.ts +3 -2
- package/lib/dist/core/rib.js +6 -2
- package/lib/dist/core/rotate.d.ts +5 -4
- package/lib/dist/core/rotate.js +4 -3
- package/lib/dist/core/shell.d.ts +3 -2
- package/lib/dist/core/shell.js +3 -2
- package/lib/dist/core/sphere.d.ts +3 -2
- package/lib/dist/core/sphere.js +2 -1
- package/lib/dist/core/translate.d.ts +7 -6
- package/lib/dist/core/translate.js +6 -5
- package/lib/dist/features/2d/arc.d.ts +8 -2
- package/lib/dist/features/2d/arc.js +94 -17
- package/lib/dist/features/2d/back.js +3 -2
- package/lib/dist/features/2d/bezier.js +16 -16
- package/lib/dist/features/2d/circle.js +4 -0
- package/lib/dist/features/2d/ellipse.js +4 -0
- package/lib/dist/features/2d/hline.d.ts +3 -0
- package/lib/dist/features/2d/hline.js +9 -2
- package/lib/dist/features/2d/line.d.ts +3 -0
- package/lib/dist/features/2d/line.js +11 -3
- package/lib/dist/features/2d/sketch.d.ts +4 -0
- package/lib/dist/features/2d/sketch.js +25 -0
- package/lib/dist/features/2d/slot.d.ts +5 -0
- package/lib/dist/features/2d/slot.js +52 -7
- package/lib/dist/features/2d/tarc-constrained.d.ts +2 -0
- package/lib/dist/features/2d/tarc-constrained.js +8 -0
- package/lib/dist/features/2d/tarc-radius-to-object.d.ts +16 -0
- package/lib/dist/features/2d/tarc-radius-to-object.js +58 -0
- package/lib/dist/features/2d/tarc-to-object.d.ts +18 -0
- package/lib/dist/features/2d/tarc-to-object.js +66 -0
- package/lib/dist/features/2d/tarc-to-point-tangent.d.ts +2 -0
- package/lib/dist/features/2d/tarc-to-point-tangent.js +6 -0
- package/lib/dist/features/2d/tarc-to-point.d.ts +2 -0
- package/lib/dist/features/2d/tarc-to-point.js +6 -0
- package/lib/dist/features/2d/tarc-with-tangent.d.ts +2 -0
- package/lib/dist/features/2d/tarc-with-tangent.js +6 -0
- package/lib/dist/features/2d/tarc.d.ts +2 -0
- package/lib/dist/features/2d/tarc.js +6 -0
- package/lib/dist/features/2d/vline.d.ts +3 -0
- package/lib/dist/features/2d/vline.js +9 -2
- package/lib/dist/features/copy-circular.d.ts +4 -3
- package/lib/dist/features/copy-circular.js +16 -9
- package/lib/dist/features/copy-circular2d.js +16 -9
- package/lib/dist/features/copy-linear.d.ts +4 -3
- package/lib/dist/features/copy-linear.js +18 -12
- package/lib/dist/features/copy-linear2d.js +18 -12
- package/lib/dist/features/extrude-base.d.ts +13 -3
- package/lib/dist/features/extrude-base.js +32 -3
- package/lib/dist/features/extrude-to-face.js +1 -5
- package/lib/dist/features/extrude-two-distances.js +1 -2
- package/lib/dist/features/extrude.js +1 -2
- package/lib/dist/features/mirror-feature.d.ts +3 -2
- package/lib/dist/features/mirror-feature.js +1 -1
- package/lib/dist/features/mirror-shape2d.js +2 -2
- package/lib/dist/features/repeat-base.d.ts +13 -0
- package/lib/dist/features/repeat-base.js +21 -0
- package/lib/dist/features/repeat-circular.d.ts +8 -7
- package/lib/dist/features/repeat-circular.js +7 -3
- package/lib/dist/features/repeat-linear.d.ts +9 -7
- package/lib/dist/features/repeat-linear.js +9 -3
- package/lib/dist/features/repeat-matrix.d.ts +3 -1
- package/lib/dist/features/repeat-matrix.js +7 -2
- package/lib/dist/features/shell.d.ts +4 -1
- package/lib/dist/features/shell.js +14 -3
- package/lib/dist/helpers/clone-transform.d.ts +2 -1
- package/lib/dist/index.d.ts +12 -1
- package/lib/dist/index.js +11 -4
- package/lib/dist/io/file-import.d.ts +7 -0
- package/lib/dist/io/file-import.js +30 -10
- package/lib/dist/math/lazy-matrix.d.ts +36 -0
- package/lib/dist/math/lazy-matrix.js +134 -0
- package/lib/dist/oc/boolean-ops.d.ts +2 -2
- package/lib/dist/oc/constraints/constraint-solver-adaptor.d.ts +5 -0
- package/lib/dist/oc/constraints/constraint-solver-adaptor.js +16 -0
- package/lib/dist/oc/constraints/constraint-solver.d.ts +4 -0
- package/lib/dist/oc/constraints/curve/curve-constraint-solver.d.ts +4 -0
- package/lib/dist/oc/constraints/curve/curve-constraint-solver.js +3 -0
- package/lib/dist/oc/constraints/geometric/geometric-constraint-solver.d.ts +6 -1
- package/lib/dist/oc/constraints/geometric/geometric-constraint-solver.js +4 -0
- package/lib/dist/oc/constraints/geometric/tangent-arc-from-point-tangent.d.ts +8 -0
- package/lib/dist/oc/constraints/geometric/tangent-arc-from-point-tangent.js +111 -0
- package/lib/dist/oc/constraints/geometric/tangent-arc-radius-to-object.d.ts +8 -0
- package/lib/dist/oc/constraints/geometric/tangent-arc-radius-to-object.js +161 -0
- package/lib/dist/oc/mesh.d.ts +9 -4
- package/lib/dist/oc/mesh.js +14 -13
- package/lib/dist/oc/shell-ops.d.ts +2 -1
- package/lib/dist/oc/shell-ops.js +5 -2
- package/lib/dist/param-registry.d.ts +34 -0
- package/lib/dist/param-registry.js +60 -0
- package/lib/dist/rendering/mesh-builder.d.ts +3 -0
- package/lib/dist/rendering/mesh-builder.js +10 -5
- package/lib/dist/rendering/render-edge.d.ts +2 -1
- package/lib/dist/rendering/render-edge.js +2 -2
- package/lib/dist/rendering/render-face.d.ts +2 -1
- package/lib/dist/rendering/render-face.js +2 -2
- package/lib/dist/rendering/render-solid.d.ts +2 -1
- package/lib/dist/rendering/render-solid.js +2 -2
- package/lib/dist/rendering/render-wire.d.ts +2 -1
- package/lib/dist/rendering/render-wire.js +2 -2
- package/lib/dist/rendering/render.d.ts +3 -0
- package/lib/dist/rendering/render.js +7 -2
- package/lib/dist/scene-manager.d.ts +4 -2
- package/lib/dist/scene-manager.js +12 -4
- package/lib/dist/tests/features/2d/arc.test.js +64 -0
- package/lib/dist/tests/features/2d/back.test.js +17 -1
- package/lib/dist/tests/features/2d/tarc.test.js +157 -0
- package/lib/dist/tests/features/copy-circular.test.js +1 -1
- package/lib/dist/tests/features/copy-linear.test.js +10 -10
- package/lib/dist/tests/features/repeat-user-repro-cache.test.d.ts +1 -0
- package/lib/dist/tests/features/repeat-user-repro-cache.test.js +97 -0
- package/lib/dist/tests/features/repeat-user-repro.test.d.ts +1 -0
- package/lib/dist/tests/features/repeat-user-repro.test.js +60 -0
- package/lib/dist/tests/features/shell.test.js +36 -0
- package/lib/dist/tests/global-setup.js +2 -1
- package/lib/dist/tests/helpers/extract-blocks.d.ts +9 -0
- package/lib/dist/tests/helpers/extract-blocks.js +56 -0
- package/lib/dist/tests/llm-docs-examples.test.d.ts +1 -0
- package/lib/dist/tests/llm-docs-examples.test.js +62 -0
- package/lib/dist/tests/setup.js +2 -1
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/llm-docs/.coverage-allowlist.txt +9 -0
- package/llm-docs/api/arc.md +48 -0
- package/llm-docs/api/axis.md +42 -0
- package/llm-docs/api/bezier.md +41 -0
- package/llm-docs/api/booleans.md +44 -0
- package/llm-docs/api/chamfer.md +40 -0
- package/llm-docs/api/circle.md +36 -0
- package/llm-docs/api/color.md +34 -0
- package/llm-docs/api/connect.md +41 -0
- package/llm-docs/api/constraint-qualifiers.md +48 -0
- package/llm-docs/api/copy.md +63 -0
- package/llm-docs/api/cursor-lines.md +50 -0
- package/llm-docs/api/cursor-move.md +61 -0
- package/llm-docs/api/cut.md +55 -0
- package/llm-docs/api/draft.md +36 -0
- package/llm-docs/api/edge-filter.md +57 -0
- package/llm-docs/api/ellipse.md +34 -0
- package/llm-docs/api/extrude.md +74 -0
- package/llm-docs/api/face-filter.md +61 -0
- package/llm-docs/api/fillet.md +51 -0
- package/llm-docs/api/index.json +139 -0
- package/llm-docs/api/line.md +42 -0
- package/llm-docs/api/load.md +37 -0
- package/llm-docs/api/local.md +38 -0
- package/llm-docs/api/loft.md +37 -0
- package/llm-docs/api/mirror.md +44 -0
- package/llm-docs/api/offset.md +36 -0
- package/llm-docs/api/part.md +40 -0
- package/llm-docs/api/plane.md +44 -0
- package/llm-docs/api/polygon.md +37 -0
- package/llm-docs/api/primitive-solids.md +39 -0
- package/llm-docs/api/project-intersect.md +48 -0
- package/llm-docs/api/rect.md +48 -0
- package/llm-docs/api/remove.md +32 -0
- package/llm-docs/api/repeat.md +79 -0
- package/llm-docs/api/revolve.md +38 -0
- package/llm-docs/api/rib.md +40 -0
- package/llm-docs/api/rotate.md +37 -0
- package/llm-docs/api/select.md +41 -0
- package/llm-docs/api/shell.md +41 -0
- package/llm-docs/api/sketch.md +76 -0
- package/llm-docs/api/slot.md +36 -0
- package/llm-docs/api/split-trim.md +42 -0
- package/llm-docs/api/sweep.md +43 -0
- package/llm-docs/api/tarc.md +45 -0
- package/llm-docs/api/tcircle.md +38 -0
- package/llm-docs/api/tline.md +42 -0
- package/llm-docs/api/translate.md +40 -0
- package/llm-docs/api/types/aline.md +35 -0
- package/llm-docs/api/types/arc-angles.md +29 -0
- package/llm-docs/api/types/arc-points.md +48 -0
- package/llm-docs/api/types/axis-like.md +38 -0
- package/llm-docs/api/types/axis.md +21 -0
- package/llm-docs/api/types/boolean-operation.md +50 -0
- package/llm-docs/api/types/circular-repeat-options.md +31 -0
- package/llm-docs/api/types/common.md +32 -0
- package/llm-docs/api/types/cut.md +125 -0
- package/llm-docs/api/types/draft.md +21 -0
- package/llm-docs/api/types/extrudable-geometry.md +23 -0
- package/llm-docs/api/types/extrude.md +194 -0
- package/llm-docs/api/types/geometry.md +51 -0
- package/llm-docs/api/types/hline.md +35 -0
- package/llm-docs/api/types/linear-repeat-options.md +31 -0
- package/llm-docs/api/types/loft.md +154 -0
- package/llm-docs/api/types/mirror.md +35 -0
- package/llm-docs/api/types/offset.md +31 -0
- package/llm-docs/api/types/plane-like.md +35 -0
- package/llm-docs/api/types/plane-transform-options.md +29 -0
- package/llm-docs/api/types/plane.md +21 -0
- package/llm-docs/api/types/point-like.md +22 -0
- package/llm-docs/api/types/point2dlike.md +26 -0
- package/llm-docs/api/types/polygon.md +46 -0
- package/llm-docs/api/types/rect.md +128 -0
- package/llm-docs/api/types/revolve.md +102 -0
- package/llm-docs/api/types/rib.md +133 -0
- package/llm-docs/api/types/scene-object.md +33 -0
- package/llm-docs/api/types/select.md +21 -0
- package/llm-docs/api/types/shell.md +54 -0
- package/llm-docs/api/types/slot.md +43 -0
- package/llm-docs/api/types/sweep.md +189 -0
- package/llm-docs/api/types/tangent-arc-two-objects.md +46 -0
- package/llm-docs/api/types/transformable.md +93 -0
- package/llm-docs/api/types/trim.md +27 -0
- package/llm-docs/api/types/two-objects-tangent-line.md +46 -0
- package/llm-docs/api/types/vertex.md +17 -0
- package/llm-docs/api/types/vline.md +35 -0
- package/llm-docs/concepts/coordinate-system.md +45 -0
- package/llm-docs/concepts/history-and-rollback.md +40 -0
- package/llm-docs/concepts/last-selection.md +49 -0
- package/llm-docs/concepts/scene-graph.md +37 -0
- package/llm-docs/index.json +1750 -0
- package/mcp/dist/client.d.ts +65 -0
- package/mcp/dist/client.js +255 -0
- package/mcp/dist/discovery.d.ts +11 -0
- package/mcp/dist/discovery.js +78 -0
- package/mcp/dist/docs-index.d.ts +81 -0
- package/mcp/dist/docs-index.js +261 -0
- package/mcp/dist/resources.d.ts +4 -0
- package/mcp/dist/resources.js +115 -0
- package/mcp/dist/server.d.ts +12 -0
- package/mcp/dist/server.js +502 -0
- package/mcp/dist/tools/coordination.d.ts +9 -0
- package/mcp/dist/tools/coordination.js +46 -0
- package/mcp/dist/tools/docs.d.ts +66 -0
- package/mcp/dist/tools/docs.js +122 -0
- package/mcp/dist/tools/engine.d.ts +72 -0
- package/mcp/dist/tools/engine.js +190 -0
- package/mcp/dist/tools/inspection.d.ts +75 -0
- package/mcp/dist/tools/inspection.js +121 -0
- package/mcp/dist/tools/screenshot.d.ts +63 -0
- package/mcp/dist/tools/screenshot.js +263 -0
- package/mcp/dist/tools/source.d.ts +84 -0
- package/mcp/dist/tools/source.js +434 -0
- package/mcp/dist/tools/workspaces.d.ts +13 -0
- package/mcp/dist/tools/workspaces.js +33 -0
- package/mcp/dist/types.d.ts +18 -0
- package/mcp/dist/types.js +11 -0
- package/package.json +27 -7
- package/server/dist/api.d.ts +37 -0
- package/server/dist/api.js +44 -0
- package/server/dist/code-editor.d.ts +100 -0
- package/server/dist/code-editor.js +528 -2
- package/server/dist/fluidcad-server.d.ts +118 -1
- package/server/dist/fluidcad-server.js +350 -62
- package/server/dist/global-registry.d.ts +30 -0
- package/server/dist/global-registry.js +126 -0
- package/server/dist/host/blocked-imports.d.ts +8 -0
- package/server/dist/host/blocked-imports.js +30 -0
- package/server/dist/{vite-manager.d.ts → host/local-scene-host.d.ts} +3 -1
- package/server/dist/{vite-manager.js → host/local-scene-host.js} +6 -26
- package/server/dist/host/scene-host.d.ts +19 -0
- package/server/dist/host/scene-host.js +1 -0
- package/server/dist/index.js +175 -123
- package/server/dist/instance-file.d.ts +31 -0
- package/server/dist/instance-file.js +73 -0
- package/server/dist/lint-fluid-js.d.ts +15 -0
- package/server/dist/lint-fluid-js.js +271 -0
- package/server/dist/model-package/capture-params.d.ts +19 -0
- package/server/dist/model-package/capture-params.js +42 -0
- package/server/dist/model-package/pack.d.ts +23 -0
- package/server/dist/model-package/pack.js +229 -0
- package/server/dist/model-package/types.d.ts +78 -0
- package/server/dist/model-package/types.js +17 -0
- package/server/dist/routes/editor.d.ts +24 -0
- package/server/dist/routes/editor.js +44 -0
- package/server/dist/routes/export.d.ts +1 -1
- package/server/dist/routes/export.js +45 -8
- package/server/dist/routes/health.d.ts +7 -0
- package/server/dist/routes/health.js +14 -0
- package/server/dist/routes/hit-test.d.ts +3 -0
- package/server/dist/routes/hit-test.js +17 -0
- package/server/dist/routes/lint.d.ts +10 -0
- package/server/dist/routes/lint.js +28 -0
- package/server/dist/routes/pack.d.ts +10 -0
- package/server/dist/routes/pack.js +47 -0
- package/server/dist/routes/params.d.ts +3 -0
- package/server/dist/routes/params.js +75 -0
- package/server/dist/routes/render.d.ts +33 -0
- package/server/dist/routes/render.js +34 -0
- package/server/dist/routes/scene.d.ts +5 -0
- package/server/dist/routes/scene.js +48 -0
- package/server/dist/routes/screenshot.js +68 -1
- package/server/dist/routes/sketch-edits.d.ts +3 -0
- package/server/dist/routes/sketch-edits.js +542 -0
- package/server/dist/routes/timeline.d.ts +3 -0
- package/server/dist/routes/timeline.js +49 -0
- package/server/dist/server-core.d.ts +53 -0
- package/server/dist/server-core.js +147 -0
- package/server/dist/ws-protocol.d.ts +156 -3
- package/ui/dist/assets/index-CDJmUpFI.css +2 -0
- package/ui/dist/assets/index-MRqwG9Vh.js +5417 -0
- package/ui/dist/index.html +2 -2
- package/server/dist/routes/actions.d.ts +0 -3
- package/server/dist/routes/actions.js +0 -309
- package/ui/dist/assets/index-CFi9p7wR.js +0 -4946
- package/ui/dist/assets/index-DR7c2Qk9.css +0 -2
|
@@ -1,91 +1,221 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
1
2
|
import { join } from 'path';
|
|
2
3
|
import { existsSync } from 'fs';
|
|
3
|
-
import {
|
|
4
|
+
import { LocalSceneHost } from "./host/local-scene-host.js";
|
|
4
5
|
import { normalizePath } from "./normalize-path.js";
|
|
5
6
|
import { BreakpointHit } from '../../lib/dist/common/breakpoint-hit.js';
|
|
7
|
+
import { createParamRegistry, getParamRegistry } from '../../lib/dist/index.js';
|
|
8
|
+
/**
|
|
9
|
+
* `sessionId` is the per-renderer state key. In desktop mode it equals the
|
|
10
|
+
* file path being edited (so per-file state survives switching files). In
|
|
11
|
+
* hub mode it's a WebSocket connection UUID (so concurrent viewers stay
|
|
12
|
+
* isolated). Map keys called `sessionId` accept either flavour.
|
|
13
|
+
*/
|
|
6
14
|
export class FluidCadServer {
|
|
7
|
-
|
|
15
|
+
host;
|
|
8
16
|
sceneManager;
|
|
17
|
+
// Per-session render output, scene cache, and param overrides. Desktop's
|
|
18
|
+
// sessionId is the normalized filePath; hub mode's sessionId is the WS
|
|
19
|
+
// connection UUID. Maps must be cleared via `destroySession` on hub-side
|
|
20
|
+
// disconnect to avoid leaks.
|
|
9
21
|
previousScenes = new Map();
|
|
10
22
|
renderingCache = new Map();
|
|
23
|
+
// Records the last successful render per session as `{ paramsHash, data }`.
|
|
24
|
+
// Any subsequent render request short-circuits when the new params hash to
|
|
25
|
+
// the same value — avoids redundant OCC work when desktop producers see the
|
|
26
|
+
// same code+params, or hub clients re-emit the same param mutation.
|
|
27
|
+
lastRendered = new Map();
|
|
28
|
+
paramOverrides = new Map();
|
|
29
|
+
// What file each session is rendering. For desktop, sessionId === filePath
|
|
30
|
+
// (set lazily on first processFile call). For hub, set explicitly via
|
|
31
|
+
// createSession with the bundle's manifest entry.
|
|
32
|
+
sessionFiles = new Map();
|
|
33
|
+
// Serializes OCC calls across all sessions. OCC isn't thread-safe and we
|
|
34
|
+
// share one engine instance per host process; concurrent param edits from
|
|
35
|
+
// multiple hub clients have to queue. Promise-chain pattern: each render
|
|
36
|
+
// awaits the previous one's settlement before starting.
|
|
37
|
+
renderMutex = Promise.resolve();
|
|
11
38
|
currentFileName = '';
|
|
12
39
|
currentFilePath = '';
|
|
40
|
+
lastRollbackStop = -1;
|
|
41
|
+
compileError = null;
|
|
42
|
+
constructor(host = new LocalSceneHost()) {
|
|
43
|
+
this.host = host;
|
|
44
|
+
}
|
|
45
|
+
getCurrentCode() {
|
|
46
|
+
if (!this.currentFileName)
|
|
47
|
+
return null;
|
|
48
|
+
return this.host.getBuffer(this.currentFileName);
|
|
49
|
+
}
|
|
13
50
|
async init(workspacePath) {
|
|
14
|
-
await this.
|
|
51
|
+
await this.host.init(workspacePath);
|
|
15
52
|
const initFilePath = normalizePath(join(workspacePath, 'init.js'));
|
|
16
53
|
if (existsSync(initFilePath)) {
|
|
17
|
-
const { default: _sceneManager } = await this.
|
|
54
|
+
const { default: _sceneManager } = await this.host.loadModule(initFilePath);
|
|
18
55
|
this.sceneManager = await _sceneManager;
|
|
19
56
|
}
|
|
20
57
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Capture an already-initialized SceneManager. Used by the hub-mode entry
|
|
60
|
+
* after running the packed bundle once to materialize the engine globals.
|
|
61
|
+
*/
|
|
62
|
+
setSceneManager(manager) {
|
|
63
|
+
this.sceneManager = manager;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Run `fn` with exclusive access to the OCC engine. The mutex is process-
|
|
67
|
+
* wide: in hub mode concurrent client sessions land here too. Order is
|
|
68
|
+
* first-come, first-served via Promise chain.
|
|
69
|
+
*/
|
|
70
|
+
async serialized(fn) {
|
|
71
|
+
const prev = this.renderMutex;
|
|
72
|
+
let release = () => { };
|
|
73
|
+
const next = new Promise((resolve) => { release = resolve; });
|
|
74
|
+
this.renderMutex = next;
|
|
75
|
+
try {
|
|
76
|
+
await prev;
|
|
77
|
+
return await fn();
|
|
24
78
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
this.currentFileName = normalizedFileName;
|
|
28
|
-
this.currentFilePath = filePath;
|
|
29
|
-
if (!ignoreCache) {
|
|
30
|
-
const fromCache = this.renderingCache.get(normalizedFileName);
|
|
31
|
-
if (fromCache) {
|
|
32
|
-
return {
|
|
33
|
-
absPath: normalizedFileName,
|
|
34
|
-
result: fromCache,
|
|
35
|
-
rollbackStop: fromCache.length - 1,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
79
|
+
finally {
|
|
80
|
+
release();
|
|
38
81
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
82
|
+
}
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Session lifecycle (hub mode)
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
createSession(sessionId, entryFilePath) {
|
|
87
|
+
this.sessionFiles.set(sessionId, normalizePath(entryFilePath));
|
|
88
|
+
}
|
|
89
|
+
destroySession(sessionId) {
|
|
90
|
+
this.previousScenes.delete(sessionId);
|
|
91
|
+
this.renderingCache.delete(sessionId);
|
|
92
|
+
this.lastRendered.delete(sessionId);
|
|
93
|
+
this.paramOverrides.delete(sessionId);
|
|
94
|
+
this.sessionFiles.delete(sessionId);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Re-render the session's entry, ignoring caches. Hub clients call this
|
|
98
|
+
* after editing a param. Returns the fresh render or null if no manager.
|
|
99
|
+
*/
|
|
100
|
+
async recomputeForSession(sessionId) {
|
|
101
|
+
const filePath = this.sessionFiles.get(sessionId);
|
|
102
|
+
if (!filePath)
|
|
103
|
+
return null;
|
|
104
|
+
this.renderingCache.delete(sessionId);
|
|
105
|
+
this.lastRendered.delete(sessionId);
|
|
106
|
+
return this.processFileInternal(sessionId, filePath, true);
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Render — internal core used by both desktop and hub entry points
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
async processFileInternal(sessionId, filePath, ignoreCache) {
|
|
112
|
+
return this.serialized(async () => {
|
|
113
|
+
if (!this.sceneManager) {
|
|
114
|
+
return null;
|
|
46
115
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
116
|
+
const normalizedFileName = filePath.replace('virtual:live-render:', '');
|
|
117
|
+
this.currentFileName = normalizedFileName;
|
|
118
|
+
this.currentFilePath = filePath;
|
|
119
|
+
if (!ignoreCache) {
|
|
120
|
+
const fromCache = this.renderingCache.get(sessionId);
|
|
121
|
+
if (fromCache) {
|
|
122
|
+
this.lastRollbackStop = fromCache.length - 1;
|
|
123
|
+
this.compileError = null;
|
|
124
|
+
return {
|
|
125
|
+
absPath: normalizedFileName,
|
|
126
|
+
result: fromCache,
|
|
127
|
+
rollbackStop: fromCache.length - 1,
|
|
128
|
+
};
|
|
53
129
|
}
|
|
54
130
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
131
|
+
try {
|
|
132
|
+
let scene = this.sceneManager.startScene();
|
|
133
|
+
this.sceneManager.setCurrentFile(normalizedFileName);
|
|
134
|
+
this.host.invalidateModule();
|
|
135
|
+
const registry = createParamRegistry();
|
|
136
|
+
const overrides = this.paramOverrides.get(sessionId);
|
|
137
|
+
if (overrides) {
|
|
138
|
+
registry.setOverrides(overrides);
|
|
139
|
+
}
|
|
140
|
+
let breakpointHit = false;
|
|
141
|
+
try {
|
|
142
|
+
await this.host.loadModule(filePath);
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
if (e instanceof BreakpointHit) {
|
|
146
|
+
breakpointHit = true;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
throw e;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const params = getParamRegistry().getDefinitions();
|
|
153
|
+
if (this.previousScenes.has(sessionId)) {
|
|
154
|
+
const previousScene = this.previousScenes.get(sessionId);
|
|
155
|
+
scene = this.sceneManager.compare(previousScene, scene);
|
|
156
|
+
}
|
|
157
|
+
this.previousScenes.set(sessionId, scene);
|
|
158
|
+
this.sceneManager.renderScene(scene);
|
|
159
|
+
const result = scene.getRenderedObjects();
|
|
160
|
+
for (const obj of result) {
|
|
161
|
+
if (obj.sourceLocation) {
|
|
162
|
+
obj.sourceLocation.filePath = obj.sourceLocation.filePath.replace('virtual:live-render:', '');
|
|
163
|
+
}
|
|
65
164
|
}
|
|
165
|
+
if (!filePath.startsWith('virtual:live-render')) {
|
|
166
|
+
this.renderingCache.set(sessionId, result);
|
|
167
|
+
}
|
|
168
|
+
this.lastRollbackStop = result.length - 1;
|
|
169
|
+
this.compileError = null;
|
|
170
|
+
return {
|
|
171
|
+
absPath: normalizedFileName,
|
|
172
|
+
result,
|
|
173
|
+
rollbackStop: result.length - 1,
|
|
174
|
+
breakpointHit,
|
|
175
|
+
params,
|
|
176
|
+
};
|
|
66
177
|
}
|
|
67
|
-
|
|
68
|
-
this.
|
|
178
|
+
catch (error) {
|
|
179
|
+
this.host.invalidateModule();
|
|
180
|
+
console.log('Error processing file:', error);
|
|
181
|
+
throw error;
|
|
69
182
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
throw error;
|
|
81
|
-
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Desktop API — sessionId is implicit (filePath)
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
async processFile(filePath, ignoreCache = false) {
|
|
189
|
+
filePath = normalizePath(filePath);
|
|
190
|
+
const sessionId = filePath.replace('virtual:live-render:', '');
|
|
191
|
+
this.sessionFiles.set(sessionId, sessionId);
|
|
192
|
+
return this.processFileInternal(sessionId, filePath, ignoreCache);
|
|
82
193
|
}
|
|
83
194
|
async updateLiveCode(fileName, code) {
|
|
84
195
|
fileName = normalizePath(fileName);
|
|
196
|
+
// Dedup against the last successful render. Multiple producers (editor
|
|
197
|
+
// live-update, save-triggered process-file, watcher, MCP /api/render)
|
|
198
|
+
// commonly hand us identical content; without this short-circuit each
|
|
199
|
+
// would trigger a redundant OCC pass. paramsHash mixes code content with
|
|
200
|
+
// current param overrides so a param change invalidates the cache.
|
|
201
|
+
const paramsHash = this.computeParamsHash(fileName, code);
|
|
202
|
+
const cached = this.lastRendered.get(fileName);
|
|
203
|
+
if (cached && cached.paramsHash === paramsHash) {
|
|
204
|
+
this.compileError = null;
|
|
205
|
+
this.currentFileName = fileName;
|
|
206
|
+
this.currentFilePath = `virtual:live-render:${fileName}`;
|
|
207
|
+
this.lastRollbackStop = cached.data.rollbackStop;
|
|
208
|
+
return cached.data;
|
|
209
|
+
}
|
|
85
210
|
const id = `virtual:live-render:${fileName}`;
|
|
86
|
-
this.
|
|
211
|
+
this.host.setBuffer(id, code);
|
|
87
212
|
this.renderingCache.delete(fileName);
|
|
88
|
-
|
|
213
|
+
this.sessionFiles.set(fileName, fileName);
|
|
214
|
+
const result = await this.processFileInternal(fileName, id, true);
|
|
215
|
+
if (result) {
|
|
216
|
+
this.lastRendered.set(fileName, { paramsHash, data: result });
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
89
219
|
}
|
|
90
220
|
async rollbackFromUI(index) {
|
|
91
221
|
return this.rollback(this.currentFileName, index);
|
|
@@ -94,9 +224,10 @@ export class FluidCadServer {
|
|
|
94
224
|
if (!this.currentFilePath) {
|
|
95
225
|
return null;
|
|
96
226
|
}
|
|
97
|
-
this.
|
|
98
|
-
this.renderingCache.delete(
|
|
99
|
-
|
|
227
|
+
const sessionId = this.currentFileName;
|
|
228
|
+
this.renderingCache.delete(sessionId);
|
|
229
|
+
this.lastRendered.delete(sessionId);
|
|
230
|
+
return this.processFileInternal(sessionId, this.currentFilePath, true);
|
|
100
231
|
}
|
|
101
232
|
async rollback(fileName, index) {
|
|
102
233
|
if (!this.sceneManager) {
|
|
@@ -110,6 +241,7 @@ export class FluidCadServer {
|
|
|
110
241
|
const rollbackIndex = index >= totalObjects - 1 ? totalObjects - 1 : index;
|
|
111
242
|
this.sceneManager.rollbackScene(scene, rollbackIndex);
|
|
112
243
|
const result = scene.getRenderedObjects();
|
|
244
|
+
this.lastRollbackStop = index;
|
|
113
245
|
return {
|
|
114
246
|
absPath: fileName,
|
|
115
247
|
result,
|
|
@@ -173,4 +305,160 @@ export class FluidCadServer {
|
|
|
173
305
|
}
|
|
174
306
|
return this.sceneManager.hitTest(scene, shapeId, rayOrigin, rayDir, edgeThreshold);
|
|
175
307
|
}
|
|
308
|
+
hitTestForSession(sessionId, shapeId, rayOrigin, rayDir, edgeThreshold) {
|
|
309
|
+
if (!this.sceneManager) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
const scene = this.previousScenes.get(sessionId);
|
|
313
|
+
if (!scene) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
return this.sceneManager.hitTest(scene, shapeId, rayOrigin, rayDir, edgeThreshold);
|
|
317
|
+
}
|
|
318
|
+
setCompileError(err) {
|
|
319
|
+
this.compileError = err;
|
|
320
|
+
}
|
|
321
|
+
getCompileError() {
|
|
322
|
+
return this.compileError;
|
|
323
|
+
}
|
|
324
|
+
setParam(sessionId, label, value) {
|
|
325
|
+
sessionId = normalizePath(sessionId);
|
|
326
|
+
if (!this.paramOverrides.has(sessionId)) {
|
|
327
|
+
this.paramOverrides.set(sessionId, new Map());
|
|
328
|
+
}
|
|
329
|
+
this.paramOverrides.get(sessionId).set(label, value);
|
|
330
|
+
this.lastRendered.delete(sessionId);
|
|
331
|
+
}
|
|
332
|
+
resetParams(sessionId) {
|
|
333
|
+
sessionId = normalizePath(sessionId);
|
|
334
|
+
this.paramOverrides.delete(sessionId);
|
|
335
|
+
this.lastRendered.delete(sessionId);
|
|
336
|
+
}
|
|
337
|
+
getParamOverrides(sessionId) {
|
|
338
|
+
const map = this.paramOverrides.get(normalizePath(sessionId));
|
|
339
|
+
if (!map)
|
|
340
|
+
return {};
|
|
341
|
+
return Object.fromEntries(map);
|
|
342
|
+
}
|
|
343
|
+
getCurrentFileName() {
|
|
344
|
+
return this.currentFileName;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Test-only seam: stage a scene under the given file name so the inspection
|
|
348
|
+
* accessors can read it without running the vite pipeline. Production code
|
|
349
|
+
* never calls this — `processFile` populates the same map.
|
|
350
|
+
*/
|
|
351
|
+
_setSceneForTesting(fileName, scene, rollbackStop = -1) {
|
|
352
|
+
this.currentFileName = fileName;
|
|
353
|
+
this.previousScenes.set(fileName, scene);
|
|
354
|
+
this.lastRollbackStop = rollbackStop;
|
|
355
|
+
}
|
|
356
|
+
getSceneSummary() {
|
|
357
|
+
if (!this.currentFileName) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
const scene = this.previousScenes.get(this.currentFileName);
|
|
361
|
+
if (!scene) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
const rendered = scene.getRenderedObjects();
|
|
365
|
+
const objects = rendered.map((r, index) => ({
|
|
366
|
+
index,
|
|
367
|
+
id: r.id,
|
|
368
|
+
kind: r.type,
|
|
369
|
+
uniqueKind: r.uniqueType,
|
|
370
|
+
name: r.name,
|
|
371
|
+
params: sanitizeParams(r.object),
|
|
372
|
+
sourceLocation: r.sourceLocation,
|
|
373
|
+
shapeIds: (r.sceneShapes ?? []).map((s) => s.shapeId),
|
|
374
|
+
fromCache: !!r.fromCache,
|
|
375
|
+
hasError: !!r.hasError,
|
|
376
|
+
errorMessage: r.errorMessage,
|
|
377
|
+
containerId: r.parentId ?? null,
|
|
378
|
+
isContainer: !!r.isContainer,
|
|
379
|
+
visible: r.visible !== false,
|
|
380
|
+
}));
|
|
381
|
+
return {
|
|
382
|
+
schemaVersion: 1,
|
|
383
|
+
file: this.currentFileName,
|
|
384
|
+
objects,
|
|
385
|
+
rollbackStop: this.lastRollbackStop,
|
|
386
|
+
compileError: this.compileError,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
getShapesList() {
|
|
390
|
+
if (!this.currentFileName) {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
const scene = this.previousScenes.get(this.currentFileName);
|
|
394
|
+
if (!scene) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
const rendered = scene.getRenderedObjects();
|
|
398
|
+
const shapes = [];
|
|
399
|
+
for (const r of rendered) {
|
|
400
|
+
const sceneShapes = (r.sceneShapes ?? []);
|
|
401
|
+
for (const s of sceneShapes) {
|
|
402
|
+
shapes.push({
|
|
403
|
+
shapeId: s.shapeId,
|
|
404
|
+
type: s.shapeType,
|
|
405
|
+
sceneObjectId: r.id,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return { shapes };
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Compose a stable cache key over the rendering inputs: the source bytes
|
|
413
|
+
* being rendered plus the param overrides currently in effect for the
|
|
414
|
+
* session. Param changes flip the hash so cached entries don't shadow a
|
|
415
|
+
* recompute, even when the code text is byte-identical.
|
|
416
|
+
*/
|
|
417
|
+
computeParamsHash(sessionId, codeOrBundle) {
|
|
418
|
+
const overrides = this.paramOverrides.get(sessionId);
|
|
419
|
+
const sortedEntries = overrides ? [...overrides.entries()].sort(([a], [b]) => a.localeCompare(b)) : [];
|
|
420
|
+
const normalized = codeOrBundle.replace(/\r\n/g, '\n');
|
|
421
|
+
return createHash('sha1')
|
|
422
|
+
.update(normalized)
|
|
423
|
+
.update('\0')
|
|
424
|
+
.update(JSON.stringify(sortedEntries))
|
|
425
|
+
.digest('hex');
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const MAX_PARAM_DEPTH = 6;
|
|
429
|
+
function sanitizeParams(value, depth = 0) {
|
|
430
|
+
if (value === null || value === undefined) {
|
|
431
|
+
return value ?? null;
|
|
432
|
+
}
|
|
433
|
+
if (typeof value === 'number') {
|
|
434
|
+
return Number.isFinite(value) ? value : null;
|
|
435
|
+
}
|
|
436
|
+
if (typeof value === 'string' || typeof value === 'boolean') {
|
|
437
|
+
return value;
|
|
438
|
+
}
|
|
439
|
+
if (depth >= MAX_PARAM_DEPTH) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
if (Array.isArray(value)) {
|
|
443
|
+
return value.map((v) => sanitizeParams(v, depth + 1));
|
|
444
|
+
}
|
|
445
|
+
if (typeof value === 'object') {
|
|
446
|
+
// A scene-object reference. Render as { ref: id } so the agent can chase
|
|
447
|
+
// it through other tools without us shipping the whole subtree.
|
|
448
|
+
const maybeId = value.id;
|
|
449
|
+
const isSceneObjectRef = typeof maybeId === 'string' &&
|
|
450
|
+
typeof value.getType === 'function';
|
|
451
|
+
if (isSceneObjectRef) {
|
|
452
|
+
return { ref: maybeId };
|
|
453
|
+
}
|
|
454
|
+
const out = {};
|
|
455
|
+
for (const [k, v] of Object.entries(value)) {
|
|
456
|
+
if (typeof v === 'function') {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
out[k] = sanitizeParams(v, depth + 1);
|
|
460
|
+
}
|
|
461
|
+
return out;
|
|
462
|
+
}
|
|
463
|
+
return null;
|
|
176
464
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare const REGISTRY_DIR_NAME = ".fluidcad";
|
|
2
|
+
export declare const REGISTRY_FILE_NAME = "instances.json";
|
|
3
|
+
export type RegistryEntry = {
|
|
4
|
+
workspacePath: string;
|
|
5
|
+
port: number;
|
|
6
|
+
pid: number;
|
|
7
|
+
version: string;
|
|
8
|
+
startedAt: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function registryFilePath(): string;
|
|
11
|
+
/**
|
|
12
|
+
* Add or replace an entry keyed by `workspacePath`. A new entry for an
|
|
13
|
+
* existing workspace (second window) supersedes the prior one.
|
|
14
|
+
*/
|
|
15
|
+
export declare function addInstance(entry: RegistryEntry): void;
|
|
16
|
+
/**
|
|
17
|
+
* Remove the entry that matches both workspacePath and pid. Matching on pid
|
|
18
|
+
* prevents a clean shutdown from clobbering a freshly-restarted instance.
|
|
19
|
+
*/
|
|
20
|
+
export declare function removeInstance(workspacePath: string, pid: number): void;
|
|
21
|
+
/**
|
|
22
|
+
* Read the registry, prune entries whose PIDs are no longer alive, and
|
|
23
|
+
* persist the pruned result. Returns the live entries.
|
|
24
|
+
*
|
|
25
|
+
* Liveness is `process.kill(pid, 0)` — sending signal 0 doesn't deliver
|
|
26
|
+
* anything but throws ESRCH if the process is gone. EPERM (running but owned
|
|
27
|
+
* by another user) counts as alive.
|
|
28
|
+
*/
|
|
29
|
+
export declare function readInstances(): RegistryEntry[];
|
|
30
|
+
export declare function isPidAlive(pid: number): boolean;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Global instance registry at ~/.fluidcad/instances.json — lets a single MCP
|
|
2
|
+
// process enumerate every running FluidCAD workspace on this machine.
|
|
3
|
+
//
|
|
4
|
+
// Read-modify-write is not crash-safe across multiple writers (we have no
|
|
5
|
+
// file lock), but the worst case is a dropped registry entry that the next
|
|
6
|
+
// successful write or stale-prune restores. Atomicity for the file itself is
|
|
7
|
+
// via tmp + rename.
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
export const REGISTRY_DIR_NAME = '.fluidcad';
|
|
12
|
+
export const REGISTRY_FILE_NAME = 'instances.json';
|
|
13
|
+
const EMPTY = { schemaVersion: 1, instances: [] };
|
|
14
|
+
function registryDir() {
|
|
15
|
+
return path.join(os.homedir(), REGISTRY_DIR_NAME);
|
|
16
|
+
}
|
|
17
|
+
export function registryFilePath() {
|
|
18
|
+
return path.join(registryDir(), REGISTRY_FILE_NAME);
|
|
19
|
+
}
|
|
20
|
+
function readRaw() {
|
|
21
|
+
try {
|
|
22
|
+
const raw = fs.readFileSync(registryFilePath(), 'utf8');
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
if (isRegistryFile(parsed)) {
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
return EMPTY;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return EMPTY;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function writeRaw(file) {
|
|
34
|
+
const dir = registryDir();
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
const destination = registryFilePath();
|
|
37
|
+
const tmp = path.join(dir, `${REGISTRY_FILE_NAME}.${process.pid}.tmp`);
|
|
38
|
+
const payload = JSON.stringify(file, null, 2) + '\n';
|
|
39
|
+
fs.writeFileSync(tmp, payload, { encoding: 'utf8', mode: 0o644 });
|
|
40
|
+
fs.renameSync(tmp, destination);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Add or replace an entry keyed by `workspacePath`. A new entry for an
|
|
44
|
+
* existing workspace (second window) supersedes the prior one.
|
|
45
|
+
*/
|
|
46
|
+
export function addInstance(entry) {
|
|
47
|
+
const file = readRaw();
|
|
48
|
+
const filtered = file.instances.filter((e) => e.workspacePath !== entry.workspacePath);
|
|
49
|
+
filtered.push(entry);
|
|
50
|
+
writeRaw({ schemaVersion: 1, instances: filtered });
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Remove the entry that matches both workspacePath and pid. Matching on pid
|
|
54
|
+
* prevents a clean shutdown from clobbering a freshly-restarted instance.
|
|
55
|
+
*/
|
|
56
|
+
export function removeInstance(workspacePath, pid) {
|
|
57
|
+
const file = readRaw();
|
|
58
|
+
const filtered = file.instances.filter((e) => !(e.workspacePath === workspacePath && e.pid === pid));
|
|
59
|
+
if (filtered.length === file.instances.length) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
writeRaw({ schemaVersion: 1, instances: filtered });
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Read the registry, prune entries whose PIDs are no longer alive, and
|
|
66
|
+
* persist the pruned result. Returns the live entries.
|
|
67
|
+
*
|
|
68
|
+
* Liveness is `process.kill(pid, 0)` — sending signal 0 doesn't deliver
|
|
69
|
+
* anything but throws ESRCH if the process is gone. EPERM (running but owned
|
|
70
|
+
* by another user) counts as alive.
|
|
71
|
+
*/
|
|
72
|
+
export function readInstances() {
|
|
73
|
+
const file = readRaw();
|
|
74
|
+
const alive = [];
|
|
75
|
+
const dead = [];
|
|
76
|
+
for (const entry of file.instances) {
|
|
77
|
+
if (isPidAlive(entry.pid)) {
|
|
78
|
+
alive.push(entry);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
dead.push(entry);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (dead.length > 0) {
|
|
85
|
+
try {
|
|
86
|
+
writeRaw({ schemaVersion: 1, instances: alive });
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Pruning is best-effort; readers still get the live set.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return alive;
|
|
93
|
+
}
|
|
94
|
+
export function isPidAlive(pid) {
|
|
95
|
+
try {
|
|
96
|
+
process.kill(pid, 0);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
if (err && err.code === 'EPERM') {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function isRegistryFile(value) {
|
|
107
|
+
if (typeof value !== 'object' || value === null) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const v = value;
|
|
111
|
+
if (v.schemaVersion !== 1 || !Array.isArray(v.instances)) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return v.instances.every(isRegistryEntry);
|
|
115
|
+
}
|
|
116
|
+
function isRegistryEntry(value) {
|
|
117
|
+
if (typeof value !== 'object' || value === null) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
const v = value;
|
|
121
|
+
return (typeof v.workspacePath === 'string' &&
|
|
122
|
+
typeof v.port === 'number' &&
|
|
123
|
+
typeof v.pid === 'number' &&
|
|
124
|
+
typeof v.version === 'string' &&
|
|
125
|
+
typeof v.startedAt === 'string');
|
|
126
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node built-in modules that user `.fluid.js` code must not import. Same set
|
|
3
|
+
* enforced by LocalSceneHost (at SSR transform time) and the model packer
|
|
4
|
+
* (at bundle time) so a packed model can't smuggle in capabilities a live
|
|
5
|
+
* workspace would have rejected.
|
|
6
|
+
*/
|
|
7
|
+
export declare const BLOCKED_NODE_MODULES: Set<string>;
|
|
8
|
+
export declare function getBlockedNodeModule(id: string): string | null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node built-in modules that user `.fluid.js` code must not import. Same set
|
|
3
|
+
* enforced by LocalSceneHost (at SSR transform time) and the model packer
|
|
4
|
+
* (at bundle time) so a packed model can't smuggle in capabilities a live
|
|
5
|
+
* workspace would have rejected.
|
|
6
|
+
*/
|
|
7
|
+
export const BLOCKED_NODE_MODULES = new Set([
|
|
8
|
+
'fs',
|
|
9
|
+
'child_process',
|
|
10
|
+
'net',
|
|
11
|
+
'dgram',
|
|
12
|
+
'tls',
|
|
13
|
+
'http',
|
|
14
|
+
'https',
|
|
15
|
+
'http2',
|
|
16
|
+
'os',
|
|
17
|
+
'worker_threads',
|
|
18
|
+
'vm',
|
|
19
|
+
'cluster',
|
|
20
|
+
'dns',
|
|
21
|
+
'module',
|
|
22
|
+
]);
|
|
23
|
+
export function getBlockedNodeModule(id) {
|
|
24
|
+
let name = id;
|
|
25
|
+
if (name.startsWith('node:')) {
|
|
26
|
+
name = name.slice(5);
|
|
27
|
+
}
|
|
28
|
+
const baseName = name.split('/')[0];
|
|
29
|
+
return BLOCKED_NODE_MODULES.has(baseName) ? baseName : null;
|
|
30
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type ViteDevServer } from 'vite';
|
|
2
|
-
|
|
2
|
+
import type { SceneHost } from './scene-host.ts';
|
|
3
|
+
export declare class LocalSceneHost implements SceneHost {
|
|
3
4
|
server: ViteDevServer;
|
|
4
5
|
private rootPath;
|
|
5
6
|
private buffers;
|
|
6
7
|
init(rootPath: string): Promise<void>;
|
|
7
8
|
setBuffer(id: string, code: string): void;
|
|
9
|
+
getBuffer(fileName: string): string | null;
|
|
8
10
|
loadModule(filePath: string): Promise<Record<string, any>>;
|
|
9
11
|
invalidateModule(): void;
|
|
10
12
|
}
|