@lampdotdev/lamp 0.0.0-stage → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (464) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +357 -2
  3. package/Resources/Devices/roku.json +410 -0
  4. package/Resources/Devices/xbox-one.json +367 -0
  5. package/Resources/GameKit/NOTICE.md +63 -0
  6. package/Resources/GameKit/apple/LampPage.swift +1646 -0
  7. package/Resources/GameKit/apple/LampPlayer.swift +46 -0
  8. package/Resources/GameKit/apple/LampWebApp.swift +63 -0
  9. package/Resources/GameKit/apple/LampWebGame.swift +87 -0
  10. package/Resources/GameKit/apple/Native.swift +12 -0
  11. package/Resources/GameKit/build.sh +55 -0
  12. package/Resources/GameKit/lamp_game.h +116 -0
  13. package/Resources/GameKit/lamp_host.h +19 -0
  14. package/Resources/GameKit/lamp_kit.cpp +61 -0
  15. package/Resources/GameKit/lamp_sound.cpp +455 -0
  16. package/Resources/GameKit/lamp_xbox.cpp +374 -0
  17. package/Resources/GameKit/libraries/HandmadeMath.h +3940 -0
  18. package/Resources/GameKit/libraries/cgltf.h +7240 -0
  19. package/Resources/GameKit/libraries/dr_mp3.h +5430 -0
  20. package/Resources/GameKit/libraries/dr_wav.h +9209 -0
  21. package/Resources/GameKit/libraries/sokol_debugtext.h +4754 -0
  22. package/Resources/GameKit/libraries/sokol_gfx.h +29558 -0
  23. package/Resources/GameKit/libraries/sokol_gl.h +4538 -0
  24. package/Resources/GameKit/libraries/sokol_log.h +335 -0
  25. package/Resources/GameKit/libraries/sokol_shape.h +1531 -0
  26. package/Resources/GameKit/libraries/stb_image.h +7988 -0
  27. package/Resources/GameKit/libraries/stb_vorbis.c +5584 -0
  28. package/Resources/GameKit/scaffold/Game/game.cpp +142 -0
  29. package/Resources/GameKit/scaffold/Game/scene.glsl +49 -0
  30. package/Resources/GameKit/scaffold-web/Web/game.js +31 -0
  31. package/Resources/GameKit/scaffold-web/Web/index.html +13 -0
  32. package/Resources/GameKit/scaffold-web/Web/making.js +33 -0
  33. package/Resources/GameKit/scaffold-web/Web/world.js +33 -0
  34. package/Resources/GameKit/web/lamp-game.js +3036 -0
  35. package/Resources/GameKit/web/lamp-phaser.js +128 -0
  36. package/Resources/WebHost/App.exe +0 -0
  37. package/Resources/WebHost/Microsoft.Web.WebView2.Core.dll +0 -0
  38. package/Resources/WebHost/WebView2-LICENSE.txt +27 -0
  39. package/Resources/WebHost/lamp_controllers.h +1506 -0
  40. package/Resources/WebHost/lamp_web_host.cpp +1173 -0
  41. package/Resources/WebLibraries/earlier.txt +318 -0
  42. package/Resources/WebLibraries/phaser/LICENSE.md +20 -0
  43. package/Resources/WebLibraries/phaser/NOTICE.md +2 -0
  44. package/Resources/WebLibraries/phaser/phaser.esm.min.js +1 -0
  45. package/Resources/WebLibraries/qrcode/LICENSE +21 -0
  46. package/Resources/WebLibraries/qrcode/NOTICE.md +4 -0
  47. package/Resources/WebLibraries/qrcode/qrcode.js +2297 -0
  48. package/Resources/WebLibraries/rapier/LICENSE +201 -0
  49. package/Resources/WebLibraries/rapier/NOTICE.md +3 -0
  50. package/Resources/WebLibraries/rapier/rapier.js +2 -0
  51. package/Resources/WebLibraries/three/LICENSE +21 -0
  52. package/Resources/WebLibraries/three/NOTICE.md +4 -0
  53. package/Resources/WebLibraries/three/addons/animation/AnimationClipCreator.js +168 -0
  54. package/Resources/WebLibraries/three/addons/animation/CCDIKSolver.js +607 -0
  55. package/Resources/WebLibraries/three/addons/controls/ArcballControls.js +3543 -0
  56. package/Resources/WebLibraries/three/addons/controls/DragControls.js +452 -0
  57. package/Resources/WebLibraries/three/addons/controls/FirstPersonControls.js +512 -0
  58. package/Resources/WebLibraries/three/addons/controls/FlyControls.js +384 -0
  59. package/Resources/WebLibraries/three/addons/controls/MapControls.js +116 -0
  60. package/Resources/WebLibraries/three/addons/controls/OrbitControls.js +1985 -0
  61. package/Resources/WebLibraries/three/addons/controls/PointerLockControls.js +264 -0
  62. package/Resources/WebLibraries/three/addons/controls/TrackballControls.js +1106 -0
  63. package/Resources/WebLibraries/three/addons/controls/TransformControls.js +2003 -0
  64. package/Resources/WebLibraries/three/addons/curves/CurveExtras.js +694 -0
  65. package/Resources/WebLibraries/three/addons/curves/NURBSCurve.js +169 -0
  66. package/Resources/WebLibraries/three/addons/curves/NURBSSurface.js +98 -0
  67. package/Resources/WebLibraries/three/addons/curves/NURBSUtils.js +532 -0
  68. package/Resources/WebLibraries/three/addons/curves/NURBSVolume.js +82 -0
  69. package/Resources/WebLibraries/three/addons/effects/AnaglyphEffect.js +274 -0
  70. package/Resources/WebLibraries/three/addons/effects/AsciiEffect.js +323 -0
  71. package/Resources/WebLibraries/three/addons/effects/OutlineEffect.js +489 -0
  72. package/Resources/WebLibraries/three/addons/effects/ParallaxBarrierEffect.js +155 -0
  73. package/Resources/WebLibraries/three/addons/effects/StereoEffect.js +91 -0
  74. package/Resources/WebLibraries/three/addons/environments/ColorEnvironment.js +59 -0
  75. package/Resources/WebLibraries/three/addons/environments/DebugEnvironment.js +102 -0
  76. package/Resources/WebLibraries/three/addons/environments/RoomEnvironment.js +185 -0
  77. package/Resources/WebLibraries/three/addons/geometries/BoxLineGeometry.js +92 -0
  78. package/Resources/WebLibraries/three/addons/geometries/ConvexGeometry.js +72 -0
  79. package/Resources/WebLibraries/three/addons/geometries/DecalGeometry.js +420 -0
  80. package/Resources/WebLibraries/three/addons/geometries/LoftGeometry.js +355 -0
  81. package/Resources/WebLibraries/three/addons/geometries/ParametricFunctions.js +100 -0
  82. package/Resources/WebLibraries/three/addons/geometries/ParametricGeometry.js +172 -0
  83. package/Resources/WebLibraries/three/addons/geometries/RoundedBoxGeometry.js +216 -0
  84. package/Resources/WebLibraries/three/addons/geometries/TeapotGeometry.js +689 -0
  85. package/Resources/WebLibraries/three/addons/geometries/TextGeometry.js +102 -0
  86. package/Resources/WebLibraries/three/addons/helpers/AnimationPathHelper.js +302 -0
  87. package/Resources/WebLibraries/three/addons/helpers/LightProbeGridHelper.js +166 -0
  88. package/Resources/WebLibraries/three/addons/helpers/LightProbeGridHelperWebGL.js +221 -0
  89. package/Resources/WebLibraries/three/addons/helpers/LightProbeHelper.js +166 -0
  90. package/Resources/WebLibraries/three/addons/helpers/LightProbeHelperGPU.js +102 -0
  91. package/Resources/WebLibraries/three/addons/helpers/OctreeHelper.js +109 -0
  92. package/Resources/WebLibraries/three/addons/helpers/PositionalAudioHelper.js +169 -0
  93. package/Resources/WebLibraries/three/addons/helpers/RapierHelper.js +59 -0
  94. package/Resources/WebLibraries/three/addons/helpers/RectAreaLightHelper.js +118 -0
  95. package/Resources/WebLibraries/three/addons/helpers/TextureHelper.js +265 -0
  96. package/Resources/WebLibraries/three/addons/helpers/TextureHelperGPU.js +214 -0
  97. package/Resources/WebLibraries/three/addons/helpers/VertexNormalsHelper.js +155 -0
  98. package/Resources/WebLibraries/three/addons/helpers/VertexTangentsHelper.js +133 -0
  99. package/Resources/WebLibraries/three/addons/helpers/ViewHelper.js +529 -0
  100. package/Resources/WebLibraries/three/addons/interactive/HTMLMesh.js +617 -0
  101. package/Resources/WebLibraries/three/addons/interactive/InteractiveGroup.js +224 -0
  102. package/Resources/WebLibraries/three/addons/interactive/SelectionBox.js +324 -0
  103. package/Resources/WebLibraries/three/addons/interactive/SelectionHelper.js +150 -0
  104. package/Resources/WebLibraries/three/addons/libs/fflate.module.js +2699 -0
  105. package/Resources/WebLibraries/three/addons/libs/lil-gui.module.min.js +8 -0
  106. package/Resources/WebLibraries/three/addons/libs/meshopt_decoder.module.js +196 -0
  107. package/Resources/WebLibraries/three/addons/libs/potpack.module.js +125 -0
  108. package/Resources/WebLibraries/three/addons/libs/stats.module.js +167 -0
  109. package/Resources/WebLibraries/three/addons/libs/tween.module.js +876 -0
  110. package/Resources/WebLibraries/three/addons/lighting/ClusteredLighting.js +153 -0
  111. package/Resources/WebLibraries/three/addons/lighting/DynamicLighting.js +82 -0
  112. package/Resources/WebLibraries/three/addons/lighting/LightMapper.js +295 -0
  113. package/Resources/WebLibraries/three/addons/lighting/LightProbeGrid.js +923 -0
  114. package/Resources/WebLibraries/three/addons/lighting/LightProbeGridUtils.js +107 -0
  115. package/Resources/WebLibraries/three/addons/lighting/LightProbeGridWebGL.js +846 -0
  116. package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGIConeTracer.js +243 -0
  117. package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGIIrradiance.js +43 -0
  118. package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGINode.js +617 -0
  119. package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGISceneCollector.js +793 -0
  120. package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGIVolume.js +1440 -0
  121. package/Resources/WebLibraries/three/addons/lights/LightProbeGenerator.js +337 -0
  122. package/Resources/WebLibraries/three/addons/lights/RectAreaLightTexturesLib.js +127 -0
  123. package/Resources/WebLibraries/three/addons/lights/RectAreaLightUniformsLib.js +40 -0
  124. package/Resources/WebLibraries/three/addons/lights/SunLight.js +100 -0
  125. package/Resources/WebLibraries/three/addons/lights/SunLightNode.js +56 -0
  126. package/Resources/WebLibraries/three/addons/lights/SunLightShadow.js +319 -0
  127. package/Resources/WebLibraries/three/addons/lights/SunShadowNode.js +312 -0
  128. package/Resources/WebLibraries/three/addons/lines/Line2.js +56 -0
  129. package/Resources/WebLibraries/three/addons/lines/LineGeometry.js +157 -0
  130. package/Resources/WebLibraries/three/addons/lines/LineMaterial.js +742 -0
  131. package/Resources/WebLibraries/three/addons/lines/LineSegments2.js +434 -0
  132. package/Resources/WebLibraries/three/addons/lines/LineSegmentsGeometry.js +298 -0
  133. package/Resources/WebLibraries/three/addons/lines/Wireframe.js +108 -0
  134. package/Resources/WebLibraries/three/addons/lines/WireframeGeometry2.js +49 -0
  135. package/Resources/WebLibraries/three/addons/lines/webgpu/Line2.js +46 -0
  136. package/Resources/WebLibraries/three/addons/lines/webgpu/LineSegments2.js +419 -0
  137. package/Resources/WebLibraries/three/addons/lines/webgpu/Wireframe.js +86 -0
  138. package/Resources/WebLibraries/three/addons/loaders/EXRLoader.js +3411 -0
  139. package/Resources/WebLibraries/three/addons/loaders/FBXLoader.js +4660 -0
  140. package/Resources/WebLibraries/three/addons/loaders/FontLoader.js +262 -0
  141. package/Resources/WebLibraries/three/addons/loaders/GLTFLoader.js +5006 -0
  142. package/Resources/WebLibraries/three/addons/loaders/HDRLoader.js +463 -0
  143. package/Resources/WebLibraries/three/addons/loaders/MTLLoader.js +601 -0
  144. package/Resources/WebLibraries/three/addons/loaders/OBJLoader.js +961 -0
  145. package/Resources/WebLibraries/three/addons/loaders/PLYLoader.js +992 -0
  146. package/Resources/WebLibraries/three/addons/loaders/RGBELoader.js +18 -0
  147. package/Resources/WebLibraries/three/addons/loaders/STLLoader.js +423 -0
  148. package/Resources/WebLibraries/three/addons/loaders/SVGLoader.js +3297 -0
  149. package/Resources/WebLibraries/three/addons/loaders/TGALoader.js +539 -0
  150. package/Resources/WebLibraries/three/addons/loaders/UltraHDRLoader.js +757 -0
  151. package/Resources/WebLibraries/three/addons/math/Capsule.js +159 -0
  152. package/Resources/WebLibraries/three/addons/math/ColorConverter.js +62 -0
  153. package/Resources/WebLibraries/three/addons/math/ColorSpaces.js +147 -0
  154. package/Resources/WebLibraries/three/addons/math/ConvexHull.js +1695 -0
  155. package/Resources/WebLibraries/three/addons/math/ImprovedNoise.js +88 -0
  156. package/Resources/WebLibraries/three/addons/math/Lut.js +317 -0
  157. package/Resources/WebLibraries/three/addons/math/MeshSurfaceSampler.js +315 -0
  158. package/Resources/WebLibraries/three/addons/math/OBB.js +535 -0
  159. package/Resources/WebLibraries/three/addons/math/Octree.js +825 -0
  160. package/Resources/WebLibraries/three/addons/math/SimplexNoise.js +470 -0
  161. package/Resources/WebLibraries/three/addons/misc/ConvexObjectBreaker.js +539 -0
  162. package/Resources/WebLibraries/three/addons/misc/GPUComputationRenderer.js +508 -0
  163. package/Resources/WebLibraries/three/addons/misc/Gyroscope.js +78 -0
  164. package/Resources/WebLibraries/three/addons/misc/MorphAnimMesh.js +119 -0
  165. package/Resources/WebLibraries/three/addons/misc/MorphBlendMesh.js +425 -0
  166. package/Resources/WebLibraries/three/addons/misc/ProgressiveLightMap.js +370 -0
  167. package/Resources/WebLibraries/three/addons/misc/ProgressiveLightMapGPU.js +322 -0
  168. package/Resources/WebLibraries/three/addons/misc/RollerCoaster.js +661 -0
  169. package/Resources/WebLibraries/three/addons/misc/Sculptor.js +1679 -0
  170. package/Resources/WebLibraries/three/addons/misc/SculptorMesh.js +1504 -0
  171. package/Resources/WebLibraries/three/addons/misc/SculptorTools.js +1180 -0
  172. package/Resources/WebLibraries/three/addons/misc/SculptorUtils.js +345 -0
  173. package/Resources/WebLibraries/three/addons/misc/TileCreasedNormalsPlugin.js +270 -0
  174. package/Resources/WebLibraries/three/addons/misc/TubePainter.js +611 -0
  175. package/Resources/WebLibraries/three/addons/misc/Volume.js +520 -0
  176. package/Resources/WebLibraries/three/addons/misc/VolumeSlice.js +275 -0
  177. package/Resources/WebLibraries/three/addons/modifiers/CurveModifier.js +374 -0
  178. package/Resources/WebLibraries/three/addons/modifiers/CurveModifierGPU.js +256 -0
  179. package/Resources/WebLibraries/three/addons/modifiers/EdgeSplitModifier.js +299 -0
  180. package/Resources/WebLibraries/three/addons/modifiers/TessellateModifier.js +340 -0
  181. package/Resources/WebLibraries/three/addons/objects/GroundedSkybox.js +69 -0
  182. package/Resources/WebLibraries/three/addons/objects/Lensflare.js +489 -0
  183. package/Resources/WebLibraries/three/addons/objects/LensflareMesh.js +376 -0
  184. package/Resources/WebLibraries/three/addons/objects/MarchingCubes.js +1261 -0
  185. package/Resources/WebLibraries/three/addons/objects/Reflector.js +390 -0
  186. package/Resources/WebLibraries/three/addons/objects/ReflectorForSSRPass.js +393 -0
  187. package/Resources/WebLibraries/three/addons/objects/Refractor.js +389 -0
  188. package/Resources/WebLibraries/three/addons/objects/ShadowMesh.js +130 -0
  189. package/Resources/WebLibraries/three/addons/objects/Sky.js +342 -0
  190. package/Resources/WebLibraries/three/addons/objects/SkyMesh.js +403 -0
  191. package/Resources/WebLibraries/three/addons/objects/Water.js +374 -0
  192. package/Resources/WebLibraries/three/addons/objects/Water2.js +403 -0
  193. package/Resources/WebLibraries/three/addons/objects/Water2Mesh.js +199 -0
  194. package/Resources/WebLibraries/three/addons/objects/WaterMesh.js +194 -0
  195. package/Resources/WebLibraries/three/addons/postprocessing/AfterimagePass.js +187 -0
  196. package/Resources/WebLibraries/three/addons/postprocessing/BloomPass.js +274 -0
  197. package/Resources/WebLibraries/three/addons/postprocessing/BokehPass.js +218 -0
  198. package/Resources/WebLibraries/three/addons/postprocessing/ClearPass.js +97 -0
  199. package/Resources/WebLibraries/three/addons/postprocessing/CubeTexturePass.js +146 -0
  200. package/Resources/WebLibraries/three/addons/postprocessing/DotScreenPass.js +114 -0
  201. package/Resources/WebLibraries/three/addons/postprocessing/EffectComposer.js +365 -0
  202. package/Resources/WebLibraries/three/addons/postprocessing/FXAAPass.js +40 -0
  203. package/Resources/WebLibraries/three/addons/postprocessing/FilmPass.js +113 -0
  204. package/Resources/WebLibraries/three/addons/postprocessing/GTAOPass.js +735 -0
  205. package/Resources/WebLibraries/three/addons/postprocessing/GlitchPass.js +177 -0
  206. package/Resources/WebLibraries/three/addons/postprocessing/HalftonePass.js +134 -0
  207. package/Resources/WebLibraries/three/addons/postprocessing/LUTPass.js +138 -0
  208. package/Resources/WebLibraries/three/addons/postprocessing/MaskPass.js +195 -0
  209. package/Resources/WebLibraries/three/addons/postprocessing/OutlinePass.js +776 -0
  210. package/Resources/WebLibraries/three/addons/postprocessing/OutputPass.js +148 -0
  211. package/Resources/WebLibraries/three/addons/postprocessing/Pass.js +191 -0
  212. package/Resources/WebLibraries/three/addons/postprocessing/RenderPass.js +193 -0
  213. package/Resources/WebLibraries/three/addons/postprocessing/RenderPixelatedPass.js +314 -0
  214. package/Resources/WebLibraries/three/addons/postprocessing/RenderTransitionPass.js +267 -0
  215. package/Resources/WebLibraries/three/addons/postprocessing/SAOPass.js +407 -0
  216. package/Resources/WebLibraries/three/addons/postprocessing/SMAAPass.js +230 -0
  217. package/Resources/WebLibraries/three/addons/postprocessing/SSAARenderPass.js +313 -0
  218. package/Resources/WebLibraries/three/addons/postprocessing/SSAOPass.js +527 -0
  219. package/Resources/WebLibraries/three/addons/postprocessing/SSRPass.js +858 -0
  220. package/Resources/WebLibraries/three/addons/postprocessing/SavePass.js +132 -0
  221. package/Resources/WebLibraries/three/addons/postprocessing/ShaderPass.js +135 -0
  222. package/Resources/WebLibraries/three/addons/postprocessing/TAARenderPass.js +243 -0
  223. package/Resources/WebLibraries/three/addons/postprocessing/TexturePass.js +131 -0
  224. package/Resources/WebLibraries/three/addons/postprocessing/UnrealBloomPass.js +541 -0
  225. package/Resources/WebLibraries/three/addons/shaders/ACESFilmicToneMappingShader.js +96 -0
  226. package/Resources/WebLibraries/three/addons/shaders/AfterimageShader.js +63 -0
  227. package/Resources/WebLibraries/three/addons/shaders/BasicShader.js +36 -0
  228. package/Resources/WebLibraries/three/addons/shaders/BleachBypassShader.js +68 -0
  229. package/Resources/WebLibraries/three/addons/shaders/BlendShader.js +56 -0
  230. package/Resources/WebLibraries/three/addons/shaders/BokehShader.js +151 -0
  231. package/Resources/WebLibraries/three/addons/shaders/BokehShader2.js +404 -0
  232. package/Resources/WebLibraries/three/addons/shaders/BrightnessContrastShader.js +62 -0
  233. package/Resources/WebLibraries/three/addons/shaders/ColorCorrectionShader.js +59 -0
  234. package/Resources/WebLibraries/three/addons/shaders/ColorifyShader.js +57 -0
  235. package/Resources/WebLibraries/three/addons/shaders/ConvolutionShader.js +74 -0
  236. package/Resources/WebLibraries/three/addons/shaders/CopyShader.js +52 -0
  237. package/Resources/WebLibraries/three/addons/shaders/DOFMipMapShader.js +63 -0
  238. package/Resources/WebLibraries/three/addons/shaders/DepthLimitedBlurShader.js +183 -0
  239. package/Resources/WebLibraries/three/addons/shaders/DigitalGlitch.js +104 -0
  240. package/Resources/WebLibraries/three/addons/shaders/DotScreenShader.js +75 -0
  241. package/Resources/WebLibraries/three/addons/shaders/ExposureShader.js +51 -0
  242. package/Resources/WebLibraries/three/addons/shaders/FXAAShader.js +298 -0
  243. package/Resources/WebLibraries/three/addons/shaders/FilmShader.js +73 -0
  244. package/Resources/WebLibraries/three/addons/shaders/FocusShader.js +94 -0
  245. package/Resources/WebLibraries/three/addons/shaders/FreiChenShader.js +103 -0
  246. package/Resources/WebLibraries/three/addons/shaders/GTAOShader.js +437 -0
  247. package/Resources/WebLibraries/three/addons/shaders/GammaCorrectionShader.js +52 -0
  248. package/Resources/WebLibraries/three/addons/shaders/HalftoneShader.js +332 -0
  249. package/Resources/WebLibraries/three/addons/shaders/HorizontalBlurShader.js +68 -0
  250. package/Resources/WebLibraries/three/addons/shaders/HorizontalTiltShiftShader.js +70 -0
  251. package/Resources/WebLibraries/three/addons/shaders/HueSaturationShader.js +74 -0
  252. package/Resources/WebLibraries/three/addons/shaders/KaleidoShader.js +65 -0
  253. package/Resources/WebLibraries/three/addons/shaders/LuminosityHighPassShader.js +68 -0
  254. package/Resources/WebLibraries/three/addons/shaders/LuminosityShader.js +54 -0
  255. package/Resources/WebLibraries/three/addons/shaders/MirrorShader.js +62 -0
  256. package/Resources/WebLibraries/three/addons/shaders/NormalMapShader.js +60 -0
  257. package/Resources/WebLibraries/three/addons/shaders/OutputShader.js +103 -0
  258. package/Resources/WebLibraries/three/addons/shaders/PoissonDenoiseShader.js +230 -0
  259. package/Resources/WebLibraries/three/addons/shaders/RGBShiftShader.js +61 -0
  260. package/Resources/WebLibraries/three/addons/shaders/SAOShader.js +201 -0
  261. package/Resources/WebLibraries/three/addons/shaders/SMAAShader.js +489 -0
  262. package/Resources/WebLibraries/three/addons/shaders/SSAOShader.js +331 -0
  263. package/Resources/WebLibraries/three/addons/shaders/SSRShader.js +396 -0
  264. package/Resources/WebLibraries/three/addons/shaders/SepiaShader.js +57 -0
  265. package/Resources/WebLibraries/three/addons/shaders/SobelOperatorShader.js +98 -0
  266. package/Resources/WebLibraries/three/addons/shaders/SubsurfaceScatteringShader.js +95 -0
  267. package/Resources/WebLibraries/three/addons/shaders/TechnicolorShader.js +51 -0
  268. package/Resources/WebLibraries/three/addons/shaders/ToonShader.js +349 -0
  269. package/Resources/WebLibraries/three/addons/shaders/TriangleBlurShader.js +79 -0
  270. package/Resources/WebLibraries/three/addons/shaders/UnpackDepthRGBAShader.js +60 -0
  271. package/Resources/WebLibraries/three/addons/shaders/VelocityShader.js +137 -0
  272. package/Resources/WebLibraries/three/addons/shaders/VerticalBlurShader.js +66 -0
  273. package/Resources/WebLibraries/three/addons/shaders/VerticalTiltShiftShader.js +70 -0
  274. package/Resources/WebLibraries/three/addons/shaders/VignetteShader.js +56 -0
  275. package/Resources/WebLibraries/three/addons/shaders/VolumeShader.js +285 -0
  276. package/Resources/WebLibraries/three/addons/shaders/WaterRefractionShader.js +106 -0
  277. package/Resources/WebLibraries/three/addons/textures/FlakesTexture.js +54 -0
  278. package/Resources/WebLibraries/three/addons/tsl/WebGLNodesHandler.js +607 -0
  279. package/Resources/WebLibraries/three/addons/tsl/display/AfterImageNode.js +256 -0
  280. package/Resources/WebLibraries/three/addons/tsl/display/AnaglyphPassNode.js +550 -0
  281. package/Resources/WebLibraries/three/addons/tsl/display/BilateralBlurNode.js +377 -0
  282. package/Resources/WebLibraries/three/addons/tsl/display/BleachBypass.js +33 -0
  283. package/Resources/WebLibraries/three/addons/tsl/display/BloomNode.js +599 -0
  284. package/Resources/WebLibraries/three/addons/tsl/display/CRT.js +150 -0
  285. package/Resources/WebLibraries/three/addons/tsl/display/ChromaticAberrationNode.js +163 -0
  286. package/Resources/WebLibraries/three/addons/tsl/display/DenoiseNode.js +354 -0
  287. package/Resources/WebLibraries/three/addons/tsl/display/DepthOfFieldNode.js +572 -0
  288. package/Resources/WebLibraries/three/addons/tsl/display/DotScreenNode.js +104 -0
  289. package/Resources/WebLibraries/three/addons/tsl/display/FSR1Node.js +481 -0
  290. package/Resources/WebLibraries/three/addons/tsl/display/FXAANode.js +357 -0
  291. package/Resources/WebLibraries/three/addons/tsl/display/FilmNode.js +101 -0
  292. package/Resources/WebLibraries/three/addons/tsl/display/GTAONode.js +727 -0
  293. package/Resources/WebLibraries/three/addons/tsl/display/GaussianBlurNode.js +412 -0
  294. package/Resources/WebLibraries/three/addons/tsl/display/GodraysNode.js +604 -0
  295. package/Resources/WebLibraries/three/addons/tsl/display/ImportanceSampledEnvironment.js +564 -0
  296. package/Resources/WebLibraries/three/addons/tsl/display/LensflareNode.js +282 -0
  297. package/Resources/WebLibraries/three/addons/tsl/display/Lut3DNode.js +109 -0
  298. package/Resources/WebLibraries/three/addons/tsl/display/MotionBlur.js +33 -0
  299. package/Resources/WebLibraries/three/addons/tsl/display/OITPassNode.js +341 -0
  300. package/Resources/WebLibraries/three/addons/tsl/display/OutlineNode.js +916 -0
  301. package/Resources/WebLibraries/three/addons/tsl/display/ParallaxBarrierPassNode.js +90 -0
  302. package/Resources/WebLibraries/three/addons/tsl/display/PixelationPassNode.js +227 -0
  303. package/Resources/WebLibraries/three/addons/tsl/display/RGBShiftNode.js +96 -0
  304. package/Resources/WebLibraries/three/addons/tsl/display/RecurrentDenoiseNode.js +812 -0
  305. package/Resources/WebLibraries/three/addons/tsl/display/RetroPassNode.js +263 -0
  306. package/Resources/WebLibraries/three/addons/tsl/display/SMAANode.js +729 -0
  307. package/Resources/WebLibraries/three/addons/tsl/display/SSAAPassNode.js +344 -0
  308. package/Resources/WebLibraries/three/addons/tsl/display/SSAONode.js +422 -0
  309. package/Resources/WebLibraries/three/addons/tsl/display/SSGINode.js +698 -0
  310. package/Resources/WebLibraries/three/addons/tsl/display/SSRNode.js +1430 -0
  311. package/Resources/WebLibraries/three/addons/tsl/display/SSSNode.js +479 -0
  312. package/Resources/WebLibraries/three/addons/tsl/display/Sepia.js +24 -0
  313. package/Resources/WebLibraries/three/addons/tsl/display/Shape.js +29 -0
  314. package/Resources/WebLibraries/three/addons/tsl/display/SharpenNode.js +284 -0
  315. package/Resources/WebLibraries/three/addons/tsl/display/SobelOperatorNode.js +168 -0
  316. package/Resources/WebLibraries/three/addons/tsl/display/StereoCompositePassNode.js +190 -0
  317. package/Resources/WebLibraries/three/addons/tsl/display/StereoPassNode.js +117 -0
  318. package/Resources/WebLibraries/three/addons/tsl/display/TAAUNode.js +700 -0
  319. package/Resources/WebLibraries/three/addons/tsl/display/TRAANode.js +609 -0
  320. package/Resources/WebLibraries/three/addons/tsl/display/TemporalReprojectNode.js +1024 -0
  321. package/Resources/WebLibraries/three/addons/tsl/display/TransitionNode.js +141 -0
  322. package/Resources/WebLibraries/three/addons/tsl/display/boxBlur.js +65 -0
  323. package/Resources/WebLibraries/three/addons/tsl/display/depthAwareBlend.js +80 -0
  324. package/Resources/WebLibraries/three/addons/tsl/display/depthAwareBlur.js +63 -0
  325. package/Resources/WebLibraries/three/addons/tsl/display/hashBlur.js +55 -0
  326. package/Resources/WebLibraries/three/addons/tsl/display/lensDirt.js +30 -0
  327. package/Resources/WebLibraries/three/addons/tsl/display/radialBlur.js +68 -0
  328. package/Resources/WebLibraries/three/addons/tsl/lighting/ClusteredLightsNode.js +994 -0
  329. package/Resources/WebLibraries/three/addons/tsl/lighting/DynamicLightsNode.js +201 -0
  330. package/Resources/WebLibraries/three/addons/tsl/lighting/LightProbeGridNode.js +183 -0
  331. package/Resources/WebLibraries/three/addons/tsl/lighting/data/AmbientLightDataNode.js +67 -0
  332. package/Resources/WebLibraries/three/addons/tsl/lighting/data/DirectionalLightDataNode.js +117 -0
  333. package/Resources/WebLibraries/three/addons/tsl/lighting/data/HemisphereLightDataNode.js +105 -0
  334. package/Resources/WebLibraries/three/addons/tsl/lighting/data/PointLightDataNode.js +140 -0
  335. package/Resources/WebLibraries/three/addons/tsl/lighting/data/SpotLightDataNode.js +167 -0
  336. package/Resources/WebLibraries/three/addons/tsl/math/Bayer.js +84 -0
  337. package/Resources/WebLibraries/three/addons/tsl/math/curlNoise.js +107 -0
  338. package/Resources/WebLibraries/three/addons/tsl/math/voronoiNoise.js +112 -0
  339. package/Resources/WebLibraries/three/addons/tsl/shadows/TileShadowNode.js +542 -0
  340. package/Resources/WebLibraries/three/addons/tsl/shadows/TileShadowNodeHelper.js +212 -0
  341. package/Resources/WebLibraries/three/addons/tsl/utils/GroundedSkybox.js +62 -0
  342. package/Resources/WebLibraries/three/addons/tsl/utils/RNoise.js +51 -0
  343. package/Resources/WebLibraries/three/addons/tsl/utils/Raymarching.js +70 -0
  344. package/Resources/WebLibraries/three/addons/tsl/utils/SoftParticles.js +53 -0
  345. package/Resources/WebLibraries/three/addons/tsl/utils/SpecularHelpers.js +302 -0
  346. package/Resources/WebLibraries/three/addons/tsl/utils/TAAUtils.js +184 -0
  347. package/Resources/WebLibraries/three/addons/utils/BufferGeometryUtils.js +1523 -0
  348. package/Resources/WebLibraries/three/addons/utils/CameraUtils.js +82 -0
  349. package/Resources/WebLibraries/three/addons/utils/ColorUtils.js +76 -0
  350. package/Resources/WebLibraries/three/addons/utils/GaussianSplatUtils.js +229 -0
  351. package/Resources/WebLibraries/three/addons/utils/GeometryCompressionUtils.js +547 -0
  352. package/Resources/WebLibraries/three/addons/utils/GeometryUtils.js +226 -0
  353. package/Resources/WebLibraries/three/addons/utils/LDrawUtils.js +211 -0
  354. package/Resources/WebLibraries/three/addons/utils/SceneOptimizer.js +459 -0
  355. package/Resources/WebLibraries/three/addons/utils/SceneUtils.js +363 -0
  356. package/Resources/WebLibraries/three/addons/utils/ShadowMapViewer.js +244 -0
  357. package/Resources/WebLibraries/three/addons/utils/ShadowMapViewerGPU.js +233 -0
  358. package/Resources/WebLibraries/three/addons/utils/SkeletonUtils.js +496 -0
  359. package/Resources/WebLibraries/three/addons/utils/SortUtils.js +175 -0
  360. package/Resources/WebLibraries/three/addons/utils/UVsDebug.js +173 -0
  361. package/Resources/WebLibraries/three/addons/utils/WebGLTextureUtils.js +115 -0
  362. package/Resources/WebLibraries/three/addons/utils/WebGPUTextureUtils.js +81 -0
  363. package/Resources/WebLibraries/three/addons/utils/WorkerPool.js +167 -0
  364. package/Resources/WebLibraries/three/addons/utils/unwrapUVs.js +865 -0
  365. package/Resources/WebLibraries/three/three.core.js +61162 -0
  366. package/Resources/WebLibraries/three/three.module.js +19537 -0
  367. package/Resources/WebLibraries/three/three.tsl.js +41903 -0
  368. package/Resources/WebLibraries/three/three.webgpu.js +53714 -0
  369. package/Resources/skills/2d-game/SKILL.md +305 -0
  370. package/Resources/skills/3d-game/SKILL.md +274 -0
  371. package/Resources/skills/accounts/SKILL.md +41 -0
  372. package/Resources/skills/android-design/SKILL.md +159 -0
  373. package/Resources/skills/app-ai/SKILL.md +154 -0
  374. package/Resources/skills/app-intents/SKILL.md +31 -0
  375. package/Resources/skills/app-kinds/SKILL.md +122 -0
  376. package/Resources/skills/apple-watch/SKILL.md +43 -0
  377. package/Resources/skills/assets/SKILL.md +183 -0
  378. package/Resources/skills/building-blocks/SKILL.md +97 -0
  379. package/Resources/skills/device-control/SKILL.md +56 -0
  380. package/Resources/skills/device-integrations/SKILL.md +278 -0
  381. package/Resources/skills/finder-files/SKILL.md +19 -0
  382. package/Resources/skills/game/SKILL.md +414 -0
  383. package/Resources/skills/game-3d/SKILL.md +12 -0
  384. package/Resources/skills/game-art/SKILL.md +11 -0
  385. package/Resources/skills/game-feel/SKILL.md +13 -0
  386. package/Resources/skills/game-levels/SKILL.md +12 -0
  387. package/Resources/skills/game-live/SKILL.md +12 -0
  388. package/Resources/skills/game-physics/SKILL.md +13 -0
  389. package/Resources/skills/game-playtest/SKILL.md +26 -0
  390. package/Resources/skills/game-ui/SKILL.md +278 -0
  391. package/Resources/skills/github-projects/SKILL.md +59 -0
  392. package/Resources/skills/icloud-sync/SKILL.md +30 -0
  393. package/Resources/skills/iphone-hardware/SKILL.md +57 -0
  394. package/Resources/skills/iphone-widgets/SKILL.md +53 -0
  395. package/Resources/skills/lg-developer-mode/SKILL.md +152 -0
  396. package/Resources/skills/mac-widgets/SKILL.md +60 -0
  397. package/Resources/skills/maps/SKILL.md +8 -0
  398. package/Resources/skills/menu-bar/SKILL.md +36 -0
  399. package/Resources/skills/more-apps/SKILL.md +53 -0
  400. package/Resources/skills/phone-controllers/SKILL.md +79 -0
  401. package/Resources/skills/phone-pages/SKILL.md +173 -0
  402. package/Resources/skills/roku-developer-mode/SKILL.md +106 -0
  403. package/Resources/skills/scene-3d/SKILL.md +27 -0
  404. package/Resources/skills/scripting/SKILL.md +17 -0
  405. package/Resources/skills/share-extension/SKILL.md +22 -0
  406. package/Resources/skills/tv-design/SKILL.md +72 -0
  407. package/Resources/skills/web-app/SKILL.md +136 -0
  408. package/Resources/skills/web-game/SKILL.md +95 -0
  409. package/Resources/skills/web-or-native/SKILL.md +103 -0
  410. package/Resources/skills/wish-links/SKILL.md +156 -0
  411. package/THIRD_PARTY_NOTICES.md +170 -0
  412. package/cli/android.mjs +941 -0
  413. package/cli/browser.mjs +268 -0
  414. package/cli/calls.mjs +390 -0
  415. package/cli/dev.mjs +773 -0
  416. package/cli/devices.mjs +984 -0
  417. package/cli/lamp.mjs +1016 -0
  418. package/cli/lampapp.mjs +332 -0
  419. package/cli/lamps/android-host/LampPageGateway.kt +345 -0
  420. package/cli/lamps/android-host/app/build.gradle.kts +40 -0
  421. package/cli/lamps/android-host/build.gradle.kts +4 -0
  422. package/cli/lamps/android-host/game/AndroidManifest.xml +33 -0
  423. package/cli/lamps/android-host/game/MainActivity.kt +173 -0
  424. package/cli/lamps/android-host/gradle/libs.versions.toml +39 -0
  425. package/cli/lamps/android-host/gradle.properties +8 -0
  426. package/cli/lamps/android-host/page/AndroidManifest.xml +35 -0
  427. package/cli/lamps/android-host/page/MainActivity.kt +193 -0
  428. package/cli/lamps/android-host/settings.gradle.kts +30 -0
  429. package/cli/lamps/android-host/tools.json +43 -0
  430. package/cli/lamps/lamp-controllers.js +2 -0
  431. package/cli/lamps/lamp-for-games.js +6863 -0
  432. package/cli/lamps/lamp.js +7023 -0
  433. package/cli/lamps/mac-starter/.lamp/meta.json +1 -0
  434. package/cli/lamps/mac-starter/APP.md +12 -0
  435. package/cli/lamps/mac-starter/Package.swift +8 -0
  436. package/cli/lamps/mac-starter/Sources/App.swift +11 -0
  437. package/cli/lamps/mac-starter/Sources/ContentView.swift +17 -0
  438. package/cli/lamps/mac-starter/Sources/LampSupport.swift +18097 -0
  439. package/cli/lamps/mac-starter/build.sh +336 -0
  440. package/cli/lamps/mac-starter/gitignore +3 -0
  441. package/cli/lamps/mac-starter/lamp-starter.json +6 -0
  442. package/cli/lamps/web-starter/APP.md +8 -0
  443. package/cli/lamps/web-starter/app.js +1 -0
  444. package/cli/lamps/web-starter/index.html +17 -0
  445. package/cli/lamps/web-starter/lamp-starter.json +5 -0
  446. package/cli/lamps/web-starter/lamp.js +7023 -0
  447. package/cli/lamps/web-starter/style.css +25 -0
  448. package/cli/link.mjs +138 -0
  449. package/cli/mac.mjs +621 -0
  450. package/cli/mdns.mjs +205 -0
  451. package/cli/msix.mjs +413 -0
  452. package/cli/network.mjs +209 -0
  453. package/cli/outside.mjs +140 -0
  454. package/cli/page.js +165 -0
  455. package/cli/place.mjs +298 -0
  456. package/cli/png.mjs +184 -0
  457. package/cli/share.mjs +202 -0
  458. package/cli/skill/SKILL.md +255 -0
  459. package/cli/skills.mjs +71 -0
  460. package/cli/state.mjs +173 -0
  461. package/cli/update.mjs +57 -0
  462. package/package.json +20 -4
  463. package/site/lib/quota.js +59 -0
  464. package/site/lib/shared-wishes.js +996 -0
package/cli/lamp.mjs ADDED
@@ -0,0 +1,1016 @@
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 devices the person's devices; lamp open --on xbox, or --on android, puts a page on one
14
+ // lamp setup teach the coding agents on this computer to work this way
15
+ // lamp skills Lamp's guidance for each kind of work; lamp skill <name> reads one
16
+
17
+ import { spawn, spawnSync } from "node:child_process";
18
+ import { chmodSync, cpSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, realpathSync, rmSync, rmdirSync, statSync, watch, writeFileSync } from "node:fs";
19
+ import { homedir } from "node:os";
20
+ import { basename, dirname, join, resolve, sep } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+ import { Problem, RESOURCES, layoutOf, manifestOf, projectOf, stateOf, running, ask } from "./state.mjs";
23
+ import { macActions, serveMac, writeMacStarter } from "./mac.mjs";
24
+ import { exportApp, unpack } from "./lampapp.mjs";
25
+ import { updateNotice } from "./update.mjs";
26
+ import { editionName, giveTo, givenTo, keepPictures, reachLamp, takeBack } from "./outside.mjs";
27
+ import { serve, ANY_BROWSER, CONTROLLERS, CONTROLLERS_FILE, DEVICE_FILE, lampJS } from "./dev.mjs";
28
+ import { allSkills } from "./skills.mjs";
29
+ import { share, unshare, ideas, finishIdea, readLink, nameOf, siteOrigin, LIMITS } from "./share.mjs";
30
+ import { Runner, allKept, fields, forget, integrationOf, isReady, look as lookForDevices, takesPages } from "./devices.mjs";
31
+ import { placeNamed } from "./place.mjs";
32
+ import { androidAdvice, androidDevices, androidDone, emulatorNamed, emulators, forgetAndroid, installTools, knowsAndroid, toolsThere } from "./android.mjs";
33
+
34
+ const here = dirname(fileURLToPath(import.meta.url));
35
+ const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
36
+
37
+ 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
38
+ Mac app Lamp made in Swift, built and run on this Mac.
39
+
40
+ lamp new <folder> [--name "Its Name"] [--web | --mac]
41
+ Make a new web game's folder from Lamp's starting files: the game runtime, three.js and a page to build on.
42
+ --web makes a new web app instead, and --mac a new Mac app in Swift, each with Lamp's building blocks.
43
+ lamp init [folder] [--update]
44
+ Give a page that's already there Lamp's game runtime: its own files beside the page, none of the page's
45
+ changed. --update writes Lamp's files again, for a newer Lamp.
46
+ lamp unpack <file.lampapp> [folder]
47
+ Unpack a Lamp app file (what Lamp's Share… makes, and a lamp.dev link carries) into a new folder beside it,
48
+ named for it, or into the folder given. A Mac app gets Lamp's own files as it's first built.
49
+ lamp export [folder] [--to file.lampapp] [--anyway]
50
+ Make the app's Lamp app file, as Lamp's Share… does: its own files, credits, licenses and what it is, for
51
+ anyone to open in Lamp. Written beside the folder, named for the app, or where --to says. Refused when it
52
+ holds a key or a license is missing; --anyway shares work its credits give no license for.
53
+ lamp dev [folder] [--open] [--port N] [--lan]
54
+ Run it, saying how each change went. --open opens a window for it; --lan lets other devices on the
55
+ network open it too.
56
+ lamp open [folder] [--on <device>] [--hidden] [--in-lamp | --here]
57
+ Show it in a window, running it in the background if it isn't running. Returns at once.
58
+ --in-lamp gives a Mac app to Lamp's app, which must be running: it shows the person its review the first time,
59
+ then builds the app and puts each change into it while it's open (live, or held for a Reload), as with its own
60
+ chat. From then on look, dev and reload go through Lamp's app and say how each change went in, and use tries the
61
+ app it has open, in plain sight, and stop quits it there. --here has lamp build and run its own copy again.
62
+ LAMP_APP names another Lamp by its bundle id.
63
+ --on puts a page on one of the person's devices instead and opens it there (--on xbox): packaged, installed
64
+ and opened, the copy that was there replaced. --on android builds Lamp's Android app around the page and
65
+ opens it in lamp's Android emulator (--on android-tv, its TV emulator), or on a phone, tablet or TV of the
66
+ person's by its name. --hidden starts the emulator with no window and no sound, for a check nobody watches.
67
+ lamp look [folder] [--reopen] [--size 800x600] [--wait seconds] [--json]
68
+ How the changes since the last look went, a picture of the page (or of a Mac app's windows), its screen as
69
+ text, and its log. --reopen opens it again from its beginning first (what it saved kept): what someone
70
+ opening it sees.
71
+ --size looks at it as a window of that size shows it, when no window of the person's is open.
72
+ --on <device> looks at the device instead: its screen as a picture, whether the app is open there, and what
73
+ the app last logged there, which for a game says how many frames a second it gets.
74
+ lamp use [folder] [--on <android device>] <one of these>
75
+ Try it as a person would, then look. --on tries it on an Android place instead: a point of its picture
76
+ clicked or dragged from, typing, one key or one controller's button at a time (no stick), and wait.
77
+ click <x> <y> a point of the picture
78
+ click "A Name" a control by its name in the screen as text
79
+ type <text> into whatever has the keyboard
80
+ key <name> [seconds] a key, or several at once (w+a), held: a letter, a digit, space,
81
+ enter, escape, tab, backspace, delete, up, down, left, right,
82
+ shift, control, alt, command, f1 to f12
83
+ drag <x> <y> <to x> <to y> press, move and let go
84
+ scroll <points> [x y] down is positive
85
+ stick <left|right> <x> <y> [seconds] [buttons]
86
+ a game's controller: x and y from -1 to 1, right and up being 1
87
+ button <name> [seconds] a game's controller: a, b, x, y, lb, rb, lt, rt, menu, view, up… (a+rt)
88
+ wait <seconds>
89
+ Several at once, with "then" between them, go one after another with nothing in between and one look at the end:
90
+ lamp use key space then stick left 1 0 3 then button a
91
+ lamp reload [folder] [--fresh]
92
+ Open the page again from its files, a game from where the player is, or with --fresh from its beginning.
93
+ A Mac app is quit and opened again.
94
+ lamp stop [folder] [--on <device>]
95
+ Stop running it. A Mac app is quit. --on ends it on a device instead (and closes an emulator lamp started
96
+ with no window).
97
+ lamp remove [folder] --on <device>
98
+ Take it off a device lamp put it on.
99
+ lamp devices [add [name] | forget <name>]
100
+ The person's devices that lamp puts a page on: an Xbox in Developer Mode, those set up and those found on the
101
+ network; and Android, lamp's emulators and the phones, tablets and TVs plugged in with USB debugging on.
102
+ "add" sets one up, a step at a time; it asks for what only the person should do (type an Xbox's Device Portal
103
+ password; accept Google's terms for Android's tools, with "add android"), so they run it in a terminal
104
+ themselves. "forget" lets one go ("forget android": lamp's emulators and its key to the person's devices).
105
+ lamp share [folder] --words "What it is" [--name N] [--line "A line under its name"] [--by "Who made it"]
106
+ [--asks "Found a bug?|What's missing?"] [--shape fill|16:9] [--no-picture] [--no-file] [--anyway]
107
+ Put the page on lamp.dev, where anyone with the link plays it and can send an idea. Public, so only when the
108
+ person asks. Again, it sends the page as it is now to the same link, with the same words unless new ones are given.
109
+ Its app file goes with it (as lamp export makes it), so anyone can open it in Lamp too; --no-file leaves it out.
110
+ --anyway shares a page that logged errors, or uses work that came with no license.
111
+ lamp unshare [folder] Take the link down.
112
+ lamp ideas [folder] [in <n> | dismiss <n>]
113
+ The ideas people playing the link have sent. "in" says one is in the game now (whoever asked to hear is
114
+ written to); "dismiss" lets one go.
115
+ lamp setup [--remove] Teach Claude Code and Codex on this computer to work this way (a skill for each).
116
+ lamp guide What that skill says, for any other agent.
117
+ lamp skills [word] Lamp's guidance for each kind of work, those about a word; lamp skill <name> reads one.
118
+ lamp blocks [folder] [Name]
119
+ Lamp's building blocks for a Mac app (saved data, AI, windows, the menu bar…) with what each is for, or one
120
+ in full: read from the app's own Sources/LampSupport.swift.
121
+
122
+ 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
123
+ 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
124
+ one, the folder you're in.`;
125
+
126
+ // What tells a coding agent how to work with lamp: one file, installed as a skill by `lamp setup` and printed by
127
+ // `lamp guide`. `{{lamp}}` in it is how lamp is run on this computer.
128
+ const SKILL_MARK = "<!-- Written by lamp setup: run it again to update, or with --remove to take it away. -->";
129
+ /** How lamp is run on this computer: `lamp` when that name runs this very file, else the file itself. */
130
+ function lampCommand() {
131
+ const me = fileURLToPath(import.meta.url);
132
+ // The copy in Lamp's app is run by its launcher beside it, on the app's own runtime, and updates with Lamp.
133
+ if (process.env.LAMP_FROM_APP === "1") { const launcher = join(here, "lamp"); return /\s/.test(launcher) ? JSON.stringify(launcher) : launcher; }
134
+ const found = spawnSync(process.platform === "win32" ? "where" : "which", ["lamp"], { encoding: "utf8" });
135
+ const onPath = found.status === 0 && found.stdout.trim().split("\n").some((path) => { try { return realpathSync(path.trim()) === realpathSync(me); } catch { return false; } });
136
+ return onPath ? "lamp" : `node ${/\s/.test(me) ? JSON.stringify(me) : me}`;
137
+ }
138
+ /**
139
+ * 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
140
+ * run and calls it lamp from there on: the whole path in every line would bury what the lines say.
141
+ */
142
+ function skillText() {
143
+ const text = readFileSync(join(here, "skill", "SKILL.md"), "utf8"), command = lampCommand();
144
+ if (command === "lamp") return text.replaceAll("{{lamp}}", "lamp");
145
+ const said = `**lamp isn't a command by that name on this computer. Wherever this says \`lamp\`, run:**\n\n ${command}\n\n`;
146
+ return text.replaceAll("{{lamp}}", "lamp").replace(/\n# [^\n]*\n\n/, (heading) => heading + said);
147
+ }
148
+ const quoted = (path) => (/\s/.test(path) ? JSON.stringify(path) : path);
149
+
150
+ /**
151
+ * Copies in Lamp's libraries that the page's import map names and its folder has none of (three.js, Phaser, Rapier:
152
+ * `Resources/WebLibraries`), as Lamp's app gives a game the ones its page uses. The names copied.
153
+ */
154
+ /**
155
+ * Lamp's own files beside a page, which its app writes at every build and a shared file never carries: for a game the
156
+ * runtime, the controllers' list and the device it's on; for any page lamp.js, Lamp's building blocks (a stand-in where
157
+ * Lamp's own isn't beside lamp, never over one); and Lamp's libraries its import map names from beside it. Only those:
158
+ * the page and its code are the project's. `update` writes the game's again over what's there.
159
+ */
160
+ function lampsFilesBeside(root, { game, update = false }) {
161
+ const runtime = join(RESOURCES, "GameKit", "web");
162
+ const wrote = [], kept = [];
163
+ if (game) {
164
+ if (![runtime, CONTROLLERS].every(existsSync)) throw new Problem("Lamp's own files aren't beside this copy of lamp");
165
+ const files = [
166
+ ...readdirSync(runtime).map((name) => [name, () => readFileSync(join(runtime, name))]),
167
+ [CONTROLLERS_FILE, () => readFileSync(CONTROLLERS)], [DEVICE_FILE, () => ANY_BROWSER],
168
+ ];
169
+ for (const [name, contents] of files) {
170
+ const file = join(root, name);
171
+ if (existsSync(file) && !update) { kept.push(name); continue; }
172
+ writeFileSync(file, contents());
173
+ wrote.push(name);
174
+ }
175
+ }
176
+ if (!existsSync(join(root, "lamp.js"))) {
177
+ writeFileSync(join(root, "lamp.js"), lampJS(game));
178
+ wrote.push("lamp.js");
179
+ }
180
+ wrote.push(...addLibraries(root));
181
+ return { wrote, kept };
182
+ }
183
+
184
+ function addLibraries(root) {
185
+ const lamps = join(RESOURCES, "WebLibraries");
186
+ let map = "";
187
+ try { map = /<script[^>]*type=["']importmap["'][^>]*>([\s\S]*?)<\/script>/i.exec(readFileSync(join(root, "index.html"), "utf8"))?.[1] ?? ""; } catch {}
188
+ const added = [];
189
+ for (const name of ["three", "phaser", "rapier"]) {
190
+ // Named as a folder beside the page: one loaded from elsewhere (a CDN) is the page's own business.
191
+ if (!new RegExp(`:\\s*["'](?:\\./)?${name}/`).test(map) || existsSync(join(root, name)) || !existsSync(join(lamps, name))) continue;
192
+ cpSync(join(lamps, name), join(root, name), { recursive: true });
193
+ added.push(`${name}/`);
194
+ }
195
+ return added;
196
+ }
197
+
198
+ function fail(problem) {
199
+ console.error(`lamp: ${problem}`);
200
+ process.exit(1);
201
+ }
202
+
203
+ /** The command's options and what's left: `--name value` for those that take one, `--name` for the rest. */
204
+ function parse(args, valued) {
205
+ const options = {}, rest = [];
206
+ for (let i = 0; i < args.length; i++) {
207
+ const arg = args[i];
208
+ if (arg === "--help" || arg === "-h") { console.log(USAGE); process.exit(0); }
209
+ if (!arg.startsWith("--")) { rest.push(arg); continue; }
210
+ const name = arg.slice(2);
211
+ if (valued.includes(name)) {
212
+ if (i + 1 >= args.length) throw new Problem(`--${name} takes a value`);
213
+ options[name] = args[++i];
214
+ } else options[name] = true;
215
+ }
216
+ return { options, rest };
217
+ }
218
+
219
+ /** The server for the folder: the one running, or one started for this command that ends when nothing uses it. */
220
+ async function serverFor(folder) {
221
+ const live = await running(folder);
222
+ if (live) return live;
223
+ const state = stateOf(folder);
224
+ mkdirSync(state, { recursive: true });
225
+ const out = openSync(join(state, "server.log"), "a");
226
+ spawn(process.execPath, [fileURLToPath(import.meta.url), "dev", folder, "--quiet", "--idle", "30"], { detached: true, stdio: ["ignore", out, out] }).unref();
227
+ for (let tries = 0; tries < 60; tries++) {
228
+ await sleep(100);
229
+ const started = await running(folder);
230
+ if (started) return started;
231
+ }
232
+ throw new Problem(`lamp dev didn't start for this folder; see ${join(state, "server.log")}`);
233
+ }
234
+
235
+ /** 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. */
236
+ function deviceReport(seen) {
237
+ 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.`)
238
+ : `On ${seen.device}: lamp hasn't put this there yet (lamp open --on does).`];
239
+ 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.`);
240
+ if (seen.log) {
241
+ const said = seen.log.split(/\r?\n/).filter((line) => line.trim());
242
+ 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.");
243
+ } else if (seen.installed) lines.push("It has logged nothing there yet.");
244
+ return lines.join("\n");
245
+ }
246
+
247
+ /** What the person types for a device's setup, read from the terminal without showing it. */
248
+ function typed(label) {
249
+ 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`);
250
+ return new Promise((done, failed) => {
251
+ const input = process.stdin;
252
+ let text = "";
253
+ const end = () => { input.setRawMode(false); input.pause(); input.off("data", heard); process.stdout.write("\n"); };
254
+ const heard = (chunk) => {
255
+ for (const letter of String(chunk)) {
256
+ if (letter === "\r" || letter === "\n") { end(); return done(text); }
257
+ if (letter === "\u0003") { end(); return failed(new Problem("stopped")); }
258
+ if (letter === "\u007f" || letter === "\b") text = text.slice(0, -1);
259
+ else if (letter >= " ") text += letter;
260
+ }
261
+ };
262
+ process.stdout.write(`${label} (not shown as you type): `);
263
+ input.setRawMode(true);
264
+ input.resume();
265
+ input.on("data", heard);
266
+ });
267
+ }
268
+
269
+ /** What a device is, by its maker and model, the maker said once. */
270
+ const called = (device) => (String(device.model).startsWith(device.maker) ? device.model : `${device.maker} ${device.model}`);
271
+
272
+ /** One line read from the person at the terminal. */
273
+ async function answered(question) {
274
+ const { createInterface } = await import("node:readline/promises");
275
+ const terminal = createInterface({ input: process.stdin, output: process.stdout });
276
+ try { return (await terminal.question(question)).trim(); } finally { terminal.close(); }
277
+ }
278
+
279
+ /** What `lamp devices` says of Android: lamp's emulators, and the person's devices adb sees. Nothing where lamp can't build for it. */
280
+ async function androidLines() {
281
+ if (!knowsAndroid()) return [];
282
+ 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.`];
283
+ const lines = ["Android:"];
284
+ for (const emulator of await emulators()) {
285
+ const standing = !emulator.downloaded ? `not downloaded yet (${lampCommand()} devices add ${emulator.word}, about a gigabyte)`
286
+ : emulator.running ? `running${emulator.hidden ? ", with no window" : ""}` : "started when a page is opened in it";
287
+ lines.push(` ${emulator.name[0].toUpperCase()}${emulator.name.slice(1)} (--on ${emulator.word}): ${standing}`);
288
+ }
289
+ const own = await androidDevices();
290
+ 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)}`);
291
+ 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.");
292
+ return lines;
293
+ }
294
+
295
+ /** `lamp devices add android`: Android's tools, downloaded once the person says so, since it accepts Google's terms for them. */
296
+ async function addAndroid(tv) {
297
+ 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");
298
+ const there = toolsThere(), word = tv ? "android-tv" : "android";
299
+ if (there.builds && there.emulatorPackages && (!tv || there.tvImage)) {
300
+ 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}`);
301
+ return;
302
+ }
303
+ 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`);
304
+ 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.`);
305
+ console.log("Downloading them accepts Google's terms for them: https://developer.android.com/studio/terms");
306
+ if (!/^y(es)?$/i.test(await answered("Download them? (y/n) "))) { console.log("Nothing was downloaded."); return; }
307
+ await installTools({ tv, said: (line) => console.log(line) });
308
+ console.log(`\nAndroid's tools are ready. Open a page in ${tv ? "the Android TV emulator" : "the Android emulator"} with: ${lampCommand()} open --on ${word}`);
309
+ }
310
+
311
+ /** `lamp devices`: the person's devices that take a page, and setting one up. */
312
+ async function devices(args) {
313
+ const { rest } = parse(args, []);
314
+ const [verb, ...words] = rest, name = words.join(" ").toLowerCase();
315
+ if (verb === "add" && emulatorNamed(name)) return addAndroid(emulatorNamed(name).emulator === "tv");
316
+ if (verb === "forget" && name === "android") {
317
+ 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."
318
+ : "lamp has made nothing for Android to forget.");
319
+ return;
320
+ }
321
+ const fits = (device) => !name || device.name.toLowerCase().includes(name) || device.integration.startsWith(name) || device.id.toLowerCase() === name;
322
+
323
+ if (verb === "forget") {
324
+ if (!name) throw new Problem("forget takes the device's name: lamp devices lists them");
325
+ const mine = allKept().filter(fits);
326
+ 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`);
327
+ forget(mine[0].id);
328
+ console.log(`Forgot ${mine[0].name}: its record, its certificate and what was typed for it. What lamp put on it is still there.`);
329
+ return;
330
+ }
331
+
332
+ if (verb !== undefined && verb !== "add") throw new Problem("devices takes add or forget: lamp devices lists them");
333
+ const all = await lookForDevices();
334
+ const pages = all.filter((device) => takesPages(integrationOf(device.integration)));
335
+
336
+ if (verb === undefined) {
337
+ const mine = pages.filter((device) => device.kept), others = pages.filter((device) => !device.kept);
338
+ if (mine.length) {
339
+ console.log("Set up with lamp:");
340
+ for (const device of mine) {
341
+ const ready = isReady(device, integrationOf(device.integration));
342
+ 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)"}`);
343
+ }
344
+ }
345
+ if (others.length) {
346
+ console.log(`${mine.length ? "\n" : ""}Found on the network, not set up (they may not be yours):`);
347
+ for (const device of others) console.log(` ${device.name}: ${called(device)} at ${device.address}`);
348
+ console.log(`\n"${lampCommand()} devices add" sets one up. It asks for what only you should type, so run it in a terminal yourself.`);
349
+ }
350
+ const elsewhere = all.filter((device) => !pages.includes(device) && device.found !== false);
351
+ 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(", ")}.`);
352
+ if (!mine.length && !others.length) {
353
+ console.log("lamp found no Xbox to put a page on. One is found in Developer Mode, on this network and switched on:");
354
+ for (const line of integrationOf("xbox-one")?.steps?.[0]?.lines ?? []) console.log(` - ${line}`);
355
+ }
356
+ const android = await androidLines();
357
+ if (android.length) console.log("\n" + android.join("\n"));
358
+ return;
359
+ }
360
+
361
+ // add: the one named, or the only one there, taken through its integration's steps in order.
362
+ const candidates = pages.filter((device) => device.found !== false && fits(device));
363
+ if (!candidates.length) {
364
+ 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`
365
+ + (integrationOf("xbox-one")?.steps?.[0]?.lines ?? []).map((line) => ` - ${line}`).join("\n"));
366
+ }
367
+ if (candidates.length > 1) throw new Problem(`lamp found ${candidates.map((device) => `"${device.name}"`).join(" and ")}: say which, lamp devices add <name>`);
368
+ const device = candidates[0], integration = integrationOf(device.integration), runner = new Runner({ integration, device });
369
+ console.log(`${device.name}: ${called(device)} at ${device.address}`);
370
+ for (const step of integration.steps ?? []) {
371
+ let result = await runner.check(step);
372
+ for (let tries = 0; !result.passed && step.field && (result.needs || result.rejected) && tries < 3; tries++) {
373
+ if (result.rejected) { fields.remove(device.id, step.field.key); console.log(`${device.name} didn't take that.`); }
374
+ else { console.log(`\n${step.title}`); for (const line of step.lines ?? []) console.log(` - ${line}`); }
375
+ fields.save(device.id, step.field.key, await typed(step.field.label));
376
+ result = await runner.check(step);
377
+ }
378
+ if (result.passed) { runner.passed(step, result.read); console.log(`Done: ${step.title}.`); continue; }
379
+ if (result.rejected) fields.remove(device.id, step.field?.key);
380
+ if (result.failed) throw new Problem(result.why);
381
+ console.log(`\nNext: ${step.title}`);
382
+ for (const line of step.lines ?? []) console.log(` - ${line}`);
383
+ if (step.link) console.log(` ${step.link}`);
384
+ console.log(`\nThen run "${lampCommand()} devices add" again.`);
385
+ process.exitCode = 1;
386
+ return;
387
+ }
388
+ 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]}`);
389
+ }
390
+
391
+ const WHERE = {
392
+ mac: "the app as it runs on this Mac",
393
+ window: "the window the person has open",
394
+ copy: "lamp's own copy of the page, which nobody sees (no window is open)",
395
+ page: "a page open in a browser lamp can't look into",
396
+ };
397
+
398
+ function report(seen) {
399
+ const lines = [];
400
+ if (seen.did) lines.push(seen.did);
401
+ 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." : ""}`);
402
+ if (seen.problem) lines.push(seen.problem);
403
+ if (seen.note) lines.push(seen.note);
404
+ if (seen.unfinished) lines.push(`Taken while ${seen.unfinished}: look again.`);
405
+ if (seen.picture) lines.push(`Picture ${seen.picture}`);
406
+ for (const other of seen.pictures ?? []) lines.push(` ${other} (another of its windows)`);
407
+ if (seen.changes.length) {
408
+ lines.push("Changes since the last look");
409
+ for (const change of seen.changes) lines.push(` ${change.files} → ${change.outcome}: ${change.means}${change.why ? ` (${change.why})` : ""}`);
410
+ } else {
411
+ lines.push(`Changes since the last look: none${seen.opened ? " to put in (the page opened with every file as it is now)" : ""}`);
412
+ }
413
+ lines.push(seen.errors.length ? "Errors since the last look" : "Errors since the last look: none");
414
+ for (const error of seen.errors) lines.push(` ${error.split("\n").join("\n ")}`);
415
+ if (seen.screen) lines.push("Screen", ...seen.screen.split("\n").map((line) => ` ${line}`));
416
+ if (seen.log.length) {
417
+ 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)}`));
418
+ } else {
419
+ lines.push("Log since the last look: nothing");
420
+ }
421
+ if (seen.opened) lines.push(`The whole log is kept in ${seen.logFile}`);
422
+ return lines.join("\n");
423
+ }
424
+
425
+ // MARK: A Mac app given to Lamp's app
426
+
427
+ /**
428
+ * The running Lamp a Mac app's folder was given to (`lamp open --in-lamp`), connected, or null for a folder lamp runs
429
+ * 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.
430
+ */
431
+ async function inLamp(layout) {
432
+ if (layout.kind !== "mac") return null;
433
+ const bundle = givenTo(layout.folder);
434
+ if (!bundle) return null;
435
+ const lamp = await reachLamp([bundle]);
436
+ 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.`);
437
+ return lamp;
438
+ }
439
+
440
+ /** What Lamp's app answered, as lamp says things: how the change went in, its words, and where its pictures are. */
441
+ function lampReport(folder, lamp, answer) {
442
+ const lines = [`${lamp.name}: ${answer.outcome ?? "answered"}.`];
443
+ if (answer.words) lines.push(answer.words);
444
+ const pictures = keepPictures(folder, answer.pictures);
445
+ if (pictures.length) lines.push(`Picture ${pictures[0]}`, ...pictures.slice(1).map((path) => ` ${path} (another of its windows)`));
446
+ return lines.join("\n");
447
+ }
448
+
449
+ /**
450
+ * A folder Lamp's app no longer has (the person took the app off its desk, or put another folder in its place) is
451
+ * lamp's own again: said once, and the command goes on as for any folder. Whether that's what Lamp answered.
452
+ */
453
+ function letGo(layout, lamp, answer) {
454
+ if (answer.facts?.taken_on !== false) return false;
455
+ takeBack(layout.folder);
456
+ 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.`);
457
+ return true;
458
+ }
459
+
460
+ /**
461
+ * Asks Lamp's app to put what the folder holds into the app (`reopen`: opened again for it), and says how it went.
462
+ * False when Lamp no longer has the folder: the caller runs its own copy instead.
463
+ */
464
+ async function applyInLamp(layout, lamp, { reopen = false } = {}) {
465
+ try {
466
+ let answer = await lamp.call("apply_change", { path: layout.folder, ...(reopen ? { reopen: true } : {}) });
467
+ if (letGo(layout, lamp, answer)) return false;
468
+ // Lamp started again since the last apply: it asks for a look at the app as it is first, and then takes the change.
469
+ if (answer.facts?.restarted) {
470
+ await lamp.call("look_at_app", { path: layout.folder });
471
+ answer = await lamp.call("apply_change", { path: layout.folder, ...(reopen ? { reopen: true } : {}) });
472
+ }
473
+ // Nothing new to put in brings no picture: the app as it is now, which the person may have used since, is looked at.
474
+ if (!answer.pictures.length && ["unchanged", "held"].includes(answer.outcome)) {
475
+ const seen = await lamp.call("look_at_app", { path: layout.folder });
476
+ answer = { ...answer, words: [answer.words, seen.words].filter(Boolean).join("\n\n"), pictures: seen.pictures };
477
+ }
478
+ console.log(lampReport(layout.folder, lamp, answer));
479
+ if (answer.failed) process.exitCode = 1;
480
+ return true;
481
+ } finally { lamp.close(); }
482
+ }
483
+
484
+ /**
485
+ * `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
486
+ * the next), and what it says of each change is said here, until the person stops it. Read as lamp's own Mac server
487
+ * reads a change: the code, the package and the app's own files, never its notes or what's hidden or built.
488
+ */
489
+ async function devInLamp(layout, lamp) {
490
+ const ignored = (path) => path.split("/").some((part) => part.startsWith(".") || part === "build" || part === "node_modules") || path.endsWith("~")
491
+ || (!path.includes("/") && /\.(md|json|txt)$/i.test(path));
492
+ const print = (text) => console.log(`${new Date().toLocaleTimeString([], { hour12: false })} ${text}`);
493
+ // What the folder holds, as sizes and times: Lamp's app reads and rewrites its own files in the folder as it applies,
494
+ // which the watcher hears, so only a folder that reads otherwise than at the last apply is a change.
495
+ const stamps = (dir = layout.folder, prefix = "", found = new Map()) => {
496
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
497
+ const path = prefix + entry.name;
498
+ if (ignored(path)) continue;
499
+ if (entry.isDirectory()) stamps(join(dir, entry.name), path + "/", found);
500
+ else if (entry.isFile()) { try { const info = statSync(join(dir, entry.name)); found.set(path, `${info.size}:${info.mtimeMs}`); } catch {} }
501
+ }
502
+ return found;
503
+ };
504
+ const same = (a, b) => a && a.size === b.size && [...a].every(([path, stamp]) => b.get(path) === stamp);
505
+ let busy = false, again = false, timer = null, applied = null, retried = false;
506
+ const apply = async () => {
507
+ if (busy) { again = true; return; }
508
+ const now = stamps();
509
+ if (same(applied, now)) return;
510
+ busy = true;
511
+ try {
512
+ lamp = lamp ?? (await reachLamp([givenTo(layout.folder)]));
513
+ if (!lamp) { print(`${editionName(givenTo(layout.folder))} isn't running: the change goes in once it is and the next save is made.`); return; }
514
+ let answer = await lamp.call("apply_change", { path: layout.folder });
515
+ if (answer.facts?.restarted) { await lamp.call("look_at_app", { path: layout.folder }); answer = await lamp.call("apply_change", { path: layout.folder }); }
516
+ if (letGo(layout, lamp, answer)) { stopWatching(false); return; }
517
+ // A folder that changed while Lamp built it is applied again by the next save, or now when it's said so.
518
+ // A folder that changed while Lamp built it goes in again now, saved or not.
519
+ if (answer.facts?.stale || answer.outcome === "stale") { applied = null; again = true; } else applied = now;
520
+ retried = false;
521
+ print(lampReport(layout.folder, lamp, answer));
522
+ } catch (error) {
523
+ try { lamp?.close(); } catch {}
524
+ // A connection Lamp closed (it quit, or started again) is opened again once, and the change goes in through it.
525
+ if (lamp && !retried) { lamp = null; retried = true; again = true; }
526
+ else { print(`${error.message}`); lamp = null; }
527
+ } finally {
528
+ busy = false;
529
+ if (again) { again = false; apply(); }
530
+ }
531
+ };
532
+ print(`${lamp.name} has the app: each save goes into it. Ctrl-C stops watching; the app stays open there.`);
533
+ // Watching from before the first apply, so a save made while it runs goes in after it.
534
+ const watcher = watch(layout.folder, { recursive: true }, (event, name) => {
535
+ if (!name || ignored(String(name).split(sep).join("/"))) return;
536
+ clearTimeout(timer);
537
+ timer = setTimeout(apply, 300);
538
+ });
539
+ const quit = () => { watcher.close(); try { lamp?.close(); } catch {} process.exit(0); };
540
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(signal, quit);
541
+ // Until it's stopped, or Lamp no longer has the app: then lamp dev goes on as for any folder (false), its own
542
+ // runner answering Ctrl-C from then on.
543
+ let stopWatching;
544
+ const watching = new Promise((done) => {
545
+ stopWatching = (handled) => {
546
+ watcher.close(); clearTimeout(timer); try { lamp?.close(); } catch {}
547
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.off(signal, quit);
548
+ done(handled);
549
+ };
550
+ });
551
+ await apply();
552
+ return await watching;
553
+ }
554
+
555
+ async function main() {
556
+ const [command, ...args] = process.argv.slice(2);
557
+ if (!command || command === "--help" || command === "-h" || command === "help") { console.log(USAGE); return; }
558
+
559
+ if (command === "new") {
560
+ const { options, rest } = parse(args, ["name"]);
561
+ if (!rest[0]) throw new Problem("new takes the folder to make: lamp new harbor-racer");
562
+ const folder = resolve(rest[0]);
563
+ if (existsSync(folder) && readdirSync(folder).length) throw new Problem(`${folder} already has things in it`);
564
+ if (options.web) {
565
+ // A new web app's page as Lamp's app makes one (`ScreenKit.fill`): the page, its style and script, lamp.js and
566
+ // its notes, from a copy Lamp's own code wrote under a stand-in name (scripts/cli-files.sh).
567
+ const starter = join(here, "lamps", "web-starter");
568
+ if (!existsSync(join(starter, "lamp-starter.json"))) {
569
+ 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.`);
570
+ }
571
+ const stood = JSON.parse(readFileSync(join(starter, "lamp-starter.json"), "utf8"));
572
+ 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();
573
+ if (!title || title.length > 60) throw new Problem("a web app's name is 60 characters at most: --name \"Its Name\"");
574
+ const inHTML = title.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
575
+ mkdirSync(folder, { recursive: true });
576
+ for (const file of readdirSync(starter)) {
577
+ if (file === "lamp-starter.json") continue;
578
+ // Lamp's building blocks are the same for every page; the rest names this one.
579
+ if (file === "lamp.js") cpSync(join(starter, file), join(folder, file));
580
+ else writeFileSync(join(folder, file), readFileSync(join(starter, file), "utf8").replaceAll(stood.name, file.endsWith(".html") ? inHTML : title));
581
+ }
582
+ console.log(`Made ${folder}: a new web app, "${title}", with Lamp's building blocks for a page (lamp.js).`);
583
+ console.log("Yours to write: index.html, style.css, app.js and APP.md. It shows a card until then.");
584
+ console.log("Lamp's, never edited: lamp.js.");
585
+ console.log(`Write the app, then see it with: ${lampCommand()} look ${quoted(folder)}`);
586
+ return;
587
+ }
588
+ if (options.mac) {
589
+ // A new Mac app as Lamp's app makes one (`Template.scaffold`): Lamp's own files and the card the app shows until
590
+ // it's written, from a copy Lamp's own code wrote under a stand-in name (scripts/cli-files.sh).
591
+ const title = String(options.name ?? basename(folder).split(/[-_\s]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join(" ")).trim();
592
+ // The name goes into the app's build script and its code as it is, so it's one that's safe in both.
593
+ 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\"");
594
+ writeMacStarter(folder, title);
595
+ // What it is, in Lamp's own words (`Sharing.Manifest`): with it, Lamp's app takes the folder on as it is
596
+ // (lamp open --in-lamp), and lamp reads it as a Mac app.
597
+ 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");
598
+ console.log(`Made ${folder}: a new Mac app, "${title}", with Lamp's building blocks (Sources/LampSupport.swift).`);
599
+ console.log("Yours to write: the app's code in Sources/, and APP.md. It shows a card until then.");
600
+ console.log("Lamp's, never edited: Sources/LampSupport.swift, build.sh and Package.swift.");
601
+ console.log(`Build and open it with: ${lampCommand()} open ${quoted(folder)}`);
602
+ return;
603
+ }
604
+ // What Lamp gives a new web game: its starting page, the game runtime and three.js, from Lamp's own folder.
605
+ const lamps = RESOURCES, scaffold = join(lamps, "GameKit", "scaffold-web", "Web");
606
+ const runtime = join(lamps, "GameKit", "web"), three = join(lamps, "WebLibraries", "three");
607
+ if (![scaffold, runtime, three, CONTROLLERS].every(existsSync)) throw new Problem("Lamp's starting files aren't beside this copy of lamp");
608
+ const name = String(options.name ?? basename(folder).split(/[-_\s]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join(" "));
609
+ const inPage = name.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
610
+ mkdirSync(folder, { recursive: true });
611
+ for (const file of readdirSync(scaffold)) {
612
+ writeFileSync(join(folder, file), readFileSync(join(scaffold, file), "utf8").replaceAll("{{NAME}}", file.endsWith(".html") ? inPage : name.replace(/[\r\n]+/g, " ")));
613
+ }
614
+ for (const file of readdirSync(runtime)) cpSync(join(runtime, file), join(folder, file));
615
+ addLibraries(folder);
616
+ // Lamp's own files beside the page, which its app writes at every build: with them the folder is a whole site,
617
+ // and runs wherever it's served from. lamp.js is the stand-in until Lamp's app takes the folder on.
618
+ cpSync(CONTROLLERS, join(folder, CONTROLLERS_FILE));
619
+ writeFileSync(join(folder, DEVICE_FILE), ANY_BROWSER);
620
+ writeFileSync(join(folder, "lamp.js"), lampJS(true));
621
+ // The app's notes, which an app made by Lamp keeps and Lamp's skills write in.
622
+ writeFileSync(join(folder, "APP.md"), `# ${name.replace(/[\r\n]+/g, " ")}\n\nA web game on Lamp's game runtime, not written yet.\n`);
623
+ console.log(`Made ${folder}: a new game, "${name}", on Lamp's game runtime, with three.js and Phaser beside it.`);
624
+ 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.");
625
+ console.log("Lamp's, never edited: lamp.js, lamp-game.js, lamp-phaser.js, lamp-controllers.js, lamp-device.js, three/ and phaser/.");
626
+ console.log(`Write the game, then see its real opening with: ${lampCommand()} look ${quoted(folder)} --reopen`);
627
+ return;
628
+ }
629
+
630
+ if (command === "export") {
631
+ const { options, rest } = parse(args, ["to"]);
632
+ const made = exportApp(rest[0] ?? ".", { anyway: !!options.anyway });
633
+ const folder = projectOf(rest[0] ?? ".").folder;
634
+ const to = resolve(options.to ?? join(dirname(folder), `${made.name}.lampapp`));
635
+ if (existsSync(to) && !options.to) throw new Problem(`${to} is there already: --to says where else to write it`);
636
+ writeFileSync(to, made.bytes);
637
+ console.log(`Made ${to}: "${made.name}", ${made.files} files, ${(made.bytes.length / 1_000_000).toFixed(1)} MB. Anyone with Lamp opens it.`);
638
+ return;
639
+ }
640
+
641
+ if (command === "unpack") {
642
+ const { rest } = parse(args, []);
643
+ if (!rest[0]) throw new Problem("unpack takes the Lamp app file: lamp unpack Harbor.lampapp");
644
+ const opened = unpack(rest[0], rest[1] ?? null);
645
+ console.log(`Unpacked "${opened.name}" into ${opened.folder}.`);
646
+ if (opened.refused) { console.log(`lamp can't run it: ${opened.refused}.`); return; }
647
+ // 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.
648
+ if (opened.project.kind === "page") lampsFilesBeside(opened.project.root, { game: opened.project.game || manifestOf(opened.folder)?.platform === "game" });
649
+ console.log(`${opened.project.kind === "mac" ? "Build and open it" : "See it"} with: ${lampCommand()} ${opened.project.kind === "mac" ? "open" : "look"} ${quoted(opened.folder)}`);
650
+ return;
651
+ }
652
+
653
+ if (command === "init") {
654
+ const { options, rest } = parse(args, []);
655
+ const layout = layoutOf(rest[0] ?? ".");
656
+ const { wrote, kept } = lampsFilesBeside(layout.root, { game: true, update: !!options.update });
657
+ 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}.`);
658
+ if (kept.length) console.log(`Left as they were: ${kept.join(", ")} (lamp init --update writes them again).`);
659
+ 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.`);
660
+ return;
661
+ }
662
+
663
+ if (command === "dev") {
664
+ const { options, rest } = parse(args, ["port", "idle"]);
665
+ const layout = projectOf(rest[0] ?? ".");
666
+ const port = options.port === undefined ? undefined : Number(options.port);
667
+ if (port !== undefined && !(Number.isInteger(port) && port > 0 && port < 65536)) throw new Problem("--port takes a number from 1 to 65535");
668
+ const live = await running(layout.folder);
669
+ if (live?.own) {
670
+ // One `lamp look` started for itself, with nobody watching it: this one takes its place.
671
+ await ask(live, "stop", {}).catch(() => {});
672
+ for (let tries = 0; tries < 30 && (await running(layout.folder)); tries++) await sleep(100);
673
+ } else if (live) {
674
+ // One server a folder: this one says where it is, and opens the window when asked.
675
+ if (options.open) await ask(live, "open", {}, { timeout: 20_000 });
676
+ console.log(`lamp dev is already running for this folder: ${live.url}`);
677
+ return;
678
+ }
679
+ // Given to Lamp's app: each save goes to it, and what it says of the change is said here.
680
+ if (layout.kind === "mac" && givenTo(layout.folder) && (await devInLamp(layout, await inLamp(layout)))) return;
681
+ // 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.
682
+ const server = layout.kind === "mac"
683
+ ? await serveMac({ folder: layout.folder, port, quiet: !!options.quiet, idle: Number(options.idle) || 0 })
684
+ : await serve({ folder: layout.folder, port, lan: !!options.lan, quiet: !!options.quiet, idle: Number(options.idle) || 0 });
685
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(signal, () => server.close().then(() => process.exit(0)));
686
+ if (options.open && layout.kind !== "mac") await server.openWindow();
687
+ return;
688
+ }
689
+
690
+ if (command === "look") {
691
+ const { options, rest } = parse(args, ["wait", "size", "on"]);
692
+ if (options.on !== undefined) {
693
+ const seen = await (await placeNamed(options.on, { command: lampCommand() })).look(layoutOf(rest[0] ?? "."));
694
+ console.log(options.json ? JSON.stringify(seen, null, 2) : deviceReport(seen));
695
+ return;
696
+ }
697
+ const layout = projectOf(rest[0] ?? ".");
698
+ // Given to Lamp's app: what the folder holds goes in (nothing, when it's in already), and Lamp's app says how.
699
+ const lamp = await inLamp(layout);
700
+ if (lamp && (await applyInLamp(layout, lamp, { reopen: !!options.reopen }))) return;
701
+ // A Mac app's change is a build, which takes longer than a page's.
702
+ const wait = options.wait === undefined ? (layout.kind === "mac" ? 180 : 20) : Math.min(Math.max(Number(options.wait) || 0, 0), 600);
703
+ const size = options.size === undefined ? null : /^(\d{3,4})x(\d{3,4})$/i.exec(options.size)?.slice(1).map(Number);
704
+ if (options.size !== undefined && !size) throw new Problem("--size takes a width and a height: --size 800x600");
705
+ const seen = await ask(await serverFor(layout.folder), "look", { wait, reopen: !!options.reopen, size }, { timeout: (wait + 60) * 1000 });
706
+ console.log(options.json ? JSON.stringify(seen, null, 2) : report(seen));
707
+ return;
708
+ }
709
+
710
+ if (command === "use") {
711
+ const { options, rest } = parse(args, ["on"]);
712
+ const kinds = ["key", "click", "type", "wait", "drag", "scroll", "stick", "button"];
713
+ const folder = rest.length && !kinds.includes(rest[0]) ? rest.shift() : ".";
714
+ // Several moves, with "then" between them, are made one after another with nothing in between, and looked at once.
715
+ const moves = [];
716
+ for (let words = rest; words.length; ) {
717
+ const next = words.indexOf("then");
718
+ const [kind, ...values] = next < 0 ? words : words.slice(0, next);
719
+ words = next < 0 ? [] : words.slice(next + 1);
720
+ if (!kinds.includes(kind)) throw new Problem("use takes key, click, type, drag, scroll, stick, button or wait: lamp --help shows each");
721
+ if (kind === "key" && !values[0]) throw new Problem("key takes a key's name: lamp use key w 2");
722
+ moves.push(kind === "key" ? { kind, key: values[0], seconds: values[1] }
723
+ : kind === "click" ? (Number.isFinite(Number(values[0])) ? { kind, x: values[0], y: values[1] } : { kind, target: values.join(" ") })
724
+ : kind === "type" ? { kind, text: values.join(" ") }
725
+ : kind === "drag" ? { kind, x: values[0], y: values[1], toX: values[2], toY: values[3] }
726
+ : kind === "scroll" ? { kind, points: values[0], x: values[1], y: values[2] }
727
+ : kind === "stick" ? { kind, side: values[0], x: values[1], y: values[2], seconds: values[3], buttons: values[4] }
728
+ : kind === "button" ? { kind, buttons: values[0], seconds: values[1] }
729
+ : { kind, seconds: values[0] });
730
+ }
731
+ if (!moves.length) throw new Problem("use takes key, click, type, drag, scroll, stick, button or wait: lamp --help shows each");
732
+ if (options.on !== undefined) {
733
+ const place = await placeNamed(options.on, { command: lampCommand() });
734
+ 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`);
735
+ const seen = await place.use(layoutOf(folder), moves);
736
+ console.log(options.json ? JSON.stringify(seen, null, 2) : deviceReport(seen));
737
+ return;
738
+ }
739
+ const layout = projectOf(folder);
740
+ // Given to Lamp's app: it uses the app it has open, in plain sight, as its own agent does.
741
+ const lamp = await inLamp(layout);
742
+ if (lamp) {
743
+ let answer;
744
+ try { answer = await lamp.call("use_app", { path: layout.folder, actions: macActions(moves) }); } finally { lamp.close(); }
745
+ if (!letGo(layout, lamp, answer)) {
746
+ console.log(lampReport(layout.folder, lamp, answer));
747
+ if (answer.failed) process.exitCode = 1;
748
+ return;
749
+ }
750
+ }
751
+ const seen = await ask(await serverFor(layout.folder), "use", moves.length === 1 ? moves[0] : { moves }, { timeout: 60_000 + moves.length * 35_000 });
752
+ console.log(options.json ? JSON.stringify(seen, null, 2) : report(seen));
753
+ return;
754
+ }
755
+
756
+ if (command === "open") {
757
+ const { options, rest } = parse(args, ["on"]);
758
+ if (options.on !== undefined) {
759
+ const layout = layoutOf(rest[0] ?? "."), place = await placeNamed(options.on, { command: lampCommand() });
760
+ const put = await place.put(layout, { said: (line) => console.log(line), hidden: !!options.hidden });
761
+ console.log(put.line);
762
+ console.log(`${lampCommand()} look --on ${options.on} shows its screen and what it logged there; ${lampCommand()} stop --on ${options.on} ends it.`);
763
+ return;
764
+ }
765
+ const layout = projectOf(rest[0] ?? ".");
766
+ if (options["in-lamp"]) {
767
+ // Lamp's app takes the folder on (its review first, which the person answers there), and from then on builds it
768
+ // and puts each change into the app it has open. lamp's own copy stops: one app, not two.
769
+ if (layout.kind !== "mac") throw new Problem("--in-lamp gives a Mac app to Lamp's app; a page runs here as it is");
770
+ const lamp = await reachLamp();
771
+ if (!lamp) throw new Problem("Lamp's app isn't running: open Lamp, then run this again");
772
+ const live = await running(layout.folder);
773
+ if (live) { await ask(live, "stop", {}).catch(() => {}); for (let tries = 0; tries < 50 && (await running(layout.folder)); tries++) await sleep(100); }
774
+ 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.`);
775
+ let answer;
776
+ try { answer = await lamp.call("open_project", { path: layout.folder }); } finally { lamp.close(); }
777
+ // Taken on is taken on, whatever its first build did: a change that didn't build goes in with the next apply.
778
+ const taken = !["not_importable", "refused", "cancelled"].includes(answer.outcome);
779
+ if (taken) giveTo(layout.folder, lamp.bundle);
780
+ console.log(lampReport(layout.folder, lamp, answer));
781
+ if (answer.failed) process.exitCode = 1;
782
+ 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.`);
783
+ return;
784
+ }
785
+ if (options.here) takeBack(layout.folder);
786
+ const lamp = await inLamp(layout);
787
+ if (lamp && (await applyInLamp(layout, lamp))) return;
788
+ const opened = await ask(await serverFor(layout.folder), "open", {}, { timeout: 30_000 });
789
+ if (opened.opened === "app") {
790
+ 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.`);
791
+ return;
792
+ }
793
+ console.log(opened.opened === "window" ? `Opened in a window: ${opened.url}` : `Opened in the browser: ${opened.url}\nlamp found no browser it can look into (Chrome, Edge, Brave or Chromium), so looks have no picture.`);
794
+ return;
795
+ }
796
+
797
+ if (command === "setup") {
798
+ const { options } = parse(args, []);
799
+ const text = `${skillText().trimEnd()}\n\n${SKILL_MARK}\n`;
800
+ let any = false;
801
+ for (const [agent, home] of [["Claude Code", join(homedir(), ".claude")], ["Codex", join(homedir(), ".codex")]]) {
802
+ const folder = join(home, "skills", "lamp"), file = join(folder, "SKILL.md");
803
+ // Only a skill lamp wrote is replaced or taken away.
804
+ const theirs = existsSync(file) && !readFileSync(file, "utf8").includes(SKILL_MARK);
805
+ if (options.remove) {
806
+ if (!existsSync(file) || theirs) continue;
807
+ rmSync(file);
808
+ try { rmdirSync(folder); } catch {}
809
+ console.log(`Took lamp's skill away from ${agent}.`);
810
+ any = true;
811
+ } else if (theirs) {
812
+ console.log(`${agent} has a skill called lamp that lamp didn't write, left as it is: ${file}`);
813
+ any = true;
814
+ } else if (existsSync(home)) {
815
+ mkdirSync(folder, { recursive: true });
816
+ writeFileSync(file, text);
817
+ console.log(`${agent} knows how to work with lamp, from its next session: ${file}`);
818
+ any = true;
819
+ }
820
+ }
821
+ 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.");
822
+ return;
823
+ }
824
+
825
+ if (command === "share") {
826
+ const { options, rest } = parse(args, ["name", "words", "line", "by", "asks", "shape"]);
827
+ const layout = layoutOf(rest[0] ?? ".");
828
+ const before = readLink(layout.folder)?.site === siteOrigin() ? readLink(layout.folder) : null;
829
+ // The link's words: what's given now, else what it went out with before. Its name, where nothing says, is the page's.
830
+ const words = {
831
+ name: String(options.name ?? before?.name ?? nameOf(layout)).trim(), words: String(options.words ?? before?.words ?? "").trim(),
832
+ line: String(options.line ?? before?.line ?? "").trim(), by: String(options.by ?? before?.by ?? "").trim(),
833
+ asks: options.asks !== undefined ? String(options.asks).split("|").map((ask) => ask.trim()).filter(Boolean) : before?.asks ?? [],
834
+ };
835
+ const count = (text) => [...new Intl.Segmenter("en", { granularity: "grapheme" }).segment(text)].length;
836
+ 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"');
837
+ if (!words.name || count(words.name) > LIMITS.name) throw new Problem(`a link's name is at most ${LIMITS.name} characters: --name "Its Name"`);
838
+ if (count(words.words) > LIMITS.words) throw new Problem(`a link's words are at most ${LIMITS.words} characters`);
839
+ if (count(words.line) > LIMITS.line) throw new Problem(`a link's line is at most ${LIMITS.line} characters`);
840
+ if (count(words.by) > LIMITS.by) throw new Problem(`who it's by is at most ${LIMITS.by} characters`);
841
+ 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`);
842
+ // The page as it runs now: its picture for the link's card, the shape it plays in, and whether it's working.
843
+ let card = { picture: null, stage: null, errors: [] };
844
+ if (!options["no-picture"]) card = await ask(await serverFor(layout.folder), "card", { wait: 30 }, { timeout: 120_000 });
845
+ if (card.errors.length && !options.anyway) {
846
+ 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.`);
847
+ }
848
+ let shape = card.stage ? Math.round((card.stage[0] / card.stage[1]) * 10000) / 10000 : "fill";
849
+ if (options.shape !== undefined) {
850
+ const ratio = /^(\d+(?:\.\d+)?)[:/x](\d+(?:\.\d+)?)$/.exec(String(options.shape));
851
+ if (options.shape !== "fill" && !ratio) throw new Problem("--shape is fill, for a page that takes any window, or a shape like 16:9");
852
+ shape = ratio ? Math.round((Number(ratio[1]) / Number(ratio[2])) * 10000) / 10000 : "fill";
853
+ }
854
+ const sent = await share({ layout, words, shape, picture: card.picture ? Buffer.from(card.picture, "base64") : null, said: (line) => console.log(line),
855
+ file: !options["no-file"], anyway: !!options.anyway });
856
+ console.log(`${before ? "Sent again to its link" : "Shared"}: ${sent.url}`);
857
+ 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" : ""}.`);
858
+ console.log("Anyone with the link can play it and send an idea: lamp ideas shows them. lamp unshare takes it down.");
859
+ return;
860
+ }
861
+
862
+ if (command === "unshare") {
863
+ const { rest } = parse(args, []);
864
+ console.log(`Taken down: ${await unshare(layoutOf(rest[0] ?? ".").folder)}`);
865
+ return;
866
+ }
867
+
868
+ if (command === "ideas") {
869
+ const { rest } = parse(args, []);
870
+ const outcomes = ["in", "dismiss"];
871
+ const folder = rest.length && !outcomes.includes(rest[0]) ? rest.shift() : ".";
872
+ const layout = layoutOf(folder);
873
+ if (rest.length) {
874
+ const n = Number(rest[1]);
875
+ if (!outcomes.includes(rest[0]) || !Number.isInteger(n) || n < 1) throw new Problem("ideas takes in <number> or dismiss <number>: lamp ideas lists them with their numbers");
876
+ const answer = await finishIdea(layout.folder, n, rest[0] === "in");
877
+ 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.`);
878
+ return;
879
+ }
880
+ const sent = await ideas(layout.folder);
881
+ if (!sent.ideas.length) { console.log(`No ideas waiting for ${sent.url}`); return; }
882
+ console.log(`Ideas sent to ${sent.url}`);
883
+ for (const idea of sent.ideas) {
884
+ console.log(` ${idea.n}. ${String(idea.text).split("\n").join("\n ")}${idea.wantsNews ? " (wants to hear when it's in)" : ""}`);
885
+ }
886
+ console.log("Say what became of one with: lamp ideas in <number>, or lamp ideas dismiss <number>.");
887
+ return;
888
+ }
889
+
890
+ if (command === "remove") {
891
+ const { options, rest } = parse(args, ["on"]);
892
+ if (options.on === undefined) throw new Problem("remove takes the device to take it off: lamp remove --on xbox");
893
+ const place = await placeNamed(options.on, { command: lampCommand() });
894
+ console.log((await place.remove(layoutOf(rest[0] ?? "."))) ? `Taken off ${place.name}.` : `lamp hasn't put this on ${place.name}.`);
895
+ return;
896
+ }
897
+
898
+ if (command === "devices") return await devices(args);
899
+
900
+ if (command === "reload" || command === "stop") {
901
+ const { options, rest } = parse(args, ["on"]);
902
+ if (options.on !== undefined) {
903
+ if (command !== "stop") throw new Problem("a change goes on a device by putting the page there again: lamp open --on " + options.on);
904
+ const place = await placeNamed(options.on, { command: lampCommand() });
905
+ console.log((await place.stop(layoutOf(rest[0] ?? "."))) ? `Stopped on ${place.name}.` : `lamp hasn't put this on ${place.name}.`);
906
+ return;
907
+ }
908
+ const layout = projectOf(rest[0] ?? ".");
909
+ if (layout.kind === "mac" && givenTo(layout.folder)) {
910
+ if (command === "stop") {
911
+ // Quit by Lamp's app as at its own Stop, which waits for an app asking about unsaved work.
912
+ const lamp = await inLamp(layout);
913
+ let answer;
914
+ try { answer = await lamp.call("close_app", { path: layout.folder }); } finally { lamp.close(); }
915
+ if (!letGo(layout, lamp, answer)) { console.log(lampReport(layout.folder, lamp, answer)); return; }
916
+ } else if (await applyInLamp(layout, await inLamp(layout), { reopen: true })) return;
917
+ }
918
+ const live = await running(layout.folder);
919
+ if (!live) { console.log("lamp dev isn't running for this folder."); return; }
920
+ const answer = await ask(live, command, { fresh: !!options.fresh });
921
+ // Stopped means gone: the next command finds no server, never one on its way out.
922
+ if (command === "stop") for (let tries = 0; tries < 50 && (await running(layout.folder)); tries++) await sleep(100);
923
+ console.log(command === "stop" ? "Stopped." : answer.pages ? (layout.kind === "mac" ? "The app is opening again." : "The page is opening again.") : "Nothing is open.");
924
+ return;
925
+ }
926
+
927
+ if (command === "guide") {
928
+ // The skill without what only a skill's file needs in front.
929
+ console.log(skillText().replace(/^---\n[\s\S]*?\n---\n+/, "").trimEnd());
930
+ return;
931
+ }
932
+
933
+ if (command === "blocks") {
934
+ // Lamp's building blocks for a Mac app, read from the app's own copy of them (Sources/LampSupport.swift), whose
935
+ // comments are the whole of how each is used: every one with what it's for, or one in full.
936
+ const { rest } = parse(args, []);
937
+ const name = rest.find((word) => /^Lamp\w*$/.test(word)) ?? null;
938
+ const project = projectOf(rest.find((word) => word !== name) ?? ".");
939
+ const file = join(project.folder, "Sources", "LampSupport.swift");
940
+ if (!existsSync(file)) throw new Problem("Lamp's building blocks are a Mac app's (Sources/LampSupport.swift): this folder has none");
941
+ const lines = readFileSync(file, "utf8").split("\n");
942
+ const declared = /^ {4}(?:@[\w.]+(?:\([^)]*\))?\s+)*(?:(?:public|final|nonisolated|indirect)\s+)*(enum|struct|class|actor|protocol)\s+(Lamp\w+)/;
943
+ const blocks = new Map();
944
+ lines.forEach((line, at) => {
945
+ const found = declared.exec(line);
946
+ if (!found || blocks.has(found[2])) return;
947
+ let above = at - 1;
948
+ while (above >= 0 && /^ {4}@/.test(lines[above])) above--;
949
+ const says = [];
950
+ for (; above >= 0 && /^ {4}\/\/\//.test(lines[above]); above--) says.unshift(lines[above].replace(/^ {4}\/\/\/ ?/, ""));
951
+ blocks.set(found[2], { at, says });
952
+ });
953
+ // What a block offers, by name: its members that aren't its own alone.
954
+ const offers = (block) => {
955
+ const names = [];
956
+ for (let at = block.at + 1; at < lines.length && !/^ {4}\}/.test(lines[at]); at++) {
957
+ const member = /^ {8}(?:@[\w.]+(?:\([^)]*\))?\s+)*((?:(?:static|public|private|fileprivate|nonisolated|mutating|final)(?:\(set\))?\s+)*)(?:func|var|let)\s+(\w+)/.exec(lines[at]);
958
+ if (member && !/private/.test(member[1]) && !names.includes(member[2])) names.push(member[2]);
959
+ }
960
+ return names;
961
+ };
962
+ if (!name) {
963
+ for (const [block, entry] of blocks) {
964
+ // What it's for, where its comment says; else what it offers, which says as much.
965
+ const first = entry.says.join(" ").split(/(?<=[.:])\s/)[0];
966
+ const said = entry.says.length ? first : (() => { const names = offers(entry); return names.length ? `${names.slice(0, 8).join(", ")}${names.length > 8 ? "…" : ""}` : ""; })();
967
+ if (said) console.log(`${block}: ${said.length > 150 ? said.slice(0, 150).replace(/\s+\S*$/, "") + "…" : said}`);
968
+ }
969
+ console.log(`\n${lampCommand()} blocks <Name> says how one is used.`);
970
+ return;
971
+ }
972
+ const block = blocks.get(name);
973
+ if (!block) throw new Problem(`there's no building block called ${name}: lamp blocks lists them`);
974
+ console.log(`${name} (Sources/LampSupport.swift:${block.at + 1})\n${block.says.length ? `\n${block.says.join("\n")}\n` : ""}`);
975
+ // What it offers: each member's own comment and how it's written, without what's inside it or what's its own alone.
976
+ for (let at = block.at + 1; at < lines.length && !/^ {4}\}/.test(lines[at]); at++) {
977
+ const line = lines[at];
978
+ if (/^ {8}\/\/\//.test(line)) { console.log(line.slice(4)); continue; }
979
+ 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);
980
+ if (member && !/\b(private|fileprivate)\b/.test(member[1])) console.log(" " + member[1].replace(/\s*\{.*$/, ""));
981
+ }
982
+ return;
983
+ }
984
+
985
+ if (command === "skills" || command === "skill") {
986
+ // Lamp's skills, from Lamp's own folder beside this command: the one place that says which of Lamp's building
987
+ // blocks to use and how.
988
+ const skills = allSkills();
989
+ if (!skills.size) throw new Problem("Lamp's skills aren't beside this copy of lamp");
990
+ if (command === "skill") {
991
+ if (!args[0]) throw new Problem("skill takes a name: lamp skills lists them");
992
+ const skill = skills.get(args[0]);
993
+ if (!skill) throw new Problem(`there's no skill "${args[0]}": lamp skills lists them`);
994
+ console.log(`# ${skill.name}\n\n${skill.description}\n\n${skill.body}`);
995
+ return;
996
+ }
997
+ // `lamp skills game` lists only those about a word.
998
+ const about = args[0] ? args[0].toLowerCase() : null;
999
+ const listed = [...skills.values()].filter((skill) => !about || `${skill.name} ${skill.description}`.toLowerCase().includes(about)).sort((a, b) => a.name.localeCompare(b.name));
1000
+ for (const skill of listed) console.log(`${skill.name}: ${skill.description.length > 170 ? skill.description.slice(0, 170).replace(/\s+\S*$/, "") + "…" : skill.description}`);
1001
+ if (about && !listed.length) console.log(`No skill is about "${about}": lamp skills lists them all.`);
1002
+ else if (!about) console.log("\n`lamp skills <word>` lists only those about a word; `lamp skill <name>` prints one.");
1003
+ return;
1004
+ }
1005
+
1006
+ throw new Problem(`there's no "${command}"\n\n${USAGE}`);
1007
+ }
1008
+
1009
+ // A newer lamp from npm is said once a day, before the command's own words (update.mjs).
1010
+ { const notice = updateNotice(); if (notice) console.error(notice); }
1011
+
1012
+ // Whatever a command did on Android, its adb server isn't left holding the person's devices (android.mjs).
1013
+ main().finally(() => androidDone().catch(() => {})).catch((error) => {
1014
+ if (error instanceof Problem) fail(error.message);
1015
+ throw error;
1016
+ });