@lampdotdev/lamp 0.0.0-stage → 0.36.2-nightly.16
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/LICENSE +21 -0
- package/README.md +397 -2
- package/Resources/Devices/roku.json +410 -0
- package/Resources/Devices/xbox-one.json +367 -0
- package/Resources/GameKit/NOTICE.md +63 -0
- package/Resources/GameKit/apple/LampPage.swift +1813 -0
- package/Resources/GameKit/apple/LampPlayer.swift +46 -0
- package/Resources/GameKit/apple/LampWebApp.swift +63 -0
- package/Resources/GameKit/apple/LampWebGame.swift +87 -0
- package/Resources/GameKit/apple/Native.swift +12 -0
- package/Resources/GameKit/build.sh +55 -0
- package/Resources/GameKit/lamp_game.h +116 -0
- package/Resources/GameKit/lamp_host.h +19 -0
- package/Resources/GameKit/lamp_kit.cpp +61 -0
- package/Resources/GameKit/lamp_sound.cpp +455 -0
- package/Resources/GameKit/lamp_xbox.cpp +374 -0
- package/Resources/GameKit/libraries/HandmadeMath.h +3940 -0
- package/Resources/GameKit/libraries/cgltf.h +7240 -0
- package/Resources/GameKit/libraries/dr_mp3.h +5430 -0
- package/Resources/GameKit/libraries/dr_wav.h +9209 -0
- package/Resources/GameKit/libraries/sokol_debugtext.h +4754 -0
- package/Resources/GameKit/libraries/sokol_gfx.h +29558 -0
- package/Resources/GameKit/libraries/sokol_gl.h +4538 -0
- package/Resources/GameKit/libraries/sokol_log.h +335 -0
- package/Resources/GameKit/libraries/sokol_shape.h +1531 -0
- package/Resources/GameKit/libraries/stb_image.h +7988 -0
- package/Resources/GameKit/libraries/stb_vorbis.c +5584 -0
- package/Resources/GameKit/page/lamp-live.js +672 -0
- package/Resources/GameKit/scaffold/Game/game.cpp +142 -0
- package/Resources/GameKit/scaffold/Game/scene.glsl +49 -0
- package/Resources/GameKit/scaffold-web/Web/game.js +31 -0
- package/Resources/GameKit/scaffold-web/Web/index.html +13 -0
- package/Resources/GameKit/scaffold-web/Web/making.js +33 -0
- package/Resources/GameKit/scaffold-web/Web/world.js +33 -0
- package/Resources/GameKit/web/lamp-game.js +3042 -0
- package/Resources/GameKit/web/lamp-phaser.js +128 -0
- package/Resources/WebHost/App.exe +0 -0
- package/Resources/WebHost/Microsoft.Web.WebView2.Core.dll +0 -0
- package/Resources/WebHost/WebView2-LICENSE.txt +27 -0
- package/Resources/WebHost/lamp_controllers.h +1506 -0
- package/Resources/WebHost/lamp_web_host.cpp +1173 -0
- package/Resources/WebLibraries/earlier.txt +318 -0
- package/Resources/WebLibraries/phaser/LICENSE.md +20 -0
- package/Resources/WebLibraries/phaser/NOTICE.md +2 -0
- package/Resources/WebLibraries/phaser/phaser.esm.min.js +1 -0
- package/Resources/WebLibraries/qrcode/LICENSE +21 -0
- package/Resources/WebLibraries/qrcode/NOTICE.md +4 -0
- package/Resources/WebLibraries/qrcode/qrcode.js +2297 -0
- package/Resources/WebLibraries/rapier/LICENSE +201 -0
- package/Resources/WebLibraries/rapier/NOTICE.md +3 -0
- package/Resources/WebLibraries/rapier/rapier.js +2 -0
- package/Resources/WebLibraries/three/LICENSE +21 -0
- package/Resources/WebLibraries/three/NOTICE.md +4 -0
- package/Resources/WebLibraries/three/addons/animation/AnimationClipCreator.js +168 -0
- package/Resources/WebLibraries/three/addons/animation/CCDIKSolver.js +607 -0
- package/Resources/WebLibraries/three/addons/controls/ArcballControls.js +3543 -0
- package/Resources/WebLibraries/three/addons/controls/DragControls.js +452 -0
- package/Resources/WebLibraries/three/addons/controls/FirstPersonControls.js +512 -0
- package/Resources/WebLibraries/three/addons/controls/FlyControls.js +384 -0
- package/Resources/WebLibraries/three/addons/controls/MapControls.js +116 -0
- package/Resources/WebLibraries/three/addons/controls/OrbitControls.js +1985 -0
- package/Resources/WebLibraries/three/addons/controls/PointerLockControls.js +264 -0
- package/Resources/WebLibraries/three/addons/controls/TrackballControls.js +1106 -0
- package/Resources/WebLibraries/three/addons/controls/TransformControls.js +2003 -0
- package/Resources/WebLibraries/three/addons/curves/CurveExtras.js +694 -0
- package/Resources/WebLibraries/three/addons/curves/NURBSCurve.js +169 -0
- package/Resources/WebLibraries/three/addons/curves/NURBSSurface.js +98 -0
- package/Resources/WebLibraries/three/addons/curves/NURBSUtils.js +532 -0
- package/Resources/WebLibraries/three/addons/curves/NURBSVolume.js +82 -0
- package/Resources/WebLibraries/three/addons/effects/AnaglyphEffect.js +274 -0
- package/Resources/WebLibraries/three/addons/effects/AsciiEffect.js +323 -0
- package/Resources/WebLibraries/three/addons/effects/OutlineEffect.js +489 -0
- package/Resources/WebLibraries/three/addons/effects/ParallaxBarrierEffect.js +155 -0
- package/Resources/WebLibraries/three/addons/effects/StereoEffect.js +91 -0
- package/Resources/WebLibraries/three/addons/environments/ColorEnvironment.js +59 -0
- package/Resources/WebLibraries/three/addons/environments/DebugEnvironment.js +102 -0
- package/Resources/WebLibraries/three/addons/environments/RoomEnvironment.js +185 -0
- package/Resources/WebLibraries/three/addons/geometries/BoxLineGeometry.js +92 -0
- package/Resources/WebLibraries/three/addons/geometries/ConvexGeometry.js +72 -0
- package/Resources/WebLibraries/three/addons/geometries/DecalGeometry.js +420 -0
- package/Resources/WebLibraries/three/addons/geometries/LoftGeometry.js +355 -0
- package/Resources/WebLibraries/three/addons/geometries/ParametricFunctions.js +100 -0
- package/Resources/WebLibraries/three/addons/geometries/ParametricGeometry.js +172 -0
- package/Resources/WebLibraries/three/addons/geometries/RoundedBoxGeometry.js +216 -0
- package/Resources/WebLibraries/three/addons/geometries/TeapotGeometry.js +689 -0
- package/Resources/WebLibraries/three/addons/geometries/TextGeometry.js +102 -0
- package/Resources/WebLibraries/three/addons/helpers/AnimationPathHelper.js +302 -0
- package/Resources/WebLibraries/three/addons/helpers/LightProbeGridHelper.js +166 -0
- package/Resources/WebLibraries/three/addons/helpers/LightProbeGridHelperWebGL.js +221 -0
- package/Resources/WebLibraries/three/addons/helpers/LightProbeHelper.js +166 -0
- package/Resources/WebLibraries/three/addons/helpers/LightProbeHelperGPU.js +102 -0
- package/Resources/WebLibraries/three/addons/helpers/OctreeHelper.js +109 -0
- package/Resources/WebLibraries/three/addons/helpers/PositionalAudioHelper.js +169 -0
- package/Resources/WebLibraries/three/addons/helpers/RapierHelper.js +59 -0
- package/Resources/WebLibraries/three/addons/helpers/RectAreaLightHelper.js +118 -0
- package/Resources/WebLibraries/three/addons/helpers/TextureHelper.js +265 -0
- package/Resources/WebLibraries/three/addons/helpers/TextureHelperGPU.js +214 -0
- package/Resources/WebLibraries/three/addons/helpers/VertexNormalsHelper.js +155 -0
- package/Resources/WebLibraries/three/addons/helpers/VertexTangentsHelper.js +133 -0
- package/Resources/WebLibraries/three/addons/helpers/ViewHelper.js +529 -0
- package/Resources/WebLibraries/three/addons/interactive/HTMLMesh.js +617 -0
- package/Resources/WebLibraries/three/addons/interactive/InteractiveGroup.js +224 -0
- package/Resources/WebLibraries/three/addons/interactive/SelectionBox.js +324 -0
- package/Resources/WebLibraries/three/addons/interactive/SelectionHelper.js +150 -0
- package/Resources/WebLibraries/three/addons/libs/fflate.module.js +2699 -0
- package/Resources/WebLibraries/three/addons/libs/lil-gui.module.min.js +8 -0
- package/Resources/WebLibraries/three/addons/libs/meshopt_decoder.module.js +196 -0
- package/Resources/WebLibraries/three/addons/libs/potpack.module.js +125 -0
- package/Resources/WebLibraries/three/addons/libs/stats.module.js +167 -0
- package/Resources/WebLibraries/three/addons/libs/tween.module.js +876 -0
- package/Resources/WebLibraries/three/addons/lighting/ClusteredLighting.js +153 -0
- package/Resources/WebLibraries/three/addons/lighting/DynamicLighting.js +82 -0
- package/Resources/WebLibraries/three/addons/lighting/LightMapper.js +295 -0
- package/Resources/WebLibraries/three/addons/lighting/LightProbeGrid.js +923 -0
- package/Resources/WebLibraries/three/addons/lighting/LightProbeGridUtils.js +107 -0
- package/Resources/WebLibraries/three/addons/lighting/LightProbeGridWebGL.js +846 -0
- package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGIConeTracer.js +243 -0
- package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGIIrradiance.js +43 -0
- package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGINode.js +617 -0
- package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGISceneCollector.js +793 -0
- package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGIVolume.js +1440 -0
- package/Resources/WebLibraries/three/addons/lights/LightProbeGenerator.js +337 -0
- package/Resources/WebLibraries/three/addons/lights/RectAreaLightTexturesLib.js +127 -0
- package/Resources/WebLibraries/three/addons/lights/RectAreaLightUniformsLib.js +40 -0
- package/Resources/WebLibraries/three/addons/lights/SunLight.js +100 -0
- package/Resources/WebLibraries/three/addons/lights/SunLightNode.js +56 -0
- package/Resources/WebLibraries/three/addons/lights/SunLightShadow.js +319 -0
- package/Resources/WebLibraries/three/addons/lights/SunShadowNode.js +312 -0
- package/Resources/WebLibraries/three/addons/lines/Line2.js +56 -0
- package/Resources/WebLibraries/three/addons/lines/LineGeometry.js +157 -0
- package/Resources/WebLibraries/three/addons/lines/LineMaterial.js +742 -0
- package/Resources/WebLibraries/three/addons/lines/LineSegments2.js +434 -0
- package/Resources/WebLibraries/three/addons/lines/LineSegmentsGeometry.js +298 -0
- package/Resources/WebLibraries/three/addons/lines/Wireframe.js +108 -0
- package/Resources/WebLibraries/three/addons/lines/WireframeGeometry2.js +49 -0
- package/Resources/WebLibraries/three/addons/lines/webgpu/Line2.js +46 -0
- package/Resources/WebLibraries/three/addons/lines/webgpu/LineSegments2.js +419 -0
- package/Resources/WebLibraries/three/addons/lines/webgpu/Wireframe.js +86 -0
- package/Resources/WebLibraries/three/addons/loaders/EXRLoader.js +3411 -0
- package/Resources/WebLibraries/three/addons/loaders/FBXLoader.js +4660 -0
- package/Resources/WebLibraries/three/addons/loaders/FontLoader.js +262 -0
- package/Resources/WebLibraries/three/addons/loaders/GLTFLoader.js +5006 -0
- package/Resources/WebLibraries/three/addons/loaders/HDRLoader.js +463 -0
- package/Resources/WebLibraries/three/addons/loaders/MTLLoader.js +601 -0
- package/Resources/WebLibraries/three/addons/loaders/OBJLoader.js +961 -0
- package/Resources/WebLibraries/three/addons/loaders/PLYLoader.js +992 -0
- package/Resources/WebLibraries/three/addons/loaders/RGBELoader.js +18 -0
- package/Resources/WebLibraries/three/addons/loaders/STLLoader.js +423 -0
- package/Resources/WebLibraries/three/addons/loaders/SVGLoader.js +3297 -0
- package/Resources/WebLibraries/three/addons/loaders/TGALoader.js +539 -0
- package/Resources/WebLibraries/three/addons/loaders/UltraHDRLoader.js +757 -0
- package/Resources/WebLibraries/three/addons/math/Capsule.js +159 -0
- package/Resources/WebLibraries/three/addons/math/ColorConverter.js +62 -0
- package/Resources/WebLibraries/three/addons/math/ColorSpaces.js +147 -0
- package/Resources/WebLibraries/three/addons/math/ConvexHull.js +1695 -0
- package/Resources/WebLibraries/three/addons/math/ImprovedNoise.js +88 -0
- package/Resources/WebLibraries/three/addons/math/Lut.js +317 -0
- package/Resources/WebLibraries/three/addons/math/MeshSurfaceSampler.js +315 -0
- package/Resources/WebLibraries/three/addons/math/OBB.js +535 -0
- package/Resources/WebLibraries/three/addons/math/Octree.js +825 -0
- package/Resources/WebLibraries/three/addons/math/SimplexNoise.js +470 -0
- package/Resources/WebLibraries/three/addons/misc/ConvexObjectBreaker.js +539 -0
- package/Resources/WebLibraries/three/addons/misc/GPUComputationRenderer.js +508 -0
- package/Resources/WebLibraries/three/addons/misc/Gyroscope.js +78 -0
- package/Resources/WebLibraries/three/addons/misc/MorphAnimMesh.js +119 -0
- package/Resources/WebLibraries/three/addons/misc/MorphBlendMesh.js +425 -0
- package/Resources/WebLibraries/three/addons/misc/ProgressiveLightMap.js +370 -0
- package/Resources/WebLibraries/three/addons/misc/ProgressiveLightMapGPU.js +322 -0
- package/Resources/WebLibraries/three/addons/misc/RollerCoaster.js +661 -0
- package/Resources/WebLibraries/three/addons/misc/Sculptor.js +1679 -0
- package/Resources/WebLibraries/three/addons/misc/SculptorMesh.js +1504 -0
- package/Resources/WebLibraries/three/addons/misc/SculptorTools.js +1180 -0
- package/Resources/WebLibraries/three/addons/misc/SculptorUtils.js +345 -0
- package/Resources/WebLibraries/three/addons/misc/TileCreasedNormalsPlugin.js +270 -0
- package/Resources/WebLibraries/three/addons/misc/TubePainter.js +611 -0
- package/Resources/WebLibraries/three/addons/misc/Volume.js +520 -0
- package/Resources/WebLibraries/three/addons/misc/VolumeSlice.js +275 -0
- package/Resources/WebLibraries/three/addons/modifiers/CurveModifier.js +374 -0
- package/Resources/WebLibraries/three/addons/modifiers/CurveModifierGPU.js +256 -0
- package/Resources/WebLibraries/three/addons/modifiers/EdgeSplitModifier.js +299 -0
- package/Resources/WebLibraries/three/addons/modifiers/TessellateModifier.js +340 -0
- package/Resources/WebLibraries/three/addons/objects/GroundedSkybox.js +69 -0
- package/Resources/WebLibraries/three/addons/objects/Lensflare.js +489 -0
- package/Resources/WebLibraries/three/addons/objects/LensflareMesh.js +376 -0
- package/Resources/WebLibraries/three/addons/objects/MarchingCubes.js +1261 -0
- package/Resources/WebLibraries/three/addons/objects/Reflector.js +390 -0
- package/Resources/WebLibraries/three/addons/objects/ReflectorForSSRPass.js +393 -0
- package/Resources/WebLibraries/three/addons/objects/Refractor.js +389 -0
- package/Resources/WebLibraries/three/addons/objects/ShadowMesh.js +130 -0
- package/Resources/WebLibraries/three/addons/objects/Sky.js +342 -0
- package/Resources/WebLibraries/three/addons/objects/SkyMesh.js +403 -0
- package/Resources/WebLibraries/three/addons/objects/Water.js +374 -0
- package/Resources/WebLibraries/three/addons/objects/Water2.js +403 -0
- package/Resources/WebLibraries/three/addons/objects/Water2Mesh.js +199 -0
- package/Resources/WebLibraries/three/addons/objects/WaterMesh.js +194 -0
- package/Resources/WebLibraries/three/addons/postprocessing/AfterimagePass.js +187 -0
- package/Resources/WebLibraries/three/addons/postprocessing/BloomPass.js +274 -0
- package/Resources/WebLibraries/three/addons/postprocessing/BokehPass.js +218 -0
- package/Resources/WebLibraries/three/addons/postprocessing/ClearPass.js +97 -0
- package/Resources/WebLibraries/three/addons/postprocessing/CubeTexturePass.js +146 -0
- package/Resources/WebLibraries/three/addons/postprocessing/DotScreenPass.js +114 -0
- package/Resources/WebLibraries/three/addons/postprocessing/EffectComposer.js +365 -0
- package/Resources/WebLibraries/three/addons/postprocessing/FXAAPass.js +40 -0
- package/Resources/WebLibraries/three/addons/postprocessing/FilmPass.js +113 -0
- package/Resources/WebLibraries/three/addons/postprocessing/GTAOPass.js +735 -0
- package/Resources/WebLibraries/three/addons/postprocessing/GlitchPass.js +177 -0
- package/Resources/WebLibraries/three/addons/postprocessing/HalftonePass.js +134 -0
- package/Resources/WebLibraries/three/addons/postprocessing/LUTPass.js +138 -0
- package/Resources/WebLibraries/three/addons/postprocessing/MaskPass.js +195 -0
- package/Resources/WebLibraries/three/addons/postprocessing/OutlinePass.js +776 -0
- package/Resources/WebLibraries/three/addons/postprocessing/OutputPass.js +148 -0
- package/Resources/WebLibraries/three/addons/postprocessing/Pass.js +191 -0
- package/Resources/WebLibraries/three/addons/postprocessing/RenderPass.js +193 -0
- package/Resources/WebLibraries/three/addons/postprocessing/RenderPixelatedPass.js +314 -0
- package/Resources/WebLibraries/three/addons/postprocessing/RenderTransitionPass.js +267 -0
- package/Resources/WebLibraries/three/addons/postprocessing/SAOPass.js +407 -0
- package/Resources/WebLibraries/three/addons/postprocessing/SMAAPass.js +230 -0
- package/Resources/WebLibraries/three/addons/postprocessing/SSAARenderPass.js +313 -0
- package/Resources/WebLibraries/three/addons/postprocessing/SSAOPass.js +527 -0
- package/Resources/WebLibraries/three/addons/postprocessing/SSRPass.js +858 -0
- package/Resources/WebLibraries/three/addons/postprocessing/SavePass.js +132 -0
- package/Resources/WebLibraries/three/addons/postprocessing/ShaderPass.js +135 -0
- package/Resources/WebLibraries/three/addons/postprocessing/TAARenderPass.js +243 -0
- package/Resources/WebLibraries/three/addons/postprocessing/TexturePass.js +131 -0
- package/Resources/WebLibraries/three/addons/postprocessing/UnrealBloomPass.js +541 -0
- package/Resources/WebLibraries/three/addons/shaders/ACESFilmicToneMappingShader.js +96 -0
- package/Resources/WebLibraries/three/addons/shaders/AfterimageShader.js +63 -0
- package/Resources/WebLibraries/three/addons/shaders/BasicShader.js +36 -0
- package/Resources/WebLibraries/three/addons/shaders/BleachBypassShader.js +68 -0
- package/Resources/WebLibraries/three/addons/shaders/BlendShader.js +56 -0
- package/Resources/WebLibraries/three/addons/shaders/BokehShader.js +151 -0
- package/Resources/WebLibraries/three/addons/shaders/BokehShader2.js +404 -0
- package/Resources/WebLibraries/three/addons/shaders/BrightnessContrastShader.js +62 -0
- package/Resources/WebLibraries/three/addons/shaders/ColorCorrectionShader.js +59 -0
- package/Resources/WebLibraries/three/addons/shaders/ColorifyShader.js +57 -0
- package/Resources/WebLibraries/three/addons/shaders/ConvolutionShader.js +74 -0
- package/Resources/WebLibraries/three/addons/shaders/CopyShader.js +52 -0
- package/Resources/WebLibraries/three/addons/shaders/DOFMipMapShader.js +63 -0
- package/Resources/WebLibraries/three/addons/shaders/DepthLimitedBlurShader.js +183 -0
- package/Resources/WebLibraries/three/addons/shaders/DigitalGlitch.js +104 -0
- package/Resources/WebLibraries/three/addons/shaders/DotScreenShader.js +75 -0
- package/Resources/WebLibraries/three/addons/shaders/ExposureShader.js +51 -0
- package/Resources/WebLibraries/three/addons/shaders/FXAAShader.js +298 -0
- package/Resources/WebLibraries/three/addons/shaders/FilmShader.js +73 -0
- package/Resources/WebLibraries/three/addons/shaders/FocusShader.js +94 -0
- package/Resources/WebLibraries/three/addons/shaders/FreiChenShader.js +103 -0
- package/Resources/WebLibraries/three/addons/shaders/GTAOShader.js +437 -0
- package/Resources/WebLibraries/three/addons/shaders/GammaCorrectionShader.js +52 -0
- package/Resources/WebLibraries/three/addons/shaders/HalftoneShader.js +332 -0
- package/Resources/WebLibraries/three/addons/shaders/HorizontalBlurShader.js +68 -0
- package/Resources/WebLibraries/three/addons/shaders/HorizontalTiltShiftShader.js +70 -0
- package/Resources/WebLibraries/three/addons/shaders/HueSaturationShader.js +74 -0
- package/Resources/WebLibraries/three/addons/shaders/KaleidoShader.js +65 -0
- package/Resources/WebLibraries/three/addons/shaders/LuminosityHighPassShader.js +68 -0
- package/Resources/WebLibraries/three/addons/shaders/LuminosityShader.js +54 -0
- package/Resources/WebLibraries/three/addons/shaders/MirrorShader.js +62 -0
- package/Resources/WebLibraries/three/addons/shaders/NormalMapShader.js +60 -0
- package/Resources/WebLibraries/three/addons/shaders/OutputShader.js +103 -0
- package/Resources/WebLibraries/three/addons/shaders/PoissonDenoiseShader.js +230 -0
- package/Resources/WebLibraries/three/addons/shaders/RGBShiftShader.js +61 -0
- package/Resources/WebLibraries/three/addons/shaders/SAOShader.js +201 -0
- package/Resources/WebLibraries/three/addons/shaders/SMAAShader.js +489 -0
- package/Resources/WebLibraries/three/addons/shaders/SSAOShader.js +331 -0
- package/Resources/WebLibraries/three/addons/shaders/SSRShader.js +396 -0
- package/Resources/WebLibraries/three/addons/shaders/SepiaShader.js +57 -0
- package/Resources/WebLibraries/three/addons/shaders/SobelOperatorShader.js +98 -0
- package/Resources/WebLibraries/three/addons/shaders/SubsurfaceScatteringShader.js +95 -0
- package/Resources/WebLibraries/three/addons/shaders/TechnicolorShader.js +51 -0
- package/Resources/WebLibraries/three/addons/shaders/ToonShader.js +349 -0
- package/Resources/WebLibraries/three/addons/shaders/TriangleBlurShader.js +79 -0
- package/Resources/WebLibraries/three/addons/shaders/UnpackDepthRGBAShader.js +60 -0
- package/Resources/WebLibraries/three/addons/shaders/VelocityShader.js +137 -0
- package/Resources/WebLibraries/three/addons/shaders/VerticalBlurShader.js +66 -0
- package/Resources/WebLibraries/three/addons/shaders/VerticalTiltShiftShader.js +70 -0
- package/Resources/WebLibraries/three/addons/shaders/VignetteShader.js +56 -0
- package/Resources/WebLibraries/three/addons/shaders/VolumeShader.js +285 -0
- package/Resources/WebLibraries/three/addons/shaders/WaterRefractionShader.js +106 -0
- package/Resources/WebLibraries/three/addons/textures/FlakesTexture.js +54 -0
- package/Resources/WebLibraries/three/addons/tsl/WebGLNodesHandler.js +607 -0
- package/Resources/WebLibraries/three/addons/tsl/display/AfterImageNode.js +256 -0
- package/Resources/WebLibraries/three/addons/tsl/display/AnaglyphPassNode.js +550 -0
- package/Resources/WebLibraries/three/addons/tsl/display/BilateralBlurNode.js +377 -0
- package/Resources/WebLibraries/three/addons/tsl/display/BleachBypass.js +33 -0
- package/Resources/WebLibraries/three/addons/tsl/display/BloomNode.js +599 -0
- package/Resources/WebLibraries/three/addons/tsl/display/CRT.js +150 -0
- package/Resources/WebLibraries/three/addons/tsl/display/ChromaticAberrationNode.js +163 -0
- package/Resources/WebLibraries/three/addons/tsl/display/DenoiseNode.js +354 -0
- package/Resources/WebLibraries/three/addons/tsl/display/DepthOfFieldNode.js +572 -0
- package/Resources/WebLibraries/three/addons/tsl/display/DotScreenNode.js +104 -0
- package/Resources/WebLibraries/three/addons/tsl/display/FSR1Node.js +481 -0
- package/Resources/WebLibraries/three/addons/tsl/display/FXAANode.js +357 -0
- package/Resources/WebLibraries/three/addons/tsl/display/FilmNode.js +101 -0
- package/Resources/WebLibraries/three/addons/tsl/display/GTAONode.js +727 -0
- package/Resources/WebLibraries/three/addons/tsl/display/GaussianBlurNode.js +412 -0
- package/Resources/WebLibraries/three/addons/tsl/display/GodraysNode.js +604 -0
- package/Resources/WebLibraries/three/addons/tsl/display/ImportanceSampledEnvironment.js +564 -0
- package/Resources/WebLibraries/three/addons/tsl/display/LensflareNode.js +282 -0
- package/Resources/WebLibraries/three/addons/tsl/display/Lut3DNode.js +109 -0
- package/Resources/WebLibraries/three/addons/tsl/display/MotionBlur.js +33 -0
- package/Resources/WebLibraries/three/addons/tsl/display/OITPassNode.js +341 -0
- package/Resources/WebLibraries/three/addons/tsl/display/OutlineNode.js +916 -0
- package/Resources/WebLibraries/three/addons/tsl/display/ParallaxBarrierPassNode.js +90 -0
- package/Resources/WebLibraries/three/addons/tsl/display/PixelationPassNode.js +227 -0
- package/Resources/WebLibraries/three/addons/tsl/display/RGBShiftNode.js +96 -0
- package/Resources/WebLibraries/three/addons/tsl/display/RecurrentDenoiseNode.js +812 -0
- package/Resources/WebLibraries/three/addons/tsl/display/RetroPassNode.js +263 -0
- package/Resources/WebLibraries/three/addons/tsl/display/SMAANode.js +729 -0
- package/Resources/WebLibraries/three/addons/tsl/display/SSAAPassNode.js +344 -0
- package/Resources/WebLibraries/three/addons/tsl/display/SSAONode.js +422 -0
- package/Resources/WebLibraries/three/addons/tsl/display/SSGINode.js +698 -0
- package/Resources/WebLibraries/three/addons/tsl/display/SSRNode.js +1430 -0
- package/Resources/WebLibraries/three/addons/tsl/display/SSSNode.js +479 -0
- package/Resources/WebLibraries/three/addons/tsl/display/Sepia.js +24 -0
- package/Resources/WebLibraries/three/addons/tsl/display/Shape.js +29 -0
- package/Resources/WebLibraries/three/addons/tsl/display/SharpenNode.js +284 -0
- package/Resources/WebLibraries/three/addons/tsl/display/SobelOperatorNode.js +168 -0
- package/Resources/WebLibraries/three/addons/tsl/display/StereoCompositePassNode.js +190 -0
- package/Resources/WebLibraries/three/addons/tsl/display/StereoPassNode.js +117 -0
- package/Resources/WebLibraries/three/addons/tsl/display/TAAUNode.js +700 -0
- package/Resources/WebLibraries/three/addons/tsl/display/TRAANode.js +609 -0
- package/Resources/WebLibraries/three/addons/tsl/display/TemporalReprojectNode.js +1024 -0
- package/Resources/WebLibraries/three/addons/tsl/display/TransitionNode.js +141 -0
- package/Resources/WebLibraries/three/addons/tsl/display/boxBlur.js +65 -0
- package/Resources/WebLibraries/three/addons/tsl/display/depthAwareBlend.js +80 -0
- package/Resources/WebLibraries/three/addons/tsl/display/depthAwareBlur.js +63 -0
- package/Resources/WebLibraries/three/addons/tsl/display/hashBlur.js +55 -0
- package/Resources/WebLibraries/three/addons/tsl/display/lensDirt.js +30 -0
- package/Resources/WebLibraries/three/addons/tsl/display/radialBlur.js +68 -0
- package/Resources/WebLibraries/three/addons/tsl/lighting/ClusteredLightsNode.js +994 -0
- package/Resources/WebLibraries/three/addons/tsl/lighting/DynamicLightsNode.js +201 -0
- package/Resources/WebLibraries/three/addons/tsl/lighting/LightProbeGridNode.js +183 -0
- package/Resources/WebLibraries/three/addons/tsl/lighting/data/AmbientLightDataNode.js +67 -0
- package/Resources/WebLibraries/three/addons/tsl/lighting/data/DirectionalLightDataNode.js +117 -0
- package/Resources/WebLibraries/three/addons/tsl/lighting/data/HemisphereLightDataNode.js +105 -0
- package/Resources/WebLibraries/three/addons/tsl/lighting/data/PointLightDataNode.js +140 -0
- package/Resources/WebLibraries/three/addons/tsl/lighting/data/SpotLightDataNode.js +167 -0
- package/Resources/WebLibraries/three/addons/tsl/math/Bayer.js +84 -0
- package/Resources/WebLibraries/three/addons/tsl/math/curlNoise.js +107 -0
- package/Resources/WebLibraries/three/addons/tsl/math/voronoiNoise.js +112 -0
- package/Resources/WebLibraries/three/addons/tsl/shadows/TileShadowNode.js +542 -0
- package/Resources/WebLibraries/three/addons/tsl/shadows/TileShadowNodeHelper.js +212 -0
- package/Resources/WebLibraries/three/addons/tsl/utils/GroundedSkybox.js +62 -0
- package/Resources/WebLibraries/three/addons/tsl/utils/RNoise.js +51 -0
- package/Resources/WebLibraries/three/addons/tsl/utils/Raymarching.js +70 -0
- package/Resources/WebLibraries/three/addons/tsl/utils/SoftParticles.js +53 -0
- package/Resources/WebLibraries/three/addons/tsl/utils/SpecularHelpers.js +302 -0
- package/Resources/WebLibraries/three/addons/tsl/utils/TAAUtils.js +184 -0
- package/Resources/WebLibraries/three/addons/utils/BufferGeometryUtils.js +1523 -0
- package/Resources/WebLibraries/three/addons/utils/CameraUtils.js +82 -0
- package/Resources/WebLibraries/three/addons/utils/ColorUtils.js +76 -0
- package/Resources/WebLibraries/three/addons/utils/GaussianSplatUtils.js +229 -0
- package/Resources/WebLibraries/three/addons/utils/GeometryCompressionUtils.js +547 -0
- package/Resources/WebLibraries/three/addons/utils/GeometryUtils.js +226 -0
- package/Resources/WebLibraries/three/addons/utils/LDrawUtils.js +211 -0
- package/Resources/WebLibraries/three/addons/utils/SceneOptimizer.js +459 -0
- package/Resources/WebLibraries/three/addons/utils/SceneUtils.js +363 -0
- package/Resources/WebLibraries/three/addons/utils/ShadowMapViewer.js +244 -0
- package/Resources/WebLibraries/three/addons/utils/ShadowMapViewerGPU.js +233 -0
- package/Resources/WebLibraries/three/addons/utils/SkeletonUtils.js +496 -0
- package/Resources/WebLibraries/three/addons/utils/SortUtils.js +175 -0
- package/Resources/WebLibraries/three/addons/utils/UVsDebug.js +173 -0
- package/Resources/WebLibraries/three/addons/utils/WebGLTextureUtils.js +115 -0
- package/Resources/WebLibraries/three/addons/utils/WebGPUTextureUtils.js +81 -0
- package/Resources/WebLibraries/three/addons/utils/WorkerPool.js +167 -0
- package/Resources/WebLibraries/three/addons/utils/unwrapUVs.js +865 -0
- package/Resources/WebLibraries/three/three.core.js +61162 -0
- package/Resources/WebLibraries/three/three.module.js +19537 -0
- package/Resources/WebLibraries/three/three.tsl.js +41903 -0
- package/Resources/WebLibraries/three/three.webgpu.js +53714 -0
- package/Resources/share-rules.json +64 -0
- package/Resources/skills/2d-game/SKILL.md +305 -0
- package/Resources/skills/3d-game/SKILL.md +274 -0
- package/Resources/skills/accounts/SKILL.md +41 -0
- package/Resources/skills/android-design/SKILL.md +159 -0
- package/Resources/skills/app-ai/SKILL.md +154 -0
- package/Resources/skills/app-intents/SKILL.md +31 -0
- package/Resources/skills/app-kinds/SKILL.md +122 -0
- package/Resources/skills/apple-watch/SKILL.md +43 -0
- package/Resources/skills/assets/SKILL.md +183 -0
- package/Resources/skills/building-blocks/SKILL.md +97 -0
- package/Resources/skills/device-control/SKILL.md +56 -0
- package/Resources/skills/device-integrations/SKILL.md +278 -0
- package/Resources/skills/finder-files/SKILL.md +19 -0
- package/Resources/skills/game/SKILL.md +416 -0
- package/Resources/skills/game-3d/SKILL.md +12 -0
- package/Resources/skills/game-art/SKILL.md +11 -0
- package/Resources/skills/game-feel/SKILL.md +13 -0
- package/Resources/skills/game-levels/SKILL.md +12 -0
- package/Resources/skills/game-live/SKILL.md +12 -0
- package/Resources/skills/game-physics/SKILL.md +13 -0
- package/Resources/skills/game-playtest/SKILL.md +26 -0
- package/Resources/skills/game-ui/SKILL.md +278 -0
- package/Resources/skills/github-projects/SKILL.md +62 -0
- package/Resources/skills/icloud-sync/SKILL.md +30 -0
- package/Resources/skills/iphone-hardware/SKILL.md +57 -0
- package/Resources/skills/iphone-widgets/SKILL.md +53 -0
- package/Resources/skills/lg-developer-mode/SKILL.md +152 -0
- package/Resources/skills/mac-widgets/SKILL.md +60 -0
- package/Resources/skills/maps/SKILL.md +8 -0
- package/Resources/skills/menu-bar/SKILL.md +36 -0
- package/Resources/skills/more-apps/SKILL.md +53 -0
- package/Resources/skills/phone-controllers/SKILL.md +79 -0
- package/Resources/skills/phone-pages/SKILL.md +173 -0
- package/Resources/skills/roku-developer-mode/SKILL.md +106 -0
- package/Resources/skills/scene-3d/SKILL.md +27 -0
- package/Resources/skills/scripting/SKILL.md +17 -0
- package/Resources/skills/share-extension/SKILL.md +22 -0
- package/Resources/skills/tv-design/SKILL.md +72 -0
- package/Resources/skills/web-app/SKILL.md +170 -0
- package/Resources/skills/web-game/SKILL.md +95 -0
- package/Resources/skills/web-or-native/SKILL.md +108 -0
- package/Resources/skills/wish-links/SKILL.md +156 -0
- package/THIRD_PARTY_NOTICES.md +170 -0
- package/cli/ai.mjs +237 -0
- package/cli/android.mjs +941 -0
- package/cli/app-ai.mjs +473 -0
- package/cli/browser.mjs +284 -0
- package/cli/calls.mjs +594 -0
- package/cli/dev.mjs +820 -0
- package/cli/devices.mjs +984 -0
- package/cli/lamp.mjs +1326 -0
- package/cli/lampapp.mjs +332 -0
- package/cli/lamps/android-host/LampPageGateway.kt +345 -0
- package/cli/lamps/android-host/app/build.gradle.kts +40 -0
- package/cli/lamps/android-host/build.gradle.kts +4 -0
- package/cli/lamps/android-host/game/AndroidManifest.xml +33 -0
- package/cli/lamps/android-host/game/MainActivity.kt +173 -0
- package/cli/lamps/android-host/gradle/libs.versions.toml +39 -0
- package/cli/lamps/android-host/gradle.properties +8 -0
- package/cli/lamps/android-host/page/AndroidManifest.xml +35 -0
- package/cli/lamps/android-host/page/MainActivity.kt +193 -0
- package/cli/lamps/android-host/settings.gradle.kts +30 -0
- package/cli/lamps/android-host/tools.json +43 -0
- package/cli/lamps/lamp-controllers.js +2 -0
- package/cli/lamps/lamp-for-games.js +6922 -0
- package/cli/lamps/lamp.js +7082 -0
- package/cli/lamps/mac-starter/.lamp/meta.json +1 -0
- package/cli/lamps/mac-starter/APP.md +12 -0
- package/cli/lamps/mac-starter/Package.swift +8 -0
- package/cli/lamps/mac-starter/Sources/App.swift +11 -0
- package/cli/lamps/mac-starter/Sources/ContentView.swift +17 -0
- package/cli/lamps/mac-starter/Sources/LampSupport.swift +18215 -0
- package/cli/lamps/mac-starter/build.sh +336 -0
- package/cli/lamps/mac-starter/gitignore +3 -0
- package/cli/lamps/mac-starter/lamp-starter.json +6 -0
- package/cli/lamps/web-starter/APP.md +8 -0
- package/cli/lamps/web-starter/app.js +1 -0
- package/cli/lamps/web-starter/index.html +17 -0
- package/cli/lamps/web-starter/lamp-starter.json +5 -0
- package/cli/lamps/web-starter/lamp.js +7082 -0
- package/cli/lamps/web-starter/style.css +25 -0
- package/cli/link.mjs +202 -0
- package/cli/local-ai.mjs +193 -0
- package/cli/mac.mjs +621 -0
- package/cli/mdns.mjs +205 -0
- package/cli/msix.mjs +413 -0
- package/cli/network.mjs +209 -0
- package/cli/openrouter.mjs +133 -0
- package/cli/outside.mjs +203 -0
- package/cli/page.js +317 -0
- package/cli/place.mjs +298 -0
- package/cli/png.mjs +184 -0
- package/cli/share.mjs +270 -0
- package/cli/skill/SKILL.md +280 -0
- package/cli/skills.mjs +71 -0
- package/cli/state.mjs +176 -0
- package/cli/update.mjs +162 -0
- package/package.json +20 -4
- package/site/lib/quota.js +59 -0
- package/site/lib/shared-wishes.js +1727 -0
package/cli/dev.mjs
ADDED
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
// lamp dev's server: serves a folder's page, watches its files, hands each saved change to the open pages, and answers
|
|
2
|
+
// lamp's other commands (look, use, open, stop) about them.
|
|
3
|
+
//
|
|
4
|
+
// A game made with Lamp's runtime (lamp-game.js beside the page) keeps the player's place: the runtime is handed the
|
|
5
|
+
// change exactly as Lamp's app around the page hands it over (`window.__lampLive`), and swaps the code in, runs it
|
|
6
|
+
// again in place, or opens the page again from where the player was. A game's page that doesn't run on the runtime is
|
|
7
|
+
// handed it too, through lamp-live.js (given after page.js), which puts in what it safely can and holds the rest for
|
|
8
|
+
// Reload. Any other page is opened again.
|
|
9
|
+
|
|
10
|
+
import { createServer } from "node:http";
|
|
11
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
12
|
+
import { watch, statSync, mkdirSync, appendFileSync, writeFileSync, readFileSync, readdirSync, createReadStream, rmSync, realpathSync } from "node:fs";
|
|
13
|
+
import { readFile, stat, realpath } from "node:fs/promises";
|
|
14
|
+
import { networkInterfaces, hostname } from "node:os";
|
|
15
|
+
import { join, resolve, extname, sep, relative, dirname } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
import { NOTES_FILE, Problem, RESOURCES, layoutOf, stateOf, logFileOf, serverFileOf, firstPortOf } from "./state.mjs";
|
|
19
|
+
import { findBrowser, noBrowser, launch, keyNamed } from "./browser.mjs";
|
|
20
|
+
import { answer as answerAI } from "./ai.mjs";
|
|
21
|
+
|
|
22
|
+
const TYPES = {
|
|
23
|
+
".html": "text/html; charset=utf-8", ".htm": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8",
|
|
24
|
+
".mjs": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json; charset=utf-8",
|
|
25
|
+
".map": "application/json", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
26
|
+
".webp": "image/webp", ".gif": "image/gif", ".ico": "image/x-icon", ".avif": "image/avif", ".wasm": "application/wasm",
|
|
27
|
+
".glb": "model/gltf-binary", ".gltf": "model/gltf+json", ".bin": "application/octet-stream", ".gz": "application/gzip",
|
|
28
|
+
".mp3": "audio/mpeg", ".ogg": "audio/ogg", ".wav": "audio/wav", ".m4a": "audio/mp4", ".mp4": "video/mp4",
|
|
29
|
+
".webm": "video/webm", ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".txt": "text/plain; charset=utf-8",
|
|
30
|
+
".ktx2": "image/ktx2", ".csv": "text/csv; charset=utf-8", ".webmanifest": "application/manifest+json",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// The two files Lamp writes beside a game's runtime in each copy of the page, which the folder's own copy doesn't
|
|
34
|
+
// hold (`GamePage.stage`): which device the copy is on, and which build it is.
|
|
35
|
+
export const DEVICE_FILE = "lamp-device.js";
|
|
36
|
+
const BUILD_FILE = "lamp-build.js";
|
|
37
|
+
// A browser on any device, as a page published to the web is given (`GamePage.anyBrowser`).
|
|
38
|
+
export const ANY_BROWSER = `// Written by lamp for a browser on any device (any-browser); don't edit.
|
|
39
|
+
export default {"keys":{"AltLeft":"Alt / Option","AltRight":"Alt / Option","Backspace":"Backspace / Delete","Enter":"Enter / Return","MetaLeft":"Win / ⌘","MetaRight":"Win / ⌘"}};
|
|
40
|
+
`;
|
|
41
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
const PAGE_SCRIPT = readFileSync(join(HERE, "page.js"), "utf8");
|
|
43
|
+
// What a game's page is given after lamp's script, as Lamp's app gives it (`LampPageLive.gameScript`): a game that
|
|
44
|
+
// doesn't run on Lamp's runtime takes a change while it's played where it can. Read from Lamp's own copy.
|
|
45
|
+
const GAME_SCRIPT = (() => { try { return readFileSync(join(RESOURCES, "GameKit", "page", "lamp-live.js"), "utf8"); } catch { return ""; } })();
|
|
46
|
+
// Lamp's building blocks for a page (saved data, the app's files, AI through Lamp's app): lamp.js, which Lamp's app
|
|
47
|
+
// writes at each build from its own code. A page whose folder has none is given it as that code writes it, when it has
|
|
48
|
+
// been written out for the command line (scripts/cli-files.sh, cli/lamps/lamp.js), and else a stand-in, so the page loads.
|
|
49
|
+
export const LAMP_JS_STAND_IN = "// lamp's stand-in for lamp.js: Lamp's building blocks for a page aren't written out for the command line yet (scripts/cli-files.sh).\n";
|
|
50
|
+
export function lampJS(game = false) {
|
|
51
|
+
// A game's is its own (`GameKit.lampJS`): without a TV's remote, which would take the keys a game reads itself.
|
|
52
|
+
for (const name of game ? ["lamp-for-games.js", "lamp.js"] : ["lamp.js"]) {
|
|
53
|
+
try { return readFileSync(join(HERE, "lamps", name), "utf8"); } catch {}
|
|
54
|
+
}
|
|
55
|
+
return LAMP_JS_STAND_IN;
|
|
56
|
+
}
|
|
57
|
+
// The controllers a game names buttons for, which Lamp's app writes beside the page at every build and the game
|
|
58
|
+
// runtime can't load without: Lamp's list as a file (a test keeps it Lamp's, `CLIFilesTests`).
|
|
59
|
+
export const CONTROLLERS_FILE = "lamp-controllers.js", CONTROLLERS = join(HERE, "lamps", CONTROLLERS_FILE);
|
|
60
|
+
// The libraries Lamp puts beside a game's page when the page uses them (`DeviceSupport`), where they are in Lamp's
|
|
61
|
+
// own folder: a game whose folder has none of one is served Lamp's copy.
|
|
62
|
+
export const LIBRARIES = join(RESOURCES, "WebLibraries");
|
|
63
|
+
function lampsCopy(name) {
|
|
64
|
+
if (name === CONTROLLERS_FILE) return CONTROLLERS;
|
|
65
|
+
if (!/^(three|phaser|rapier)\//.test(name)) return null;
|
|
66
|
+
const file = resolve(join(LIBRARIES, name));
|
|
67
|
+
return file.startsWith(LIBRARIES + sep) ? file : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// How a page says a change went: the word lamp's commands answer with, and what it means.
|
|
71
|
+
const OUTCOMES = {
|
|
72
|
+
swapped: ["live", "swapped in while it ran"],
|
|
73
|
+
rerun: ["reran", "the game's code ran again, the player where they were"],
|
|
74
|
+
reloading: ["reopened", "the page opened again"],
|
|
75
|
+
held: ["held", "waiting for Reload to be pressed in the page, since the page must open again for it"],
|
|
76
|
+
};
|
|
77
|
+
// A change that only waits for a pause in the game goes in by itself then.
|
|
78
|
+
const HELD_FOR_A_PAUSE = "waiting for a pause in the game (the end of a round, a menu), and goes in by itself then";
|
|
79
|
+
// What a page says once it has finished with a change, or with opening.
|
|
80
|
+
const DONE = new Set(["swapped", "restored", "fresh", "held", "started", "shown"]);
|
|
81
|
+
const READY = new Set(["page-playing", "restored", "fresh", "started", "shown"]);
|
|
82
|
+
|
|
83
|
+
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
84
|
+
/**
|
|
85
|
+
* Log lines as a look shows them: a line said over and over is shown once. The game's own lines are the same only
|
|
86
|
+
* when their words and numbers are (the time at their front aside); the runtime's own, which say the frame rate every
|
|
87
|
+
* few seconds, when only their numbers differ.
|
|
88
|
+
*/
|
|
89
|
+
function collapsed(lines) {
|
|
90
|
+
const shown = [];
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
const text = `${line.level !== "log" ? line.level + ": " : ""}${line.text}`;
|
|
93
|
+
const said = text.replace(/^\[[\d.]+ s\] /, ""), shape = said.startsWith("Lamp: ") ? said.replace(/[\d.]+/g, "#") : said;
|
|
94
|
+
const last = shown[shown.length - 1];
|
|
95
|
+
if (last && last.shape === shape) { last.text = text; last.times += 1; } else shown.push({ text, shape, times: 1 });
|
|
96
|
+
}
|
|
97
|
+
return shown.map((line) => (line.times > 1 ? `${line.text} (the last of ${line.times} like it)` : line.text));
|
|
98
|
+
}
|
|
99
|
+
const isError = (line) => line.level === "error" || /^(\[[\d.]+ s\] )?Error: /.test(line.text);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Starts serving the folder. `port` asks for one port and no other; without it the folder's last is used again when
|
|
103
|
+
* it's free (a game's saves belong to the address), else the folder's own (`firstPortOf`) or the next free. `lan` lets other devices on the
|
|
104
|
+
* network open the page. `quiet` prints nothing. `idle` (minutes) ends a server nothing has used for that long, which
|
|
105
|
+
* one started by `lamp look` is given.
|
|
106
|
+
*/
|
|
107
|
+
export async function serve({ folder: given, port: asked, lan = false, quiet = false, idle = 0 } = {}) {
|
|
108
|
+
const layout = layoutOf(given ?? ".");
|
|
109
|
+
const { folder, root } = layout;
|
|
110
|
+
const state = stateOf(folder), logFile = logFileOf(layout), run = randomBytes(6).toString("hex"), token = randomBytes(24).toString("hex");
|
|
111
|
+
mkdirSync(state, { recursive: true });
|
|
112
|
+
mkdirSync(dirname(logFile), { recursive: true });
|
|
113
|
+
writeFileSync(logFile, "");
|
|
114
|
+
const print = (text) => { if (!quiet) console.log(`${new Date().toLocaleTimeString([], { hour12: false })} ${text}`); };
|
|
115
|
+
|
|
116
|
+
// MARK: The files and their changes
|
|
117
|
+
|
|
118
|
+
// What's never part of a change: hidden files and folders, what tools keep beside the code, Lamp's own two files,
|
|
119
|
+
// and the app's notes, which no page runs (other Markdown may be content a page loads, so it is a change).
|
|
120
|
+
const ignored = (path) => path.split("/").some((part) => part.startsWith(".") || part === "node_modules") || path.endsWith("~")
|
|
121
|
+
|| path === DEVICE_FILE || path === BUILD_FILE || path === NOTES_FILE;
|
|
122
|
+
const filesUnder = (dir = root, prefix = "", found = []) => {
|
|
123
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
124
|
+
const path = prefix + entry.name;
|
|
125
|
+
if (ignored(path)) continue;
|
|
126
|
+
if (entry.isDirectory()) filesUnder(join(dir, entry.name), path + "/", found);
|
|
127
|
+
else if (entry.isFile()) found.push(path);
|
|
128
|
+
}
|
|
129
|
+
return found;
|
|
130
|
+
};
|
|
131
|
+
// A file as it last was, to tell a real change from a save that changed nothing: its size and when it was written.
|
|
132
|
+
const stamp = (path) => { try { const s = statSync(join(root, path)); return s.isFile() ? `${s.size}:${s.mtimeMs}` : null; } catch { return null; } };
|
|
133
|
+
const known = new Map(filesUnder().map((path) => [path, stamp(path)]));
|
|
134
|
+
// A name for the page's files as they are, so a copy opened after a change knows it's a newer build.
|
|
135
|
+
const buildName = () => {
|
|
136
|
+
const hash = createHash("sha256");
|
|
137
|
+
for (const path of [...known.keys()].sort()) hash.update(path).update(known.get(path) ?? "");
|
|
138
|
+
return hash.digest("hex").slice(0, 16);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
let gen = 0, lastLookGen = 0;
|
|
142
|
+
const changes = []; // the last changes: { gen, changed, removed, said: Map(tab → { state, why }) }
|
|
143
|
+
const pages = new Map(); // page id → { response, tab, at: the change it has taken, ready, since }
|
|
144
|
+
const reopening = new Map(); // tab → when the page it said it's opening again is given up on
|
|
145
|
+
const early = new Map(); // what a page said before its line for changes was open
|
|
146
|
+
// Every file of the page a page asked for, by any page since the server started: a change says them, so a game's page
|
|
147
|
+
// that doesn't run on the runtime tells a file it uses from one it doesn't (lamp-live.js), which no page can see itself.
|
|
148
|
+
const served = new Set();
|
|
149
|
+
const printed = new Set();
|
|
150
|
+
|
|
151
|
+
const named = (change) => {
|
|
152
|
+
const what = [...change.changed, ...change.removed.map((path) => `${path} (removed)`)];
|
|
153
|
+
return what.length > 4 ? `${what.slice(0, 3).join(", ")} and ${what.length - 3} more` : what.join(", ");
|
|
154
|
+
};
|
|
155
|
+
const tell = (page, event, data, id) => page.response.write(`${id === undefined ? "" : `id: ${id}\n`}event: ${event}\ndata: ${data}\n\n`);
|
|
156
|
+
|
|
157
|
+
// What the coding agent said it's changing (`lamp changing`), for the card each page shows as a change goes in: the
|
|
158
|
+
// changes saved in the few minutes after say it, then it's let go.
|
|
159
|
+
let changing = null;
|
|
160
|
+
// What's changing, as the agent last said it: it stands for every save after it, until it says something new or half an
|
|
161
|
+
// hour has gone by.
|
|
162
|
+
const changingWords = () => (changing && Date.now() - changing.at < 30 * 60_000 ? { words: changing.words } : {});
|
|
163
|
+
|
|
164
|
+
function broadcast(changed, removed) {
|
|
165
|
+
gen += 1;
|
|
166
|
+
const change = { gen, changed, removed, said: new Map(), line: logCount };
|
|
167
|
+
changes.push(change);
|
|
168
|
+
if (changes.length > 40) changes.shift();
|
|
169
|
+
const data = JSON.stringify({ gen, changed, removed, now: false, pill: true, read: [...served], ...changingWords() });
|
|
170
|
+
for (const page of pages.values()) { page.told = Date.now(); tell(page, "change", data, gen); }
|
|
171
|
+
if (!pages.size) print(`${named(change)}: no page is open; it's in the next one opened`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Hands the pages whichever of these files have changed or gone since they were last seen. */
|
|
175
|
+
function check(paths) {
|
|
176
|
+
const changed = [], removed = [];
|
|
177
|
+
for (const path of paths) {
|
|
178
|
+
const now = stamp(path), before = known.get(path);
|
|
179
|
+
if (now === before || (now === null && before === undefined)) continue;
|
|
180
|
+
if (now === null) { known.delete(path); removed.push(path); }
|
|
181
|
+
else { known.set(path, now); changed.push(path); }
|
|
182
|
+
}
|
|
183
|
+
if (changed.length || removed.length) broadcast(changed.sort(), removed.sort());
|
|
184
|
+
}
|
|
185
|
+
// Saves are gathered for a moment, so a change of several files goes to the pages as one.
|
|
186
|
+
let pending = new Set(), timer = null;
|
|
187
|
+
function noticed(path) {
|
|
188
|
+
if (!path) return;
|
|
189
|
+
path = path.split(sep).join("/");
|
|
190
|
+
if (ignored(path)) return;
|
|
191
|
+
pending.add(path);
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
timer = setTimeout(() => { timer = null; const paths = pending; pending = new Set(); check(paths); }, 120);
|
|
194
|
+
}
|
|
195
|
+
// A look straight after a save can come before the system says the file changed: every file is checked then.
|
|
196
|
+
function checkAll() {
|
|
197
|
+
clearTimeout(timer);
|
|
198
|
+
timer = null;
|
|
199
|
+
pending = new Set();
|
|
200
|
+
check(new Set([...known.keys(), ...filesUnder()]));
|
|
201
|
+
}
|
|
202
|
+
const watcher = watch(root, { recursive: true }, (_event, name) => noticed(name));
|
|
203
|
+
// A folder taken away while it's watched ends the watching, not lamp: a look still checks every file.
|
|
204
|
+
watcher.on("error", () => {});
|
|
205
|
+
// The page's files are those in its folder: a link in it to somewhere else isn't followed out.
|
|
206
|
+
const rootReal = realpathSync(root);
|
|
207
|
+
|
|
208
|
+
// MARK: What the pages say
|
|
209
|
+
|
|
210
|
+
// The log: the last lines kept here, each with its number, and all of it in the file.
|
|
211
|
+
const log = [];
|
|
212
|
+
let logCount = 0, lastLookLine = 0, errorsPrinted = 0, errorsFrom = 0, lastErrorAt = 0;
|
|
213
|
+
let takenOver = null; // why the person has the game's controller back, when they took it from lamp
|
|
214
|
+
function logged(lines) {
|
|
215
|
+
for (const line of lines) {
|
|
216
|
+
const entry = { n: ++logCount, level: String(line.level || "log"), text: String(line.text ?? "") };
|
|
217
|
+
log.push(entry);
|
|
218
|
+
if (isError(entry)) {
|
|
219
|
+
lastErrorAt = Date.now();
|
|
220
|
+
// A page stuck on an error says it every frame: a few a second are enough here, and all are in the file.
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
if (now - errorsFrom > 1000) { errorsFrom = now; errorsPrinted = 0; }
|
|
223
|
+
if (++errorsPrinted <= 3) print(` ! ${entry.text.split("\n")[0].slice(0, 300)}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (log.length > 600) log.splice(0, log.length - 600);
|
|
227
|
+
try { appendFileSync(logFile, lines.map((line) => `${line.level && line.level !== "log" ? line.level + ": " : ""}${line.text}\n`).join("")); } catch {}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function said(message) {
|
|
231
|
+
const { page: id, gen: about, state, why, pause } = message;
|
|
232
|
+
if (state === "taken-over") { takenOver = why || "the person played"; return; }
|
|
233
|
+
const page = pages.get(id);
|
|
234
|
+
if (!page && id) {
|
|
235
|
+
// Its line for changes isn't open yet: this is heard once it is.
|
|
236
|
+
if (early.size > 20) early.clear();
|
|
237
|
+
early.set(id, [...(early.get(id) ?? []), message].slice(-10));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (page) {
|
|
241
|
+
// A page on Lamp's runtime says so as the runtime loads, and says when the game is playing; any other page is
|
|
242
|
+
// taken as open once it has loaded, which is all it can say.
|
|
243
|
+
if (state === "page-loading") page.runtime = true;
|
|
244
|
+
if ((READY.has(state) || (state === "page-loaded" && !page.runtime)) && !page.ready) { page.ready = true; page.readyAt = Date.now(); }
|
|
245
|
+
if (DONE.has(state)) {
|
|
246
|
+
page.at = typeof about === "number" ? Math.max(page.at, about) : gen;
|
|
247
|
+
// The game's code has run again: its page was put back as it first was, loading screen and all, and is a
|
|
248
|
+
// moment putting that away, as a page just opened is.
|
|
249
|
+
if (page.rerunning) page.readyAt = Date.now();
|
|
250
|
+
page.rerunning = 0;
|
|
251
|
+
}
|
|
252
|
+
if (state === "rerun") page.rerunning = Date.now() + 15_000;
|
|
253
|
+
if (state === "reloading") reopening.set(page.tab, Date.now() + 30_000);
|
|
254
|
+
}
|
|
255
|
+
const change = changes.find((change) => change.gen === about);
|
|
256
|
+
// How the change went in a tab is the last such thing its page said of it (code that couldn't run again opens the
|
|
257
|
+
// page again), with the first reason it gave: why it wasn't simply swapped in.
|
|
258
|
+
if (change && OUTCOMES[state] && page) change.said.set(page.tab, { state, pause: !!pause, why: change.said.get(page.tab)?.why || why || "" });
|
|
259
|
+
if (!OUTCOMES[state] && state !== "restored" && state !== "fresh") return;
|
|
260
|
+
const key = `${about}:${state}`;
|
|
261
|
+
if (printed.has(key)) return;
|
|
262
|
+
printed.add(key);
|
|
263
|
+
if (printed.size > 400) printed.clear();
|
|
264
|
+
const words = state === "held" && pause ? HELD_FOR_A_PAUSE : OUTCOMES[state]?.[1] ?? (state === "restored" ? "back where the player was" : "started fresh");
|
|
265
|
+
print(`${change ? named(change) + " → " : ""}${words}${why ? ` (${why})` : ""}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* What's still under way, in words, or null once the page has taken the last change and is past opening: the page
|
|
270
|
+
* in this tab when one is named (the one being looked at), else every open page.
|
|
271
|
+
*/
|
|
272
|
+
function underWay(tab) {
|
|
273
|
+
const now = Date.now();
|
|
274
|
+
for (const [one, until] of reopening) if (until < now) reopening.delete(one);
|
|
275
|
+
if (timer || pending.size) return "a save was just made";
|
|
276
|
+
if (tab ? reopening.has(tab) : reopening.size) return "the page was opening again";
|
|
277
|
+
const mine = [...pages.values()].filter((page) => !tab || page.tab === tab);
|
|
278
|
+
if (tab && !mine.length) return "the page was opening";
|
|
279
|
+
for (const page of mine) {
|
|
280
|
+
// A page that logged an error and then went quiet isn't going to say more: what it shows is looked at as it is.
|
|
281
|
+
const failed = (since) => lastErrorAt > since && now - lastErrorAt > 1500;
|
|
282
|
+
// A game with an older runtime never says it's playing: it's taken as open after a while.
|
|
283
|
+
if (!page.ready && now - page.since < 25_000 && !failed(page.since)) return page.runtime ? "the game was still loading" : "the page was still opening";
|
|
284
|
+
if (page.at < gen && now - page.told < 20_000 && !failed(page.told)) return "the page hadn't said how the last change went";
|
|
285
|
+
if (page.rerunning > now && !failed(page.told)) return "the game's code was running again";
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
async function waitUntilSettled(ms, tab) {
|
|
290
|
+
const until = Date.now() + ms;
|
|
291
|
+
while (Date.now() < until) { if (!underWay(tab)) return null; await sleep(50); }
|
|
292
|
+
return underWay(tab);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// MARK: The browser
|
|
296
|
+
|
|
297
|
+
let address = "";
|
|
298
|
+
let windowBrowser = null; // the window the person uses
|
|
299
|
+
let copyBrowser = null; // a copy with no window, looked at when there's none
|
|
300
|
+
let copyTimer = null, lastUsed = Date.now(), pictures = 0, copySized = false;
|
|
301
|
+
// The pictures of an earlier run are no use to this one.
|
|
302
|
+
for (const name of readdirSync(state)) if (/^look(-\d+)?\.png$/.test(name)) rmSync(join(state, name), { force: true });
|
|
303
|
+
const lookers = new Map(); // a look asked of a page itself → who's waiting for it
|
|
304
|
+
|
|
305
|
+
async function openWindow() {
|
|
306
|
+
lastUsed = Date.now();
|
|
307
|
+
if (windowBrowser?.alive) { (await windowBrowser.page(address))?.front(); return "window"; }
|
|
308
|
+
const path = findBrowser();
|
|
309
|
+
if (!path) {
|
|
310
|
+
// No browser lamp can look into: the person's own opens the page, and looks are the page's own words.
|
|
311
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
312
|
+
spawn(opener, [address], { stdio: "ignore", shell: process.platform === "win32", detached: true }).unref();
|
|
313
|
+
return "browser";
|
|
314
|
+
}
|
|
315
|
+
const browser = await launch({ path, url: address, profile: join(state, "browser"), window: true });
|
|
316
|
+
windowBrowser = browser;
|
|
317
|
+
browser.onGone(() => { if (windowBrowser === browser) windowBrowser = null; });
|
|
318
|
+
// The window is what's looked at from now on: a copy running the same game beside it would only slow it.
|
|
319
|
+
copyBrowser?.close();
|
|
320
|
+
return "window";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** The page lamp looks at and uses: the person's window when it's open, else the copy. Null with no browser to drive. */
|
|
324
|
+
async function watched() {
|
|
325
|
+
lastUsed = Date.now();
|
|
326
|
+
if (windowBrowser?.alive) {
|
|
327
|
+
const page = await windowBrowser.page(address);
|
|
328
|
+
if (page) return { page, where: "window", fresh: false };
|
|
329
|
+
}
|
|
330
|
+
const fresh = !copyBrowser?.alive;
|
|
331
|
+
if (fresh) {
|
|
332
|
+
const path = findBrowser();
|
|
333
|
+
if (!path) return null;
|
|
334
|
+
const browser = await launch({ path, url: address, profile: join(state, "browser-copy"), window: false });
|
|
335
|
+
copyBrowser = browser;
|
|
336
|
+
browser.onGone(() => { if (copyBrowser === browser) copyBrowser = null; });
|
|
337
|
+
}
|
|
338
|
+
// The copy runs the page for as long as it's looked at, and a while after.
|
|
339
|
+
clearTimeout(copyTimer);
|
|
340
|
+
copyTimer = setTimeout(() => copyBrowser?.close(), 10 * 60_000);
|
|
341
|
+
copyTimer.unref();
|
|
342
|
+
const page = await copyBrowser.page(address);
|
|
343
|
+
return page ? { page, where: "copy", fresh } : null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** What the page shows now: how the changes since the last look went, a picture, the screen as text, and the log. */
|
|
347
|
+
async function look({ wait = 20, reopen = false, size = null } = {}) {
|
|
348
|
+
checkAll();
|
|
349
|
+
let target = null, problem = null;
|
|
350
|
+
try { target = await watched(); } catch (error) { problem = error.message; }
|
|
351
|
+
// With no browser lamp can drive, the page is opened in one it can't, and a look straight after can come before
|
|
352
|
+
// the page has said it's open: the look waits for it, as long as it's given to wait.
|
|
353
|
+
if (!target && !problem) {
|
|
354
|
+
const until = Date.now() + Math.max(0, wait) * 1000;
|
|
355
|
+
while (!pages.size && Date.now() < until) await sleep(100);
|
|
356
|
+
}
|
|
357
|
+
// Another size is for lamp's own copy: the person's window is the size they made it.
|
|
358
|
+
if (target?.where === "copy") {
|
|
359
|
+
if (size || copySized) await target.page.size(size?.[0], size?.[1]).catch(() => {});
|
|
360
|
+
copySized = !!size;
|
|
361
|
+
if (size) await sleep(400);
|
|
362
|
+
} else if (size) {
|
|
363
|
+
problem = "The size asked for wasn't used: the person's window is open, and it's the size they made it. Close it to look at other sizes.";
|
|
364
|
+
}
|
|
365
|
+
// Which tab that is, which lasts through the page opening again: only its page is waited for.
|
|
366
|
+
let tab = null;
|
|
367
|
+
for (let tries = 0; target && !tab && tries < 40; tries++) {
|
|
368
|
+
tab = await target.page.evaluate("window.__lampTab || null", { timeout: 1000 });
|
|
369
|
+
if (!tab) await sleep(100);
|
|
370
|
+
}
|
|
371
|
+
let unfinished = await waitUntilSettled(Math.max(0, wait) * 1000, tab);
|
|
372
|
+
if (reopen) {
|
|
373
|
+
// The page opens again from its beginning, a game with its saves kept: what a person opening it sees. A copy
|
|
374
|
+
// only just started is opened again too, since a game puts back the place it was last left in.
|
|
375
|
+
const open = [...pages.values()].filter((page) => !tab || page.tab === tab);
|
|
376
|
+
for (const page of open) { reopening.set(page.tab, Date.now() + 30_000); page.calm = false; tell(page, "reload", "fresh"); }
|
|
377
|
+
if (open.length) unfinished = await waitUntilSettled(Math.max(0, wait) * 1000, tab);
|
|
378
|
+
}
|
|
379
|
+
// A page that has only just opened is still putting its loading screen away.
|
|
380
|
+
const mine = [...pages.values()].filter((page) => !tab || page.tab === tab);
|
|
381
|
+
const opened = Math.max(0, ...mine.map((page) => page.readyAt ?? 0));
|
|
382
|
+
if (Date.now() - opened < 1000) await sleep(1000 - (Date.now() - opened));
|
|
383
|
+
// A page that isn't on Lamp's runtime can't say when what it loads after opening is in: the first look at it
|
|
384
|
+
// waits for its screen to stop changing, for a while. A screen that's the same twice running has stopped; one
|
|
385
|
+
// where only numbers change may be a counter of what's loading or a game's own (its frame rate, a clock), so it's
|
|
386
|
+
// given a few seconds of that before it's taken as it is.
|
|
387
|
+
const plain = target && !unfinished ? mine.find((page) => !page.runtime && !page.calm) : null;
|
|
388
|
+
if (plain) {
|
|
389
|
+
const words = async () => String((await target.page.evaluate("window.__lampLook ? window.__lampLook().screen : ''")) ?? "");
|
|
390
|
+
const shape = (text) => text.replace(/[\d.,:%]+/g, "#");
|
|
391
|
+
const until = Date.now() + Math.min(12_000, Math.max(0, wait) * 1000);
|
|
392
|
+
let before = await words(), sameShapeSince = Date.now();
|
|
393
|
+
while (Date.now() < until) {
|
|
394
|
+
await sleep(700);
|
|
395
|
+
const now = await words();
|
|
396
|
+
if (now === before) break;
|
|
397
|
+
if (shape(now) !== shape(before)) sameShapeSince = Date.now();
|
|
398
|
+
else if (Date.now() - sameShapeSince > 5000) break;
|
|
399
|
+
before = now;
|
|
400
|
+
}
|
|
401
|
+
plain.calm = true;
|
|
402
|
+
}
|
|
403
|
+
let seen = null, picture = null, where = target?.where ?? null;
|
|
404
|
+
if (target) {
|
|
405
|
+
// Two frames on, so what's pictured has the change drawn, and long enough for what the page just logged to come.
|
|
406
|
+
await Promise.all([sleep(300), target.page.evaluate("new Promise(done => { setTimeout(done, 400); requestAnimationFrame(() => requestAnimationFrame(done)); })", { timeout: 1500 })]);
|
|
407
|
+
seen = await target.page.evaluate("window.__lampLook ? window.__lampLook() : null");
|
|
408
|
+
try {
|
|
409
|
+
// Each look's picture is a file of its own, so one already read is never taken for the next; a few are kept.
|
|
410
|
+
const bytes = await target.page.picture(seen ?? {});
|
|
411
|
+
picture = join(state, `look-${++pictures}.png`);
|
|
412
|
+
writeFileSync(picture, bytes);
|
|
413
|
+
rmSync(join(state, `look-${pictures - 40}.png`), { force: true });
|
|
414
|
+
} catch (error) { problem = `No picture: ${error.message}.`; }
|
|
415
|
+
} else if (pages.size) {
|
|
416
|
+
// Any open page says what's on it; there's no picture without a browser lamp can look into.
|
|
417
|
+
const ask = randomBytes(4).toString("hex"), page = [...pages.values()].pop();
|
|
418
|
+
seen = await new Promise((resolve) => {
|
|
419
|
+
// An answer ends the wait for one, so nothing is left waiting after it.
|
|
420
|
+
const given = setTimeout(() => { lookers.delete(ask); resolve(null); }, 3000);
|
|
421
|
+
lookers.set(ask, (answer) => { clearTimeout(given); resolve(answer); });
|
|
422
|
+
tell(page, "look", ask);
|
|
423
|
+
});
|
|
424
|
+
where = "page";
|
|
425
|
+
problem ??= `No picture. ${noBrowser()}`;
|
|
426
|
+
} else {
|
|
427
|
+
problem ??= `No page has opened${wait > 0 ? ` in ${wait} s` : ""}: open ${address} in a browser, then look again. ${noBrowser()}`;
|
|
428
|
+
}
|
|
429
|
+
// How each change since the last look went in the page looked at, or in any page when that one never said. A
|
|
430
|
+
// change a look said was held is said again once it has gone in.
|
|
431
|
+
const since = changes.map((change) => {
|
|
432
|
+
const answer = change.said.get(seen?.tab ?? tab) ?? [...change.said.values()][0];
|
|
433
|
+
const state = answer?.state ?? null;
|
|
434
|
+
if (change.gen <= lastLookGen && !(change.shown === "held" && state !== "held")) return null;
|
|
435
|
+
const wasHeld = change.shown === "held";
|
|
436
|
+
change.shown = state;
|
|
437
|
+
if (answer) {
|
|
438
|
+
const means = state === "held" && answer.pause ? HELD_FOR_A_PAUSE : OUTCOMES[state][1];
|
|
439
|
+
return { files: named(change), outcome: OUTCOMES[state][0], means: wasHeld ? `${means}; it was held, and has gone in now` : means, why: answer.why };
|
|
440
|
+
}
|
|
441
|
+
return pages.size ? { files: named(change), outcome: "unanswered", means: "the page hasn't said how it went", why: "" }
|
|
442
|
+
: { files: named(change), outcome: "opened", means: "no page was open; it's in the one opened now", why: "" };
|
|
443
|
+
}).filter(Boolean);
|
|
444
|
+
const fresh = log.filter((line) => line.n > lastLookLine);
|
|
445
|
+
// An error said over and over is said once, with how many times. One logged before the last change went out may
|
|
446
|
+
// be of a save that change replaced, so it says which it is.
|
|
447
|
+
const lastChange = changes[changes.length - 1], counts = new Map();
|
|
448
|
+
for (const line of fresh.filter(isError)) {
|
|
449
|
+
const text = lastChange && lastChange.gen > lastLookGen && line.n <= lastChange.line ? `${line.text} (before the last change)` : line.text;
|
|
450
|
+
counts.set(text, (counts.get(text) ?? 0) + 1);
|
|
451
|
+
}
|
|
452
|
+
const errors = [...counts].map(([text, times]) => (times > 1 ? `${text} (${times} times)` : text));
|
|
453
|
+
lastLookGen = gen;
|
|
454
|
+
lastLookLine = logCount;
|
|
455
|
+
return {
|
|
456
|
+
where, opened: !!target?.fresh, playing: mine.some((page) => page.runtime && page.ready) ? true : mine.some((page) => page.runtime) ? false : null, address, picture, title: seen?.title ?? null, width: seen?.width ?? null, height: seen?.height ?? null,
|
|
457
|
+
screen: seen?.screen ?? null, game: layout.game, unfinished, changes: since, errors: errors.slice(-20),
|
|
458
|
+
// What the page logged since the last look: the end of it when there's a lot.
|
|
459
|
+
log: collapsed(fresh).slice(-25), logLines: fresh.length, logFile, problem,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* What a link to the page shows of it: a picture of it as it is now (a JPEG, small enough for the link's card), the
|
|
465
|
+
* stage a game is drawn on when it has one, and the errors a look would report. No picture with no browser to look in.
|
|
466
|
+
*/
|
|
467
|
+
async function card(options = {}) {
|
|
468
|
+
const seen = await look(options);
|
|
469
|
+
let target = null;
|
|
470
|
+
try { target = await watched(); } catch {}
|
|
471
|
+
if (!target) return { picture: null, stage: null, errors: seen.errors, playing: seen.playing };
|
|
472
|
+
const view = await target.page.evaluate("window.__lampLook ? window.__lampLook() : null");
|
|
473
|
+
let picture = null;
|
|
474
|
+
try { picture = (await target.page.picture({ ...(view ?? {}), jpeg: true })).toString("base64"); } catch {}
|
|
475
|
+
return { picture, stage: view?.stage ?? null, errors: seen.errors, playing: seen.playing };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Things done in the page as a person would, one after another with nothing between them, then a look. */
|
|
479
|
+
async function use(asked) {
|
|
480
|
+
const moves = Array.isArray(asked?.moves) ? asked.moves : [asked];
|
|
481
|
+
const target = await watched();
|
|
482
|
+
if (!target) throw new Problem(`Nothing was done. ${noBrowser()}`);
|
|
483
|
+
// A page still opening, or opening again after a save, would lose what's pressed before it's in: each use waits
|
|
484
|
+
// for its tab, and makes no move in a page that hasn't settled by then.
|
|
485
|
+
let tab = null;
|
|
486
|
+
for (let tries = 0; !tab && tries < 40; tries++) { tab = await target.page.evaluate("window.__lampTab || null", { timeout: 1000 }); if (!tab) await sleep(100); }
|
|
487
|
+
const unsettled = await waitUntilSettled(30_000, tab);
|
|
488
|
+
if (unsettled) throw new Problem(`Nothing was done: ${unsettled}. Try again in a moment.`);
|
|
489
|
+
await target.page.focus();
|
|
490
|
+
const did = [];
|
|
491
|
+
for (const move of moves) {
|
|
492
|
+
did.push(await act(target.page, move));
|
|
493
|
+
// The game is the person's again: nothing more is pressed in it.
|
|
494
|
+
if (takenOver) { if (did.length < moves.length) did.push(`Left the other ${moves.length - did.length} undone.`); break; }
|
|
495
|
+
}
|
|
496
|
+
return { did: did.join("\n"), ...(await look({ wait: 5 })) };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function act(page, action) {
|
|
500
|
+
let did;
|
|
501
|
+
const number = (value, what) => { const n = Number(value); if (!Number.isFinite(n)) throw new Problem(what); return n; };
|
|
502
|
+
if (action.kind === "key") {
|
|
503
|
+
const seconds = Math.min(Math.max(Number(action.seconds) || 0, 0), 10);
|
|
504
|
+
for (const one of String(action.key).split("+")) keyNamed(one);
|
|
505
|
+
await page.key(action.key, seconds);
|
|
506
|
+
did = seconds ? `Held ${action.key} for ${seconds} s.` : `Pressed ${action.key}.`;
|
|
507
|
+
} else if (action.kind === "stick" || action.kind === "button") {
|
|
508
|
+
// Lamp's own controller in the game (lamp-game.js, "Lamp's hands on the controller"): the first player's, held
|
|
509
|
+
// in place of the person's own until the hold ends or they play.
|
|
510
|
+
const buttons = await page.evaluate("import(new URL('lamp-game.js', document.baseURI).href).then(m => window.__lampDrive ? m.BUTTON : null, () => null)");
|
|
511
|
+
if (!buttons) throw new Problem("this page has no controller of Lamp's to hold (it isn't a game on Lamp's runtime): use key");
|
|
512
|
+
const pad = { lx: 0, ly: 0, rx: 0, ry: 0, lt: 0, rt: 0, down: 0 };
|
|
513
|
+
const held = String(action.kind === "button" ? action.buttons ?? "" : action.buttons ?? "").toLowerCase().split("+").filter(Boolean);
|
|
514
|
+
for (const one of held) {
|
|
515
|
+
if (one === "lt" || one === "rt") pad[one] = 1;
|
|
516
|
+
else if (buttons[one.toUpperCase()]) pad.down |= buttons[one.toUpperCase()];
|
|
517
|
+
else throw new Problem(`${one} isn't a button: ${Object.keys(buttons).map((name) => name.toLowerCase()).join(", ")}, lt, rt`);
|
|
518
|
+
}
|
|
519
|
+
let words;
|
|
520
|
+
if (action.kind === "stick") {
|
|
521
|
+
const side = String(action.side ?? "").toLowerCase();
|
|
522
|
+
const x = number(action.x, "stick takes left or right, then x and y from -1 to 1 (right and up are 1): lamp use stick left 0 1 2");
|
|
523
|
+
const y = number(action.y, "stick takes left or right, then x and y from -1 to 1 (right and up are 1): lamp use stick left 0 1 2");
|
|
524
|
+
if ((side !== "left" && side !== "right") || Math.abs(x) > 1 || Math.abs(y) > 1) throw new Problem("stick takes left or right, then x and y from -1 to 1 (right and up are 1): lamp use stick left 0 1 2");
|
|
525
|
+
pad[side === "left" ? "lx" : "rx"] = x;
|
|
526
|
+
pad[side === "left" ? "ly" : "ry"] = y;
|
|
527
|
+
words = `the ${side} stick at ${x}, ${y}${held.length ? ` with ${held.join(" and ").toUpperCase()}` : ""}`;
|
|
528
|
+
} else {
|
|
529
|
+
if (!held.length) throw new Problem("button takes a button's name: lamp use button a");
|
|
530
|
+
words = held.join(" and ").toUpperCase();
|
|
531
|
+
}
|
|
532
|
+
const seconds = Math.min(Math.max(Number(action.seconds) || (action.kind === "stick" ? 1 : 0.15), 0.05), 10);
|
|
533
|
+
const batch = randomBytes(4).toString("hex"), name = `${batch}-1`, quoted = JSON.stringify;
|
|
534
|
+
takenOver = null;
|
|
535
|
+
await page.evaluate(`(() => { window.__lampOwn && window.__lampOwn(${seconds + 1}, ${quoted(batch)}); return window.__lampDrive(${quoted({ ...pad, seconds })}, ${quoted(name)}, ${quoted(batch)}); })()`);
|
|
536
|
+
const until = Date.now() + seconds * 1000;
|
|
537
|
+
while (Date.now() < until && !takenOver) await sleep(40);
|
|
538
|
+
await page.evaluate(`(() => { window.__lampDrive && window.__lampDrive(null, ${quoted(name)}); window.__lampOwn && window.__lampOwn(null, ${quoted(batch)}); })()`);
|
|
539
|
+
did = takenOver ? `Let go of ${words}: the person took the controller back (${takenOver}). The game is theirs; don't take it again unless they ask.`
|
|
540
|
+
: `Held ${words} for ${seconds} s.`;
|
|
541
|
+
} else if (action.kind === "drag") {
|
|
542
|
+
const usage = "drag takes where it starts and where it ends: lamp use drag 400 300 700 300";
|
|
543
|
+
const [x, y, toX, toY] = [action.x, action.y, action.toX, action.toY].map((value) => number(value, usage));
|
|
544
|
+
await page.drag(x, y, toX, toY);
|
|
545
|
+
did = `Dragged from ${x},${y} to ${toX},${toY}.`;
|
|
546
|
+
} else if (action.kind === "scroll") {
|
|
547
|
+
const points = number(action.points, "scroll takes how far, down being positive: lamp use scroll 600");
|
|
548
|
+
const size = (await page.evaluate("({ w: innerWidth, h: innerHeight })")) ?? { w: 800, h: 600 };
|
|
549
|
+
await page.scroll(Number.isFinite(Number(action.x)) ? Number(action.x) : size.w / 2, Number.isFinite(Number(action.y)) ? Number(action.y) : size.h / 2, points);
|
|
550
|
+
await sleep(250);
|
|
551
|
+
did = `Scrolled ${points > 0 ? "down" : "up"} ${Math.abs(points)}.`;
|
|
552
|
+
} else if (action.kind === "click") {
|
|
553
|
+
// By its name in the screen as text, or by a point of the picture.
|
|
554
|
+
const named = action.target ? await page.evaluate(`window.__lampPoint ? window.__lampPoint(${JSON.stringify(String(action.target))}) : null`) : null;
|
|
555
|
+
if (action.target && !named) throw new Problem(`nothing on the screen is called "${action.target}": use a name from the screen as text, or a point of the picture`);
|
|
556
|
+
const x = named ? named[0] : Number(action.x), y = named ? named[1] : Number(action.y);
|
|
557
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Problem("click takes a point or a name: lamp use click 640 400, or lamp use click \"Add\"");
|
|
558
|
+
const at = await page.evaluate(`window.__lampAt ? window.__lampAt(${x}, ${y}) : null`);
|
|
559
|
+
await page.click(x, y);
|
|
560
|
+
did = `Clicked ${x},${y}${at ? `: ${at}` : ""}.`;
|
|
561
|
+
} else if (action.kind === "type") {
|
|
562
|
+
await page.type(String(action.text ?? ""));
|
|
563
|
+
did = `Typed "${action.text}".`;
|
|
564
|
+
} else if (action.kind === "wait") {
|
|
565
|
+
const asked = Math.max(Number(action.seconds) || 1, 0), seconds = Math.min(asked, 30);
|
|
566
|
+
await sleep(seconds * 1000);
|
|
567
|
+
did = `Waited ${seconds} s${asked > seconds ? " (the most one wait takes: wait again for longer)" : ""}.`;
|
|
568
|
+
} else {
|
|
569
|
+
throw new Problem("use takes key, click, type, drag, scroll, stick, button or wait");
|
|
570
|
+
}
|
|
571
|
+
return did;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// One look or use at a time: two at once would each start a copy of the page, and press keys over each other.
|
|
575
|
+
let turns = Promise.resolve();
|
|
576
|
+
const inTurn = (work) => {
|
|
577
|
+
const mine = turns.then(work, work);
|
|
578
|
+
turns = mine.catch(() => {});
|
|
579
|
+
return mine;
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// MARK: The server
|
|
583
|
+
|
|
584
|
+
// Whose requests are answered: this computer's own name for itself, and with `lan` its names on the network. A page
|
|
585
|
+
// elsewhere that points a name of its own at this computer is refused.
|
|
586
|
+
const hosts = new Set(["localhost", "127.0.0.1"]);
|
|
587
|
+
const lanAddresses = [];
|
|
588
|
+
if (lan) {
|
|
589
|
+
for (const list of Object.values(networkInterfaces())) {
|
|
590
|
+
for (const one of list ?? []) if (one.family === "IPv4" && !one.internal) { hosts.add(one.address); lanAddresses.push(one.address); }
|
|
591
|
+
}
|
|
592
|
+
const name = hostname().toLowerCase();
|
|
593
|
+
hosts.add(name);
|
|
594
|
+
hosts.add(name.endsWith(".local") ? name : `${name}.local`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function body(request) {
|
|
598
|
+
const chunks = [];
|
|
599
|
+
let size = 0;
|
|
600
|
+
for await (const chunk of request) { chunks.push(chunk); size += chunk.length; if (size > 4_000_000) return null; }
|
|
601
|
+
try { return JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch { return null; }
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const server = createServer((request, response) => {
|
|
605
|
+
// Nothing a request holds ends lamp: what can't be answered is refused.
|
|
606
|
+
answer(request, response).catch(() => { try { if (!response.headersSent) response.writeHead(400); response.end(); } catch {} });
|
|
607
|
+
});
|
|
608
|
+
async function answer(request, response) {
|
|
609
|
+
const url = new URL(request.url, "http://localhost");
|
|
610
|
+
const headers = { "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" };
|
|
611
|
+
const send = (status, type, content) => {
|
|
612
|
+
response.writeHead(status, { "Content-Type": type, ...headers });
|
|
613
|
+
response.end(request.method === "HEAD" ? undefined : content);
|
|
614
|
+
};
|
|
615
|
+
const json = (status, value) => send(status, TYPES[".json"], JSON.stringify(value));
|
|
616
|
+
const host = String(request.headers.host || "").replace(/:\d+$/, "").toLowerCase();
|
|
617
|
+
if (!hosts.has(host)) return send(403, "text/plain", "Not this page's server");
|
|
618
|
+
// Nothing is changed here by a page of another site.
|
|
619
|
+
if (request.method !== "GET" && request.method !== "HEAD" && request.headers.origin && request.headers.origin !== `http://${request.headers.host}`) {
|
|
620
|
+
return send(403, "text/plain", "Not this page's server");
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// lamp's own commands: they hold the token this server wrote for them.
|
|
624
|
+
if (url.pathname.startsWith("/__lamp/cli/")) {
|
|
625
|
+
if (request.headers["x-lamp-token"] !== token) return json(403, { problem: "that isn't lamp's own command" });
|
|
626
|
+
const what = url.pathname.slice("/__lamp/cli/".length);
|
|
627
|
+
try {
|
|
628
|
+
if (what === "hello") return json(200, { folder, url: address, pid: process.pid, run, own: idle > 0 && !windowBrowser?.alive });
|
|
629
|
+
if (request.method !== "POST") return json(405, { problem: "not a thing to ask that way" });
|
|
630
|
+
const asked = (await body(request)) ?? {};
|
|
631
|
+
if (what === "look") return json(200, await inTurn(() => look(asked)));
|
|
632
|
+
if (what === "use") return json(200, await inTurn(() => use(asked)));
|
|
633
|
+
if (what === "card") return json(200, await inTurn(() => card(asked)));
|
|
634
|
+
if (what === "open") return json(200, { opened: await openWindow(), url: address });
|
|
635
|
+
if (what === "changing") {
|
|
636
|
+
const words = String(asked.words ?? "").replace(/\s+/g, " ").trim();
|
|
637
|
+
changing = words ? { words: words.length > 120 ? `${words.slice(0, 119).trimEnd()}…` : words, at: Date.now() } : null;
|
|
638
|
+
return json(200, { pages: pages.size });
|
|
639
|
+
}
|
|
640
|
+
if (what === "reload") { for (const page of pages.values()) tell(page, "reload", asked.fresh ? "fresh" : "now"); return json(200, { pages: pages.size }); }
|
|
641
|
+
if (what === "stop") { json(200, { stopped: true }); setTimeout(() => close().then(() => process.exit(0)), 50); return; }
|
|
642
|
+
return json(404, { problem: `lamp dev doesn't know "${what}"` });
|
|
643
|
+
} catch (error) {
|
|
644
|
+
return json(error instanceof Problem ? 400 : 500, { problem: error.message });
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (url.pathname === "/__lamp/page.js") return send(200, TYPES[".js"], `const LAMP = ${JSON.stringify({ gen, run })};\n${PAGE_SCRIPT}${layout.game ? `\n${GAME_SCRIPT}` : ""}`);
|
|
649
|
+
if (url.pathname === "/__lamp/events") {
|
|
650
|
+
const id = url.searchParams.get("page") || randomBytes(4).toString("hex"), tab = url.searchParams.get("tab") || id;
|
|
651
|
+
// The last change this page has: what its files were served at, or what it last heard when the line broke.
|
|
652
|
+
const since = Math.max(Number(url.searchParams.get("since")) || 0, Number(request.headers["last-event-id"]) || 0);
|
|
653
|
+
response.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-store", Connection: "keep-alive" });
|
|
654
|
+
const page = { response, tab, at: Math.min(since, gen), ready: false, since: Date.now(), told: Date.now(), rerunning: 0 };
|
|
655
|
+
pages.set(id, page);
|
|
656
|
+
reopening.delete(tab);
|
|
657
|
+
for (const message of early.get(id) ?? []) said(message);
|
|
658
|
+
early.delete(id);
|
|
659
|
+
tell(page, "hello", run);
|
|
660
|
+
// What was saved while it was opening, as one change.
|
|
661
|
+
const missed = changes.filter((change) => change.gen > since);
|
|
662
|
+
if (missed.length && missed[0].gen === since + 1) {
|
|
663
|
+
const removed = new Set(), changed = new Set();
|
|
664
|
+
for (const change of missed) {
|
|
665
|
+
for (const path of change.changed) { changed.add(path); removed.delete(path); }
|
|
666
|
+
for (const path of change.removed) { removed.add(path); changed.delete(path); }
|
|
667
|
+
}
|
|
668
|
+
tell(page, "change", JSON.stringify({ gen, changed: [...changed], removed: [...removed], now: false, pill: true, read: [...served], ...changingWords() }), gen);
|
|
669
|
+
} else if (missed.length) {
|
|
670
|
+
// More was saved than is remembered: the page opens again from the files as they are.
|
|
671
|
+
tell(page, "reload", "now");
|
|
672
|
+
}
|
|
673
|
+
const beat = setInterval(() => response.write(": still here\n\n"), 20_000);
|
|
674
|
+
request.on("close", () => { clearInterval(beat); if (pages.get(id) === page) pages.delete(id); });
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
// The page's AI (LampAI in lamp.js, through `lampGatewayHost` in page.js): answered here with the person's own
|
|
678
|
+
// Claude Code or Codex (ai.mjs), only for the page itself on this Mac. Never another site in the person's browser,
|
|
679
|
+
// which can't send this request's header from another origin, nor another device the server is open to with --lan.
|
|
680
|
+
if (request.method === "POST" && url.pathname === "/__lamp/ai") {
|
|
681
|
+
const from = request.socket.remoteAddress || "";
|
|
682
|
+
const local = from === "127.0.0.1" || from === "::1" || from === "::ffff:127.0.0.1";
|
|
683
|
+
const origin = request.headers.origin;
|
|
684
|
+
if (!local || request.headers["x-lamp-page"] !== "1" || (origin && new URL(origin).host !== request.headers.host)) {
|
|
685
|
+
return json(403, { status: 403, body: JSON.stringify({ error: "The AI answers only the page on this Mac." }) });
|
|
686
|
+
}
|
|
687
|
+
const asked = await body(request);
|
|
688
|
+
const path = typeof asked?.path === "string" ? asked.path : "";
|
|
689
|
+
let given = {};
|
|
690
|
+
try { given = typeof asked?.body === "string" ? JSON.parse(asked.body) : asked?.body ?? {}; } catch {}
|
|
691
|
+
const [status, reply] = await answerAI(path, given);
|
|
692
|
+
return json(200, { status, body: JSON.stringify(reply) });
|
|
693
|
+
}
|
|
694
|
+
if (request.method === "POST" && url.pathname.startsWith("/__lamp/")) {
|
|
695
|
+
const message = await body(request);
|
|
696
|
+
if (message && url.pathname === "/__lamp/said") said(message);
|
|
697
|
+
else if (message && url.pathname === "/__lamp/log" && Array.isArray(message.lines)) logged(message.lines.slice(0, 500));
|
|
698
|
+
else if (message && url.pathname === "/__lamp/looked") { lookers.get(message.ask)?.(message.seen ?? null); lookers.delete(message.ask); }
|
|
699
|
+
return send(204, "text/plain", "");
|
|
700
|
+
}
|
|
701
|
+
if (request.method !== "GET" && request.method !== "HEAD") return send(405, "text/plain", "Method not allowed");
|
|
702
|
+
|
|
703
|
+
let path;
|
|
704
|
+
try { path = decodeURIComponent(url.pathname); } catch { return send(400, "text/plain", "Bad path"); }
|
|
705
|
+
if (path.endsWith("/")) path += "index.html";
|
|
706
|
+
let file = resolve(join(root, path));
|
|
707
|
+
if (file !== root && !file.startsWith(root + sep)) return send(403, "text/plain", "Outside the page");
|
|
708
|
+
const name = relative(root, file).split(sep).join("/");
|
|
709
|
+
served.add(name);
|
|
710
|
+
// Hidden files (.git, .lamp, .env) are no part of the page.
|
|
711
|
+
if (name.split("/").some((part) => part.startsWith("."))) return send(404, "text/plain", "Not found");
|
|
712
|
+
let info;
|
|
713
|
+
try {
|
|
714
|
+
info = await stat(file);
|
|
715
|
+
const real = await realpath(file);
|
|
716
|
+
if (real !== rootReal && !real.startsWith(rootReal + sep)) return send(404, "text/plain", "Not found");
|
|
717
|
+
} catch {
|
|
718
|
+
// A file the page asks for and the folder doesn't have is an error the page itself often can't say: a module
|
|
719
|
+
// that imports one never runs, with nothing in the page's log.
|
|
720
|
+
const missing = () => {
|
|
721
|
+
if (!/(^|\/)favicon\.ico$|\.map$/.test(name)) logged([{ level: "error", text: `${name} isn't in the folder, and the page asked for it.` }]);
|
|
722
|
+
return send(404, "text/plain", "Not found");
|
|
723
|
+
};
|
|
724
|
+
if (!layout.game) return missing();
|
|
725
|
+
if (name === DEVICE_FILE) return send(200, TYPES[".js"], ANY_BROWSER);
|
|
726
|
+
if (name === BUILD_FILE) return send(200, TYPES[".js"], `export default "${buildName()}";\n`);
|
|
727
|
+
if (name === "lamp.js") return send(200, TYPES[".js"], lampJS(layout.game));
|
|
728
|
+
const own = lampsCopy(name);
|
|
729
|
+
try { info = own ? await stat(own) : null; } catch { info = null; }
|
|
730
|
+
if (!info) return missing();
|
|
731
|
+
file = own;
|
|
732
|
+
}
|
|
733
|
+
if (info.isDirectory()) { response.writeHead(302, { Location: url.pathname + "/" + url.search, ...headers }); return response.end(); }
|
|
734
|
+
const type = TYPES[extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
735
|
+
if (type.startsWith("text/html")) {
|
|
736
|
+
// lamp's script first in the page, before any script of its own.
|
|
737
|
+
let html;
|
|
738
|
+
try { html = await readFile(file, "utf8"); } catch { return send(404, "text/plain", "Not found"); }
|
|
739
|
+
const tag = '<script src="/__lamp/page.js"></script>';
|
|
740
|
+
return send(200, type, /<head[^>]*>/i.test(html) ? html.replace(/<head[^>]*>/i, (found) => found + tag) : tag + html);
|
|
741
|
+
}
|
|
742
|
+
// The rest is sent as it's read, a part of it when a part is asked for (sound and video are).
|
|
743
|
+
const range = /^bytes=(\d*)-(\d*)$/.exec(request.headers.range || "");
|
|
744
|
+
let start = 0, end = info.size - 1, status = 200;
|
|
745
|
+
if (range && info.size && (range[1] || range[2])) {
|
|
746
|
+
start = range[1] ? Number(range[1]) : Math.max(0, info.size - Number(range[2]));
|
|
747
|
+
end = range[1] && range[2] ? Math.min(Number(range[2]), info.size - 1) : info.size - 1;
|
|
748
|
+
if (start > end || start >= info.size) { response.writeHead(416, { "Content-Range": `bytes */${info.size}`, ...headers }); return response.end(); }
|
|
749
|
+
status = 206;
|
|
750
|
+
}
|
|
751
|
+
response.writeHead(status, {
|
|
752
|
+
"Content-Type": type, "Content-Length": info.size ? end - start + 1 : 0, "Accept-Ranges": "bytes", ...headers,
|
|
753
|
+
...(status === 206 ? { "Content-Range": `bytes ${start}-${end}/${info.size}` } : {}),
|
|
754
|
+
});
|
|
755
|
+
if (request.method === "HEAD" || !info.size) return response.end();
|
|
756
|
+
const stream = createReadStream(file, { start, end });
|
|
757
|
+
stream.on("error", () => response.destroy());
|
|
758
|
+
stream.pipe(response);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// The port: the one asked for and no other, else the folder's last when it's free, else the folder's own and the
|
|
762
|
+
// ones after it.
|
|
763
|
+
const portFile = join(state, "port");
|
|
764
|
+
const remembered = Number((() => { try { return readFileSync(portFile, "utf8"); } catch { return ""; } })()) || 0;
|
|
765
|
+
const first = firstPortOf(folder);
|
|
766
|
+
const tries = asked ? [asked] : [...new Set([remembered || first, ...Array.from({ length: 30 }, (_, i) => first + i)])];
|
|
767
|
+
let port = 0;
|
|
768
|
+
for (const candidate of tries) {
|
|
769
|
+
try {
|
|
770
|
+
await new Promise((resolve, reject) => {
|
|
771
|
+
server.once("error", reject);
|
|
772
|
+
server.listen(candidate, lan ? "0.0.0.0" : "127.0.0.1", () => { server.off("error", reject); resolve(); });
|
|
773
|
+
});
|
|
774
|
+
port = candidate;
|
|
775
|
+
break;
|
|
776
|
+
} catch (error) {
|
|
777
|
+
if (error.code !== "EADDRINUSE") throw new Problem(error.message);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (!port) throw new Problem(asked ? `port ${asked} is in use; leave --port out and lamp finds one` : `no port from ${first} to ${first + 29} is free`);
|
|
781
|
+
address = `http://localhost:${port}/`;
|
|
782
|
+
writeFileSync(portFile, String(port));
|
|
783
|
+
writeFileSync(serverFileOf(folder), JSON.stringify({ port, pid: process.pid, token, folder }), { mode: 0o600 });
|
|
784
|
+
|
|
785
|
+
let closed = false;
|
|
786
|
+
async function close() {
|
|
787
|
+
if (closed) return;
|
|
788
|
+
closed = true;
|
|
789
|
+
clearInterval(idleTimer);
|
|
790
|
+
clearTimeout(copyTimer);
|
|
791
|
+
clearTimeout(timer);
|
|
792
|
+
watcher.close();
|
|
793
|
+
windowBrowser?.close();
|
|
794
|
+
copyBrowser?.close();
|
|
795
|
+
for (const page of pages.values()) page.response.end();
|
|
796
|
+
try { if (JSON.parse(readFileSync(serverFileOf(folder), "utf8")).pid === process.pid) rmSync(serverFileOf(folder)); } catch {}
|
|
797
|
+
server.closeAllConnections?.();
|
|
798
|
+
await new Promise((done) => server.close(done));
|
|
799
|
+
}
|
|
800
|
+
// As lamp goes, however it goes, the browsers it started go with it.
|
|
801
|
+
process.on("exit", () => { windowBrowser?.kill(); copyBrowser?.kill(); });
|
|
802
|
+
|
|
803
|
+
// A server `lamp look` started for itself ends once nothing has used it for a while and no window is open.
|
|
804
|
+
const idleTimer = setInterval(() => {
|
|
805
|
+
if (!idle || windowBrowser?.alive || Date.now() - lastUsed < idle * 60_000) return;
|
|
806
|
+
if (pages.size > (copyBrowser?.alive ? 1 : 0)) return;
|
|
807
|
+
close().then(() => process.exit(0));
|
|
808
|
+
}, 30_000);
|
|
809
|
+
idleTimer.unref();
|
|
810
|
+
|
|
811
|
+
if (!quiet) {
|
|
812
|
+
console.log(`lamp dev ${layout.game ? "a game" : "a page"} in ${root}`);
|
|
813
|
+
console.log(` Open ${address}`);
|
|
814
|
+
for (const one of lanAddresses) console.log(` http://${one}:${port}/ (other devices on this network)`);
|
|
815
|
+
console.log(` Look lamp look how a change went, a picture, the screen as text and the log`);
|
|
816
|
+
console.log(` Log ${logFile}`);
|
|
817
|
+
console.log(` Each save goes into the open page.${layout.game ? " A game keeps the player's place." : ""}\n`);
|
|
818
|
+
}
|
|
819
|
+
return { url: address, port, token, folder, state, logFile, look: (options) => inTurn(() => look(options)), use: (action) => inTurn(() => use(action)), openWindow, close };
|
|
820
|
+
}
|