@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/lamp.mjs
ADDED
|
@@ -0,0 +1,1326 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lamp: runs a web app or web game from its folder and puts each saved change into the open page, for whoever is
|
|
3
|
+
// making it, a person or a coding agent. It needs no Mac app and no build step. No dependencies: Node 20 or later.
|
|
4
|
+
//
|
|
5
|
+
// lamp new <folder> a new web game's folder, from Lamp's starting files
|
|
6
|
+
// lamp init [folder] Lamp's game runtime, beside a page that's already there
|
|
7
|
+
// lamp dev [folder] run it, each save going into the open page
|
|
8
|
+
// lamp open [folder] show it in a window, running it if it isn't
|
|
9
|
+
// lamp look [folder] how the last change went, a picture, the screen as text and the log
|
|
10
|
+
// lamp use [folder] … try it: a key, a click, some typing
|
|
11
|
+
// lamp stop [folder] stop running it
|
|
12
|
+
// lamp share [folder] a link on lamp.dev to play it, and lamp ideas for what people send back
|
|
13
|
+
// lamp play <link> play an app shared on lamp.dev here, its AI answered on this computer
|
|
14
|
+
// lamp ai the AI of pages shared on lamp.dev, played here, from the person's own Claude Code, Codex or OpenRouter key
|
|
15
|
+
// lamp devices the person's devices; lamp open --on xbox, or --on android, puts a page on one
|
|
16
|
+
// lamp setup teach the coding agents on this computer to work this way
|
|
17
|
+
// lamp skills Lamp's guidance for each kind of work; lamp skill <name> reads one
|
|
18
|
+
|
|
19
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
20
|
+
import { chmodSync, cpSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, realpathSync, rmSync, rmdirSync, statSync, watch, writeFileSync } from "node:fs";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { Problem, RESOURCES, layoutOf, manifestOf, projectOf, stateOf, running, ask } from "./state.mjs";
|
|
25
|
+
import { macActions, serveMac, writeMacStarter } from "./mac.mjs";
|
|
26
|
+
import { MAX_ARCHIVE, exportApp, unpack } from "./lampapp.mjs";
|
|
27
|
+
import { latestOf } from "./link.mjs";
|
|
28
|
+
import { updateNotice, versionLine } from "./update.mjs";
|
|
29
|
+
import { editionName, giveTo, givenTo, keepInLamp, keepPictures, lampOwning, lampsKeptLink, reachLamp, takeBack } from "./outside.mjs";
|
|
30
|
+
import { serve, ANY_BROWSER, CONTROLLERS, CONTROLLERS_FILE, DEVICE_FILE, LAMP_JS_STAND_IN, lampJS } from "./dev.mjs";
|
|
31
|
+
import { findBrowser, noBrowser } from "./browser.mjs";
|
|
32
|
+
import { allSkills } from "./skills.mjs";
|
|
33
|
+
import { engine } from "./ai.mjs";
|
|
34
|
+
import { allowed, forgetAllowed, serveLocalAI } from "./local-ai.mjs";
|
|
35
|
+
import { accountFor, keepAccount } from "./share.mjs";
|
|
36
|
+
import { accountName, askAccount } from "./link.mjs";
|
|
37
|
+
import { share, unshare, ideas, finishIdea, answerIdea, makerEmail, readLink, writeLink, forgetLink, nameOf, siteOrigin, LIMITS } from "./share.mjs";
|
|
38
|
+
import { Runner, allKept, fields, forget, integrationOf, isReady, look as lookForDevices, takesPages } from "./devices.mjs";
|
|
39
|
+
import { placeNamed } from "./place.mjs";
|
|
40
|
+
import { androidAdvice, androidDevices, androidDone, emulatorNamed, emulators, forgetAndroid, installTools, knowsAndroid, toolsThere } from "./android.mjs";
|
|
41
|
+
|
|
42
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
44
|
+
|
|
45
|
+
const USAGE = `lamp runs the app in a folder and puts each saved change into it: a web app or game, shown in a browser, or a
|
|
46
|
+
Mac app Lamp made in Swift, built and run on this Mac.
|
|
47
|
+
|
|
48
|
+
lamp new <folder> [--name "Its Name"] [--web | --mac]
|
|
49
|
+
Make a new web game's folder from Lamp's starting files: the game runtime, three.js and a page to build on.
|
|
50
|
+
--web makes a new web app instead, and --mac a new Mac app in Swift, each with Lamp's building blocks.
|
|
51
|
+
lamp init [folder] [--update]
|
|
52
|
+
Give a page that's already there Lamp's game runtime: its own files beside the page, none of the page's
|
|
53
|
+
changed. --update writes Lamp's files again, for a newer Lamp.
|
|
54
|
+
lamp unpack <file.lampapp> [folder]
|
|
55
|
+
Unpack a Lamp app file (what Lamp's Share… makes, and a lamp.dev link carries) into a new folder beside it,
|
|
56
|
+
named for it, or into the folder given. A Mac app gets Lamp's own files as it's first built.
|
|
57
|
+
lamp export [folder] [--to file.lampapp] [--anyway]
|
|
58
|
+
Make the app's Lamp app file, as Lamp's Share… does: its own files, credits, licenses and what it is, for
|
|
59
|
+
anyone to open in Lamp. Written beside the folder, named for the app, or where --to says. Refused when it
|
|
60
|
+
holds a key or a license is missing; --anyway shares work its credits give no license for.
|
|
61
|
+
lamp dev [folder] [--open] [--port N] [--lan]
|
|
62
|
+
Run it, saying how each change went. --open opens a window for it; --lan lets other devices on the
|
|
63
|
+
network open it too.
|
|
64
|
+
lamp open [folder] [--on <device>] [--hidden] [--in-lamp | --here]
|
|
65
|
+
Show it in a window, running it in the background if it isn't running. Returns at once.
|
|
66
|
+
--in-lamp gives a Mac app to Lamp's app, which must be running: it shows the person its review the first time,
|
|
67
|
+
then builds the app and puts each change into it while it's open (live, or held for a Reload), as with its own
|
|
68
|
+
chat. From then on look, dev and reload go through Lamp's app and say how each change went in, and use tries the
|
|
69
|
+
app it has open, in plain sight, and stop quits it there. --here has lamp build and run its own copy again.
|
|
70
|
+
LAMP_APP names another Lamp by its bundle id.
|
|
71
|
+
--on puts a page on one of the person's devices instead and opens it there (--on xbox): packaged, installed
|
|
72
|
+
and opened, the copy that was there replaced. --on android builds Lamp's Android app around the page and
|
|
73
|
+
opens it in lamp's Android emulator (--on android-tv, its TV emulator), or on a phone, tablet or TV of the
|
|
74
|
+
person's by its name. --hidden starts the emulator with no window and no sound, for a check nobody watches.
|
|
75
|
+
lamp look [folder] [--reopen] [--size 800x600] [--wait seconds] [--json]
|
|
76
|
+
How the changes since the last look went, a picture of the page (or of a Mac app's windows), its screen as
|
|
77
|
+
text, and its log. --reopen opens it again from its beginning first (what it saved kept): what someone
|
|
78
|
+
opening it sees.
|
|
79
|
+
--size looks at it as a window of that size shows it, when no window of the person's is open.
|
|
80
|
+
--on <device> looks at the device instead: its screen as a picture, whether the app is open there, and what
|
|
81
|
+
the app last logged there, which for a game says how many frames a second it gets.
|
|
82
|
+
lamp use [folder] [--on <android device>] <one of these>
|
|
83
|
+
Try it as a person would, then look. --on tries it on an Android place instead: a point of its picture
|
|
84
|
+
clicked or dragged from, typing, one key or one controller's button at a time (no stick), and wait.
|
|
85
|
+
click <x> <y> a point of the picture
|
|
86
|
+
click "A Name" a control by its name in the screen as text
|
|
87
|
+
type <text> into whatever has the keyboard
|
|
88
|
+
key <name> [seconds] a key, or several at once (w+a), held: a letter, a digit, space,
|
|
89
|
+
enter, escape, tab, backspace, delete, up, down, left, right,
|
|
90
|
+
shift, control, alt, command, f1 to f12
|
|
91
|
+
drag <x> <y> <to x> <to y> press, move and let go
|
|
92
|
+
scroll <points> [x y] down is positive
|
|
93
|
+
stick <left|right> <x> <y> [seconds] [buttons]
|
|
94
|
+
a game's controller: x and y from -1 to 1, right and up being 1
|
|
95
|
+
button <name> [seconds] a game's controller: a, b, x, y, lb, rb, lt, rt, menu, view, up… (a+rt)
|
|
96
|
+
wait <seconds>
|
|
97
|
+
Several at once, with "then" between them, go one after another with nothing in between and one look at the end:
|
|
98
|
+
lamp use key space then stick left 1 0 3 then button a
|
|
99
|
+
lamp reload [folder] [--fresh]
|
|
100
|
+
Open the page again from its files, a game from where the player is, or with --fresh from its beginning.
|
|
101
|
+
A Mac app is quit and opened again.
|
|
102
|
+
lamp changing [folder] <words>
|
|
103
|
+
Say what you're about to change, in a few words from the person's request ("Rain on the roads"): the open
|
|
104
|
+
page shows it on the card it puts up as the change goes in, for the next few minutes of changes.
|
|
105
|
+
lamp stop [folder] [--on <device>]
|
|
106
|
+
Stop running it. A Mac app is quit. --on ends it on a device instead (and closes an emulator lamp started
|
|
107
|
+
with no window).
|
|
108
|
+
lamp remove [folder] --on <device>
|
|
109
|
+
Take it off a device lamp put it on.
|
|
110
|
+
lamp devices [add [name] | forget <name>]
|
|
111
|
+
The person's devices that lamp puts a page on: an Xbox in Developer Mode, those set up and those found on the
|
|
112
|
+
network; and Android, lamp's emulators and the phones, tablets and TVs plugged in with USB debugging on.
|
|
113
|
+
"add" sets one up, a step at a time; it asks for what only the person should do (type an Xbox's Device Portal
|
|
114
|
+
password; accept Google's terms for Android's tools, with "add android"), so they run it in a terminal
|
|
115
|
+
themselves. "forget" lets one go ("forget android": lamp's emulators and its key to the person's devices).
|
|
116
|
+
lamp share [folder] --words "What it is" [--name N] [--line "A line under its name"] [--by "Who made it"]
|
|
117
|
+
[--asks "Found a bug?|What's missing?"] [--shape fill|16:9] [--no-picture] [--no-file] [--anyway]
|
|
118
|
+
Put the page on lamp.dev, where anyone with the link plays it and can send an idea. Public, so only when the
|
|
119
|
+
person asks. Again, it sends the page as it is now to the same link, with the same words unless new ones are given.
|
|
120
|
+
Its app file goes with it (as lamp export makes it), so anyone can open it in Lamp too; --no-file leaves it out.
|
|
121
|
+
--anyway shares a page that logged errors, or uses work that came with no license.
|
|
122
|
+
An app of Lamp's (its folder is in Lamp's own, or a link to it) is shared by Lamp's app, which must be running,
|
|
123
|
+
as its Share Link card shares it: the card's own words, the questions and what's new Lamp writes, the app's
|
|
124
|
+
file and its GitHub release, with --line, --by, --asks "a|b", --notes "What's new|And more", --no-file,
|
|
125
|
+
--no-github and --put-on-github in their place. --preview says what would go and sends nothing. Its link
|
|
126
|
+
stays Lamp's; --new-link has lamp make one of its own instead.
|
|
127
|
+
lamp unshare [folder] Take the link down.
|
|
128
|
+
lamp ideas [folder] [--earlier | in <n> | dismiss <n> | reply <n> | done <n> | email <address|off>] [note]
|
|
129
|
+
The ideas people playing the link have sent, with a picture of the app as they saw it when they kept one
|
|
130
|
+
in, and what's been said about each since. "in" says one is in the game now (whoever asked to hear is
|
|
131
|
+
written to); "dismiss" lets one go. Words after the number are a note to whoever sent it, on the link's
|
|
132
|
+
page and in that email; they can answer it by email. "reply" says something more, and "done" leaves
|
|
133
|
+
their answer without one. --earlier lists those seen to, to look back on or answer again. "email"
|
|
134
|
+
sends new ideas and replies to your own address too (once you confirm it), and you can answer from there.
|
|
135
|
+
lamp account [username] [--email you@example.com]
|
|
136
|
+
Your account on lamp.dev, which your links go under (lamp.dev/<username>/<app>): with a username and an email,
|
|
137
|
+
asks for it, and an email's link confirms it; with only your email, signs in to the one you have. On its own,
|
|
138
|
+
says which you're signed in as.
|
|
139
|
+
lamp play <link> [--to folder] [--newest]
|
|
140
|
+
Play an app shared on lamp.dev on this computer: its file, unpacked into a folder of its own in
|
|
141
|
+
~/Lamp Games (or where --to says), run and opened as lamp dev runs any page, so its AI is answered here in any
|
|
142
|
+
browser, Safari too. The same link again plays the same folder; --newest takes the link's latest version
|
|
143
|
+
into a new one.
|
|
144
|
+
lamp ai [--forget]
|
|
145
|
+
Answer the AI of pages shared on lamp.dev, played in a browser on this computer, with your own Claude Code
|
|
146
|
+
or Codex, or OpenRouter with your key in OPENROUTER_API_KEY, until stopped. Lamp does the same while it's
|
|
147
|
+
open, with the AI you set up there. Each page asks here once, and you allow it or not. --forget takes back
|
|
148
|
+
every page allowed, so each asks again.
|
|
149
|
+
lamp setup [--remove] Teach Claude Code and Codex on this computer to work this way (a skill for each).
|
|
150
|
+
lamp guide What that skill says, for any other agent.
|
|
151
|
+
lamp skills [word] Lamp's guidance for each kind of work, those about a word; lamp skill <name> reads one.
|
|
152
|
+
lamp blocks [folder] [Name]
|
|
153
|
+
Lamp's building blocks for a Mac app (saved data, AI, windows, the menu bar…) with what each is for, or one
|
|
154
|
+
in full: read from the app's own Sources/LampSupport.swift.
|
|
155
|
+
|
|
156
|
+
The folder is one Lamp made (a page in Web/, or a Mac app's Package.swift and build.sh), one in Lamp's format (its
|
|
157
|
+
lamp-app.json says what it is: a web app, a game, a screen or a Mac app) or any folder with an index.html; without
|
|
158
|
+
one, the folder you're in.`;
|
|
159
|
+
|
|
160
|
+
// What tells a coding agent how to work with lamp: one file, installed as a skill by `lamp setup` and printed by
|
|
161
|
+
// `lamp guide`. `{{lamp}}` in it is how lamp is run on this computer.
|
|
162
|
+
const SKILL_MARK = "<!-- Written by lamp setup: run it again to update, or with --remove to take it away. -->";
|
|
163
|
+
/** How lamp is run on this computer: `lamp` when that name runs this very file, else the file itself. */
|
|
164
|
+
function lampCommand() {
|
|
165
|
+
const me = fileURLToPath(import.meta.url);
|
|
166
|
+
// The copy in Lamp's app is run by its launcher beside it, on the app's own runtime, and updates with Lamp.
|
|
167
|
+
// A newer copy fetched from npm is run by that same launcher (lamp.sh names it): the app's, never one beside this.
|
|
168
|
+
if (process.env.LAMP_FROM_APP === "1") { const launcher = process.env.LAMP_LAUNCHER_PATH || join(here, "lamp"); return /\s/.test(launcher) ? JSON.stringify(launcher) : launcher; }
|
|
169
|
+
const found = spawnSync(process.platform === "win32" ? "where" : "which", ["lamp"], { encoding: "utf8" });
|
|
170
|
+
const onPath = found.status === 0 && found.stdout.trim().split("\n").some((path) => { try { return realpathSync(path.trim()) === realpathSync(me); } catch { return false; } });
|
|
171
|
+
return onPath ? "lamp" : `node ${/\s/.test(me) ? JSON.stringify(me) : me}`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The skill as it's read on this computer. Where lamp isn't a command by its own name, the skill says once how it's
|
|
175
|
+
* run and calls it lamp from there on: the whole path in every line would bury what the lines say.
|
|
176
|
+
*/
|
|
177
|
+
function skillText() {
|
|
178
|
+
const text = readFileSync(join(here, "skill", "SKILL.md"), "utf8"), command = lampCommand();
|
|
179
|
+
if (command === "lamp") return text.replaceAll("{{lamp}}", "lamp");
|
|
180
|
+
const said = `**lamp isn't a command by that name on this computer. Wherever this says \`lamp\`, run:**\n\n ${command}\n\n`;
|
|
181
|
+
return text.replaceAll("{{lamp}}", "lamp").replace(/\n# [^\n]*\n\n/, (heading) => heading + said);
|
|
182
|
+
}
|
|
183
|
+
const quoted = (path) => (/\s/.test(path) ? JSON.stringify(path) : path);
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Copies in Lamp's libraries that the page's import map names and its folder has none of (three.js, Phaser, Rapier:
|
|
187
|
+
* `Resources/WebLibraries`), as Lamp's app gives a game the ones its page uses. The names copied.
|
|
188
|
+
*/
|
|
189
|
+
/**
|
|
190
|
+
* Lamp's own files beside a page, which its app writes at every build and a shared file never carries: for a game the
|
|
191
|
+
* runtime, the controllers' list and the device it's on; for any page lamp.js, Lamp's building blocks (a stand-in where
|
|
192
|
+
* Lamp's own isn't beside lamp, never over one); and Lamp's libraries its import map names from beside it. Only those:
|
|
193
|
+
* the page and its code are the project's. `update` writes again those of them the folder has (lamp.js too), and adds
|
|
194
|
+
* the game's only to a game: a page that never had them gets none.
|
|
195
|
+
*/
|
|
196
|
+
function lampsFilesBeside(root, { game, update = false }) {
|
|
197
|
+
const runtime = join(RESOURCES, "GameKit", "web");
|
|
198
|
+
const wrote = [], kept = [];
|
|
199
|
+
if (game) {
|
|
200
|
+
if (![runtime, CONTROLLERS].every(existsSync)) throw new Problem("Lamp's own files aren't beside this copy of lamp");
|
|
201
|
+
const files = [
|
|
202
|
+
...readdirSync(runtime).map((name) => [name, () => readFileSync(join(runtime, name))]),
|
|
203
|
+
[CONTROLLERS_FILE, () => readFileSync(CONTROLLERS)], [DEVICE_FILE, () => ANY_BROWSER],
|
|
204
|
+
];
|
|
205
|
+
for (const [name, contents] of files) {
|
|
206
|
+
const file = join(root, name);
|
|
207
|
+
if (existsSync(file) && !update) { kept.push(name); continue; }
|
|
208
|
+
writeFileSync(file, contents());
|
|
209
|
+
wrote.push(name);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Updating, only over Lamp's own: a copy of lamp with only the stand-in (a checkout whose generated files aren't
|
|
213
|
+
// written) never replaces a page's working lamp.js with it.
|
|
214
|
+
const js = lampJS(game);
|
|
215
|
+
if (!existsSync(join(root, "lamp.js")) || (update && js !== LAMP_JS_STAND_IN)) {
|
|
216
|
+
writeFileSync(join(root, "lamp.js"), js);
|
|
217
|
+
wrote.push("lamp.js");
|
|
218
|
+
} else if (update) kept.push("lamp.js");
|
|
219
|
+
wrote.push(...addLibraries(root));
|
|
220
|
+
return { wrote, kept };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function addLibraries(root) {
|
|
224
|
+
const lamps = join(RESOURCES, "WebLibraries");
|
|
225
|
+
let map = "";
|
|
226
|
+
try { map = /<script[^>]*type=["']importmap["'][^>]*>([\s\S]*?)<\/script>/i.exec(readFileSync(join(root, "index.html"), "utf8"))?.[1] ?? ""; } catch {}
|
|
227
|
+
const added = [];
|
|
228
|
+
for (const name of ["three", "phaser", "rapier"]) {
|
|
229
|
+
// Named as a folder beside the page: one loaded from elsewhere (a CDN) is the page's own business.
|
|
230
|
+
if (!new RegExp(`:\\s*["'](?:\\./)?${name}/`).test(map) || existsSync(join(root, name)) || !existsSync(join(lamps, name))) continue;
|
|
231
|
+
cpSync(join(lamps, name), join(root, name), { recursive: true });
|
|
232
|
+
added.push(`${name}/`);
|
|
233
|
+
}
|
|
234
|
+
return added;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function fail(problem) {
|
|
238
|
+
console.error(`lamp: ${problem}`);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The command's options and what's left: `--name value` for those that take one, `--name` for the rest. */
|
|
243
|
+
function parse(args, valued) {
|
|
244
|
+
const options = {}, rest = [];
|
|
245
|
+
for (let i = 0; i < args.length; i++) {
|
|
246
|
+
const arg = args[i];
|
|
247
|
+
if (arg === "--help" || arg === "-h") { console.log(USAGE); process.exit(0); }
|
|
248
|
+
if (!arg.startsWith("--")) { rest.push(arg); continue; }
|
|
249
|
+
const name = arg.slice(2);
|
|
250
|
+
if (valued.includes(name)) {
|
|
251
|
+
if (i + 1 >= args.length) throw new Problem(`--${name} takes a value`);
|
|
252
|
+
options[name] = args[++i];
|
|
253
|
+
} else options[name] = true;
|
|
254
|
+
}
|
|
255
|
+
return { options, rest };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Who a link says it's by when nothing says, as Lamp's Share Link card fills it (`WishLink.sharer`): the name last given
|
|
260
|
+
* with `--by`, else the one Lamp's app last shared with, else this computer's account name. The share says it, and
|
|
261
|
+
* `--by` changes it.
|
|
262
|
+
*/
|
|
263
|
+
function sharer() {
|
|
264
|
+
try { const kept = readFileSync(sharerFile(), "utf8").trim(); if (kept) return kept; } catch {}
|
|
265
|
+
for (const bundle of ["dev.lamp.host", "dev.lamp.host.nightly"]) {
|
|
266
|
+
const said = spawnSync("/usr/bin/defaults", ["read", bundle, "wishLinkSharer"], { encoding: "utf8" });
|
|
267
|
+
if (said.status === 0 && said.stdout.trim()) return said.stdout.trim();
|
|
268
|
+
}
|
|
269
|
+
const name = spawnSync("/usr/bin/id", ["-F"], { encoding: "utf8" });
|
|
270
|
+
return name.status === 0 ? [...name.stdout.trim()].slice(0, LIMITS.by).join("") : "";
|
|
271
|
+
}
|
|
272
|
+
const sharerFile = () => join(process.env.LAMP_DEV_HOME || join(homedir(), ".lamp", "dev"), "sharer");
|
|
273
|
+
function keepSharer(name) {
|
|
274
|
+
mkdirSync(dirname(sharerFile()), { recursive: true });
|
|
275
|
+
writeFileSync(sharerFile(), name);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The server for the folder: the one running, or one started for this command that ends when nothing uses it. */
|
|
279
|
+
async function serverFor(folder) {
|
|
280
|
+
const live = await running(folder);
|
|
281
|
+
if (live) return live;
|
|
282
|
+
const state = stateOf(folder);
|
|
283
|
+
mkdirSync(state, { recursive: true });
|
|
284
|
+
const out = openSync(join(state, "server.log"), "a");
|
|
285
|
+
spawn(process.execPath, [fileURLToPath(import.meta.url), "dev", folder, "--quiet", "--idle", "30"], { detached: true, stdio: ["ignore", out, out] }).unref();
|
|
286
|
+
for (let tries = 0; tries < 60; tries++) {
|
|
287
|
+
await sleep(100);
|
|
288
|
+
const started = await running(folder);
|
|
289
|
+
if (started) return started;
|
|
290
|
+
}
|
|
291
|
+
throw new Problem(`lamp dev didn't start for this folder; see ${join(state, "server.log")}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** A look at a device, as words: whether the app is open there, where the picture of its screen is, and what the app logged. */
|
|
295
|
+
function deviceReport(seen) {
|
|
296
|
+
const lines = [...(seen.did ? [seen.did] : []), seen.installed ? (seen.running === false ? `On ${seen.device}: not open now (lamp open --on puts it there again).` : `On ${seen.device}: open.`)
|
|
297
|
+
: `On ${seen.device}: lamp hasn't put this there yet (lamp open --on does).`];
|
|
298
|
+
if (seen.picture) lines.push(`Picture: ${seen.picture}${seen.size ? ` (${seen.size.width}x${seen.size.height})` : ""}, the device's screen now, whatever is on it.`);
|
|
299
|
+
if (seen.log) {
|
|
300
|
+
const said = seen.log.split(/\r?\n/).filter((line) => line.trim());
|
|
301
|
+
lines.push(said.length ? `Its log there, the last ${Math.min(said.length, 40)} lines of ${said.length}:\n${said.slice(-40).map((line) => " " + line).join("\n")}` : "Its log there is empty.");
|
|
302
|
+
} else if (seen.installed) lines.push("It has logged nothing there yet.");
|
|
303
|
+
return lines.join("\n");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** What the person types for a device's setup, read from the terminal without showing it. */
|
|
307
|
+
function typed(label) {
|
|
308
|
+
if (!process.stdin.isTTY) throw new Problem(`this needs ${label.toLowerCase()}, which only the person types: they run "${lampCommand()} devices add" in a terminal themselves`);
|
|
309
|
+
return new Promise((done, failed) => {
|
|
310
|
+
const input = process.stdin;
|
|
311
|
+
let text = "";
|
|
312
|
+
const end = () => { input.setRawMode(false); input.pause(); input.off("data", heard); process.stdout.write("\n"); };
|
|
313
|
+
const heard = (chunk) => {
|
|
314
|
+
for (const letter of String(chunk)) {
|
|
315
|
+
if (letter === "\r" || letter === "\n") { end(); return done(text); }
|
|
316
|
+
if (letter === "\u0003") { end(); return failed(new Problem("stopped")); }
|
|
317
|
+
if (letter === "\u007f" || letter === "\b") text = text.slice(0, -1);
|
|
318
|
+
else if (letter >= " ") text += letter;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
process.stdout.write(`${label} (not shown as you type): `);
|
|
322
|
+
input.setRawMode(true);
|
|
323
|
+
input.resume();
|
|
324
|
+
input.on("data", heard);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** What a device is, by its maker and model, the maker said once. */
|
|
329
|
+
const called = (device) => (String(device.model).startsWith(device.maker) ? device.model : `${device.maker} ${device.model}`);
|
|
330
|
+
|
|
331
|
+
/** One line read from the person at the terminal. */
|
|
332
|
+
async function answered(question) {
|
|
333
|
+
const { createInterface } = await import("node:readline/promises");
|
|
334
|
+
const terminal = createInterface({ input: process.stdin, output: process.stdout });
|
|
335
|
+
try { return (await terminal.question(question)).trim(); } finally { terminal.close(); }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** What `lamp devices` says of Android: lamp's emulators, and the person's devices adb sees. Nothing where lamp can't build for it. */
|
|
339
|
+
async function androidLines() {
|
|
340
|
+
if (!knowsAndroid()) return [];
|
|
341
|
+
if (!toolsThere().builds) return [`Android: its tools aren't on this computer yet. "${lampCommand()} devices add android" downloads them (about 2 GB); it accepts Google's terms for them, so run it in a terminal yourself.`];
|
|
342
|
+
const lines = ["Android:"];
|
|
343
|
+
for (const emulator of await emulators()) {
|
|
344
|
+
const standing = !emulator.downloaded ? `not downloaded yet (${lampCommand()} devices add ${emulator.word}, about a gigabyte)`
|
|
345
|
+
: emulator.running ? `running${emulator.hidden ? ", with no window" : ""}` : "started when a page is opened in it";
|
|
346
|
+
lines.push(` ${emulator.name[0].toUpperCase()}${emulator.name.slice(1)} (--on ${emulator.word}): ${standing}`);
|
|
347
|
+
}
|
|
348
|
+
const own = await androidDevices();
|
|
349
|
+
for (const device of own) lines.push(` ${device.name} (--on ${/\s/.test(device.name) ? `"${device.name}"` : device.name}): ${device.state === "device" ? `ready${device.tv ? ", a TV" : ""}` : androidAdvice(device.name, device.state)}`);
|
|
350
|
+
if (!own.length) lines.push(" A phone, tablet or TV of the person's is listed once it's plugged in with USB debugging on.");
|
|
351
|
+
return lines;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** `lamp devices add android`: Android's tools, downloaded once the person says so, since it accepts Google's terms for them. */
|
|
355
|
+
async function addAndroid(tv) {
|
|
356
|
+
if (!knowsAndroid()) throw new Problem(process.platform === "darwin" ? "Lamp's Android app for a page isn't beside this copy of lamp: scripts/cli-files.sh writes it" : "lamp builds for Android on a Mac, so far");
|
|
357
|
+
const there = toolsThere(), word = tv ? "android-tv" : "android";
|
|
358
|
+
if (there.builds && there.emulatorPackages && (!tv || there.tvImage)) {
|
|
359
|
+
console.log(`Android's tools are on this computer. Open a page in ${tv ? "the Android TV emulator" : "the Android emulator"} with: ${lampCommand()} open --on ${word}`);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (!process.stdin.isTTY) throw new Problem(`this downloads Google's Android tools and accepts Google's terms for them, which only the person does: they run "${lampCommand()} devices add ${word}" in a terminal themselves`);
|
|
363
|
+
console.log(`lamp makes Android apps with Google's free tools, and ${there.builds && there.emulatorPackages ? "the Android TV emulator isn't" : "they aren't all"} on this computer yet: ${there.builds && there.emulatorPackages ? "about a gigabyte" : "about 2 GB"}, kept in ~/Library/Developer/Lamp for every Lamp here.`);
|
|
364
|
+
console.log("Downloading them accepts Google's terms for them: https://developer.android.com/studio/terms");
|
|
365
|
+
if (!/^y(es)?$/i.test(await answered("Download them? (y/n) "))) { console.log("Nothing was downloaded."); return; }
|
|
366
|
+
await installTools({ tv, said: (line) => console.log(line) });
|
|
367
|
+
console.log(`\nAndroid's tools are ready. Open a page in ${tv ? "the Android TV emulator" : "the Android emulator"} with: ${lampCommand()} open --on ${word}`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** `lamp devices`: the person's devices that take a page, and setting one up. */
|
|
371
|
+
async function devices(args) {
|
|
372
|
+
const { rest } = parse(args, []);
|
|
373
|
+
const [verb, ...words] = rest, name = words.join(" ").toLowerCase();
|
|
374
|
+
if (verb === "add" && emulatorNamed(name)) return addAndroid(emulatorNamed(name).emulator === "tv");
|
|
375
|
+
if (verb === "forget" && name === "android") {
|
|
376
|
+
console.log((await forgetAndroid()) ? "Forgot Android: lamp's emulators and its key to the person's devices are gone. Android's tools stay, for every Lamp here, and what lamp put on a device is still there."
|
|
377
|
+
: "lamp has made nothing for Android to forget.");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const fits = (device) => !name || device.name.toLowerCase().includes(name) || device.integration.startsWith(name) || device.id.toLowerCase() === name;
|
|
381
|
+
|
|
382
|
+
if (verb === "forget") {
|
|
383
|
+
if (!name) throw new Problem("forget takes the device's name: lamp devices lists them");
|
|
384
|
+
const mine = allKept().filter(fits);
|
|
385
|
+
if (mine.length !== 1) throw new Problem(mine.length ? `"${words.join(" ")}" could be ${mine.map((device) => `"${device.name}"`).join(" or ")}` : `lamp has no device called "${words.join(" ")}" set up`);
|
|
386
|
+
forget(mine[0].id);
|
|
387
|
+
console.log(`Forgot ${mine[0].name}: its record, its certificate and what was typed for it. What lamp put on it is still there.`);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (verb !== undefined && verb !== "add") throw new Problem("devices takes add or forget: lamp devices lists them");
|
|
392
|
+
const all = await lookForDevices();
|
|
393
|
+
const pages = all.filter((device) => takesPages(integrationOf(device.integration)));
|
|
394
|
+
|
|
395
|
+
if (verb === undefined) {
|
|
396
|
+
const mine = pages.filter((device) => device.kept), others = pages.filter((device) => !device.kept);
|
|
397
|
+
if (mine.length) {
|
|
398
|
+
console.log("Set up with lamp:");
|
|
399
|
+
for (const device of mine) {
|
|
400
|
+
const ready = isReady(device, integrationOf(device.integration));
|
|
401
|
+
console.log(` ${device.name}: ${device.model}, ${device.found === false ? `not answering now (last at ${device.address})` : `at ${device.address}`}${ready ? "" : ", setup not finished (lamp devices add)"}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (others.length) {
|
|
405
|
+
console.log(`${mine.length ? "\n" : ""}Found on the network, not set up (they may not be yours):`);
|
|
406
|
+
for (const device of others) console.log(` ${device.name}: ${called(device)} at ${device.address}`);
|
|
407
|
+
console.log(`\n"${lampCommand()} devices add" sets one up. It asks for what only you should type, so run it in a terminal yourself.`);
|
|
408
|
+
}
|
|
409
|
+
const elsewhere = all.filter((device) => !pages.includes(device) && device.found !== false);
|
|
410
|
+
if (elsewhere.length) console.log(`${mine.length || others.length ? "\n" : ""}Also there, which lamp's command line doesn't put a page on: ${elsewhere.map((device) => `${device.name} (${called(device)})`).join(", ")}.`);
|
|
411
|
+
if (!mine.length && !others.length) {
|
|
412
|
+
console.log("lamp found no Xbox to put a page on. One is found in Developer Mode, on this network and switched on:");
|
|
413
|
+
for (const line of integrationOf("xbox-one")?.steps?.[0]?.lines ?? []) console.log(` - ${line}`);
|
|
414
|
+
}
|
|
415
|
+
const android = await androidLines();
|
|
416
|
+
if (android.length) console.log("\n" + android.join("\n"));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// add: the one named, or the only one there, taken through its integration's steps in order.
|
|
421
|
+
const candidates = pages.filter((device) => device.found !== false && fits(device));
|
|
422
|
+
if (!candidates.length) {
|
|
423
|
+
throw new Problem(`lamp found no ${name ? `device called "${words.join(" ")}" on the network` : "device on the network that it puts a page on"}. An Xbox answers once it's on and in Developer Mode:\n`
|
|
424
|
+
+ (integrationOf("xbox-one")?.steps?.[0]?.lines ?? []).map((line) => ` - ${line}`).join("\n"));
|
|
425
|
+
}
|
|
426
|
+
if (candidates.length > 1) throw new Problem(`lamp found ${candidates.map((device) => `"${device.name}"`).join(" and ")}: say which, lamp devices add <name>`);
|
|
427
|
+
const device = candidates[0], integration = integrationOf(device.integration), runner = new Runner({ integration, device });
|
|
428
|
+
console.log(`${device.name}: ${called(device)} at ${device.address}`);
|
|
429
|
+
for (const step of integration.steps ?? []) {
|
|
430
|
+
let result = await runner.check(step);
|
|
431
|
+
for (let tries = 0; !result.passed && step.field && (result.needs || result.rejected) && tries < 3; tries++) {
|
|
432
|
+
if (result.rejected) { fields.remove(device.id, step.field.key); console.log(`${device.name} didn't take that.`); }
|
|
433
|
+
else { console.log(`\n${step.title}`); for (const line of step.lines ?? []) console.log(` - ${line}`); }
|
|
434
|
+
fields.save(device.id, step.field.key, await typed(step.field.label));
|
|
435
|
+
result = await runner.check(step);
|
|
436
|
+
}
|
|
437
|
+
if (result.passed) { runner.passed(step, result.read); console.log(`Done: ${step.title}.`); continue; }
|
|
438
|
+
if (result.rejected) fields.remove(device.id, step.field?.key);
|
|
439
|
+
if (result.failed) throw new Problem(result.why);
|
|
440
|
+
console.log(`\nNext: ${step.title}`);
|
|
441
|
+
for (const line of step.lines ?? []) console.log(` - ${line}`);
|
|
442
|
+
if (step.link) console.log(` ${step.link}`);
|
|
443
|
+
console.log(`\nThen run "${lampCommand()} devices add" again.`);
|
|
444
|
+
process.exitCode = 1;
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
console.log(`\n${device.name} is set up${device.model ? ` (${device.model})` : ""}. Put a page on it with: ${lampCommand()} open --on ${integration.id.split("-")[0]}`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const WHERE = {
|
|
451
|
+
mac: "the app as it runs on this Mac",
|
|
452
|
+
window: "the window the person has open",
|
|
453
|
+
copy: "lamp's own copy of the page, which nobody sees (no window is open)",
|
|
454
|
+
page: "a page open in a browser lamp can't take pictures of or play",
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
function report(seen) {
|
|
458
|
+
const lines = [];
|
|
459
|
+
if (seen.did) lines.push(seen.did);
|
|
460
|
+
if (seen.where) lines.push(`Looked at ${WHERE[seen.where]}${seen.title ? `: "${seen.title}"` : ""}${seen.width ? `, ${seen.width}×${seen.height}` : ""}.${seen.opened ? " It was opened just now, from the files as they are." : ""}${seen.playing ? " The game says it's playing." : seen.playing === false ? " The game hasn't said it's playing yet." : ""}`);
|
|
461
|
+
if (seen.problem) lines.push(seen.problem);
|
|
462
|
+
if (seen.note) lines.push(seen.note);
|
|
463
|
+
if (seen.unfinished) lines.push(`Taken while ${seen.unfinished}: look again.`);
|
|
464
|
+
if (seen.picture) lines.push(`Picture ${seen.picture}`);
|
|
465
|
+
for (const other of seen.pictures ?? []) lines.push(` ${other} (another of its windows)`);
|
|
466
|
+
if (seen.changes.length) {
|
|
467
|
+
lines.push("Changes since the last look");
|
|
468
|
+
for (const change of seen.changes) lines.push(` ${change.files} → ${change.outcome}: ${change.means}${change.why ? ` (${change.why})` : ""}`);
|
|
469
|
+
} else {
|
|
470
|
+
lines.push(`Changes since the last look: none${seen.opened ? " to put in (the page opened with every file as it is now)" : ""}`);
|
|
471
|
+
}
|
|
472
|
+
lines.push(seen.errors.length ? "Errors since the last look" : "Errors since the last look: none");
|
|
473
|
+
for (const error of seen.errors) lines.push(` ${error.split("\n").join("\n ")}`);
|
|
474
|
+
if (seen.screen) lines.push("Screen", ...seen.screen.split("\n").map((line) => ` ${line}`));
|
|
475
|
+
if (seen.log.length) {
|
|
476
|
+
lines.push(`Log since the last look${seen.logLines > 25 ? ` (its end: all ${seen.logLines} lines are in ${seen.logFile})` : ""}`, ...seen.log.map((line) => ` ${line.split("\n")[0].slice(0, 300)}`));
|
|
477
|
+
} else {
|
|
478
|
+
lines.push("Log since the last look: nothing");
|
|
479
|
+
}
|
|
480
|
+
if (seen.opened) lines.push(`The whole log is kept in ${seen.logFile}`);
|
|
481
|
+
return lines.join("\n");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// MARK: A Mac app given to Lamp's app
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* The running Lamp a Mac app's folder was given to (`lamp open --in-lamp`), connected, or null for a folder lamp runs
|
|
488
|
+
* itself. One given to a Lamp that isn't running is said so: lamp doesn't build a second copy of it behind Lamp's back.
|
|
489
|
+
*/
|
|
490
|
+
async function inLamp(layout) {
|
|
491
|
+
if (layout.kind !== "mac") return null;
|
|
492
|
+
const bundle = givenTo(layout.folder);
|
|
493
|
+
if (!bundle) return null;
|
|
494
|
+
const lamp = await reachLamp([bundle]);
|
|
495
|
+
if (!lamp) throw new Problem(`This app is given to ${editionName(bundle)}, which isn't running: open it, or run ${lampCommand()} open --here to have lamp build and run its own copy again.`);
|
|
496
|
+
return lamp;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* A folder whose link is Lamp's app's, not lamp's: one of Lamp's apps that lamp has no link of its own for. Its ideas
|
|
501
|
+
* and taking it down are on Lamp's Share Link card, said so rather than "no link".
|
|
502
|
+
*/
|
|
503
|
+
function lampsLink(folder) {
|
|
504
|
+
let real = resolve(folder);
|
|
505
|
+
try { real = realpathSync(real); } catch {}
|
|
506
|
+
const owner = lampOwning(real);
|
|
507
|
+
if (!owner) return null;
|
|
508
|
+
// The link Lamp keeps for its app is lamp's to use too, Lamp open or not.
|
|
509
|
+
lampsCopy(real);
|
|
510
|
+
return readLink(real) ? null : editionName(owner);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* For an app in a Lamp's own folder: brings lamp's copy of its link in step with the one that Lamp keeps, which is the
|
|
515
|
+
* link's own record (the person may have changed its words, or stopped sharing and made a new one, in Lamp since).
|
|
516
|
+
* False when the folder has a link of lamp's own instead (`share --new-link`), which is left as it is.
|
|
517
|
+
*/
|
|
518
|
+
function lampsCopy(folder) {
|
|
519
|
+
const mine = readLink(folder);
|
|
520
|
+
if (mine && !mine.fromLamp) return false;
|
|
521
|
+
const kept = lampsKeptLink(folder);
|
|
522
|
+
if (kept) writeLink(folder, { ...(mine?.id === kept.id ? mine : {}), ...kept, fromLamp: true });
|
|
523
|
+
else if (mine) forgetLink(folder);
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** What Lamp's app answered, as lamp says things: how the change went in, its words, and where its pictures are. */
|
|
528
|
+
function lampReport(folder, lamp, answer) {
|
|
529
|
+
const lines = [`${lamp.name}: ${answer.outcome ?? "answered"}.`];
|
|
530
|
+
if (answer.words) lines.push(answer.words);
|
|
531
|
+
const pictures = keepPictures(folder, answer.pictures);
|
|
532
|
+
if (pictures.length) lines.push(`Picture ${pictures[0]}`, ...pictures.slice(1).map((path) => ` ${path} (another of its windows)`));
|
|
533
|
+
return lines.join("\n");
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* A folder Lamp's app no longer has (the person took the app off its desk, or put another folder in its place) is
|
|
538
|
+
* lamp's own again: said once, and the command goes on as for any folder. Whether that's what Lamp answered.
|
|
539
|
+
*/
|
|
540
|
+
function letGo(layout, lamp, answer) {
|
|
541
|
+
if (answer.facts?.taken_on !== false) return false;
|
|
542
|
+
takeBack(layout.folder);
|
|
543
|
+
console.log(`${lamp.name} doesn't have this app any more, so lamp runs its own copy again. ${lampCommand()} open --in-lamp gives it back.`);
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** What the agent last said it's changing in a folder Lamp's app has (`lamp changing`), for half an hour after. */
|
|
548
|
+
function changingFor(folder) {
|
|
549
|
+
try {
|
|
550
|
+
const said = JSON.parse(readFileSync(join(stateOf(folder), "changing.json"), "utf8"));
|
|
551
|
+
return typeof said.words === "string" && Date.now() - said.at < 30 * 60_000 ? { words: said.words } : {};
|
|
552
|
+
} catch { return {}; }
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Asks Lamp's app to put what the folder holds into the app (`reopen`: opened again for it), and says how it went.
|
|
557
|
+
* False when Lamp no longer has the folder: the caller runs its own copy instead.
|
|
558
|
+
*/
|
|
559
|
+
async function applyInLamp(layout, lamp, { reopen = false } = {}) {
|
|
560
|
+
try {
|
|
561
|
+
let answer = await lamp.call("apply_change", { path: layout.folder, ...(reopen ? { reopen: true } : {}), ...changingFor(layout.folder) });
|
|
562
|
+
if (letGo(layout, lamp, answer)) return false;
|
|
563
|
+
// Lamp started again since the last apply: it asks for a look at the app as it is first, and then takes the change.
|
|
564
|
+
if (answer.facts?.restarted) {
|
|
565
|
+
await lamp.call("look_at_app", { path: layout.folder });
|
|
566
|
+
answer = await lamp.call("apply_change", { path: layout.folder, ...(reopen ? { reopen: true } : {}), ...changingFor(layout.folder) });
|
|
567
|
+
}
|
|
568
|
+
// Nothing new to put in brings no picture: the app as it is now, which the person may have used since, is looked at.
|
|
569
|
+
if (!answer.pictures.length && ["unchanged", "held"].includes(answer.outcome)) {
|
|
570
|
+
const seen = await lamp.call("look_at_app", { path: layout.folder });
|
|
571
|
+
answer = { ...answer, words: [answer.words, seen.words].filter(Boolean).join("\n\n"), pictures: seen.pictures };
|
|
572
|
+
}
|
|
573
|
+
console.log(lampReport(layout.folder, lamp, answer));
|
|
574
|
+
if (answer.failed) process.exitCode = 1;
|
|
575
|
+
return true;
|
|
576
|
+
} finally { lamp.close(); }
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* `lamp dev` for a Mac app given to Lamp's app: each save goes to it, one at a time (saves made meanwhile go in with
|
|
581
|
+
* the next), and what it says of each change is said here, until the person stops it. Read as lamp's own Mac server
|
|
582
|
+
* reads a change: the code, the package and the app's own files, never its notes or what's hidden or built.
|
|
583
|
+
*/
|
|
584
|
+
async function devInLamp(layout, lamp) {
|
|
585
|
+
const ignored = (path) => path.split("/").some((part) => part.startsWith(".") || part === "build" || part === "node_modules") || path.endsWith("~")
|
|
586
|
+
|| (!path.includes("/") && /\.(md|json|txt)$/i.test(path));
|
|
587
|
+
const print = (text) => console.log(`${new Date().toLocaleTimeString([], { hour12: false })} ${text}`);
|
|
588
|
+
// What the folder holds, as sizes and times: Lamp's app reads and rewrites its own files in the folder as it applies,
|
|
589
|
+
// which the watcher hears, so only a folder that reads otherwise than at the last apply is a change.
|
|
590
|
+
const stamps = (dir = layout.folder, prefix = "", found = new Map()) => {
|
|
591
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
592
|
+
const path = prefix + entry.name;
|
|
593
|
+
if (ignored(path)) continue;
|
|
594
|
+
if (entry.isDirectory()) stamps(join(dir, entry.name), path + "/", found);
|
|
595
|
+
else if (entry.isFile()) { try { const info = statSync(join(dir, entry.name)); found.set(path, `${info.size}:${info.mtimeMs}`); } catch {} }
|
|
596
|
+
}
|
|
597
|
+
return found;
|
|
598
|
+
};
|
|
599
|
+
const same = (a, b) => a && a.size === b.size && [...a].every(([path, stamp]) => b.get(path) === stamp);
|
|
600
|
+
let busy = false, again = false, timer = null, applied = null, retried = false;
|
|
601
|
+
const apply = async () => {
|
|
602
|
+
if (busy) { again = true; return; }
|
|
603
|
+
const now = stamps();
|
|
604
|
+
if (same(applied, now)) return;
|
|
605
|
+
busy = true;
|
|
606
|
+
try {
|
|
607
|
+
lamp = lamp ?? (await reachLamp([givenTo(layout.folder)]));
|
|
608
|
+
if (!lamp) { print(`${editionName(givenTo(layout.folder))} isn't running: the change goes in once it is and the next save is made.`); return; }
|
|
609
|
+
let answer = await lamp.call("apply_change", { path: layout.folder, ...changingFor(layout.folder) });
|
|
610
|
+
if (answer.facts?.restarted) { await lamp.call("look_at_app", { path: layout.folder }); answer = await lamp.call("apply_change", { path: layout.folder, ...changingFor(layout.folder) }); }
|
|
611
|
+
if (letGo(layout, lamp, answer)) { stopWatching(false); return; }
|
|
612
|
+
// A folder that changed while Lamp built it is applied again by the next save, or now when it's said so.
|
|
613
|
+
// A folder that changed while Lamp built it goes in again now, saved or not.
|
|
614
|
+
if (answer.facts?.stale || answer.outcome === "stale") { applied = null; again = true; } else applied = now;
|
|
615
|
+
retried = false;
|
|
616
|
+
print(lampReport(layout.folder, lamp, answer));
|
|
617
|
+
} catch (error) {
|
|
618
|
+
try { lamp?.close(); } catch {}
|
|
619
|
+
// A connection Lamp closed (it quit, or started again) is opened again once, and the change goes in through it.
|
|
620
|
+
if (lamp && !retried) { lamp = null; retried = true; again = true; }
|
|
621
|
+
else { print(`${error.message}`); lamp = null; }
|
|
622
|
+
} finally {
|
|
623
|
+
busy = false;
|
|
624
|
+
if (again) { again = false; apply(); }
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
print(`${lamp.name} has the app: each save goes into it. Ctrl-C stops watching; the app stays open there.`);
|
|
628
|
+
// Watching from before the first apply, so a save made while it runs goes in after it.
|
|
629
|
+
const watcher = watch(layout.folder, { recursive: true }, (event, name) => {
|
|
630
|
+
if (!name || ignored(String(name).split(sep).join("/"))) return;
|
|
631
|
+
clearTimeout(timer);
|
|
632
|
+
timer = setTimeout(apply, 300);
|
|
633
|
+
});
|
|
634
|
+
const quit = () => { watcher.close(); try { lamp?.close(); } catch {} process.exit(0); };
|
|
635
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(signal, quit);
|
|
636
|
+
// Until it's stopped, or Lamp no longer has the app: then lamp dev goes on as for any folder (false), its own
|
|
637
|
+
// runner answering Ctrl-C from then on.
|
|
638
|
+
let stopWatching;
|
|
639
|
+
const watching = new Promise((done) => {
|
|
640
|
+
stopWatching = (handled) => {
|
|
641
|
+
watcher.close(); clearTimeout(timer); try { lamp?.close(); } catch {}
|
|
642
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.off(signal, quit);
|
|
643
|
+
done(handled);
|
|
644
|
+
};
|
|
645
|
+
});
|
|
646
|
+
await apply();
|
|
647
|
+
return await watching;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async function main() {
|
|
651
|
+
const [command, ...args] = process.argv.slice(2);
|
|
652
|
+
if (!command || command === "--help" || command === "-h" || command === "help") { console.log(USAGE); return; }
|
|
653
|
+
if (command === "--version" || command === "-v" || command === "version") { console.log(versionLine()); return; }
|
|
654
|
+
|
|
655
|
+
if (command === "new") {
|
|
656
|
+
const { options, rest } = parse(args, ["name"]);
|
|
657
|
+
if (!rest[0]) throw new Problem("new takes the folder to make: lamp new harbor-racer");
|
|
658
|
+
const folder = resolve(rest[0]);
|
|
659
|
+
if (existsSync(folder) && readdirSync(folder).length) throw new Problem(`${folder} already has things in it`);
|
|
660
|
+
if (options.web) {
|
|
661
|
+
// A new web app's page as Lamp's app makes one (`ScreenKit.fill`): the page, its style and script, lamp.js and
|
|
662
|
+
// its notes, from a copy Lamp's own code wrote under a stand-in name (scripts/cli-files.sh).
|
|
663
|
+
const starter = join(here, "lamps", "web-starter");
|
|
664
|
+
if (!existsSync(join(starter, "lamp-starter.json"))) {
|
|
665
|
+
throw new Problem(`Lamp's starting files for a web app aren't made yet. In Lamp's folder, run scripts/cli-files.sh: it writes them from Lamp's own code.`);
|
|
666
|
+
}
|
|
667
|
+
const stood = JSON.parse(readFileSync(join(starter, "lamp-starter.json"), "utf8"));
|
|
668
|
+
const title = String(options.name ?? basename(folder).split(/[-_\s]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join(" ")).replace(/[\r\n]+/g, " ").trim();
|
|
669
|
+
if (!title || title.length > 60) throw new Problem("a web app's name is 60 characters at most: --name \"Its Name\"");
|
|
670
|
+
const inHTML = title.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
671
|
+
mkdirSync(folder, { recursive: true });
|
|
672
|
+
for (const file of readdirSync(starter)) {
|
|
673
|
+
if (file === "lamp-starter.json") continue;
|
|
674
|
+
// Lamp's building blocks are the same for every page; the rest names this one.
|
|
675
|
+
if (file === "lamp.js") cpSync(join(starter, file), join(folder, file));
|
|
676
|
+
else writeFileSync(join(folder, file), readFileSync(join(starter, file), "utf8").replaceAll(stood.name, file.endsWith(".html") ? inHTML : title));
|
|
677
|
+
}
|
|
678
|
+
console.log(`Made ${folder}: a new web app, "${title}", with Lamp's building blocks for a page (lamp.js).`);
|
|
679
|
+
console.log("Yours to write: index.html, style.css, app.js and APP.md. It shows a card until then.");
|
|
680
|
+
console.log("Lamp's, never edited: lamp.js.");
|
|
681
|
+
console.log(`Write the app, then see it with: ${lampCommand()} look ${quoted(folder)}`);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (options.mac) {
|
|
685
|
+
// A new Mac app as Lamp's app makes one (`Template.scaffold`): Lamp's own files and the card the app shows until
|
|
686
|
+
// it's written, from a copy Lamp's own code wrote under a stand-in name (scripts/cli-files.sh).
|
|
687
|
+
const title = String(options.name ?? basename(folder).split(/[-_\s]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join(" ")).trim();
|
|
688
|
+
// The name goes into the app's build script and its code as it is, so it's one that's safe in both.
|
|
689
|
+
if (!/^[A-Za-z][A-Za-z0-9 ]{0,39}$/.test(title)) throw new Problem("a Mac app's name here is letters, digits and spaces, starting with a letter, 40 at most: --name \"Its Name\"");
|
|
690
|
+
writeMacStarter(folder, title);
|
|
691
|
+
// What it is, in Lamp's own words (`Sharing.Manifest`): with it, Lamp's app takes the folder on as it is
|
|
692
|
+
// (lamp open --in-lamp), and lamp reads it as a Mac app.
|
|
693
|
+
writeFileSync(join(folder, "lamp-app.json"), JSON.stringify({ exportedAt: new Date().toISOString().replace(/\.\d+Z$/, "Z"), format: 2, name: title, platform: "mac" }, null, 2) + "\n");
|
|
694
|
+
console.log(`Made ${folder}: a new Mac app, "${title}", with Lamp's building blocks (Sources/LampSupport.swift).`);
|
|
695
|
+
console.log("Yours to write: the app's code in Sources/, and APP.md. It shows a card until then.");
|
|
696
|
+
console.log("Lamp's, never edited: Sources/LampSupport.swift, build.sh and Package.swift.");
|
|
697
|
+
console.log(`Build and open it with: ${lampCommand()} open ${quoted(folder)}`);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
// What Lamp gives a new web game: its starting page, the game runtime and three.js, from Lamp's own folder.
|
|
701
|
+
const lamps = RESOURCES, scaffold = join(lamps, "GameKit", "scaffold-web", "Web");
|
|
702
|
+
const runtime = join(lamps, "GameKit", "web"), three = join(lamps, "WebLibraries", "three");
|
|
703
|
+
if (![scaffold, runtime, three, CONTROLLERS].every(existsSync)) throw new Problem("Lamp's starting files aren't beside this copy of lamp");
|
|
704
|
+
const name = String(options.name ?? basename(folder).split(/[-_\s]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join(" "));
|
|
705
|
+
const inPage = name.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
706
|
+
mkdirSync(folder, { recursive: true });
|
|
707
|
+
for (const file of readdirSync(scaffold)) {
|
|
708
|
+
writeFileSync(join(folder, file), readFileSync(join(scaffold, file), "utf8").replaceAll("{{NAME}}", file.endsWith(".html") ? inPage : name.replace(/[\r\n]+/g, " ")));
|
|
709
|
+
}
|
|
710
|
+
for (const file of readdirSync(runtime)) cpSync(join(runtime, file), join(folder, file));
|
|
711
|
+
addLibraries(folder);
|
|
712
|
+
// Lamp's own files beside the page, which its app writes at every build: with them the folder is a whole site,
|
|
713
|
+
// and runs wherever it's served from. lamp.js is the stand-in until Lamp's app takes the folder on.
|
|
714
|
+
cpSync(CONTROLLERS, join(folder, CONTROLLERS_FILE));
|
|
715
|
+
writeFileSync(join(folder, DEVICE_FILE), ANY_BROWSER);
|
|
716
|
+
writeFileSync(join(folder, "lamp.js"), lampJS(true));
|
|
717
|
+
// The app's notes, which an app made by Lamp keeps and Lamp's skills write in.
|
|
718
|
+
writeFileSync(join(folder, "APP.md"), `# ${name.replace(/[\r\n]+/g, " ")}\n\nA web game on Lamp's game runtime, not written yet.\n`);
|
|
719
|
+
console.log(`Made ${folder}: a new game, "${name}", on Lamp's game runtime, with three.js and Phaser beside it.`);
|
|
720
|
+
console.log("Yours to write: index.html (keep its head), game.js and modules beside it, and APP.md. world.js and making.js are the card it shows until then: the game replaces them.");
|
|
721
|
+
console.log("Lamp's, never edited: lamp.js, lamp-game.js, lamp-phaser.js, lamp-controllers.js, lamp-device.js, three/ and phaser/.");
|
|
722
|
+
console.log(`Write the game, then see its real opening with: ${lampCommand()} look ${quoted(folder)} --reopen`);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (command === "export") {
|
|
727
|
+
const { options, rest } = parse(args, ["to"]);
|
|
728
|
+
const made = exportApp(rest[0] ?? ".", { anyway: !!options.anyway });
|
|
729
|
+
const folder = projectOf(rest[0] ?? ".").folder;
|
|
730
|
+
const to = resolve(options.to ?? join(dirname(folder), `${made.name}.lampapp`));
|
|
731
|
+
if (existsSync(to) && !options.to) throw new Problem(`${to} is there already: --to says where else to write it`);
|
|
732
|
+
writeFileSync(to, made.bytes);
|
|
733
|
+
console.log(`Made ${to}: "${made.name}", ${made.files} files, ${(made.bytes.length / 1_000_000).toFixed(1)} MB. Anyone with Lamp opens it.`);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (command === "unpack") {
|
|
738
|
+
const { rest } = parse(args, []);
|
|
739
|
+
if (!rest[0]) throw new Problem("unpack takes the Lamp app file: lamp unpack Harbor.lampapp");
|
|
740
|
+
const opened = unpack(rest[0], rest[1] ?? null);
|
|
741
|
+
console.log(`Unpacked "${opened.name}" into ${opened.folder}.`);
|
|
742
|
+
if (opened.refused) { console.log(`lamp can't run it: ${opened.refused}.`); return; }
|
|
743
|
+
// A page's file carries none of Lamp's own files around it: lamp puts them in, as Lamp's app does on the way in.
|
|
744
|
+
if (opened.project.kind === "page") lampsFilesBeside(opened.project.root, { game: opened.project.game || manifestOf(opened.folder)?.platform === "game" });
|
|
745
|
+
console.log(`${opened.project.kind === "mac" ? "Build and open it" : "See it"} with: ${lampCommand()} ${opened.project.kind === "mac" ? "open" : "look"} ${quoted(opened.folder)}`);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (command === "init") {
|
|
750
|
+
const { options, rest } = parse(args, []);
|
|
751
|
+
const layout = layoutOf(rest[0] ?? ".");
|
|
752
|
+
// Updating, a page is a game only if it has the runtime already; setting one up, it becomes one.
|
|
753
|
+
const game = options.update ? existsSync(join(layout.root, "lamp-game.js")) : true;
|
|
754
|
+
const { wrote, kept } = lampsFilesBeside(layout.root, { game, update: !!options.update });
|
|
755
|
+
console.log(wrote.length ? `Lamp's own files are beside the page in ${layout.root}: ${wrote.join(", ")}.` : `Lamp's own files were beside the page already in ${layout.root}.`);
|
|
756
|
+
if (kept.length) console.log(`Left as they were: ${kept.join(", ")} (lamp init --update writes them again).`);
|
|
757
|
+
if (game) console.log(`The game takes the runtime up in its own code. Lamp's game-live skill says how (${lampCommand()} skill game-live), and lamp-game.js's own comments are the whole interface.`);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (command === "dev") {
|
|
762
|
+
const { options, rest } = parse(args, ["port", "idle"]);
|
|
763
|
+
const layout = projectOf(rest[0] ?? ".");
|
|
764
|
+
const port = options.port === undefined ? undefined : Number(options.port);
|
|
765
|
+
if (port !== undefined && !(Number.isInteger(port) && port > 0 && port < 65536)) throw new Problem("--port takes a number from 1 to 65535");
|
|
766
|
+
const live = await running(layout.folder);
|
|
767
|
+
if (live?.own) {
|
|
768
|
+
// One `lamp look` started for itself, with nobody watching it: this one takes its place.
|
|
769
|
+
await ask(live, "stop", {}).catch(() => {});
|
|
770
|
+
for (let tries = 0; tries < 30 && (await running(layout.folder)); tries++) await sleep(100);
|
|
771
|
+
} else if (live) {
|
|
772
|
+
// One server a folder: this one says where it is, and opens the window when asked.
|
|
773
|
+
if (options.open) await ask(live, "open", {}, { timeout: 20_000 });
|
|
774
|
+
console.log(`lamp dev is already running for this folder: ${live.url}`);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
// Given to Lamp's app: each save goes to it, and what it says of the change is said here.
|
|
778
|
+
if (layout.kind === "mac" && givenTo(layout.folder) && (await devInLamp(layout, await inLamp(layout)))) return;
|
|
779
|
+
// A page is served and shown in a browser; a Mac app is built and run. Each is the folder's server from here on.
|
|
780
|
+
const server = layout.kind === "mac"
|
|
781
|
+
? await serveMac({ folder: layout.folder, port, quiet: !!options.quiet, idle: Number(options.idle) || 0 })
|
|
782
|
+
: await serve({ folder: layout.folder, port, lan: !!options.lan, quiet: !!options.quiet, idle: Number(options.idle) || 0 });
|
|
783
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(signal, () => server.close().then(() => process.exit(0)));
|
|
784
|
+
if (options.open && layout.kind !== "mac") await server.openWindow();
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (command === "look") {
|
|
789
|
+
const { options, rest } = parse(args, ["wait", "size", "on"]);
|
|
790
|
+
if (options.on !== undefined) {
|
|
791
|
+
const seen = await (await placeNamed(options.on, { command: lampCommand() })).look(layoutOf(rest[0] ?? "."));
|
|
792
|
+
console.log(options.json ? JSON.stringify(seen, null, 2) : deviceReport(seen));
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const layout = projectOf(rest[0] ?? ".");
|
|
796
|
+
// Given to Lamp's app: what the folder holds goes in (nothing, when it's in already), and Lamp's app says how.
|
|
797
|
+
const lamp = await inLamp(layout);
|
|
798
|
+
if (lamp && (await applyInLamp(layout, lamp, { reopen: !!options.reopen }))) return;
|
|
799
|
+
// A Mac app's change is a build, which takes longer than a page's.
|
|
800
|
+
const wait = options.wait === undefined ? (layout.kind === "mac" ? 180 : 20) : Math.min(Math.max(Number(options.wait) || 0, 0), 600);
|
|
801
|
+
const size = options.size === undefined ? null : /^(\d{3,4})x(\d{3,4})$/i.exec(options.size)?.slice(1).map(Number);
|
|
802
|
+
if (options.size !== undefined && !size) throw new Problem("--size takes a width and a height: --size 800x600");
|
|
803
|
+
const seen = await ask(await serverFor(layout.folder), "look", { wait, reopen: !!options.reopen, size }, { timeout: (wait + 60) * 1000 });
|
|
804
|
+
console.log(options.json ? JSON.stringify(seen, null, 2) : report(seen));
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (command === "use") {
|
|
809
|
+
const { options, rest } = parse(args, ["on"]);
|
|
810
|
+
const kinds = ["key", "click", "type", "wait", "drag", "scroll", "stick", "button"];
|
|
811
|
+
const folder = rest.length && !kinds.includes(rest[0]) ? rest.shift() : ".";
|
|
812
|
+
// Several moves, with "then" between them, are made one after another with nothing in between, and looked at once.
|
|
813
|
+
const moves = [];
|
|
814
|
+
for (let words = rest; words.length; ) {
|
|
815
|
+
const next = words.indexOf("then");
|
|
816
|
+
const [kind, ...values] = next < 0 ? words : words.slice(0, next);
|
|
817
|
+
words = next < 0 ? [] : words.slice(next + 1);
|
|
818
|
+
if (!kinds.includes(kind)) throw new Problem("use takes key, click, type, drag, scroll, stick, button or wait: lamp --help shows each");
|
|
819
|
+
if (kind === "key" && !values[0]) throw new Problem("key takes a key's name: lamp use key w 2");
|
|
820
|
+
moves.push(kind === "key" ? { kind, key: values[0], seconds: values[1] }
|
|
821
|
+
: kind === "click" ? (Number.isFinite(Number(values[0])) ? { kind, x: values[0], y: values[1] } : { kind, target: values.join(" ") })
|
|
822
|
+
: kind === "type" ? { kind, text: values.join(" ") }
|
|
823
|
+
: kind === "drag" ? { kind, x: values[0], y: values[1], toX: values[2], toY: values[3] }
|
|
824
|
+
: kind === "scroll" ? { kind, points: values[0], x: values[1], y: values[2] }
|
|
825
|
+
: kind === "stick" ? { kind, side: values[0], x: values[1], y: values[2], seconds: values[3], buttons: values[4] }
|
|
826
|
+
: kind === "button" ? { kind, buttons: values[0], seconds: values[1] }
|
|
827
|
+
: { kind, seconds: values[0] });
|
|
828
|
+
}
|
|
829
|
+
if (!moves.length) throw new Problem("use takes key, click, type, drag, scroll, stick, button or wait: lamp --help shows each");
|
|
830
|
+
if (options.on !== undefined) {
|
|
831
|
+
const place = await placeNamed(options.on, { command: lampCommand() });
|
|
832
|
+
if (!place.use) throw new Problem(`lamp can't press a controller on ${place.name}: try it in its window on this computer (lamp use), and see how it runs there with lamp look --on`);
|
|
833
|
+
const seen = await place.use(layoutOf(folder), moves);
|
|
834
|
+
console.log(options.json ? JSON.stringify(seen, null, 2) : deviceReport(seen));
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const layout = projectOf(folder);
|
|
838
|
+
// Given to Lamp's app: it uses the app it has open, in plain sight, as its own agent does.
|
|
839
|
+
const lamp = await inLamp(layout);
|
|
840
|
+
if (lamp) {
|
|
841
|
+
let answer;
|
|
842
|
+
try { answer = await lamp.call("use_app", { path: layout.folder, actions: macActions(moves) }); } finally { lamp.close(); }
|
|
843
|
+
if (!letGo(layout, lamp, answer)) {
|
|
844
|
+
console.log(lampReport(layout.folder, lamp, answer));
|
|
845
|
+
if (answer.failed) process.exitCode = 1;
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
const seen = await ask(await serverFor(layout.folder), "use", moves.length === 1 ? moves[0] : { moves }, { timeout: 60_000 + moves.length * 35_000 });
|
|
850
|
+
console.log(options.json ? JSON.stringify(seen, null, 2) : report(seen));
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if (command === "open") {
|
|
855
|
+
const { options, rest } = parse(args, ["on"]);
|
|
856
|
+
if (options.on !== undefined) {
|
|
857
|
+
const layout = layoutOf(rest[0] ?? "."), place = await placeNamed(options.on, { command: lampCommand() });
|
|
858
|
+
const put = await place.put(layout, { said: (line) => console.log(line), hidden: !!options.hidden });
|
|
859
|
+
console.log(put.line);
|
|
860
|
+
console.log(`${lampCommand()} look --on ${options.on} shows its screen and what it logged there; ${lampCommand()} stop --on ${options.on} ends it.`);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const layout = projectOf(rest[0] ?? ".");
|
|
864
|
+
if (options["in-lamp"]) {
|
|
865
|
+
// Lamp's app takes the folder on (its review first, which the person answers there), and from then on builds it
|
|
866
|
+
// and puts each change into the app it has open. lamp's own copy stops: one app, not two.
|
|
867
|
+
if (layout.kind !== "mac") throw new Problem("--in-lamp gives a Mac app to Lamp's app; a page runs here as it is");
|
|
868
|
+
const lamp = await reachLamp();
|
|
869
|
+
if (!lamp) throw new Problem("Lamp's app isn't running: open Lamp, then run this again");
|
|
870
|
+
const live = await running(layout.folder);
|
|
871
|
+
if (live) { await ask(live, "stop", {}).catch(() => {}); for (let tries = 0; tries < 50 && (await running(layout.folder)); tries++) await sleep(100); }
|
|
872
|
+
console.log(`Asking ${lamp.name} to take it on. The first time, it shows the person its review of the app, and builds it once they choose Build & Open.`);
|
|
873
|
+
let answer;
|
|
874
|
+
try { answer = await lamp.call("open_project", { path: layout.folder }); } finally { lamp.close(); }
|
|
875
|
+
// Taken on is taken on, whatever its first build did: a change that didn't build goes in with the next apply.
|
|
876
|
+
const taken = !["not_importable", "refused", "cancelled"].includes(answer.outcome);
|
|
877
|
+
if (taken) giveTo(layout.folder, lamp.bundle);
|
|
878
|
+
console.log(lampReport(layout.folder, lamp, answer));
|
|
879
|
+
if (answer.failed) process.exitCode = 1;
|
|
880
|
+
if (taken) console.log(`From here on, look, dev and reload go through ${lamp.name}. ${lampCommand()} open --here has lamp run its own copy again.`);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (options.here) takeBack(layout.folder);
|
|
884
|
+
const lamp = await inLamp(layout);
|
|
885
|
+
if (lamp && (await applyInLamp(layout, lamp))) return;
|
|
886
|
+
const opened = await ask(await serverFor(layout.folder), "open", {}, { timeout: 30_000 });
|
|
887
|
+
if (opened.opened === "app") {
|
|
888
|
+
console.log(opened.running ? `${opened.url} is open.` : opened.building ? `${opened.url} is being built, and opens when it is: lamp look waits for it.` : `${opened.url} didn't build: lamp look says why.`);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
console.log(opened.opened === "window" ? `Opened in a window: ${opened.url}` : `Opened in the browser: ${opened.url}\n${noBrowser()}`);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (command === "setup") {
|
|
896
|
+
const { options } = parse(args, []);
|
|
897
|
+
const text = `${skillText().trimEnd()}\n\n${SKILL_MARK}\n`;
|
|
898
|
+
let any = false;
|
|
899
|
+
for (const [agent, home] of [["Claude Code", join(homedir(), ".claude")], ["Codex", join(homedir(), ".codex")]]) {
|
|
900
|
+
const folder = join(home, "skills", "lamp"), file = join(folder, "SKILL.md");
|
|
901
|
+
// Only a skill lamp wrote is replaced or taken away.
|
|
902
|
+
const theirs = existsSync(file) && !readFileSync(file, "utf8").includes(SKILL_MARK);
|
|
903
|
+
if (options.remove) {
|
|
904
|
+
if (!existsSync(file) || theirs) continue;
|
|
905
|
+
rmSync(file);
|
|
906
|
+
try { rmdirSync(folder); } catch {}
|
|
907
|
+
console.log(`Took lamp's skill away from ${agent}.`);
|
|
908
|
+
any = true;
|
|
909
|
+
} else if (theirs) {
|
|
910
|
+
console.log(`${agent} has a skill called lamp that lamp didn't write, left as it is: ${file}`);
|
|
911
|
+
any = true;
|
|
912
|
+
} else if (existsSync(home)) {
|
|
913
|
+
mkdirSync(folder, { recursive: true });
|
|
914
|
+
writeFileSync(file, text);
|
|
915
|
+
console.log(`${agent} knows how to work with lamp, from its next session: ${file}`);
|
|
916
|
+
any = true;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (!any) console.log(options.remove ? "There was no skill of lamp's to take away." : "Neither Claude Code nor Codex is set up on this computer. `lamp guide` prints what to give another agent.");
|
|
920
|
+
// Web apps and games are looked at and played in a browser lamp drives: said now, before a first look comes back
|
|
921
|
+
// without a picture. Only said, never a failure: a Mac app needs no browser, and one can be installed later.
|
|
922
|
+
if (!options.remove && !findBrowser()) console.log(`For web apps and games: ${noBrowser()}`);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (command === "share") {
|
|
927
|
+
const { options, rest } = parse(args, ["name", "words", "line", "by", "asks", "shape", "notes"]);
|
|
928
|
+
// An app of Lamp's, with no link of lamp's own: shared by Lamp's app, as its Share Link card shares it, filled as the
|
|
929
|
+
// card fills itself (its last words, the questions and what's new Lamp writes, its file, its GitHub), with what's
|
|
930
|
+
// given here in their place. Its link's secret stays in Lamp.
|
|
931
|
+
// By its folder alone: Lamp's app shares kinds lamp doesn't run itself (an iPhone app, a wish of several). Only a
|
|
932
|
+
// folder that's a Lamp's: one in its own folder, or one given to it (`lamp open --in-lamp`). Any other folder is
|
|
933
|
+
// lamp's to share, whatever Lamp is running.
|
|
934
|
+
const project = { folder: (() => { const given = resolve(rest[0] ?? "."); try { return realpathSync(given); } catch { return given; } })() };
|
|
935
|
+
const owner = lampOwning(project.folder), given = owner ? null : givenTo(project.folder);
|
|
936
|
+
const hasPage = (() => { try { layoutOf(project.folder); return true; } catch { return false; } })();
|
|
937
|
+
// An app with a page in a Lamp's own folder is lamp's to share, whether that Lamp is open or not, to the link the
|
|
938
|
+
// Lamp keeps for it, and its Share Link card is told what went (`keepInLamp`). Only what lamp doesn't pack itself
|
|
939
|
+
// (a Mac app, an iPhone app, a wish of several) goes through Lamp's app.
|
|
940
|
+
// A link of lamp's own (--new-link) is a new one, never lamp's copy of the link Lamp keeps.
|
|
941
|
+
if (options["new-link"] && readLink(project.folder)?.fromLamp) forgetLink(project.folder);
|
|
942
|
+
const lampsOwn = owner && hasPage && !options["new-link"] && lampsCopy(project.folder);
|
|
943
|
+
if (!lampsOwn && !readLink(project.folder) && !options["new-link"] && (owner || given)) {
|
|
944
|
+
const lamp = await reachLamp([owner ?? given]);
|
|
945
|
+
if (lamp) {
|
|
946
|
+
const list = (text) => String(text).split("|").map((item) => item.trim()).filter(Boolean);
|
|
947
|
+
const values = {
|
|
948
|
+
path: project.folder, ...(options.preview ? { preview: true } : {}),
|
|
949
|
+
...(options.by !== undefined ? { by: String(options.by) } : {}),
|
|
950
|
+
...(options.line ?? options.words) !== undefined ? { line: String(options.line ?? options.words) } : {},
|
|
951
|
+
...(options.asks !== undefined ? { asks: list(options.asks) } : {}), ...(options.notes !== undefined ? { notes: list(options.notes) } : {}),
|
|
952
|
+
...(options["no-file"] ? { file: false } : {}), ...(options["no-github"] ? { github: false } : {}),
|
|
953
|
+
...(options["put-on-github"] ? { put_on_github: true } : {}),
|
|
954
|
+
};
|
|
955
|
+
console.log(options.preview ? `This is an app of ${lamp.name}'s: it shares it, to the link it has, as its Share Link card would.`
|
|
956
|
+
: `Sharing through ${lamp.name}, as its Share Link does…`);
|
|
957
|
+
let answer;
|
|
958
|
+
try { answer = await lamp.call("share_link", values); } finally { lamp.close(); }
|
|
959
|
+
// A Lamp from before it shared for lamp: an app of its own waits for it to be updated, since a link of lamp's
|
|
960
|
+
// would be a second one; a folder only given to it is lamp's to share.
|
|
961
|
+
// lamp shares only a page, so a Mac app given to it waits for Lamp too.
|
|
962
|
+
const older = /has no tool share_link\b/.test(answer.words ?? "");
|
|
963
|
+
if (older && (owner || !hasPage)) throw new Problem(`${lamp.name} is older than this lamp and can't share for it: update ${lamp.name}, then run this again${hasPage ? " (or --new-link to make lamp a link of its own)" : ""}.`);
|
|
964
|
+
if (older) console.log(`${lamp.name} can't share for lamp yet, so lamp shares it with a link of its own.`);
|
|
965
|
+
else if (answer.facts?.taken_on !== false) {
|
|
966
|
+
console.log(answer.words);
|
|
967
|
+
if (answer.failed) process.exitCode = 1;
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
} else if (owner) {
|
|
971
|
+
throw new Problem(`${editionName(owner)} packs this kind of app for its link: open ${editionName(owner)}, then run this again.`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
const layout = layoutOf(rest[0] ?? ".");
|
|
975
|
+
const before = readLink(layout.folder)?.site === siteOrigin() ? readLink(layout.folder) : null;
|
|
976
|
+
// The link's words: what's given now, else what it went out with before. Its name, where nothing says, is the page's.
|
|
977
|
+
const words = {
|
|
978
|
+
name: String(options.name ?? before?.name ?? nameOf(layout)).trim(), words: String(options.words ?? before?.words ?? "").trim(),
|
|
979
|
+
line: String(options.line ?? before?.line ?? "").trim(), by: String(options.by ?? before?.by ?? sharer()).trim(),
|
|
980
|
+
asks: options.asks !== undefined ? String(options.asks).split("|").map((ask) => ask.trim()).filter(Boolean) : before?.asks ?? [],
|
|
981
|
+
};
|
|
982
|
+
const count = (text) => [...new Intl.Segmenter("en", { granularity: "grapheme" }).segment(text)].length;
|
|
983
|
+
if (options.preview) {
|
|
984
|
+
console.log(`Nothing was shared. This is what would go:\n${before ? `The link ${before.url}` : "A new link"} for "${words.name}"${words.by ? ` by ${words.by}` : ""}: ${words.line || words.words || "(no words yet: give --words)"}`);
|
|
985
|
+
console.log(`Questions its page asks: ${words.asks.length ? words.asks.join(" · ") : "the page's own"}\nThe app's file: ${options["no-file"] ? "doesn't go" : "goes with it"}`);
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (!words.words) throw new Problem('a link says what the page is: share it with --words "What it is, in a sentence or a few"');
|
|
989
|
+
if (!words.name || count(words.name) > LIMITS.name) throw new Problem(`a link's name is at most ${LIMITS.name} characters: --name "Its Name"`);
|
|
990
|
+
if (count(words.words) > LIMITS.words) throw new Problem(`a link's words are at most ${LIMITS.words} characters`);
|
|
991
|
+
if (count(words.line) > LIMITS.line) throw new Problem(`a link's line is at most ${LIMITS.line} characters`);
|
|
992
|
+
if (count(words.by) > LIMITS.by) throw new Problem(`who it's by is at most ${LIMITS.by} characters`);
|
|
993
|
+
if (words.asks.length > LIMITS.asks || words.asks.some((ask) => count(ask) > LIMITS.ask)) throw new Problem(`a link asks at most ${LIMITS.asks} things, each of at most ${LIMITS.ask} characters`);
|
|
994
|
+
// The page as it runs now: its picture for the link's card, the shape it plays in, and whether it's working.
|
|
995
|
+
let card = { picture: null, stage: null, errors: [] };
|
|
996
|
+
if (!options["no-picture"]) card = await ask(await serverFor(layout.folder), "card", { wait: 30 }, { timeout: 120_000 });
|
|
997
|
+
if (card.errors.length && !options.anyway) {
|
|
998
|
+
throw new Problem(`the page logged errors, and a link shows it to anyone:\n ${card.errors.slice(0, 5).join("\n ")}\nPut them right, or share it as it is with --anyway.`);
|
|
999
|
+
}
|
|
1000
|
+
let shape = card.stage ? Math.round((card.stage[0] / card.stage[1]) * 10000) / 10000 : "fill";
|
|
1001
|
+
if (options.shape !== undefined) {
|
|
1002
|
+
const ratio = /^(\d+(?:\.\d+)?)[:/x](\d+(?:\.\d+)?)$/.exec(String(options.shape));
|
|
1003
|
+
if (options.shape !== "fill" && !ratio) throw new Problem("--shape is fill, for a page that takes any window, or a shape like 16:9");
|
|
1004
|
+
shape = ratio ? Math.round((Number(ratio[1]) / Number(ratio[2])) * 10000) / 10000 : "fill";
|
|
1005
|
+
}
|
|
1006
|
+
// A name given is the one every later link says it's by, as Lamp's Share Link card keeps it: kept once the share is
|
|
1007
|
+
// checked and about to go, so a share refused leaves it as it was.
|
|
1008
|
+
if (options.by !== undefined) keepSharer(words.by);
|
|
1009
|
+
let sent;
|
|
1010
|
+
try {
|
|
1011
|
+
sent = await share({ layout, words, shape, picture: card.picture ? Buffer.from(card.picture, "base64") : null, said: (line) => console.log(line),
|
|
1012
|
+
file: !options["no-file"], anyway: !!options.anyway, fromLamp: !!lampsOwn });
|
|
1013
|
+
} finally {
|
|
1014
|
+
// Lamp is told of the link even when its page didn't go, so its card has the link that's out, not a second one.
|
|
1015
|
+
const link = lampsOwn ? readLink(layout.folder) : null;
|
|
1016
|
+
if (link) keepInLamp(layout.folder, link);
|
|
1017
|
+
}
|
|
1018
|
+
console.log(`${before ? "Sent again to its link" : "Shared"}: ${sent.url}`);
|
|
1019
|
+
console.log(`"${words.name}"${words.by ? ` by ${words.by}` : ""}: ${sent.files} files, ${(sent.bytes / 1_000_000).toFixed(1)} MB packed, ${shape === "fill" ? "filling the window" : "on its own stage"}${sent.picture ? ", with a picture of it" : ", with no picture"}${sent.file ? ", and its app file, for anyone to open in Lamp" : ""}${sent.github ? `, with a link to its code on GitHub (${sent.github})` : ""}.`);
|
|
1020
|
+
console.log("Anyone with the link can play it and send an idea: lamp ideas shows them. lamp unshare takes it down.");
|
|
1021
|
+
if (words.by && options.by === undefined) console.log(`It says it's by ${words.by}; --by "Another Name" changes that, for this link and the next.`);
|
|
1022
|
+
if (/\/w\//.test(sent.url)) console.log(`${lampCommand()} account <username> --email you@example.com gives your links lamp.dev/<username>/<app>, this one at its next share.`);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (command === "unshare") {
|
|
1027
|
+
const { rest } = parse(args, []);
|
|
1028
|
+
const lamps = lampsLink(rest[0] ?? ".");
|
|
1029
|
+
if (lamps) throw new Problem(`${lamps} keeps this app's link: take it down from its Share Link card (Stop Sharing).`);
|
|
1030
|
+
const folder = layoutOf(rest[0] ?? ".").folder;
|
|
1031
|
+
// Lamp's record goes only when the link taken down is the one it keeps, not a link of lamp's own beside it.
|
|
1032
|
+
const taken = readLink(folder);
|
|
1033
|
+
const lampsKept = lampOwning(folder) ? lampsKeptLink(folder) : null;
|
|
1034
|
+
console.log(`Taken down: ${await unshare(folder)}`);
|
|
1035
|
+
if (lampsKept && taken?.id === lampsKept.id) keepInLamp(folder, null);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (command === "account") {
|
|
1040
|
+
const { options, rest } = parse(args, ["email"]);
|
|
1041
|
+
const site = siteOrigin();
|
|
1042
|
+
if (options.email === undefined) {
|
|
1043
|
+
const kept = accountFor(site);
|
|
1044
|
+
if (!kept) { console.log(`No account yet: ${lampCommand()} account <username> --email you@example.com asks for one.`); return; }
|
|
1045
|
+
const name = await accountName({ site, key: kept.key });
|
|
1046
|
+
console.log(name ? `Signed in as ${name}: your links go under ${new URL(site).host}/${name}.`
|
|
1047
|
+
: `Waiting for you to open the link in the email for ${kept.username}. Until then, links are shared as before.`);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
const asked = await askAccount({ site, username: rest[0], email: String(options.email) });
|
|
1051
|
+
keepAccount(site, { username: asked.username, key: asked.key, email: String(options.email) });
|
|
1052
|
+
console.log(`${asked.existing ? `Signing in as ${asked.username}` : `Asking for ${asked.username}`}: open the link in the email sent to ${options.email} to confirm it.`
|
|
1053
|
+
+ (asked.sent === false ? " (The site couldn't send it just now: ask again in a moment.)" : ""));
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
if (command === "play") {
|
|
1058
|
+
const { options, rest } = parse(args, ["to"]);
|
|
1059
|
+
let id = /(?:^|\/w\/)([A-Za-z0-9]{10})(?:-|$|\/|\?|#)/.exec(rest[0] ?? "")?.[1];
|
|
1060
|
+
// A link by its maker's username and its name there (lamp.dev/<username>/<app>): its id, asked of the site.
|
|
1061
|
+
let latest;
|
|
1062
|
+
if (!id) {
|
|
1063
|
+
const named = /^https?:\/\/[^/]+\/([a-z0-9-]{1,60})\/([a-z0-9-]{1,60})\/?(?:[?#]|$)/.exec(rest[0] ?? "");
|
|
1064
|
+
if (named) { latest = await latestOf({ user: named[1], app: named[2] }); id = latest?.id ?? null; }
|
|
1065
|
+
if (named && !id) throw new Problem("that link isn't on lamp.dev, or it has been taken down");
|
|
1066
|
+
}
|
|
1067
|
+
if (!id) throw new Problem("play takes a link to an app shared on lamp.dev: lamp play https://lamp.dev/matt-rubens/bricks");
|
|
1068
|
+
// Which folder each link was played into, so playing it again picks up where it was, saves and all: a file of its
|
|
1069
|
+
// own for each link, so two links played at once never write over each other's.
|
|
1070
|
+
const playedFile = join(process.env.LAMP_DEV_HOME || join(homedir(), ".lamp", "dev"), "played", id);
|
|
1071
|
+
let recorded = null;
|
|
1072
|
+
try { recorded = readFileSync(playedFile, "utf8").trim() || null; } catch {}
|
|
1073
|
+
const wanted = options.to ? resolve(options.to) : null;
|
|
1074
|
+
let folder = !options.newest && recorded && (!wanted || wanted === recorded) && existsSync(join(recorded, "lamp-app.json")) ? recorded : null;
|
|
1075
|
+
if (folder) console.log(`Playing the copy in ${folder}. --newest takes the link's latest version instead.`);
|
|
1076
|
+
else {
|
|
1077
|
+
latest ??= await latestOf({ id });
|
|
1078
|
+
if (!latest) throw new Problem("that link isn't on lamp.dev, or it has been taken down");
|
|
1079
|
+
if (!latest.file) throw new Problem(`"${latest.name ?? "That app"}" was shared without its file, so it plays only on lamp.dev`);
|
|
1080
|
+
const answer = await fetch(latest.file);
|
|
1081
|
+
const bytes = answer.ok ? Buffer.from(await answer.arrayBuffer()) : null;
|
|
1082
|
+
if (!bytes) throw new Problem("its file couldn't be downloaded; try again in a moment");
|
|
1083
|
+
if (bytes.length > MAX_ARCHIVE) throw new Problem("its file is bigger than a Lamp app file is let be");
|
|
1084
|
+
const name = String(latest.name || "Shared app").replace(/[\r\n/:\\]+/g, " ").trim().slice(0, 60) || "Shared app";
|
|
1085
|
+
const games = join(homedir(), "Lamp Games");
|
|
1086
|
+
folder = wanted ?? join(games, name);
|
|
1087
|
+
for (let n = 2; !options.to && existsSync(folder) && readdirSync(folder).length; n++) folder = join(games, `${name} ${n}`);
|
|
1088
|
+
const work = join(stateOf(folder), "download.lampapp");
|
|
1089
|
+
mkdirSync(dirname(work), { recursive: true });
|
|
1090
|
+
writeFileSync(work, bytes);
|
|
1091
|
+
let opened;
|
|
1092
|
+
try { opened = unpack(work, folder); } finally { rmSync(work, { force: true }); }
|
|
1093
|
+
if (opened.refused) throw new Problem(`it's unpacked in ${folder}, but lamp can't run it: ${opened.refused}. Open it in Lamp instead`);
|
|
1094
|
+
if (opened.project.kind === "page") lampsFilesBeside(opened.project.root, { game: opened.project.game || manifestOf(folder)?.platform === "game" });
|
|
1095
|
+
mkdirSync(dirname(playedFile), { recursive: true });
|
|
1096
|
+
writeFileSync(playedFile, folder);
|
|
1097
|
+
console.log(`"${opened.name}", version ${latest.version}, is in ${folder}.`);
|
|
1098
|
+
}
|
|
1099
|
+
const opened = await ask(await serverFor(projectOf(folder).folder), "open", {}, { timeout: 30_000 });
|
|
1100
|
+
if (opened.opened === "app") console.log(opened.running ? `${opened.url} is open.` : `${opened.url} is being built, and opens when it is.`);
|
|
1101
|
+
else console.log(opened.opened === "window" ? `Opened in a window: ${opened.url}` : `Opened in the browser: ${opened.url}`);
|
|
1102
|
+
console.log("Its AI is answered here, with your own Claude Code, Codex or OpenRouter key. A change you make stays in this copy.");
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
if (command === "ai") {
|
|
1107
|
+
const { options } = parse(args, []);
|
|
1108
|
+
if (options.forget) { forgetAllowed(); console.log("Every shared page asks again before it uses your AI here."); return; }
|
|
1109
|
+
const which = engine();
|
|
1110
|
+
if (!which) throw new Problem("AI here needs Claude Code or Codex on this computer, signed in, or an OpenRouter key in OPENROUTER_API_KEY.");
|
|
1111
|
+
try { await serveLocalAI(); } catch (error) {
|
|
1112
|
+
if (error?.code === "EADDRINUSE") throw new Problem("Lamp, or lamp ai in another terminal, already answers shared pages' AI on this computer.");
|
|
1113
|
+
throw error;
|
|
1114
|
+
}
|
|
1115
|
+
const allowedNow = Object.values(allowed());
|
|
1116
|
+
console.log(`Answering the AI of pages shared on lamp.dev with your ${which === "codex" ? "Codex" : which === "openrouter" ? "OpenRouter key" : "Claude Code"}, as they're played in a browser on this computer. Ctrl-C stops.`);
|
|
1117
|
+
if (allowedNow.length) console.log(`Allowed: ${allowedNow.map((page) => (page.name ? `"${page.name}"` : `lamp.dev/w/${page.id}`)).join(", ")}.`);
|
|
1118
|
+
await new Promise(() => {});
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
if (command === "ideas") {
|
|
1122
|
+
const outcomes = ["in", "dismiss", "reply", "done", "email"];
|
|
1123
|
+
// Everything after the outcome's number is a note to whoever sent it, word for word: never read as options, so
|
|
1124
|
+
// "--fullscreen" or "--help" in it stays in it.
|
|
1125
|
+
const at = args.findIndex((arg) => outcomes.includes(arg));
|
|
1126
|
+
const { options, rest } = parse(at < 0 ? args : args.slice(0, at + 2), []);
|
|
1127
|
+
const noted = at < 0 ? [] : args.slice(at + 2);
|
|
1128
|
+
const folder = rest.length && !outcomes.includes(rest[0]) ? rest.shift() : ".";
|
|
1129
|
+
const lamps = lampsLink(folder);
|
|
1130
|
+
if (lamps) throw new Problem(`${lamps} keeps this app's link, and the ideas people send to it: they're on its Share Link card in ${lamps}.`);
|
|
1131
|
+
const layout = layoutOf(folder);
|
|
1132
|
+
if (rest[0] === "email") {
|
|
1133
|
+
// The maker's own address for this link's ideas and replies: confirmed by an email first; "off" stops them.
|
|
1134
|
+
const given = [...rest.slice(1), ...noted].join(" ").trim();
|
|
1135
|
+
if (!given) throw new Problem("email takes your address, or off: lamp ideas email you@example.com");
|
|
1136
|
+
const answer = await makerEmail(layout.folder, given === "off" ? "" : given);
|
|
1137
|
+
if (given !== "off" && !answer.confirmed && answer.sent === false) throw new Problem(`lamp.dev couldn't send the confirmation to ${answer.email} just now: try again in a minute`);
|
|
1138
|
+
console.log(given === "off" ? "No more emails about this link's ideas."
|
|
1139
|
+
: answer.confirmed ? `Ideas and replies go to ${answer.email}.` : `Sent a confirmation to ${answer.email}: ideas and replies come there once you confirm it.`);
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (rest.length) {
|
|
1143
|
+
const n = Number(rest[1]);
|
|
1144
|
+
if (!outcomes.includes(rest[0]) || !Number.isInteger(n) || n < 1) throw new Problem("ideas takes in, dismiss, reply or done, and a number: lamp ideas lists them with their numbers");
|
|
1145
|
+
// On the link's page, and in the email if they asked for one.
|
|
1146
|
+
const note = [...rest.slice(2), ...noted].join(" ").trim() || undefined;
|
|
1147
|
+
if (rest[0] === "reply" || rest[0] === "done") {
|
|
1148
|
+
if (rest[0] === "reply" && !note) throw new Problem("reply takes what to say after the number: lamp ideas reply <number> <words>");
|
|
1149
|
+
const answer = await answerIdea(layout.folder, n, rest[0] === "reply" ? note : undefined);
|
|
1150
|
+
console.log(rest[0] === "done" ? `Idea ${n} is seen to.` : `Said to whoever sent idea ${n}${answer.notified ? ", by email too" : ", on the link's page"}.`);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
const answer = await finishIdea(layout.folder, n, rest[0] === "in", note);
|
|
1154
|
+
console.log(rest[0] === "in" ? `Idea ${n} is in.${answer.notified ? " Whoever sent it asked to hear, and has been written to." : ""}` : `Idea ${n} is let go.${note ? " Your note is on the link's page for them." : ""}`);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
const [sent, maker] = await Promise.all([ideas(layout.folder, { earlier: options.earlier === true }), makerEmail(layout.folder).catch(() => null)]);
|
|
1158
|
+
if (maker?.email) console.log(maker.confirmed ? `Ideas and replies go to ${maker.email} too.` : `Waiting for ${maker.email} to be confirmed: the link is in the email lamp.dev sent it.`);
|
|
1159
|
+
if (!sent.ideas.length) { console.log(options.earlier ? `Nothing seen to yet on ${sent.url}` : `No ideas waiting for ${sent.url}`); return; }
|
|
1160
|
+
console.log(options.earlier ? `Ideas seen to on ${sent.url}, the latest first` : `Ideas sent to ${sent.url}`);
|
|
1161
|
+
for (const idea of sent.ideas) {
|
|
1162
|
+
const state = options.earlier ? ` (${idea.status === "in" ? "in" : "dismissed"})` : idea.waiting ? ` (${idea.status === "in" ? "in" : idea.status === "dismissed" ? "dismissed" : "open"}, they answered)` : idea.wantsNews ? " (wants to hear back)" : "";
|
|
1163
|
+
const from = typeof idea.name === "string" && idea.name ? idea.name : null;
|
|
1164
|
+
console.log(` ${idea.n}. ${from ? `${from}: ` : ""}${String(idea.text).split("\n").join("\n ")}${state}`);
|
|
1165
|
+
// What's been said since, each way.
|
|
1166
|
+
for (const said of Array.isArray(idea.thread) ? idea.thread : [])
|
|
1167
|
+
console.log(` ${said.by === "maker" ? "You" : from ?? "They"}: ${String(said.text).split("\n").join("\n ")}`);
|
|
1168
|
+
// The app as they saw it when they opened the box, if they kept it in: there until the idea has its outcome.
|
|
1169
|
+
if (typeof idea.picture === "string" && idea.picture.startsWith("https://")) console.log(` picture: ${idea.picture}`);
|
|
1170
|
+
}
|
|
1171
|
+
if (options.earlier) { console.log("Say something more with: lamp ideas reply <number> <words>."); return; }
|
|
1172
|
+
console.log("Say what became of one with: lamp ideas in <number>, or lamp ideas dismiss <number>, with a note to them after it if you like.");
|
|
1173
|
+
if (sent.ideas.some((idea) => idea.waiting)) console.log("Answer one with: lamp ideas reply <number> <words>, or lamp ideas done <number> to leave it.");
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (command === "remove") {
|
|
1178
|
+
const { options, rest } = parse(args, ["on"]);
|
|
1179
|
+
if (options.on === undefined) throw new Problem("remove takes the device to take it off: lamp remove --on xbox");
|
|
1180
|
+
const place = await placeNamed(options.on, { command: lampCommand() });
|
|
1181
|
+
console.log((await place.remove(layoutOf(rest[0] ?? "."))) ? `Taken off ${place.name}.` : `lamp hasn't put this on ${place.name}.`);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
if (command === "devices") return await devices(args);
|
|
1186
|
+
|
|
1187
|
+
if (command === "changing") {
|
|
1188
|
+
// What the next change is, in the person's words, for the card the page shows as it goes in.
|
|
1189
|
+
const named = args.length > 1 && existsSync(args[0]) && statSync(args[0]).isDirectory();
|
|
1190
|
+
const folder = named ? args[0] : ".";
|
|
1191
|
+
const words = (named ? args.slice(1) : args).join(" ").trim();
|
|
1192
|
+
if (!words) throw new Problem('changing takes what you\'re about to change, in a few words: lamp changing "Rain on the roads"');
|
|
1193
|
+
const layout = projectOf(folder);
|
|
1194
|
+
// For a folder Lamp's app has, it goes with the next apply_change (`changingFor`), and its card says it.
|
|
1195
|
+
if (givenTo(layout.folder)) {
|
|
1196
|
+
mkdirSync(stateOf(layout.folder), { recursive: true });
|
|
1197
|
+
writeFileSync(join(stateOf(layout.folder), "changing.json"), JSON.stringify({ words, at: Date.now() }));
|
|
1198
|
+
console.log("The app says so as the change goes in.");
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
// A Mac app lamp runs itself has no card for a change: nothing to say it on, and nothing wrong.
|
|
1202
|
+
if (layout.kind === "mac") { console.log("A Mac app lamp runs itself shows no card for a change: nothing says it."); return; }
|
|
1203
|
+
const live = await running(layout.folder);
|
|
1204
|
+
if (!live) { console.log("lamp dev isn't running for this folder: nothing shows it."); return; }
|
|
1205
|
+
await ask(live, "changing", { words });
|
|
1206
|
+
console.log("The page says so as the change goes in.");
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
if (command === "reload" || command === "stop") {
|
|
1211
|
+
const { options, rest } = parse(args, ["on"]);
|
|
1212
|
+
if (options.on !== undefined) {
|
|
1213
|
+
if (command !== "stop") throw new Problem("a change goes on a device by putting the page there again: lamp open --on " + options.on);
|
|
1214
|
+
const place = await placeNamed(options.on, { command: lampCommand() });
|
|
1215
|
+
console.log((await place.stop(layoutOf(rest[0] ?? "."))) ? `Stopped on ${place.name}.` : `lamp hasn't put this on ${place.name}.`);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const layout = projectOf(rest[0] ?? ".");
|
|
1219
|
+
if (layout.kind === "mac" && givenTo(layout.folder)) {
|
|
1220
|
+
if (command === "stop") {
|
|
1221
|
+
// Quit by Lamp's app as at its own Stop, which waits for an app asking about unsaved work.
|
|
1222
|
+
const lamp = await inLamp(layout);
|
|
1223
|
+
let answer;
|
|
1224
|
+
try { answer = await lamp.call("close_app", { path: layout.folder }); } finally { lamp.close(); }
|
|
1225
|
+
if (!letGo(layout, lamp, answer)) { console.log(lampReport(layout.folder, lamp, answer)); return; }
|
|
1226
|
+
} else if (await applyInLamp(layout, await inLamp(layout), { reopen: true })) return;
|
|
1227
|
+
}
|
|
1228
|
+
const live = await running(layout.folder);
|
|
1229
|
+
if (!live) { console.log("lamp dev isn't running for this folder."); return; }
|
|
1230
|
+
const answer = await ask(live, command, { fresh: !!options.fresh });
|
|
1231
|
+
// Stopped means gone: the next command finds no server, never one on its way out.
|
|
1232
|
+
if (command === "stop") for (let tries = 0; tries < 50 && (await running(layout.folder)); tries++) await sleep(100);
|
|
1233
|
+
console.log(command === "stop" ? "Stopped." : answer.pages ? (layout.kind === "mac" ? "The app is opening again." : "The page is opening again.") : "Nothing is open.");
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
if (command === "guide") {
|
|
1238
|
+
// The skill without what only a skill's file needs in front.
|
|
1239
|
+
console.log(skillText().replace(/^---\n[\s\S]*?\n---\n+/, "").trimEnd());
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
if (command === "blocks") {
|
|
1244
|
+
// Lamp's building blocks for a Mac app, read from the app's own copy of them (Sources/LampSupport.swift), whose
|
|
1245
|
+
// comments are the whole of how each is used: every one with what it's for, or one in full.
|
|
1246
|
+
const { rest } = parse(args, []);
|
|
1247
|
+
const name = rest.find((word) => /^Lamp\w*$/.test(word)) ?? null;
|
|
1248
|
+
const project = projectOf(rest.find((word) => word !== name) ?? ".");
|
|
1249
|
+
const file = join(project.folder, "Sources", "LampSupport.swift");
|
|
1250
|
+
if (!existsSync(file)) throw new Problem("Lamp's building blocks are a Mac app's (Sources/LampSupport.swift): this folder has none");
|
|
1251
|
+
const lines = readFileSync(file, "utf8").split("\n");
|
|
1252
|
+
const declared = /^ {4}(?:@[\w.]+(?:\([^)]*\))?\s+)*(?:(?:public|final|nonisolated|indirect)\s+)*(enum|struct|class|actor|protocol)\s+(Lamp\w+)/;
|
|
1253
|
+
const blocks = new Map();
|
|
1254
|
+
lines.forEach((line, at) => {
|
|
1255
|
+
const found = declared.exec(line);
|
|
1256
|
+
if (!found || blocks.has(found[2])) return;
|
|
1257
|
+
let above = at - 1;
|
|
1258
|
+
while (above >= 0 && /^ {4}@/.test(lines[above])) above--;
|
|
1259
|
+
const says = [];
|
|
1260
|
+
for (; above >= 0 && /^ {4}\/\/\//.test(lines[above]); above--) says.unshift(lines[above].replace(/^ {4}\/\/\/ ?/, ""));
|
|
1261
|
+
blocks.set(found[2], { at, says });
|
|
1262
|
+
});
|
|
1263
|
+
// What a block offers, by name: its members that aren't its own alone.
|
|
1264
|
+
const offers = (block) => {
|
|
1265
|
+
const names = [];
|
|
1266
|
+
for (let at = block.at + 1; at < lines.length && !/^ {4}\}/.test(lines[at]); at++) {
|
|
1267
|
+
const member = /^ {8}(?:@[\w.]+(?:\([^)]*\))?\s+)*((?:(?:static|public|private|fileprivate|nonisolated|mutating|final)(?:\(set\))?\s+)*)(?:func|var|let)\s+(\w+)/.exec(lines[at]);
|
|
1268
|
+
if (member && !/private/.test(member[1]) && !names.includes(member[2])) names.push(member[2]);
|
|
1269
|
+
}
|
|
1270
|
+
return names;
|
|
1271
|
+
};
|
|
1272
|
+
if (!name) {
|
|
1273
|
+
for (const [block, entry] of blocks) {
|
|
1274
|
+
// What it's for, where its comment says; else what it offers, which says as much.
|
|
1275
|
+
const first = entry.says.join(" ").split(/(?<=[.:])\s/)[0];
|
|
1276
|
+
const said = entry.says.length ? first : (() => { const names = offers(entry); return names.length ? `${names.slice(0, 8).join(", ")}${names.length > 8 ? "…" : ""}` : ""; })();
|
|
1277
|
+
if (said) console.log(`${block}: ${said.length > 150 ? said.slice(0, 150).replace(/\s+\S*$/, "") + "…" : said}`);
|
|
1278
|
+
}
|
|
1279
|
+
console.log(`\n${lampCommand()} blocks <Name> says how one is used.`);
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
const block = blocks.get(name);
|
|
1283
|
+
if (!block) throw new Problem(`there's no building block called ${name}: lamp blocks lists them`);
|
|
1284
|
+
console.log(`${name} (Sources/LampSupport.swift:${block.at + 1})\n${block.says.length ? `\n${block.says.join("\n")}\n` : ""}`);
|
|
1285
|
+
// What it offers: each member's own comment and how it's written, without what's inside it or what's its own alone.
|
|
1286
|
+
for (let at = block.at + 1; at < lines.length && !/^ {4}\}/.test(lines[at]); at++) {
|
|
1287
|
+
const line = lines[at];
|
|
1288
|
+
if (/^ {8}\/\/\//.test(line)) { console.log(line.slice(4)); continue; }
|
|
1289
|
+
const member = /^ {8}((?:@[\w.]+(?:\([^)]*\))?\s+)*(?:(?:static|public|nonisolated|mutating|final|indirect|convenience)\s+)*(?:func|var|let|init|case|enum|struct|class|subscript|typealias)\b.*)$/.exec(line);
|
|
1290
|
+
if (member && !/\b(private|fileprivate)\b/.test(member[1])) console.log(" " + member[1].replace(/\s*\{.*$/, ""));
|
|
1291
|
+
}
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
if (command === "skills" || command === "skill") {
|
|
1296
|
+
// Lamp's skills, from Lamp's own folder beside this command: the one place that says which of Lamp's building
|
|
1297
|
+
// blocks to use and how.
|
|
1298
|
+
const skills = allSkills();
|
|
1299
|
+
if (!skills.size) throw new Problem("Lamp's skills aren't beside this copy of lamp");
|
|
1300
|
+
if (command === "skill") {
|
|
1301
|
+
if (!args[0]) throw new Problem("skill takes a name: lamp skills lists them");
|
|
1302
|
+
const skill = skills.get(args[0]);
|
|
1303
|
+
if (!skill) throw new Problem(`there's no skill "${args[0]}": lamp skills lists them`);
|
|
1304
|
+
console.log(`# ${skill.name}\n\n${skill.description}\n\n${skill.body}`);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
// `lamp skills game` lists only those about a word.
|
|
1308
|
+
const about = args[0] ? args[0].toLowerCase() : null;
|
|
1309
|
+
const listed = [...skills.values()].filter((skill) => !about || `${skill.name} ${skill.description}`.toLowerCase().includes(about)).sort((a, b) => a.name.localeCompare(b.name));
|
|
1310
|
+
for (const skill of listed) console.log(`${skill.name}: ${skill.description.length > 170 ? skill.description.slice(0, 170).replace(/\s+\S*$/, "") + "…" : skill.description}`);
|
|
1311
|
+
if (about && !listed.length) console.log(`No skill is about "${about}": lamp skills lists them all.`);
|
|
1312
|
+
else if (!about) console.log("\n`lamp skills <word>` lists only those about a word; `lamp skill <name>` prints one.");
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
throw new Problem(`there's no "${command}"\n\n${USAGE}`);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// A newer lamp from npm is said once a day, before the command's own words (update.mjs).
|
|
1320
|
+
{ const notice = updateNotice(); if (notice) console.error(notice); }
|
|
1321
|
+
|
|
1322
|
+
// Whatever a command did on Android, its adb server isn't left holding the person's devices (android.mjs).
|
|
1323
|
+
main().finally(() => androidDone().catch(() => {})).catch((error) => {
|
|
1324
|
+
if (error instanceof Problem) fail(error.message);
|
|
1325
|
+
throw error;
|
|
1326
|
+
});
|