undercity 1.0.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 (382) hide show
  1. package/AGENTS.md +26 -0
  2. package/README.md +58 -0
  3. package/actions/AGENTS.md +41 -0
  4. package/actions/_shared/container.js +16 -0
  5. package/actions/auth/ask-login/action.json +15 -0
  6. package/actions/auth/ask-signup/action.json +14 -0
  7. package/actions/display/clear/action.json +18 -0
  8. package/actions/display/clear/action.test.js +32 -0
  9. package/actions/display/markdown/action.json +12 -0
  10. package/actions/display/markdown/action.test.js +32 -0
  11. package/actions/display/rawHtml/action.json +24 -0
  12. package/actions/display/rawHtml/action.test.js +32 -0
  13. package/actions/display/safeHtml/action.json +24 -0
  14. package/actions/display/safeHtml/action.test.js +32 -0
  15. package/actions/display/text/action.js +9 -0
  16. package/actions/display/text/action.json +12 -0
  17. package/actions/display/text/action.test.js +40 -0
  18. package/actions/display/value/action.json +24 -0
  19. package/actions/display/value/action.test.js +32 -0
  20. package/actions/dom/addClass/action.json +23 -0
  21. package/actions/dom/addClass/action.test.js +32 -0
  22. package/actions/dom/focus/action.json +17 -0
  23. package/actions/dom/focus/action.test.js +32 -0
  24. package/actions/dom/hide/action.json +18 -0
  25. package/actions/dom/hide/action.test.js +32 -0
  26. package/actions/dom/removeClass/action.json +22 -0
  27. package/actions/dom/removeClass/action.test.js +32 -0
  28. package/actions/dom/scroll/action.json +29 -0
  29. package/actions/dom/scroll/action.test.js +32 -0
  30. package/actions/dom/setAttr/action.json +29 -0
  31. package/actions/dom/setAttr/action.test.js +32 -0
  32. package/actions/dom/setHtml/action.json +22 -0
  33. package/actions/dom/setHtml/action.test.js +32 -0
  34. package/actions/dom/setStyle/action.json +29 -0
  35. package/actions/dom/setStyle/action.test.js +32 -0
  36. package/actions/dom/setText/action.json +24 -0
  37. package/actions/dom/setText/action.test.js +32 -0
  38. package/actions/dom/show/action.json +18 -0
  39. package/actions/dom/show/action.test.js +32 -0
  40. package/actions/dom/toggle/action.json +17 -0
  41. package/actions/dom/toggle/action.test.js +32 -0
  42. package/actions/dom/toggleClass/action.json +22 -0
  43. package/actions/dom/toggleClass/action.test.js +32 -0
  44. package/actions/event/emit/action.json +24 -0
  45. package/actions/event/emit/action.test.js +32 -0
  46. package/actions/event/on/action.json +23 -0
  47. package/actions/event/on/action.test.js +32 -0
  48. package/actions/event/waitFor/action.json +28 -0
  49. package/actions/event/waitFor/action.test.js +32 -0
  50. package/actions/forms/bindField/action.js +27 -0
  51. package/actions/forms/bindField/action.json +24 -0
  52. package/actions/forms/bindField/action.test.js +20 -0
  53. package/actions/forms/check/action.json +22 -0
  54. package/actions/forms/check/action.test.js +32 -0
  55. package/actions/forms/clearErrors/action.json +11 -0
  56. package/actions/forms/clearErrors/action.test.js +35 -0
  57. package/actions/forms/clearField/action.json +17 -0
  58. package/actions/forms/clearField/action.test.js +32 -0
  59. package/actions/forms/getField/action.json +24 -0
  60. package/actions/forms/getField/action.test.js +32 -0
  61. package/actions/forms/getRange/action.json +22 -0
  62. package/actions/forms/getRange/action.test.js +32 -0
  63. package/actions/forms/getSelect/action.json +22 -0
  64. package/actions/forms/getSelect/action.test.js +32 -0
  65. package/actions/forms/index.js +140 -0
  66. package/actions/forms/serialize/action.json +24 -0
  67. package/actions/forms/serialize/action.test.js +32 -0
  68. package/actions/forms/setCheck/action.json +23 -0
  69. package/actions/forms/setCheck/action.test.js +32 -0
  70. package/actions/forms/setError/action.json +22 -0
  71. package/actions/forms/setError/action.test.js +32 -0
  72. package/actions/forms/setField/action.js +14 -0
  73. package/actions/forms/setField/action.json +24 -0
  74. package/actions/forms/setField/action.test.js +32 -0
  75. package/actions/forms/submit/action.json +18 -0
  76. package/actions/forms/submit/action.test.js +32 -0
  77. package/actions/forms/validate/action.json +24 -0
  78. package/actions/forms/validate/action.test.js +32 -0
  79. package/actions/http/delete/action.json +22 -0
  80. package/actions/http/delete/action.test.js +32 -0
  81. package/actions/http/get/action.json +24 -0
  82. package/actions/http/get/action.test.js +32 -0
  83. package/actions/http/post/action.json +30 -0
  84. package/actions/http/post/action.test.js +32 -0
  85. package/actions/http/put/action.json +27 -0
  86. package/actions/http/put/action.test.js +32 -0
  87. package/actions/http/upload/action.json +35 -0
  88. package/actions/http/upload/action.test.js +32 -0
  89. package/actions/index.js +306 -0
  90. package/actions/index.json +5 -0
  91. package/actions/input/askChoice/action.json +29 -0
  92. package/actions/input/askChoice/action.test.js +32 -0
  93. package/actions/input/askConfirm/action.json +24 -0
  94. package/actions/input/askConfirm/action.test.js +32 -0
  95. package/actions/input/askDate/action.json +24 -0
  96. package/actions/input/askDate/action.test.js +32 -0
  97. package/actions/input/askEmail/action.json +24 -0
  98. package/actions/input/askEmail/action.test.js +32 -0
  99. package/actions/input/askNumber/action.json +35 -0
  100. package/actions/input/askNumber/action.test.js +32 -0
  101. package/actions/input/askPassword/action.json +24 -0
  102. package/actions/input/askPassword/action.test.js +32 -0
  103. package/actions/input/askText/action.json +36 -0
  104. package/actions/input/askText/action.test.js +32 -0
  105. package/actions/logic/delay/action.json +18 -0
  106. package/actions/logic/delay/action.test.js +32 -0
  107. package/actions/logic/if/action.json +28 -0
  108. package/actions/logic/if/action.test.js +32 -0
  109. package/actions/logic/log/action.json +18 -0
  110. package/actions/logic/log/action.test.js +32 -0
  111. package/actions/logic/random/action.json +36 -0
  112. package/actions/logic/random/action.test.js +32 -0
  113. package/actions/logic/transform/action.json +24 -0
  114. package/actions/logic/transform/action.test.js +32 -0
  115. package/actions/media/askAudioUpload/action.json +30 -0
  116. package/actions/media/askAudioUpload/action.test.js +32 -0
  117. package/actions/media/askFileUpload/action.json +36 -0
  118. package/actions/media/askFileUpload/action.test.js +32 -0
  119. package/actions/media/askImageUpload/action.json +37 -0
  120. package/actions/media/askImageUpload/action.test.js +32 -0
  121. package/actions/media/askVideoUpload/action.js +74 -0
  122. package/actions/media/askVideoUpload/action.json +44 -0
  123. package/actions/media/askVideoUpload/action.test.js +51 -0
  124. package/actions/media/captureWebcam/action.json +24 -0
  125. package/actions/media/captureWebcam/action.test.js +32 -0
  126. package/actions/nav/back/action.json +11 -0
  127. package/actions/nav/back/action.test.js +35 -0
  128. package/actions/nav/goto/action.json +18 -0
  129. package/actions/nav/goto/action.test.js +32 -0
  130. package/actions/nav/redirect/action.json +28 -0
  131. package/actions/nav/redirect/action.test.js +32 -0
  132. package/actions/nav/reload/action.json +11 -0
  133. package/actions/nav/reload/action.test.js +35 -0
  134. package/actions/nav/reset/action.json +11 -0
  135. package/actions/nav/reset/action.test.js +35 -0
  136. package/actions/render/alert/action.js +12 -0
  137. package/actions/render/alert/action.json +36 -0
  138. package/actions/render/alert/action.test.js +32 -0
  139. package/actions/render/button/action.js +15 -0
  140. package/actions/render/button/action.json +44 -0
  141. package/actions/render/button/action.test.js +37 -0
  142. package/actions/render/clear/action.js +7 -0
  143. package/actions/render/clear/action.json +11 -0
  144. package/actions/render/clear/action.test.js +35 -0
  145. package/actions/render/divider/action.js +8 -0
  146. package/actions/render/divider/action.json +11 -0
  147. package/actions/render/divider/action.test.js +35 -0
  148. package/actions/render/field/action.js +17 -0
  149. package/actions/render/field/action.json +59 -0
  150. package/actions/render/field/action.test.js +57 -0
  151. package/actions/render/link/action.js +20 -0
  152. package/actions/render/link/action.json +30 -0
  153. package/actions/render/link/action.test.js +32 -0
  154. package/actions/render/markdown/action.json +18 -0
  155. package/actions/render/markdown/action.test.js +32 -0
  156. package/actions/render/paragraph/action.js +9 -0
  157. package/actions/render/paragraph/action.json +32 -0
  158. package/actions/render/paragraph/action.test.js +32 -0
  159. package/actions/render/section/action.js +10 -0
  160. package/actions/render/section/action.json +18 -0
  161. package/actions/render/section/action.test.js +32 -0
  162. package/actions/render/subtitle/action.js +9 -0
  163. package/actions/render/subtitle/action.json +18 -0
  164. package/actions/render/subtitle/action.test.js +32 -0
  165. package/actions/render/title/action.js +9 -0
  166. package/actions/render/title/action.json +30 -0
  167. package/actions/render/title/action.test.js +44 -0
  168. package/actions/session/clear/action.json +11 -0
  169. package/actions/session/clear/action.test.js +35 -0
  170. package/actions/session/load/action.json +22 -0
  171. package/actions/session/load/action.test.js +32 -0
  172. package/actions/session/local/action.json +22 -0
  173. package/actions/session/local/action.test.js +32 -0
  174. package/actions/session/save/action.json +22 -0
  175. package/actions/session/save/action.test.js +32 -0
  176. package/actions/ui/accordion/action.json +23 -0
  177. package/actions/ui/accordion/action.test.js +32 -0
  178. package/actions/ui/badge/action.json +22 -0
  179. package/actions/ui/badge/action.test.js +32 -0
  180. package/actions/ui/collapse/action.json +23 -0
  181. package/actions/ui/collapse/action.test.js +32 -0
  182. package/actions/ui/hideModal/action.json +17 -0
  183. package/actions/ui/hideModal/action.test.js +32 -0
  184. package/actions/ui/loading/action.json +18 -0
  185. package/actions/ui/loading/action.test.js +32 -0
  186. package/actions/ui/modal/action.json +18 -0
  187. package/actions/ui/modal/action.test.js +32 -0
  188. package/actions/ui/progress/action.json +24 -0
  189. package/actions/ui/progress/action.test.js +32 -0
  190. package/actions/ui/toast/action.json +29 -0
  191. package/actions/ui/toast/action.test.js +32 -0
  192. package/actions/ui/tooltip/action.json +17 -0
  193. package/actions/ui/tooltip/action.test.js +32 -0
  194. package/actions/user/carry/action.json +25 -0
  195. package/actions/user/carry/action.test.js +32 -0
  196. package/actions/user/check/action.json +24 -0
  197. package/actions/user/check/action.test.js +32 -0
  198. package/actions/user/clear/action.json +11 -0
  199. package/actions/user/clear/action.test.js +35 -0
  200. package/actions/user/delete/action.json +17 -0
  201. package/actions/user/delete/action.test.js +32 -0
  202. package/actions/user/dump/action.json +11 -0
  203. package/actions/user/dump/action.test.js +35 -0
  204. package/actions/user/get/action.json +24 -0
  205. package/actions/user/get/action.test.js +32 -0
  206. package/actions/user/merge/action.json +18 -0
  207. package/actions/user/merge/action.test.js +32 -0
  208. package/actions/user/set/action.json +24 -0
  209. package/actions/user/set/action.test.js +32 -0
  210. package/generator/base/css/bootstrap.min.css +6 -0
  211. package/generator/base/icons/app-indicator.svg +4 -0
  212. package/generator/base/icons/backpack.svg +4 -0
  213. package/generator/base/icons/broadcast.svg +3 -0
  214. package/generator/base/icons/bullseye.svg +6 -0
  215. package/generator/base/icons/chat-dots.svg +4 -0
  216. package/generator/base/icons/check-circle.svg +4 -0
  217. package/generator/base/icons/clipboard-check.svg +5 -0
  218. package/generator/base/icons/clipboard.svg +4 -0
  219. package/generator/base/icons/copy.svg +3 -0
  220. package/generator/base/icons/cursor.svg +3 -0
  221. package/generator/base/icons/diamond.svg +3 -0
  222. package/generator/base/icons/exclamation-triangle.svg +4 -0
  223. package/generator/base/icons/film.svg +3 -0
  224. package/generator/base/icons/floppy.svg +4 -0
  225. package/generator/base/icons/gear-wide-connected.svg +3 -0
  226. package/generator/base/icons/gear.svg +4 -0
  227. package/generator/base/icons/globe.svg +3 -0
  228. package/generator/base/icons/image.svg +4 -0
  229. package/generator/base/icons/layout-text-window.svg +4 -0
  230. package/generator/base/icons/lightning-charge.svg +3 -0
  231. package/generator/base/icons/magic.svg +3 -0
  232. package/generator/base/icons/pencil-square.svg +4 -0
  233. package/generator/base/icons/record-circle.svg +4 -0
  234. package/generator/base/icons/robot.svg +4 -0
  235. package/generator/base/icons/shield-check.svg +4 -0
  236. package/generator/base/icons/shield-lock.svg +4 -0
  237. package/generator/base/icons/signpost.svg +3 -0
  238. package/generator/base/icons/stars.svg +3 -0
  239. package/generator/base/icons/type.svg +3 -0
  240. package/generator/base/js/bootstrap.bundle.min.js +7 -0
  241. package/package.json +14 -0
  242. package/packages/undercity-http-server/index.js +249 -0
  243. package/packages/undercity-http-server/package.json +10 -0
  244. package/packages/undercity-parser/index.js +323 -0
  245. package/packages/undercity-parser/lexer.js +128 -0
  246. package/packages/undercity-parser/package.json +11 -0
  247. package/plugins/forms.js +397 -0
  248. package/plugins/index.js +83 -0
  249. package/plugins/multipage.js +165 -0
  250. package/plugins/wizard.js +239 -0
  251. package/projects/asd/project.json +1031 -0
  252. package/projects/test-1/project.json +335 -0
  253. package/projects/test-a/project.json +456 -0
  254. package/public/icons/arrows-angle-expand.svg +3 -0
  255. package/public/icons/arrows-fullscreen.svg +3 -0
  256. package/public/icons/bezier2.svg +3 -0
  257. package/public/icons/bootstrap/app-indicator.svg +4 -0
  258. package/public/icons/bootstrap/arrow-clockwise.svg +4 -0
  259. package/public/icons/bootstrap/arrow-counterclockwise.svg +4 -0
  260. package/public/icons/bootstrap/arrow-left.svg +3 -0
  261. package/public/icons/bootstrap/arrows-angle-expand.svg +3 -0
  262. package/public/icons/bootstrap/arrows-fullscreen.svg +3 -0
  263. package/public/icons/bootstrap/backpack.svg +4 -0
  264. package/public/icons/bootstrap/bezier2.svg +3 -0
  265. package/public/icons/bootstrap/bookmark-check.svg +4 -0
  266. package/public/icons/bootstrap/bookmark-plus.svg +4 -0
  267. package/public/icons/bootstrap/box-arrow-right.svg +4 -0
  268. package/public/icons/bootstrap/box-arrow-up.svg +4 -0
  269. package/public/icons/bootstrap/broadcast.svg +3 -0
  270. package/public/icons/bootstrap/bullseye.svg +6 -0
  271. package/public/icons/bootstrap/chat-dots.svg +4 -0
  272. package/public/icons/bootstrap/check-circle.svg +4 -0
  273. package/public/icons/bootstrap/check2.svg +3 -0
  274. package/public/icons/bootstrap/clipboard-check.svg +5 -0
  275. package/public/icons/bootstrap/clipboard.svg +4 -0
  276. package/public/icons/bootstrap/clock-history.svg +5 -0
  277. package/public/icons/bootstrap/command.svg +3 -0
  278. package/public/icons/bootstrap/copy.svg +3 -0
  279. package/public/icons/bootstrap/cursor.svg +3 -0
  280. package/public/icons/bootstrap/diagram-3.svg +3 -0
  281. package/public/icons/bootstrap/diamond.svg +3 -0
  282. package/public/icons/bootstrap/exclamation-triangle.svg +4 -0
  283. package/public/icons/bootstrap/eye.svg +4 -0
  284. package/public/icons/bootstrap/file-earmark-plus.svg +4 -0
  285. package/public/icons/bootstrap/film.svg +3 -0
  286. package/public/icons/bootstrap/floppy.svg +4 -0
  287. package/public/icons/bootstrap/folder2-open.svg +3 -0
  288. package/public/icons/bootstrap/gear-wide-connected.svg +3 -0
  289. package/public/icons/bootstrap/gear.svg +4 -0
  290. package/public/icons/bootstrap/globe.svg +3 -0
  291. package/public/icons/bootstrap/grid-3x3-gap.svg +3 -0
  292. package/public/icons/bootstrap/house-door.svg +3 -0
  293. package/public/icons/bootstrap/image.svg +4 -0
  294. package/public/icons/bootstrap/layout-text-window.svg +4 -0
  295. package/public/icons/bootstrap/lightning-charge.svg +3 -0
  296. package/public/icons/bootstrap/magic.svg +3 -0
  297. package/public/icons/bootstrap/pencil-square.svg +4 -0
  298. package/public/icons/bootstrap/pencil.svg +3 -0
  299. package/public/icons/bootstrap/play.svg +3 -0
  300. package/public/icons/bootstrap/plus-circle.svg +4 -0
  301. package/public/icons/bootstrap/plus-lg.svg +3 -0
  302. package/public/icons/bootstrap/record-circle.svg +4 -0
  303. package/public/icons/bootstrap/robot.svg +4 -0
  304. package/public/icons/bootstrap/save.svg +3 -0
  305. package/public/icons/bootstrap/scissors.svg +3 -0
  306. package/public/icons/bootstrap/shield-check.svg +4 -0
  307. package/public/icons/bootstrap/shield-lock.svg +4 -0
  308. package/public/icons/bootstrap/signpost.svg +3 -0
  309. package/public/icons/bootstrap/stars.svg +3 -0
  310. package/public/icons/bootstrap/stop-circle.svg +4 -0
  311. package/public/icons/bootstrap/terminal.svg +4 -0
  312. package/public/icons/bootstrap/trash3.svg +3 -0
  313. package/public/icons/bootstrap/type.svg +3 -0
  314. package/public/icons/bootstrap/x-circle.svg +4 -0
  315. package/public/icons/bootstrap/x-lg.svg +3 -0
  316. package/public/icons/bootstrap/zoom-in.svg +5 -0
  317. package/public/icons/bootstrap/zoom-out.svg +5 -0
  318. package/public/icons/bullseye.svg +6 -0
  319. package/public/icons/check2.svg +3 -0
  320. package/public/icons/cursor.svg +3 -0
  321. package/public/icons/diamond.svg +3 -0
  322. package/public/icons/eye.svg +4 -0
  323. package/public/icons/file-earmark-plus.svg +4 -0
  324. package/public/icons/floppy.svg +4 -0
  325. package/public/icons/gear.svg +4 -0
  326. package/public/icons/lightning-charge.svg +3 -0
  327. package/public/icons/pencil.svg +3 -0
  328. package/public/icons/play.svg +3 -0
  329. package/public/icons/plus-circle.svg +4 -0
  330. package/public/icons/record-circle.svg +4 -0
  331. package/public/icons/robot.svg +4 -0
  332. package/public/icons/save.svg +3 -0
  333. package/public/icons/stop-circle.svg +4 -0
  334. package/public/icons/terminal.svg +4 -0
  335. package/public/icons/trash3.svg +3 -0
  336. package/public/icons/x-circle.svg +4 -0
  337. package/public/icons/zoom-in.svg +5 -0
  338. package/public/icons/zoom-out.svg +5 -0
  339. package/public/index.html +424 -0
  340. package/public/testbench.html +899 -0
  341. package/scripts/extract-actions.js +128 -0
  342. package/server.js +11 -0
  343. package/src/emitter.js +48 -0
  344. package/src/generator/css.js +135 -0
  345. package/src/generator/index.js +122 -0
  346. package/src/generator/md-renderer-src.js +77 -0
  347. package/src/generator/page.js +300 -0
  348. package/src/generator/runtime.js +1632 -0
  349. package/src/generator/templates.js +508 -0
  350. package/src/ide/action-library.js +856 -0
  351. package/src/ide/af-icons.js +127 -0
  352. package/src/ide/app.js +1375 -0
  353. package/src/ide/command-line/commands.js +242 -0
  354. package/src/ide/command-line/index.js +329 -0
  355. package/src/ide/command-line/parser.js +21 -0
  356. package/src/ide/css/ide.css +1501 -0
  357. package/src/ide/graph.js +282 -0
  358. package/src/ide/history.js +46 -0
  359. package/src/ide/map-builder.js +583 -0
  360. package/src/ide/project-api.js +39 -0
  361. package/src/ide/savant-chat.js +513 -0
  362. package/src/ide/savant.js +1287 -0
  363. package/src/ide/thing-library.js +89 -0
  364. package/src/ide/undercity-map.js +978 -0
  365. package/src/lib/icons.js +72 -0
  366. package/src/lib/scope.js +88 -0
  367. package/src/lib/signal.js +155 -0
  368. package/src/lib/state-machine.js +113 -0
  369. package/src/server/index.js +96 -0
  370. package/src/server/routes/actions.js +144 -0
  371. package/src/server/routes/ai.js +176 -0
  372. package/src/server/routes/generate.js +54 -0
  373. package/src/server/routes/projects.js +106 -0
  374. package/src/server/routes/reset.js +30 -0
  375. package/src/server/routes/submit.js +30 -0
  376. package/src/server/routes/templates.js +139 -0
  377. package/src/server/routes/things.js +33 -0
  378. package/templates/auth-flow.json +335 -0
  379. package/templates/blank.json +39 -0
  380. package/things/auth-server/thing.json +17 -0
  381. package/things/persona-live/thing.json +20 -0
  382. package/things/workflow/thing.json +15 -0
@@ -0,0 +1,72 @@
1
+ const ICON_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2
+
3
+ export const DEFAULT_ICON_NAME = 'stars';
4
+
5
+ export const LEGACY_ICON_MAP = Object.freeze({
6
+ '🔐': 'shield-lock',
7
+ '⬜': 'app-indicator',
8
+ '✦': 'stars',
9
+ '✅': 'check-circle',
10
+ '✓': 'check-circle',
11
+ '⚠️': 'exclamation-triangle',
12
+ '⚠': 'exclamation-triangle',
13
+ '📋': 'clipboard-check',
14
+ '✎': 'pencil-square',
15
+ '✏️': 'pencil-square',
16
+ '🤖': 'robot',
17
+ '✨': 'magic',
18
+ '⚡': 'lightning-charge',
19
+ '⌘': 'command',
20
+ '⊡': 'arrows-angle-expand',
21
+ '⟳': 'cursor',
22
+ '●': 'record-circle',
23
+ '◆': 'diamond',
24
+ '◎': 'bullseye',
25
+ '→': 'box-arrow-right',
26
+ '✕': 'x-lg',
27
+ '⧉': 'copy',
28
+ '🧭': 'signpost',
29
+ '🎒': 'backpack',
30
+ '🖼': 'image',
31
+ '🎬': 'film',
32
+ '🌐': 'globe',
33
+ '💬': 'chat-dots',
34
+ '📡': 'broadcast',
35
+ '⚙️': 'gear',
36
+ '💾': 'floppy',
37
+ });
38
+
39
+ function escapeAttr(str) {
40
+ return String(str ?? '')
41
+ .replace(/&/g, '&')
42
+ .replace(/"/g, '"')
43
+ .replace(/</g, '&lt;')
44
+ .replace(/>/g, '&gt;');
45
+ }
46
+
47
+ export function normalizeIconName(value, fallback = DEFAULT_ICON_NAME) {
48
+ if (typeof value !== 'string') return fallback;
49
+ const trimmed = value.trim();
50
+ if (!trimmed) return fallback;
51
+
52
+ const normalized = trimmed.toLowerCase();
53
+ if (ICON_NAME_RE.test(normalized)) return normalized;
54
+
55
+ return LEGACY_ICON_MAP[trimmed] ?? fallback;
56
+ }
57
+
58
+ export function renderAfIcon(value, attrs = {}, fallback = DEFAULT_ICON_NAME) {
59
+ const name = normalizeIconName(value, fallback);
60
+ const parts = [`name="${escapeAttr(name)}"`];
61
+
62
+ for (const [key, rawValue] of Object.entries(attrs)) {
63
+ if (rawValue === false || rawValue === null || rawValue === undefined) continue;
64
+ if (rawValue === true) {
65
+ parts.push(key);
66
+ continue;
67
+ }
68
+ parts.push(`${key}="${escapeAttr(rawValue)}"`);
69
+ }
70
+
71
+ return `<af-icon ${parts.join(' ')}></af-icon>`;
72
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * scope.js — Hierarchical resource management.
3
+ *
4
+ * Inspired by dirge (github.com/nicholasgasior/dirge).
5
+ * A Scope holds cleanup callbacks in a named tree.
6
+ * On dispose(), children run first (depth-first), then local resources LIFO.
7
+ *
8
+ * Accepts three cleanup resource shapes:
9
+ * function — called directly
10
+ * { dispose } — Disposable from signal.js (subscription, event handle)
11
+ * { [Symbol.dispose] } — TC39 explicit resource management
12
+ *
13
+ * Usage — package lifecycle:
14
+ *
15
+ * let _scope = null;
16
+ *
17
+ * export function activate(env) {
18
+ * _scope = new Scope('my-package');
19
+ *
20
+ * const opener = env.workspace.addOpener(uri => { ... });
21
+ * _scope.add(() => opener.dispose());
22
+ *
23
+ * const handler = () => { ... };
24
+ * document.addEventListener('my-event', handler);
25
+ * _scope.add(() => document.removeEventListener('my-event', handler));
26
+ * }
27
+ *
28
+ * export function deactivate() {
29
+ * _scope?.dispose(); // all resources cleaned up in one call
30
+ * _scope = null;
31
+ * }
32
+ *
33
+ * Usage — child scopes for different lifetimes:
34
+ *
35
+ * // Per-render listeners that must be replaced on next render:
36
+ * const perRender = this.#scope.scope('per-render');
37
+ * perRender.dispose(); // remove previous listeners
38
+ * perRender.add(() => el.removeEventListener('dragover', onDragover));
39
+ *
40
+ * // The parent scope disposes all children on its own dispose().
41
+ */
42
+
43
+ export class Scope {
44
+ #fns = [];
45
+ #children = new Map(); // name → Scope
46
+
47
+ /**
48
+ * Add a cleanup resource. Returns `this` for fluent chaining.
49
+ * scope.add(signal.subscribe(fn))
50
+ * scope.add(() => el.removeEventListener('click', handler))
51
+ */
52
+ add(resource) {
53
+ this.#fns.push(resource);
54
+ return this;
55
+ }
56
+
57
+ /**
58
+ * Get or create a named child scope.
59
+ * The child disposes before this scope's own resources.
60
+ * Calling scope.scope(name).dispose() clears only that child;
61
+ * scope.scope(name) again returns the same (now empty) child.
62
+ */
63
+ scope(name) {
64
+ if (!this.#children.has(name)) this.#children.set(name, new Scope());
65
+ return this.#children.get(name);
66
+ }
67
+
68
+ /**
69
+ * Dispose all children (depth-first), then local resources (LIFO).
70
+ * After disposal the scope is empty and can be reused.
71
+ */
72
+ dispose() {
73
+ for (const child of this.#children.values()) child.dispose();
74
+ // Don't clear children Map — named children remain accessible for reuse.
75
+ // (Their resources are empty after dispose; adding to them works again.)
76
+
77
+ for (let i = this.#fns.length - 1; i >= 0; i--) {
78
+ const r = this.#fns[i];
79
+ try {
80
+ if (typeof r === 'function') r();
81
+ else r?.dispose?.() ?? r?.[Symbol.dispose]?.();
82
+ } catch (err) {
83
+ console.error('[Scope] dispose error:', err);
84
+ }
85
+ }
86
+ this.#fns.length = 0;
87
+ }
88
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * signal.js — Reactive primitives
3
+ *
4
+ * Signal — reactive value cell. subscribe(fn, autorun=true) fires fn immediately
5
+ * by default, then on every change. autorun=false defers first fire.
6
+ * Static helpers: combineLatest(), derive(), from(). Instance: map().
7
+ * Emitter — typed event emitter.
8
+ * Disposable — single cleanup handle.
9
+ * CompositeDisposable — group of disposables, disposed in reverse order.
10
+ * Repeater — keyed list renderer (reconciles add/remove/reorder without clearing).
11
+ * on() — DOM addEventListener returning a Disposable.
12
+ */
13
+
14
+ export class Signal {
15
+ #value;
16
+ #subs = new Set();
17
+ #collected = [];
18
+
19
+ constructor(init) { this.#value = init; }
20
+
21
+ get value() { return this.#value; }
22
+ set value(v) {
23
+ if (v === this.#value) return;
24
+ this.#value = v;
25
+ for (const fn of [...this.#subs]) fn(v);
26
+ }
27
+
28
+ /** Subscribe to changes. autorun=true fires fn immediately with current value. */
29
+ subscribe(fn, autorun = true) {
30
+ this.#subs.add(fn);
31
+ if (autorun) fn(this.#value);
32
+ return new Disposable(() => this.#subs.delete(fn));
33
+ }
34
+
35
+ /** Re-fire all subscribers with the current value (used after in-place mutations). */
36
+ notify() {
37
+ for (const fn of [...this.#subs]) fn(this.#value);
38
+ }
39
+
40
+ peek() { return this.#value; }
41
+
42
+ /** Attach a cleanup function that runs when this signal is disposed. */
43
+ collect(fn) { this.#collected.push(fn); }
44
+
45
+ /** Run all collected cleanup functions and clear the list. */
46
+ dispose() {
47
+ const fns = this.#collected.splice(0);
48
+ for (const fn of fns) fn();
49
+ }
50
+
51
+ /**
52
+ * Combine multiple signals into one Signal whose value is an array of
53
+ * the current values of all inputs. Fires whenever any input changes.
54
+ * The returned signal has a dispose() that cleans up all subscriptions.
55
+ */
56
+ static combineLatest(signals) {
57
+ const out = new Signal(signals.map(s => s.value));
58
+ const subs = signals.map((s, i) => s.subscribe(v => {
59
+ const next = out.value.slice();
60
+ next[i] = v;
61
+ out.value = next;
62
+ }, false));
63
+ out.collect(() => subs.forEach(sub => sub.dispose()));
64
+ return out;
65
+ }
66
+
67
+ /** Create a derived Signal that updates when source changes. Disposable via .dispose(). */
68
+ static derive(source, transform) {
69
+ const derived = new Signal(transform(source.value));
70
+ const sub = source.subscribe(v => { derived.value = transform(v); }, false);
71
+ derived.collect(() => sub.dispose());
72
+ return derived;
73
+ }
74
+
75
+ /** Wrap a Promise as a Signal. Starts at `initial`, resolves to the value when ready. */
76
+ static from(promise, initial = null) {
77
+ const sig = new Signal(initial);
78
+ Promise.resolve(promise).then(v => { sig.value = v; }).catch(() => {});
79
+ return sig;
80
+ }
81
+
82
+ /** Shorthand for Signal.derive(this, fn). */
83
+ map(fn) { return Signal.derive(this, fn); }
84
+ }
85
+
86
+ export class Emitter {
87
+ #map = new Map();
88
+
89
+ on(event, fn) {
90
+ let set = this.#map.get(event);
91
+ if (!set) { set = new Set(); this.#map.set(event, set); }
92
+ set.add(fn);
93
+ return new Disposable(() => set.delete(fn));
94
+ }
95
+
96
+ emit(event, data) {
97
+ for (const fn of this.#map.get(event) ?? []) fn(data);
98
+ }
99
+ }
100
+
101
+ export class Disposable {
102
+ #fn; #done = false;
103
+ constructor(fn) { this.#fn = fn; }
104
+ dispose() { if (this.#done) return; this.#done = true; this.#fn(); }
105
+ }
106
+
107
+ export class CompositeDisposable {
108
+ #items = []; #done = false;
109
+ add(...items) { this.#items.push(...items); return this; }
110
+ dispose() {
111
+ if (this.#done) return; this.#done = true;
112
+ for (const d of [...this.#items].reverse()) d.dispose();
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Keyed list renderer — reconciles add/remove/reorder without clearing the container.
118
+ * Implements dispose() so it can be passed directly to a Scope:
119
+ * scope.add(new Repeater(container, signal, render))
120
+ */
121
+ export class Repeater {
122
+ #container; #render; #key; #nodes = new Map();
123
+ #sub;
124
+
125
+ constructor(container, signal, render, { key = 'id' } = {}) {
126
+ this.#container = container;
127
+ this.#render = render;
128
+ this.#key = typeof key === 'function' ? key : item => item[key];
129
+ this.#sub = signal.subscribe(items => this.#update(items));
130
+ }
131
+
132
+ dispose() {
133
+ this.#sub.dispose();
134
+ this.#nodes.clear();
135
+ }
136
+
137
+ #update(items) {
138
+ const incoming = new Map(items.map(item => [this.#key(item), item]));
139
+ for (const [k, node] of this.#nodes) {
140
+ if (!incoming.has(k)) { node.remove(); this.#nodes.delete(k); }
141
+ }
142
+ for (const item of items) {
143
+ const k = this.#key(item);
144
+ let node = this.#nodes.get(k);
145
+ if (!node) { node = this.#render(item); this.#nodes.set(k, node); }
146
+ this.#container.appendChild(node);
147
+ }
148
+ }
149
+ }
150
+
151
+ /** DOM addEventListener returning a Disposable. */
152
+ export function on(target, type, handler, opts) {
153
+ target.addEventListener(type, handler, opts);
154
+ return new Disposable(() => target.removeEventListener(type, handler, opts));
155
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * state-machine — JSON-declared hybrid state machine.
3
+ *
4
+ * Each context key in `config.state` becomes a named Signal on the machine,
5
+ * so consumers subscribe directly: `machine.dirty.subscribe(v => ...)`.
6
+ *
7
+ * The event bus (framework Emitter) drives transitions automatically.
8
+ * Calling `machine.emit('save')` fires the bus; the machine reads the current
9
+ * state, finds the next state in `states[current].on.save`, and enters it.
10
+ *
11
+ * `enter` patches can be plain values or functions:
12
+ * enter: { filename: ({ payload }) => payload ?? '' }
13
+ *
14
+ * Usage:
15
+ * import { createMachine } from 'state-machine';
16
+ *
17
+ * const machine = createMachine({
18
+ * state: { dirty: false, saving: false },
19
+ * initial: 'idle',
20
+ * states: {
21
+ * idle: { enter: { dirty: false, saving: false }, on: { edit: 'dirty' } },
22
+ * dirty: { enter: { dirty: true }, on: { save: 'saving' } },
23
+ * saving: { enter: { saving: true }, on: { 'save-ok': 'idle', 'save-fail': 'dirty' } },
24
+ * },
25
+ * });
26
+ *
27
+ * machine.dirty.subscribe(v => console.log('dirty:', v));
28
+ * machine.emit('edit'); // dirty → true
29
+ * machine.emit('save'); // saving → true
30
+ * machine.emit('save-ok');// dirty → false, saving → false
31
+ */
32
+
33
+ import { Signal, Emitter } from 'framework';
34
+
35
+ // ── MapSet ─────────────────────────────────────────────────────────────────────
36
+ // Map<key, Set<value>> — useful for multi-listener registries.
37
+
38
+ export class MapSet {
39
+ #map = new Map();
40
+
41
+ add(key, value) {
42
+ let set = this.#map.get(key);
43
+ if (!set) { set = new Set(); this.#map.set(key, set); }
44
+ set.add(value);
45
+ return this;
46
+ }
47
+
48
+ delete(key, value) {
49
+ const set = this.#map.get(key);
50
+ if (!set) return false;
51
+ const deleted = set.delete(value);
52
+ if (set.size === 0) this.#map.delete(key);
53
+ return deleted;
54
+ }
55
+
56
+ get(key) { return this.#map.get(key); }
57
+ keys() { return this.#map.keys(); }
58
+ values() { return this.#map.values(); }
59
+ entries() { return this.#map.entries(); }
60
+ has(key) { return this.#map.has(key); }
61
+ get size() { return this.#map.size; }
62
+ }
63
+
64
+ // ── createMachine ──────────────────────────────────────────────────────────────
65
+
66
+ export function createMachine(config) {
67
+ const bus = new Emitter();
68
+
69
+ const machine = {
70
+ bus,
71
+ current: new Signal(config.initial),
72
+ signals: {},
73
+ /** Dispatch an event into the machine. payload is optional. */
74
+ emit(event, payload) { bus.emit(event, payload); },
75
+ };
76
+
77
+ // Create a named Signal for each context key.
78
+ for (const [key, initialValue] of Object.entries(config.state)) {
79
+ const sig = new Signal(initialValue);
80
+ machine.signals[key] = sig;
81
+ machine[key] = sig;
82
+ }
83
+
84
+ function applyPatch(patch = {}, payload) {
85
+ for (const [key, value] of Object.entries(patch)) {
86
+ if (!machine.signals[key]) throw new Error(`state-machine: unknown signal "${key}" in enter patch`);
87
+ machine.signals[key].value = typeof value === 'function'
88
+ ? value({ payload, machine, current: machine.current.value })
89
+ : value;
90
+ }
91
+ }
92
+
93
+ function enterState(name, payload) {
94
+ const stateDef = config.states[name];
95
+ if (!stateDef) throw new Error(`state-machine: unknown state "${name}"`);
96
+ machine.current.value = name;
97
+ applyPatch(stateDef.enter ?? {}, payload);
98
+ }
99
+
100
+ // Auto-discover all event names and wire them to transitions.
101
+ const events = new Set(
102
+ Object.values(config.states).flatMap(s => Object.keys(s.on ?? {}))
103
+ );
104
+ for (const event of events) {
105
+ bus.on(event, payload => {
106
+ const next = config.states[machine.current.value]?.on?.[event];
107
+ if (next) enterState(next, payload);
108
+ });
109
+ }
110
+
111
+ enterState(config.initial);
112
+ return machine;
113
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * src/server/index.js — HTTP server setup.
3
+ *
4
+ * createServer(options?) builds the Express-compatible app.
5
+ * listen(app, port?) starts it and returns {server, port, url, close}.
6
+ *
7
+ * Options accepted by createServer():
8
+ * projDir — absolute path to the projects directory (default: <root>/projects)
9
+ * genDir — absolute path to the generated directory (default: <root>/generated)
10
+ *
11
+ * Passing explicit directories is how integration tests achieve full isolation:
12
+ * const app = createServer({ projDir: tmpDir, genDir: tmpGenDir });
13
+ * const { url, close } = await listen(app, 0); // port 0 → OS picks one
14
+ */
15
+
16
+ import { join, dirname } from 'path';
17
+ import { fileURLToPath } from 'url';
18
+
19
+ import express from '../../packages/undercity-http-server/index.js';
20
+
21
+ import { registerProjectRoutes } from './routes/projects.js';
22
+ import { registerGenerateRoute } from './routes/generate.js';
23
+ import { registerAIRoute } from './routes/ai.js';
24
+ import { registerSubmitRoute } from './routes/submit.js';
25
+ import { registerTemplatesRoute } from './routes/templates.js';
26
+ import { registerActionsRoute } from './routes/actions.js';
27
+ import { registerResetRoute } from './routes/reset.js';
28
+ import { registerThingsRoute } from './routes/things.js';
29
+
30
+ const __dir = dirname(fileURLToPath(import.meta.url));
31
+ const ROOT = join(__dir, '..', '..');
32
+
33
+ const DEFAULT_PROJ = join(ROOT, 'projects');
34
+ const DEFAULT_GENDIR = join(ROOT, 'generated');
35
+ const DEFAULT_PORT = process.env.PORT ?? 3000;
36
+
37
+ // ── App factory ────────────────────────────────────────────────────────────────
38
+
39
+ export function createServer({ projDir = DEFAULT_PROJ, genDir = DEFAULT_GENDIR } = {}) {
40
+ const app = express();
41
+
42
+ // Middleware
43
+ app.use(express.json({ limit: '4mb' }));
44
+
45
+ // Static assets
46
+ app.use('/lib/bootstrap', express.static(join(ROOT, 'generator', 'base')));
47
+ app.use('/packages', express.static(join(ROOT, 'packages')));
48
+ app.use('/src', express.static(join(ROOT, 'src')));
49
+ app.use('/generated', express.static(genDir));
50
+ app.use('/actions', express.static(join(ROOT, 'actions')));
51
+ app.use('/', express.static(join(ROOT, 'public')));
52
+
53
+ // API routes
54
+ registerProjectRoutes(app, projDir);
55
+ registerGenerateRoute(app, projDir, genDir);
56
+ registerAIRoute(app);
57
+ registerSubmitRoute(app);
58
+ registerTemplatesRoute(app);
59
+ registerActionsRoute(app);
60
+ registerResetRoute(app, projDir, genDir);
61
+ registerThingsRoute(app);
62
+
63
+ // Serve thing.json and associated assets
64
+ app.use('/things', express.static(join(ROOT, 'things')));
65
+
66
+ return app;
67
+ }
68
+
69
+ // ── Server lifecycle ───────────────────────────────────────────────────────────
70
+
71
+ /**
72
+ * Start the app listening. Returns a promise that resolves once the server
73
+ * is ready, yielding { server, port, url, close }.
74
+ *
75
+ * Pass port = 0 for a random available port (useful in tests).
76
+ */
77
+ export function listen(app, port) {
78
+ const listenPort = port ?? Number(DEFAULT_PORT);
79
+ return new Promise((resolve, reject) => {
80
+ const srv = app.listen(listenPort, () => {
81
+ const addr = srv.address();
82
+ const p = addr.port;
83
+ if (!port) {
84
+ // Production boot — emit to stdout
85
+ console.log(`\n Undercity IDE → http://localhost:${p}\n`);
86
+ }
87
+ resolve({
88
+ server: srv,
89
+ port: p,
90
+ url: `http://localhost:${p}`,
91
+ close: () => new Promise((res, rej) => srv.close(err => err ? rej(err) : res())),
92
+ });
93
+ });
94
+ srv.on('error', reject);
95
+ });
96
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * routes/actions.js — Action plugin discovery API.
3
+ *
4
+ * GET /api/actions/plugins
5
+ * Scans <root>/actions/<category>/<action-name>/action.json and returns
6
+ * all plugin definitions grouped by category, ready to merge into the IDE's
7
+ * ACTION_LIBRARY.
8
+ *
9
+ * Response shape:
10
+ * {
11
+ * "<categoryId>": {
12
+ * label: string,
13
+ * icon: string,
14
+ * color: string,
15
+ * actions: {
16
+ * "<action.id>": { label, desc, params, ... }
17
+ * }
18
+ * },
19
+ * ...
20
+ * }
21
+ *
22
+ * Each action.json must have at minimum: id, category, label, desc, params[].
23
+ * Optional fields: categoryLabel, icon, color, version.
24
+ */
25
+
26
+ import { readdir, readFile } from 'fs/promises';
27
+ import { join, dirname } from 'path';
28
+ import { fileURLToPath } from 'url';
29
+
30
+ const __dir = dirname(fileURLToPath(import.meta.url));
31
+ const ACTIONS_DIR = join(__dir, '..', '..', '..', 'actions');
32
+
33
+ // Default icon/color per category (fallback when action.json omits them)
34
+ const CATEGORY_DEFAULTS = {
35
+ display: { icon: 'type', color: 'var(--sol-cyan)' },
36
+ render: { icon: 'layout-text-window', color: 'var(--sol-violet)' },
37
+ auth: { icon: 'shield-lock', color: 'var(--sol-blue)' },
38
+ media: { icon: 'film', color: 'var(--sol-magenta)' },
39
+ data: { icon: 'database', color: 'var(--sol-yellow)' },
40
+ notification: { icon: 'bell', color: 'var(--sol-orange)' },
41
+ };
42
+
43
+ export function registerActionsRoute(app) {
44
+ /**
45
+ * GET /api/actions/tests
46
+ * Returns a list of { actionId, url } objects for all discovered action.test.js files.
47
+ * The testbench can dynamically import these to run per-action tests.
48
+ */
49
+ app.get('/api/actions/tests', async (_req, res) => {
50
+ try {
51
+ const tests = [];
52
+ let catDirs;
53
+ try { catDirs = await readdir(ACTIONS_DIR, { withFileTypes: true }); }
54
+ catch { return res.json([]); }
55
+
56
+ for (const catEnt of catDirs) {
57
+ if (!catEnt.isDirectory()) continue;
58
+ const catDir = join(ACTIONS_DIR, catEnt.name);
59
+ let actionDirs;
60
+ try { actionDirs = await readdir(catDir, { withFileTypes: true }); }
61
+ catch { continue; }
62
+
63
+ for (const actEnt of actionDirs) {
64
+ if (!actEnt.isDirectory()) continue;
65
+ const testPath = join(catDir, actEnt.name, 'action.test.js');
66
+ try {
67
+ await readFile(testPath); // check existence
68
+ tests.push({
69
+ actionId: `${catEnt.name}.${actEnt.name}`,
70
+ url: `/actions/${catEnt.name}/${actEnt.name}/action.test.js`,
71
+ });
72
+ } catch { /* no test file */ }
73
+ }
74
+ }
75
+ res.json(tests);
76
+ } catch (err) {
77
+ console.error('[actions/tests]', err);
78
+ res.status(500).json({ error: err.message });
79
+ }
80
+ });
81
+
82
+ /**
83
+ * GET /api/actions/plugins
84
+ * Returns all discovered plugins merged into ACTION_LIBRARY-compatible shape.
85
+ */
86
+ app.get('/api/actions/plugins', async (_req, res) => {
87
+ try {
88
+ const result = {};
89
+
90
+ // Each sub-directory of actions/ is a category
91
+ let catDirs;
92
+ try {
93
+ catDirs = await readdir(ACTIONS_DIR, { withFileTypes: true });
94
+ } catch {
95
+ return res.json({}); // actions/ dir missing → no plugins
96
+ }
97
+
98
+ for (const catEnt of catDirs) {
99
+ if (!catEnt.isDirectory()) continue;
100
+ const catId = catEnt.name;
101
+ const catDir = join(ACTIONS_DIR, catId);
102
+
103
+ let actionDirs;
104
+ try { actionDirs = await readdir(catDir, { withFileTypes: true }); }
105
+ catch { continue; }
106
+
107
+ for (const actEnt of actionDirs) {
108
+ if (!actEnt.isDirectory()) continue;
109
+ const manifestPath = join(catDir, actEnt.name, 'action.json');
110
+
111
+ let manifest;
112
+ try {
113
+ manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
114
+ } catch { continue; } // skip malformed / missing manifests
115
+
116
+ if (!manifest.id || !manifest.label) continue;
117
+
118
+ // Initialise category bucket if first action from this category
119
+ if (!result[catId]) {
120
+ const defaults = CATEGORY_DEFAULTS[catId] ?? {};
121
+ result[catId] = {
122
+ label: manifest.categoryLabel ?? catId.charAt(0).toUpperCase() + catId.slice(1),
123
+ icon: manifest.icon ?? defaults.icon ?? 'puzzle',
124
+ color: manifest.color ?? defaults.color ?? 'var(--sol-base1)',
125
+ actions: {},
126
+ };
127
+ }
128
+
129
+ result[catId].actions[manifest.id] = {
130
+ label: manifest.label,
131
+ desc: manifest.desc ?? '',
132
+ params: manifest.params ?? [],
133
+ ...(manifest.version ? { version: manifest.version } : {}),
134
+ };
135
+ }
136
+ }
137
+
138
+ res.json(result);
139
+ } catch (err) {
140
+ console.error('[actions/plugins]', err);
141
+ res.status(500).json({ error: err.message });
142
+ }
143
+ });
144
+ }