@webship.co/workspace 1.0.0-beta1 → 1.0.0-beta2

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 (630) hide show
  1. package/LICENSE +339 -0
  2. package/README.md +28 -364
  3. package/core/bin/workspace.js +135 -0
  4. package/core/config/settings.labels.yml +514 -0
  5. package/core/config/settings.template.yml +292 -0
  6. package/core/config/style.yml +53 -0
  7. package/core/config/theme.default.yml +161 -0
  8. package/core/config/theme.dispatch.yml +166 -0
  9. package/core/config/theme.drupal.yml +175 -0
  10. package/core/config/theme.ember.yml +160 -0
  11. package/core/config/theme.github.yml +172 -0
  12. package/core/config/theme.gitlab.yml +168 -0
  13. package/core/config/theme.press.yml +161 -0
  14. package/core/config/theme.slate.yml +160 -0
  15. package/core/config/theme.upsun.yml +174 -0
  16. package/core/docs/architecture.svg +65 -0
  17. package/core/docs/getting-started.md +51 -0
  18. package/core/docs/plugins.md +27 -0
  19. package/core/docs/releases.md +23 -0
  20. package/core/docs/testing.md +15 -0
  21. package/core/docs/workspaces.md +25 -0
  22. package/core/scripts/args/arg-ai-items.sh +65 -0
  23. package/core/scripts/args/arg-autostart.sh +31 -0
  24. package/core/scripts/args/arg-avatar-agent.sh +28 -0
  25. package/core/scripts/args/arg-avatar-catalog.sh +12 -0
  26. package/core/scripts/args/arg-avatar-character.sh +68 -0
  27. package/core/scripts/args/arg-avatar-voice.sh +24 -0
  28. package/core/scripts/args/arg-avatar.sh +22 -0
  29. package/core/scripts/args/arg-checks.sh +36 -0
  30. package/core/scripts/args/arg-clone.sh +42 -0
  31. package/core/scripts/args/arg-commands.sh +51 -0
  32. package/core/scripts/args/arg-cucumber.sh +34 -0
  33. package/core/scripts/args/arg-distribution.sh +30 -0
  34. package/core/scripts/args/arg-drupal.sh +39 -0
  35. package/core/scripts/args/arg-drupal_cms.sh +18 -0
  36. package/core/scripts/args/arg-gitlab-ci-local.sh +25 -0
  37. package/core/scripts/args/arg-graphify.sh +36 -0
  38. package/core/scripts/args/arg-graphs.sh +12 -0
  39. package/core/scripts/args/arg-hostnames.sh +23 -0
  40. package/core/scripts/args/arg-milvus-backup.sh +8 -0
  41. package/core/scripts/args/arg-milvus-mcp.sh +12 -0
  42. package/core/scripts/args/arg-milvus.sh +32 -0
  43. package/core/scripts/args/arg-obsidian.sh +17 -0
  44. package/core/scripts/args/arg-odoo.sh +18 -0
  45. package/core/scripts/args/arg-penpot-backup.sh +8 -0
  46. package/core/scripts/args/arg-penpot-mcp.sh +15 -0
  47. package/core/scripts/args/arg-penpot-token.sh +17 -0
  48. package/core/scripts/args/arg-penpot.sh +44 -0
  49. package/core/scripts/args/arg-rag.sh +21 -0
  50. package/core/scripts/args/arg-ragify.sh +35 -0
  51. package/core/scripts/args/arg-reclaim.sh +18 -0
  52. package/core/scripts/args/arg-release.sh +21 -0
  53. package/core/scripts/args/arg-screen.sh +28 -0
  54. package/core/scripts/args/arg-site-template.sh +13 -0
  55. package/core/scripts/args/arg-spdd.sh +16 -0
  56. package/core/scripts/args/arg-specs.sh +22 -0
  57. package/core/scripts/args/arg-storage.sh +36 -0
  58. package/core/scripts/args/arg-testing.sh +36 -0
  59. package/core/scripts/args/arg-webship.sh +34 -0
  60. package/core/scripts/args/arg-webships-project.sh +22 -0
  61. package/core/scripts/args/arg-webships-site-template.sh +17 -0
  62. package/core/scripts/args/arg-webships.sh +34 -0
  63. package/core/scripts/args/arg-website-site-template.sh +17 -0
  64. package/core/scripts/args/arg-website.sh +25 -0
  65. package/core/scripts/args/arg-wordpress.sh +18 -0
  66. package/core/scripts/autostart-run.sh +162 -0
  67. package/core/scripts/autostart-stop.sh +30 -0
  68. package/core/scripts/autostart-watch.sh +43 -0
  69. package/core/scripts/autostart.sh +26 -0
  70. package/core/scripts/bootstrap-functions.sh +117 -0
  71. package/core/scripts/bootstrap-libraries.sh +10 -0
  72. package/core/scripts/bootstrap.sh +71 -0
  73. package/core/scripts/ddev-shim/ddev +122 -0
  74. package/core/scripts/docker-gid.sh +119 -0
  75. package/core/scripts/functions/arg-site-template.sh +28 -0
  76. package/core/scripts/functions/fun-ai-items-lock.sh +175 -0
  77. package/core/scripts/functions/fun-ai-items-sources.sh +298 -0
  78. package/core/scripts/functions/fun-ai-items.sh +621 -0
  79. package/core/scripts/functions/fun-ai-providers.sh +72 -0
  80. package/core/scripts/functions/fun-autostart.sh +429 -0
  81. package/core/scripts/functions/fun-avatar.sh +441 -0
  82. package/core/scripts/functions/fun-backup-item.sh +50 -0
  83. package/core/scripts/functions/fun-build-distribution.sh +226 -0
  84. package/core/scripts/functions/fun-build-site-template.sh +104 -0
  85. package/core/scripts/functions/fun-build-webships.sh +161 -0
  86. package/core/scripts/functions/fun-build-website.sh +199 -0
  87. package/core/scripts/functions/fun-checks.sh +243 -0
  88. package/core/scripts/functions/fun-clone-project.sh +289 -0
  89. package/core/scripts/functions/fun-commands.sh +521 -0
  90. package/core/scripts/functions/fun-database.sh +63 -0
  91. package/core/scripts/functions/fun-ddev-hostnames.sh +213 -0
  92. package/core/scripts/functions/fun-ddev-nodejs.sh +15 -0
  93. package/core/scripts/functions/fun-ddev-telemetry.sh +15 -0
  94. package/core/scripts/functions/fun-default-settings.sh +28 -0
  95. package/core/scripts/functions/fun-distribution-cucumber.sh +45 -0
  96. package/core/scripts/functions/fun-distribution-drupal.sh +25 -0
  97. package/core/scripts/functions/fun-distribution-drupal_cms.sh +35 -0
  98. package/core/scripts/functions/fun-distribution-webship.sh +28 -0
  99. package/core/scripts/functions/fun-distribution-webships.sh +33 -0
  100. package/core/scripts/functions/fun-distribution-website.sh +19 -0
  101. package/core/scripts/functions/fun-drush.sh +48 -0
  102. package/core/scripts/functions/fun-elevenlabs.sh +179 -0
  103. package/core/scripts/functions/fun-files.sh +19 -0
  104. package/core/scripts/functions/fun-git-worktrees.sh +142 -0
  105. package/core/scripts/functions/fun-gitlab-ci.sh +156 -0
  106. package/core/scripts/functions/fun-hostnames-cli.sh +88 -0
  107. package/core/scripts/functions/fun-milvus.sh +584 -0
  108. package/core/scripts/functions/fun-odoo.sh +121 -0
  109. package/core/scripts/functions/fun-penpot.sh +724 -0
  110. package/core/scripts/functions/fun-project-name.sh +20 -0
  111. package/core/scripts/functions/fun-rag.sh +253 -0
  112. package/core/scripts/functions/fun-ragify.sh +165 -0
  113. package/core/scripts/functions/fun-reclaim.sh +297 -0
  114. package/core/scripts/functions/fun-release.sh +159 -0
  115. package/core/scripts/functions/fun-remove-project.sh +319 -0
  116. package/core/scripts/functions/fun-screen.sh +96 -0
  117. package/core/scripts/functions/fun-spdd.sh +676 -0
  118. package/core/scripts/functions/fun-specs-share.sh +237 -0
  119. package/core/scripts/functions/fun-storage.sh +347 -0
  120. package/core/scripts/functions/fun-testing.sh +329 -0
  121. package/core/scripts/functions/fun-users.sh +87 -0
  122. package/core/scripts/functions/fun-version.sh +72 -0
  123. package/core/scripts/functions/fun-video-edit.sh +48 -0
  124. package/core/scripts/functions/fun-voice.sh +45 -0
  125. package/core/scripts/functions/fun-wordpress.sh +59 -0
  126. package/core/scripts/host-mounts.sh +161 -0
  127. package/core/scripts/install.sh +602 -0
  128. package/core/scripts/libs/__pycache__/video_edit.cpython-312.pyc +0 -0
  129. package/core/scripts/libs/argparse.sh +93 -0
  130. package/core/scripts/libs/bash-yaml.sh +52 -0
  131. package/core/scripts/libs/checks_plan.js +312 -0
  132. package/core/scripts/libs/elevenlabs.py +627 -0
  133. package/core/scripts/libs/progress-bar.sh +19 -0
  134. package/core/scripts/libs/rag_index.py +446 -0
  135. package/core/scripts/libs/video_edit.py +636 -0
  136. package/core/scripts/libs/voice.py +430 -0
  137. package/core/scripts/plugins/jira/host-install.sh +119 -0
  138. package/core/scripts/plugins/jira/host-mounts.txt +2 -0
  139. package/core/scripts/plugins/upsun/args/arg-upsun.sh +51 -0
  140. package/core/scripts/plugins/upsun/functions/fun-upsun.sh +304 -0
  141. package/core/scripts/plugins/upsun/host-install.sh +99 -0
  142. package/core/scripts/plugins/upsun/host-mounts.txt +1 -0
  143. package/core/scripts/plugins/upsun/upsun-ai.sh +133 -0
  144. package/core/scripts/plugins.sh +152 -0
  145. package/core/scripts/release.sh +69 -0
  146. package/core/scripts/storage.sh +39 -0
  147. package/core/scripts/sync-tooling.sh +246 -0
  148. package/core/scripts/tests/cmd-smoke-test.sh +109 -0
  149. package/core/scripts/trust-https.sh +257 -0
  150. package/core/workspace-app/.ddev/commands/web/test-dashboard +49 -0
  151. package/core/workspace-app/.ddev/config.yaml +400 -0
  152. package/core/workspace-app/.ddev/docker-compose.dood.yaml +45 -0
  153. package/core/workspace-app/.ddev/docker-compose.plugin-jira-twg.yaml +26 -0
  154. package/core/workspace-app/.ddev/docker-compose.plugin-upsun-upsun-cli.yaml +8 -0
  155. package/core/workspace-app/.ddev/docker-compose.screens.yaml +15 -0
  156. package/core/workspace-app/.ddev/nginx_full/nginx-site.conf +114 -0
  157. package/core/workspace-app/.ddev/web-build/Dockerfile.workspace-app +203 -0
  158. package/core/workspace-app/.ddev/web-build/ddev-shim-ddev +122 -0
  159. package/core/workspace-app/README.md +380 -0
  160. package/{actions → core/workspace-app/actions}/ddev.js +0 -1
  161. package/{actions → core/workspace-app/actions}/rag.js +0 -1
  162. package/{actions → core/workspace-app/actions}/testing.js +0 -1
  163. package/{assistant.js → core/workspace-app/assistant.js} +1 -10
  164. package/{git-info.js → core/workspace-app/git-info.js} +17 -1
  165. package/core/workspace-app/package.json +29 -0
  166. package/core/workspace-app/plugin-catalog.js +50 -0
  167. package/{plugins-page.js → core/workspace-app/plugins-page.js} +28 -2
  168. package/{server.js → core/workspace-app/server.js} +18 -0
  169. package/core/workspace-app/tests/features/actions-rail.feature +44 -0
  170. package/core/workspace-app/tests/features/ai-items.feature +23 -0
  171. package/core/workspace-app/tests/features/assistant-speech.feature +63 -0
  172. package/core/workspace-app/tests/features/checks.feature +39 -0
  173. package/core/workspace-app/tests/features/clone-project.feature +30 -0
  174. package/core/workspace-app/tests/features/command-authoring.feature +58 -0
  175. package/core/workspace-app/tests/features/dashboard.feature +57 -0
  176. package/core/workspace-app/tests/features/menus.feature +29 -0
  177. package/core/workspace-app/tests/features/plugins/jira/jira.feature +47 -0
  178. package/core/workspace-app/tests/features/plugins.feature +20 -0
  179. package/core/workspace-app/tests/features/project-actions.feature +40 -0
  180. package/core/workspace-app/tests/features/screens.feature +29 -0
  181. package/core/workspace-app/tests/features/settings.feature +46 -0
  182. package/core/workspace-app/tests/features/test-reports.feature +32 -0
  183. package/core/workspace-app/tests/features/theme.feature +40 -0
  184. package/core/workspace-app/tests/step-definitions/actions-post.steps.js +25 -0
  185. package/core/workspace-app/tests/step-definitions/assistant-speech.steps.js +152 -0
  186. package/core/workspace-app/tests/step-definitions/preconditions.steps.js +100 -0
  187. package/core/workspace-app/tests/unit/_isolated-config.js +31 -0
  188. package/core/workspace-app/tests/unit/accessibility.test.js +124 -0
  189. package/core/workspace-app/tests/unit/actions-rail-ux.test.js +56 -0
  190. package/core/workspace-app/tests/unit/actions-rail.test.js +51 -0
  191. package/core/workspace-app/tests/unit/ai-inventory.test.js +64 -0
  192. package/core/workspace-app/tests/unit/app-docs.test.js +64 -0
  193. package/core/workspace-app/tests/unit/assistant-avatar.test.js +110 -0
  194. package/core/workspace-app/tests/unit/assistant-cast.test.js +105 -0
  195. package/core/workspace-app/tests/unit/assistant-send-animation.test.js +15 -0
  196. package/core/workspace-app/tests/unit/assistant-suggestions.test.js +112 -0
  197. package/core/workspace-app/tests/unit/assistant-switches.test.js +53 -0
  198. package/core/workspace-app/tests/unit/assistant-views.test.js +146 -0
  199. package/core/workspace-app/tests/unit/assistant-working-indicator.test.js +49 -0
  200. package/core/workspace-app/tests/unit/assistant.test.js +61 -0
  201. package/core/workspace-app/tests/unit/audio-player.test.js +147 -0
  202. package/core/workspace-app/tests/unit/audio-voice.test.js +634 -0
  203. package/core/workspace-app/tests/unit/auth-google.test.js +180 -0
  204. package/core/workspace-app/tests/unit/autostart-watch.test.js +76 -0
  205. package/core/workspace-app/tests/unit/browsers.test.js +76 -0
  206. package/core/workspace-app/tests/unit/build-distribution.test.js +125 -0
  207. package/core/workspace-app/tests/unit/build-order.test.js +153 -0
  208. package/core/workspace-app/tests/unit/builder-args.test.js +113 -0
  209. package/core/workspace-app/tests/unit/builders.test.js +158 -0
  210. package/core/workspace-app/tests/unit/card-state-toggle.test.js +29 -0
  211. package/core/workspace-app/tests/unit/changelog.test.js +177 -0
  212. package/core/workspace-app/tests/unit/chat-thread.test.js +92 -0
  213. package/core/workspace-app/tests/unit/checks-plan.test.js +205 -0
  214. package/core/workspace-app/tests/unit/claude-args.test.js +182 -0
  215. package/core/workspace-app/tests/unit/claude-code-here.test.js +15 -0
  216. package/core/workspace-app/tests/unit/claude-resume.test.js +147 -0
  217. package/core/workspace-app/tests/unit/claude-screen-options.test.js +84 -0
  218. package/core/workspace-app/tests/unit/claude-session.test.js +41 -0
  219. package/core/workspace-app/tests/unit/command-flags.test.js +45 -0
  220. package/core/workspace-app/tests/unit/command-palette.test.js +105 -0
  221. package/core/workspace-app/tests/unit/commands-center.test.js +338 -0
  222. package/core/workspace-app/tests/unit/composer-menu.test.js +187 -0
  223. package/core/workspace-app/tests/unit/config-yaml.test.js +116 -0
  224. package/core/workspace-app/tests/unit/confirm-dialog.test.js +69 -0
  225. package/core/workspace-app/tests/unit/create-card.test.js +85 -0
  226. package/core/workspace-app/tests/unit/ddev-addons.test.js +144 -0
  227. package/core/workspace-app/tests/unit/ddev-auth-ssh.test.js +40 -0
  228. package/core/workspace-app/tests/unit/ddev-custom-commands.test.js +56 -0
  229. package/core/workspace-app/tests/unit/ddev-diagnose.test.js +57 -0
  230. package/core/workspace-app/tests/unit/ddev-domains.test.js +153 -0
  231. package/core/workspace-app/tests/unit/ddev-hostnames.test.js +143 -0
  232. package/core/workspace-app/tests/unit/ddev-import.test.js +82 -0
  233. package/core/workspace-app/tests/unit/ddev-mail.test.js +135 -0
  234. package/core/workspace-app/tests/unit/ddev-menu-and-create.test.js +28 -0
  235. package/core/workspace-app/tests/unit/ddev-mutagen.test.js +37 -0
  236. package/core/workspace-app/tests/unit/ddev-options.test.js +51 -0
  237. package/core/workspace-app/tests/unit/ddev-share.test.js +40 -0
  238. package/core/workspace-app/tests/unit/ddev-shim.test.js +57 -0
  239. package/core/workspace-app/tests/unit/ddev-snapshots.test.js +142 -0
  240. package/core/workspace-app/tests/unit/ddev-start-daemons.test.js +62 -0
  241. package/core/workspace-app/tests/unit/ddev-xdebug.test.js +41 -0
  242. package/core/workspace-app/tests/unit/deep-chat-origin.test.js +38 -0
  243. package/core/workspace-app/tests/unit/density.test.js +52 -0
  244. package/core/workspace-app/tests/unit/elevenlabs.test.js +453 -0
  245. package/core/workspace-app/tests/unit/event-log.test.js +102 -0
  246. package/core/workspace-app/tests/unit/favicon.test.js +68 -0
  247. package/core/workspace-app/tests/unit/files-browser.test.js +224 -0
  248. package/core/workspace-app/tests/unit/git-info.test.js +26 -0
  249. package/core/workspace-app/tests/unit/grid-and-list.test.js +103 -0
  250. package/core/workspace-app/tests/unit/host-tool-wrapper.test.js +18 -0
  251. package/core/workspace-app/tests/unit/html-patterns.test.js +51 -0
  252. package/core/workspace-app/tests/unit/ide-infra.test.js +28 -0
  253. package/core/workspace-app/tests/unit/images-ai.test.js +201 -0
  254. package/core/workspace-app/tests/unit/images-create.test.js +59 -0
  255. package/core/workspace-app/tests/unit/images-providers.test.js +248 -0
  256. package/core/workspace-app/tests/unit/images-svg.test.js +213 -0
  257. package/core/workspace-app/tests/unit/images-workspace.test.js +144 -0
  258. package/core/workspace-app/tests/unit/jobs-fragment.test.js +66 -0
  259. package/core/workspace-app/tests/unit/media-ux.test.js +164 -0
  260. package/core/workspace-app/tests/unit/modal-fit.test.js +87 -0
  261. package/core/workspace-app/tests/unit/modules-load.test.js +23 -0
  262. package/core/workspace-app/tests/unit/narration.test.js +186 -0
  263. package/core/workspace-app/tests/unit/navigation-editor.test.js +260 -0
  264. package/core/workspace-app/tests/unit/navigation.test.js +254 -0
  265. package/core/workspace-app/tests/unit/new-folder.test.js +121 -0
  266. package/core/workspace-app/tests/unit/new-screen-form.test.js +149 -0
  267. package/core/workspace-app/tests/unit/node-menu.test.js +161 -0
  268. package/core/workspace-app/tests/unit/notes.test.js +128 -0
  269. package/core/workspace-app/tests/unit/page-structure.test.js +40 -0
  270. package/core/workspace-app/tests/unit/page-width.test.js +59 -0
  271. package/core/workspace-app/tests/unit/php-version.test.js +90 -0
  272. package/core/workspace-app/tests/unit/plan-card.test.js +44 -0
  273. package/core/workspace-app/tests/unit/plugin-catalog.test.js +31 -0
  274. package/core/workspace-app/tests/unit/plugin-system.test.js +251 -0
  275. package/core/workspace-app/tests/unit/plugins-page.test.js +28 -0
  276. package/core/workspace-app/tests/unit/plugins.test.js +58 -0
  277. package/core/workspace-app/tests/unit/project-activity-filter.test.js +180 -0
  278. package/core/workspace-app/tests/unit/project-make.test.js +143 -0
  279. package/core/workspace-app/tests/unit/project-name.test.js +202 -0
  280. package/core/workspace-app/tests/unit/reader.test.js +62 -0
  281. package/core/workspace-app/tests/unit/release.test.js +92 -0
  282. package/core/workspace-app/tests/unit/remove-confirm.test.js +37 -0
  283. package/core/workspace-app/tests/unit/report-pdf.test.js +171 -0
  284. package/core/workspace-app/tests/unit/round-11.test.js +31 -0
  285. package/core/workspace-app/tests/unit/round-12.test.js +37 -0
  286. package/core/workspace-app/tests/unit/row-layout.test.js +585 -0
  287. package/core/workspace-app/tests/unit/row-menus-fold.test.js +29 -0
  288. package/core/workspace-app/tests/unit/rows-add-modal.test.js +68 -0
  289. package/core/workspace-app/tests/unit/screen-runner-queue.test.js +41 -0
  290. package/core/workspace-app/tests/unit/screen-window.test.js +116 -0
  291. package/core/workspace-app/tests/unit/screens-beside-running.test.js +19 -0
  292. package/core/workspace-app/tests/unit/screens-columns.test.js +79 -0
  293. package/core/workspace-app/tests/unit/screens-menu.test.js +84 -0
  294. package/core/workspace-app/tests/unit/screens-modal-overlay.test.js +14 -0
  295. package/core/workspace-app/tests/unit/screens-waiting.test.js +129 -0
  296. package/core/workspace-app/tests/unit/screens.test.js +74 -0
  297. package/core/workspace-app/tests/unit/second-storage.test.js +218 -0
  298. package/core/workspace-app/tests/unit/session-name.test.js +64 -0
  299. package/core/workspace-app/tests/unit/settings-file.test.js +173 -0
  300. package/core/workspace-app/tests/unit/settings-nested-list.test.js +54 -0
  301. package/core/workspace-app/tests/unit/settings-raw.test.js +177 -0
  302. package/core/workspace-app/tests/unit/shared-prefs.test.js +49 -0
  303. package/core/workspace-app/tests/unit/speech.test.js +290 -0
  304. package/core/workspace-app/tests/unit/static-caching.test.js +39 -0
  305. package/core/workspace-app/tests/unit/status-language.test.js +47 -0
  306. package/core/workspace-app/tests/unit/tab-modal.test.js +69 -0
  307. package/core/workspace-app/tests/unit/tab-title.test.js +37 -0
  308. package/core/workspace-app/tests/unit/template-literals.test.js +52 -0
  309. package/core/workspace-app/tests/unit/terminal-scrollback.test.js +61 -0
  310. package/core/workspace-app/tests/unit/terminal-size.test.js +103 -0
  311. package/core/workspace-app/tests/unit/terminal-style.test.js +84 -0
  312. package/core/workspace-app/tests/unit/terminal-windows.test.js +154 -0
  313. package/core/workspace-app/tests/unit/themes.test.js +453 -0
  314. package/core/workspace-app/tests/unit/tool-labels.test.js +34 -0
  315. package/core/workspace-app/tests/unit/topbar-icons.test.js +47 -0
  316. package/core/workspace-app/tests/unit/update-docker-gid.test.js +51 -0
  317. package/core/workspace-app/tests/unit/user-guide.test.js +60 -0
  318. package/core/workspace-app/tests/unit/version-check.test.js +98 -0
  319. package/core/workspace-app/tests/unit/version-link.test.js +50 -0
  320. package/core/workspace-app/tests/unit/video-borrow.test.js +198 -0
  321. package/core/workspace-app/tests/unit/video-edit-ask.test.js +93 -0
  322. package/core/workspace-app/tests/unit/video-edit-short.test.js +42 -0
  323. package/core/workspace-app/tests/unit/video-edit.test.js +417 -0
  324. package/core/workspace-app/tests/unit/video-transcript.test.js +237 -0
  325. package/core/workspace-app/tests/unit/waiting.test.js +48 -0
  326. package/core/workspace-app/tests/unit/wordpress-odoo-builders.test.js +38 -0
  327. package/core/workspace-app/tests/unit/workspace-bin.test.js +48 -0
  328. package/core/workspace-app/tests/unit/workspace-card-tools.test.js +30 -0
  329. package/core/workspace-app/tests/unit/workspace-delete.test.js +77 -0
  330. package/core/workspace-app/tests/unit/workspace-types.test.js +68 -0
  331. package/core/workspace-app/tests/unit/worktree-clone.test.js +250 -0
  332. package/package.json +38 -36
  333. /package/{PLUGINS.md → core/workspace-app/PLUGINS.md} +0 -0
  334. /package/{WORKSPACES.md → core/workspace-app/WORKSPACES.md} +0 -0
  335. /package/{actions → core/workspace-app/actions}/ai-items.js +0 -0
  336. /package/{actions → core/workspace-app/actions}/audio.js +0 -0
  337. /package/{actions → core/workspace-app/actions}/build-order.js +0 -0
  338. /package/{actions → core/workspace-app/actions}/checks.js +0 -0
  339. /package/{actions → core/workspace-app/actions}/claude-session.js +0 -0
  340. /package/{actions → core/workspace-app/actions}/commands-center.js +0 -0
  341. /package/{actions → core/workspace-app/actions}/composer.js +0 -0
  342. /package/{actions → core/workspace-app/actions}/ddev-addons.js +0 -0
  343. /package/{actions → core/workspace-app/actions}/ddev-mail.js +0 -0
  344. /package/{actions → core/workspace-app/actions}/folders.js +0 -0
  345. /package/{actions → core/workspace-app/actions}/images-svg.js +0 -0
  346. /package/{actions → core/workspace-app/actions}/images.js +0 -0
  347. /package/{actions → core/workspace-app/actions}/navigation.js +0 -0
  348. /package/{actions → core/workspace-app/actions}/node.js +0 -0
  349. /package/{actions → core/workspace-app/actions}/notes.js +0 -0
  350. /package/{actions → core/workspace-app/actions}/plugins.js +0 -0
  351. /package/{actions → core/workspace-app/actions}/project-make.js +0 -0
  352. /package/{actions → core/workspace-app/actions}/row-actions.js +0 -0
  353. /package/{actions → core/workspace-app/actions}/screens.js +0 -0
  354. /package/{actions → core/workspace-app/actions}/video-edit.js +0 -0
  355. /package/{ai-inventory.js → core/workspace-app/ai-inventory.js} +0 -0
  356. /package/{ai-items.js → core/workspace-app/ai-items.js} +0 -0
  357. /package/{assistant-cast.js → core/workspace-app/assistant-cast.js} +0 -0
  358. /package/{assistant-suggestions.js → core/workspace-app/assistant-suggestions.js} +0 -0
  359. /package/{audio-voice.js → core/workspace-app/audio-voice.js} +0 -0
  360. /package/{auth-google.js → core/workspace-app/auth-google.js} +0 -0
  361. /package/{build-ai.js → core/workspace-app/build-ai.js} +0 -0
  362. /package/{build-order-page.js → core/workspace-app/build-order-page.js} +0 -0
  363. /package/{build-order.js → core/workspace-app/build-order.js} +0 -0
  364. /package/{builder-args.js → core/workspace-app/builder-args.js} +0 -0
  365. /package/{changelog.js → core/workspace-app/changelog.js} +0 -0
  366. /package/{chat-threads.js → core/workspace-app/chat-threads.js} +0 -0
  367. /package/{checks.js → core/workspace-app/checks.js} +0 -0
  368. /package/{claude-args.js → core/workspace-app/claude-args.js} +0 -0
  369. /package/{claude-sessions.js → core/workspace-app/claude-sessions.js} +0 -0
  370. /package/{command-author.js → core/workspace-app/command-author.js} +0 -0
  371. /package/{command-block.js → core/workspace-app/command-block.js} +0 -0
  372. /package/{commands → core/workspace-app/commands}/defaults.yml +0 -0
  373. /package/{commands-page.js → core/workspace-app/commands-page.js} +0 -0
  374. /package/{commands.js → core/workspace-app/commands.js} +0 -0
  375. /package/{cucumber.js → core/workspace-app/cucumber.js} +0 -0
  376. /package/{ddev-addons-page.js → core/workspace-app/ddev-addons-page.js} +0 -0
  377. /package/{ddev-addons.js → core/workspace-app/ddev-addons.js} +0 -0
  378. /package/{ddev-custom-commands.js → core/workspace-app/ddev-custom-commands.js} +0 -0
  379. /package/{ddev-domains.js → core/workspace-app/ddev-domains.js} +0 -0
  380. /package/{ddev-import.js → core/workspace-app/ddev-import.js} +0 -0
  381. /package/{ddev-mail-page.js → core/workspace-app/ddev-mail-page.js} +0 -0
  382. /package/{ddev-mail.js → core/workspace-app/ddev-mail.js} +0 -0
  383. /package/{ddev-options.js → core/workspace-app/ddev-options.js} +0 -0
  384. /package/{ddev-snapshots.js → core/workspace-app/ddev-snapshots.js} +0 -0
  385. /package/{ddev.js → core/workspace-app/ddev.js} +0 -0
  386. /package/{docs → core/workspace-app/docs}/DEVELOPER-GUIDE.md +0 -0
  387. /package/{docs → core/workspace-app/docs}/README.md +0 -0
  388. /package/{docs → core/workspace-app/docs}/REFERENCE.md +0 -0
  389. /package/{docs → core/workspace-app/docs}/USER-GUIDE.md +0 -0
  390. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-board.png +0 -0
  391. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-coffee.png +0 -0
  392. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-colours.png +0 -0
  393. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-icons.png +0 -0
  394. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-moves.png +0 -0
  395. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-pick-cookieman.png +0 -0
  396. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-settings-voices.png +0 -0
  397. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-status.png +0 -0
  398. /package/{docs → core/workspace-app/docs}/screenshots/create-card/create-card.png +0 -0
  399. /package/{docs → core/workspace-app/docs}/screenshots/create-card/start-ai.png +0 -0
  400. /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-address.png +0 -0
  401. /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-worktree-off.png +0 -0
  402. /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-worktree-on.png +0 -0
  403. /package/{docs → core/workspace-app/docs}/screenshots/guide-ddev-menu.png +0 -0
  404. /package/{docs → core/workspace-app/docs}/screenshots/guide-domains.png +0 -0
  405. /package/{docs → core/workspace-app/docs}/screenshots/guide-home.png +0 -0
  406. /package/{docs → core/workspace-app/docs}/screenshots/guide-ide-menu.png +0 -0
  407. /package/{docs → core/workspace-app/docs}/screenshots/guide-remove-refusal.png +0 -0
  408. /package/{docs → core/workspace-app/docs}/screenshots/guide-rows-worktree.png +0 -0
  409. /package/{docs → core/workspace-app/docs}/screenshots/guide-screens.png +0 -0
  410. /package/{docs → core/workspace-app/docs}/screenshots/guide-settings.png +0 -0
  411. /package/{docs → core/workspace-app/docs}/screenshots/guide-workspace-page.png +0 -0
  412. /package/{docs → core/workspace-app/docs}/screenshots/home/card-settings.png +0 -0
  413. /package/{docs → core/workspace-app/docs}/screenshots/home/drag-reorder.png +0 -0
  414. /package/{docs → core/workspace-app/docs}/screenshots/home/edit-card.png +0 -0
  415. /package/{docs → core/workspace-app/docs}/screenshots/images/claude-code-image.png +0 -0
  416. /package/{docs → core/workspace-app/docs}/screenshots/images/edit-with-ai.png +0 -0
  417. /package/{docs → core/workspace-app/docs}/screenshots/images/gallery.png +0 -0
  418. /package/{docs → core/workspace-app/docs}/screenshots/images/generate-description.png +0 -0
  419. /package/{docs → core/workspace-app/docs}/screenshots/images/generate-plan.png +0 -0
  420. /package/{docs → core/workspace-app/docs}/screenshots/images/providers.png +0 -0
  421. /package/{docs → core/workspace-app/docs}/screenshots/images/svg-form.png +0 -0
  422. /package/{docs → core/workspace-app/docs}/screenshots/images/svg-result.png +0 -0
  423. /package/{docs → core/workspace-app/docs}/screenshots/images/upload.png +0 -0
  424. /package/{docs → core/workspace-app/docs}/screenshots/new-folder/2-form.png +0 -0
  425. /package/{docs → core/workspace-app/docs}/screenshots/new-folder/8-ddev-options.png +0 -0
  426. /package/{docs → core/workspace-app/docs}/screenshots/new-folder/files-folder.png +0 -0
  427. /package/{docs → core/workspace-app/docs}/screenshots/projects/ddev-custom-commands.png +0 -0
  428. /package/{docs → core/workspace-app/docs}/screenshots/projects/state-chip-stop.png +0 -0
  429. /package/{docs → core/workspace-app/docs}/screenshots/readme/dashboard-dark.png +0 -0
  430. /package/{docs → core/workspace-app/docs}/screenshots/readme/dashboard-light.png +0 -0
  431. /package/{docs → core/workspace-app/docs}/screenshots/readme/dev-dark.png +0 -0
  432. /package/{docs → core/workspace-app/docs}/screenshots/readme/dev-light.png +0 -0
  433. /package/{docs → core/workspace-app/docs}/screenshots/readme/docs-dark.png +0 -0
  434. /package/{docs → core/workspace-app/docs}/screenshots/readme/docs-light.png +0 -0
  435. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/ai.svg +0 -0
  436. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/code.svg +0 -0
  437. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/docs.svg +0 -0
  438. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/services.svg +0 -0
  439. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/sites.svg +0 -0
  440. /package/{docs → core/workspace-app/docs}/screenshots/readme/test-report-dark.png +0 -0
  441. /package/{docs → core/workspace-app/docs}/screenshots/readme/test-report-light.png +0 -0
  442. /package/{docs → core/workspace-app/docs}/screenshots/readme/test-scenarios.png +0 -0
  443. /package/{docs → core/workspace-app/docs}/screenshots/readme/test-steps.png +0 -0
  444. /package/{docs → core/workspace-app/docs}/screenshots/readme/videos-dark.png +0 -0
  445. /package/{docs → core/workspace-app/docs}/screenshots/readme/videos-light.png +0 -0
  446. /package/{docs → core/workspace-app/docs}/screenshots/screens-menu.png +0 -0
  447. /package/{docs → core/workspace-app/docs}/screenshots/screens-new.png +0 -0
  448. /package/{docs → core/workspace-app/docs}/screenshots/settings/auth-google-lists.png +0 -0
  449. /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-1-row.png +0 -0
  450. /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-2-confirm.png +0 -0
  451. /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-3-type-name.png +0 -0
  452. /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-4-type-yes.png +0 -0
  453. /package/{event-log.js → core/workspace-app/event-log.js} +0 -0
  454. /package/{files-browser.js → core/workspace-app/files-browser.js} +0 -0
  455. /package/{folders.js → core/workspace-app/folders.js} +0 -0
  456. /package/{format.js → core/workspace-app/format.js} +0 -0
  457. /package/{github-auth.js → core/workspace-app/github-auth.js} +0 -0
  458. /package/{html.js → core/workspace-app/html.js} +0 -0
  459. /package/{icons.js → core/workspace-app/icons.js} +0 -0
  460. /package/{images-ai.js → core/workspace-app/images-ai.js} +0 -0
  461. /package/{images-create.js → core/workspace-app/images-create.js} +0 -0
  462. /package/{images-page.js → core/workspace-app/images-page.js} +0 -0
  463. /package/{images-records.js → core/workspace-app/images-records.js} +0 -0
  464. /package/{images-render.js → core/workspace-app/images-render.js} +0 -0
  465. /package/{images-svg-core.js → core/workspace-app/images-svg-core.js} +0 -0
  466. /package/{images-svg-sanitize.js → core/workspace-app/images-svg-sanitize.js} +0 -0
  467. /package/{images-svg.js → core/workspace-app/images-svg.js} +0 -0
  468. /package/{images.js → core/workspace-app/images.js} +0 -0
  469. /package/{jobs.js → core/workspace-app/jobs.js} +0 -0
  470. /package/{linger.js → core/workspace-app/linger.js} +0 -0
  471. /package/{lists.js → core/workspace-app/lists.js} +0 -0
  472. /package/{media-info.js → core/workspace-app/media-info.js} +0 -0
  473. /package/{navigation-page.js → core/workspace-app/navigation-page.js} +0 -0
  474. /package/{navigation.js → core/workspace-app/navigation.js} +0 -0
  475. /package/{notes.js → core/workspace-app/notes.js} +0 -0
  476. /package/{palette-index.js → core/workspace-app/palette-index.js} +0 -0
  477. /package/{playwright.config.js → core/workspace-app/playwright.config.js} +0 -0
  478. /package/{plugin-install.js → core/workspace-app/plugin-install.js} +0 -0
  479. /package/{plugin-loader.js → core/workspace-app/plugin-loader.js} +0 -0
  480. /package/{plugin-manifest.js → core/workspace-app/plugin-manifest.js} +0 -0
  481. /package/{plugin-settings.js → core/workspace-app/plugin-settings.js} +0 -0
  482. /package/{plugins → core/workspace-app/plugins}/workspace-notes/index.js +0 -0
  483. /package/{project-make.js → core/workspace-app/project-make.js} +0 -0
  484. /package/{project-name.js → core/workspace-app/project-name.js} +0 -0
  485. /package/{public → core/workspace-app/public}/assistant-avatar-moves.js +0 -0
  486. /package/{public → core/workspace-app/public}/assistant-avatar-ui.js +0 -0
  487. /package/{public → core/workspace-app/public}/assistant-avatar.js +0 -0
  488. /package/{public → core/workspace-app/public}/avatar/druplicon.vrm +0 -0
  489. /package/{public → core/workspace-app/public}/css/base.css +0 -0
  490. /package/{public → core/workspace-app/public}/css/components/ai-assistant.css +0 -0
  491. /package/{public → core/workspace-app/public}/css/components/assistant-avatar.css +0 -0
  492. /package/{public → core/workspace-app/public}/css/components/assistant-views.css +0 -0
  493. /package/{public → core/workspace-app/public}/css/components/build-order.css +0 -0
  494. /package/{public → core/workspace-app/public}/css/components/clone-as-a-git-worktree.css +0 -0
  495. /package/{public → core/workspace-app/public}/css/components/command-palette.css +0 -0
  496. /package/{public → core/workspace-app/public}/css/components/commands-center.css +0 -0
  497. /package/{public → core/workspace-app/public}/css/components/confirm-dialog.css +0 -0
  498. /package/{public → core/workspace-app/public}/css/components/create-card.css +0 -0
  499. /package/{public → core/workspace-app/public}/css/components/ddev-menu.css +0 -0
  500. /package/{public → core/workspace-app/public}/css/components/deep-chat.css +0 -0
  501. /package/{public → core/workspace-app/public}/css/components/density.css +0 -0
  502. /package/{public → core/workspace-app/public}/css/components/floating-assistant-workspace-pages.css +0 -0
  503. /package/{public → core/workspace-app/public}/css/components/folders.css +0 -0
  504. /package/{public → core/workspace-app/public}/css/components/full-height-left-assistant-sidebar-all-pages.css +0 -0
  505. /package/{public → core/workspace-app/public}/css/components/full-height-right-actions-rail-pages-that-have-actions.css +0 -0
  506. /package/{public → core/workspace-app/public}/css/components/grid-and-list.css +0 -0
  507. /package/{public → core/workspace-app/public}/css/components/help-page.css +0 -0
  508. /package/{public → core/workspace-app/public}/css/components/hero.css +0 -0
  509. /package/{public → core/workspace-app/public}/css/components/images.css +0 -0
  510. /package/{public → core/workspace-app/public}/css/components/layout-rhythm-and-spacing-polish.css +0 -0
  511. /package/{public → core/workspace-app/public}/css/components/mic-on-any-field-that-wants-typing.css +0 -0
  512. /package/{public → core/workspace-app/public}/css/components/modal-fit.css +0 -0
  513. /package/{public → core/workspace-app/public}/css/components/page-width.css +0 -0
  514. /package/{public → core/workspace-app/public}/css/components/plugins-page.css +0 -0
  515. /package/{public → core/workspace-app/public}/css/components/project-cards.css +0 -0
  516. /package/{public → core/workspace-app/public}/css/components/project-rows.css +0 -0
  517. /package/{public → core/workspace-app/public}/css/components/reordering-the-workspace-cards.css +0 -0
  518. /package/{public → core/workspace-app/public}/css/components/responsive-home-and-the-workspace-pages.css +0 -0
  519. /package/{public → core/workspace-app/public}/css/components/running-commands-as-toasts.css +0 -0
  520. /package/{public → core/workspace-app/public}/css/components/settings-editor-core-config-yml.css +0 -0
  521. /package/{public → core/workspace-app/public}/css/components/short-results-as-a-toast-at-the-top.css +0 -0
  522. /package/{public → core/workspace-app/public}/css/components/sound-player.css +0 -0
  523. /package/{public → core/workspace-app/public}/css/components/suggest-agents-skills-commands.css +0 -0
  524. /package/{public → core/workspace-app/public}/css/components/tab-modal.css +0 -0
  525. /package/{public → core/workspace-app/public}/css/components/terminals.css +0 -0
  526. /package/{public → core/workspace-app/public}/css/components/test-report-media-one-file-per-row-paged.css +0 -0
  527. /package/{public → core/workspace-app/public}/css/components/top-toolbar-logo-breadcrumb.css +0 -0
  528. /package/{public → core/workspace-app/public}/css/components/video-edit.css +0 -0
  529. /package/{public → core/workspace-app/public}/css/components/web-ide-open-with.css +0 -0
  530. /package/{public → core/workspace-app/public}/css/components/working-indicators.css +0 -0
  531. /package/{public → core/workspace-app/public}/css/components/workspace-cards.css +0 -0
  532. /package/{public → core/workspace-app/public}/favicon.ico +0 -0
  533. /package/{public → core/workspace-app/public}/favicon.png +0 -0
  534. /package/{public → core/workspace-app/public}/htmx.min.js +0 -0
  535. /package/{public → core/workspace-app/public}/logo.png +0 -0
  536. /package/{public → core/workspace-app/public}/media-player.js +0 -0
  537. /package/{public → core/workspace-app/public}/mic.js +0 -0
  538. /package/{public → core/workspace-app/public}/narration.js +0 -0
  539. /package/{public → core/workspace-app/public}/palette.js +0 -0
  540. /package/{public → core/workspace-app/public}/sortable.js +0 -0
  541. /package/{public → core/workspace-app/public}/suggest.js +0 -0
  542. /package/{public → core/workspace-app/public}/terminal.js +0 -0
  543. /package/{public → core/workspace-app/public}/themes/default/theme.css +0 -0
  544. /package/{public → core/workspace-app/public}/themes/ember/theme.css +0 -0
  545. /package/{public → core/workspace-app/public}/themes/press/theme.css +0 -0
  546. /package/{public → core/workspace-app/public}/themes/slate/theme.css +0 -0
  547. /package/{public → core/workspace-app/public}/touch-icon.png +0 -0
  548. /package/{public → core/workspace-app/public}/ui.js +0 -0
  549. /package/{public → core/workspace-app/public}/vendor/ace/LICENSE.txt +0 -0
  550. /package/{public → core/workspace-app/public}/vendor/ace/ace.js +0 -0
  551. /package/{public → core/workspace-app/public}/vendor/ace/ext-language_tools.js +0 -0
  552. /package/{public → core/workspace-app/public}/vendor/ace/ext-searchbox.js +0 -0
  553. /package/{public → core/workspace-app/public}/vendor/ace/mode-css.js +0 -0
  554. /package/{public → core/workspace-app/public}/vendor/ace/mode-html.js +0 -0
  555. /package/{public → core/workspace-app/public}/vendor/ace/mode-javascript.js +0 -0
  556. /package/{public → core/workspace-app/public}/vendor/ace/mode-markdown.js +0 -0
  557. /package/{public → core/workspace-app/public}/vendor/ace/mode-sh.js +0 -0
  558. /package/{public → core/workspace-app/public}/vendor/ace/mode-yaml.js +0 -0
  559. /package/{public → core/workspace-app/public}/vendor/ace/theme-textmate.js +0 -0
  560. /package/{public → core/workspace-app/public}/vendor/ace/theme-tomorrow_night.js +0 -0
  561. /package/{public → core/workspace-app/public}/vendor/ace/worker-yaml.js +0 -0
  562. /package/{public → core/workspace-app/public}/vendor/deep-chat.LICENSE +0 -0
  563. /package/{public → core/workspace-app/public}/vendor/deep-chat.bundle.js +0 -0
  564. /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-LICENSE.txt +0 -0
  565. /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
  566. /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
  567. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-LICENSE.txt +0 -0
  568. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
  569. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
  570. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
  571. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
  572. /package/{public → core/workspace-app/public}/vendor/fonts/jetbrains-mono-LICENSE.txt +0 -0
  573. /package/{public → core/workspace-app/public}/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  574. /package/{public → core/workspace-app/public}/vendor/fonts/ubuntu-mono-LICENSE.txt +0 -0
  575. /package/{public → core/workspace-app/public}/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
  576. /package/{public → core/workspace-app/public}/vendor/tabler-LICENSE.txt +0 -0
  577. /package/{public → core/workspace-app/public}/vendor/tabler-sprite.svg +0 -0
  578. /package/{public → core/workspace-app/public}/vendor/three/LICENSE +0 -0
  579. /package/{public → core/workspace-app/public}/vendor/three/addons/loaders/GLTFLoader.js +0 -0
  580. /package/{public → core/workspace-app/public}/vendor/three/addons/utils/BufferGeometryUtils.js +0 -0
  581. /package/{public → core/workspace-app/public}/vendor/three/addons/utils/SkeletonUtils.js +0 -0
  582. /package/{public → core/workspace-app/public}/vendor/three/three.core.min.js +0 -0
  583. /package/{public → core/workspace-app/public}/vendor/three/three.module.min.js +0 -0
  584. /package/{public → core/workspace-app/public}/vendor/three-vrm/LICENSE +0 -0
  585. /package/{public → core/workspace-app/public}/vendor/three-vrm/three-vrm.module.min.js +0 -0
  586. /package/{public → core/workspace-app/public}/vendor/uikit-icons.min.js +0 -0
  587. /package/{public → core/workspace-app/public}/vendor/uikit.min.css +0 -0
  588. /package/{public → core/workspace-app/public}/vendor/uikit.min.js +0 -0
  589. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/bookmark-outlined.svg +0 -0
  590. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/headset.svg +0 -0
  591. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/menu.svg +0 -0
  592. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/pause.svg +0 -0
  593. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/play.svg +0 -0
  594. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/share.svg +0 -0
  595. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/sound-off.svg +0 -0
  596. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/sound-on.svg +0 -0
  597. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/stop.svg +0 -0
  598. /package/{public → core/workspace-app/public}/vendor/xterm/addon-fit.js +0 -0
  599. /package/{public → core/workspace-app/public}/vendor/xterm/addon-unicode11.js +0 -0
  600. /package/{public → core/workspace-app/public}/vendor/xterm/addon-web-links.js +0 -0
  601. /package/{public → core/workspace-app/public}/vendor/xterm/xterm.css +0 -0
  602. /package/{public → core/workspace-app/public}/vendor/xterm/xterm.js +0 -0
  603. /package/{rag.js → core/workspace-app/rag.js} +0 -0
  604. /package/{reader.js → core/workspace-app/reader.js} +0 -0
  605. /package/{repo-blobs.js → core/workspace-app/repo-blobs.js} +0 -0
  606. /package/{report-pdf.js → core/workspace-app/report-pdf.js} +0 -0
  607. /package/{row-layout.js → core/workspace-app/row-layout.js} +0 -0
  608. /package/{screenrc → core/workspace-app/screenrc} +0 -0
  609. /package/{screens.js → core/workspace-app/screens.js} +0 -0
  610. /package/{scripts → core/workspace-app/scripts}/code-server-start.sh +0 -0
  611. /package/{scripts → core/workspace-app/scripts}/images-claude-finish.js +0 -0
  612. /package/{scripts → core/workspace-app/scripts}/screen-host.sh +0 -0
  613. /package/{scripts → core/workspace-app/scripts}/screen-runner.js +0 -0
  614. /package/{scripts → core/workspace-app/scripts}/screen-then.js +0 -0
  615. /package/{scripts → core/workspace-app/scripts}/update.sh +0 -0
  616. /package/{settings-file.js → core/workspace-app/settings-file.js} +0 -0
  617. /package/{speech.js → core/workspace-app/speech.js} +0 -0
  618. /package/{tab-modal.js → core/workspace-app/tab-modal.js} +0 -0
  619. /package/{test-reports.js → core/workspace-app/test-reports.js} +0 -0
  620. /package/{themes.js → core/workspace-app/themes.js} +0 -0
  621. /package/{transcripts.js → core/workspace-app/transcripts.js} +0 -0
  622. /package/{urls.js → core/workspace-app/urls.js} +0 -0
  623. /package/{validate.js → core/workspace-app/validate.js} +0 -0
  624. /package/{version-check.js → core/workspace-app/version-check.js} +0 -0
  625. /package/{video-edit.js → core/workspace-app/video-edit.js} +0 -0
  626. /package/{waiting.js → core/workspace-app/waiting.js} +0 -0
  627. /package/{workspace-card-tools.js → core/workspace-app/workspace-card-tools.js} +0 -0
  628. /package/{workspace-delete.js → core/workspace-app/workspace-delete.js} +0 -0
  629. /package/{workspace-types.js → core/workspace-app/workspace-types.js} +0 -0
  630. /package/{workspaces.js → core/workspace-app/workspaces.js} +0 -0
@@ -0,0 +1,585 @@
1
+ /**
2
+ * Commands → Buttons: rows management for project rows, and the actions someone adds.
3
+ *
4
+ * What must never drift: with no file every row is exactly the default; every button is placed once and
5
+ * a new one appears in its default row; rearranging never breaks a menu (markup it cannot read is left
6
+ * alone); the dashboard's own commands can be moved and hidden but never deleted; an action's line can
7
+ * never carry $ or a backtick, and a value reaches it only checked and single-quoted; and the row, the
8
+ * SCREENS fragment and the Settings page are wired to it.
9
+ */
10
+
11
+ const assert = require('node:assert');
12
+ const { test } = require('node:test');
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const path = require('path');
16
+
17
+ // The GIT menu is a plugin's now (@webship.co/workspace-git); its slot is registered here the way the loader does.
18
+ require('../../row-layout').registerButton({ id: 'ide', label: 'IDE', row: 0 });
19
+ require('../../row-layout').registerButton({ id: 'graph', label: 'Graph', row: 1 });
20
+ require('../../row-layout').registerButton({ id: 'git', label: 'GIT', row: 0 });
21
+
22
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'rowlayout-'));
23
+ process.env.ROW_ACTIONS_FILE = path.join(tmp, 'row-actions.yml');
24
+ process.env.ROW_ACTIONS_SEEN_FILE = path.join(tmp, 'row-actions.seen.json');
25
+ process.env.ROW_ACTIONS_HISTORY_FILE = path.join(tmp, 'row-actions.history.jsonl');
26
+ process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
27
+
28
+ const rl = require('../../row-layout');
29
+
30
+ const action = (over = {}) => ({ id: 'cr', label: 'Rebuild caches', button: 'ddev', group: 'drupal', when: 'ddev', command: 'ddev drush cr', ...over });
31
+ const clean = (over) => rl.cleanCustom(action(over));
32
+
33
+ const DDEV_INNER = `
34
+ <li class="uk-nav-header" data-group="project">site1</li>
35
+ <li data-entry="start-stop"><a href hx-post="/actions/ddev-stop">Stop</a></li>
36
+ <li><a href hx-post="/actions/ddev-restart">Restart</a></li>
37
+ <li><a href hx-post="/actions/ddev-describe">Describe</a></li>
38
+ <li class="uk-nav-divider"></li>
39
+ <li class="uk-nav-header">build</li>
40
+ <li><a href hx-post="/actions/ddev-composer-install">Composer install</a></li>
41
+ <li class="uk-nav-divider"></li>
42
+ <!-- a comment the arrangement may drop -->
43
+ <li class="uk-nav-header">database</li>
44
+ <li><a href hx-post="/actions/ddev-export-db">Export the database</a></li>
45
+ <li class="uk-parent" data-entry="checks"><a href>Checks</a><ul class="uk-nav-sub"><li><a href>one</a></li><li><a href>two</a></li></ul></li>
46
+ <li class="uk-nav-divider"></li>
47
+ <li class="uk-nav-header">a note with no entries</li>`;
48
+
49
+ const texts = (html) => rl.splitTopLevelLi(html).map((li) => rl.plainText(li));
50
+
51
+ // ------------------------------------------------------------------ rows
52
+
53
+ test('with no file there are two rows, and they are the row as it was', () => {
54
+ const layout = rl.loadRowLayoutFresh();
55
+ assert.deepStrictEqual(rl.DEFAULT_ROWS, [
56
+ ['ddev', 'tests', 'ide', 'git'],
57
+ ['screens', 'composer', 'node', 'backup', 'clone', 'rag', 'more', 'graph'],
58
+ ]);
59
+ assert.deepStrictEqual(layout.rows, rl.DEFAULT_ROWS);
60
+ assert.deepStrictEqual([layout.hidden, layout.custom, layout.menus], [[], [], {}]);
61
+ });
62
+
63
+ test('rows: every button placed exactly once, extra rows kept, a new button in its default row', () => {
64
+ const l = rl.normalizeLayout({
65
+ rows: [['git', 'nope', 'ddev', 'git'], [], ['composer', 'ddev']],
66
+ hidden: ['rag', 'rag', 'x-unknown'],
67
+ custom: [action({ id: 'deploy', button: 'tools', when: 'any', command: 'ddev deploy' })],
68
+ });
69
+ assert.strictEqual(l.rows.length, 3, 'an empty row someone added stays');
70
+ assert.deepStrictEqual(l.rows[0].slice(0, 2), ['git', 'ddev'], 'the saved order first, duplicates dropped');
71
+ assert.strictEqual(l.rows[2][0], 'composer');
72
+ assert.deepStrictEqual(l.hidden, ['rag']);
73
+ const all = [...l.rows.flat(), ...l.hidden];
74
+ assert.strictEqual(all.length, new Set(all).size, 'no button twice');
75
+ for (const b of rl.BUILTIN_BUTTONS) assert.ok(all.includes(b.id), `${b.id} is missing`);
76
+ assert.ok(l.rows[0].includes('ide'), 'a button the file never mentions keeps its default row');
77
+ assert.ok(l.rows[1].includes('tools'), 'a button made by an action goes to the second row');
78
+ assert.ok(!all.includes('nope') && !all.includes('x-unknown'));
79
+
80
+ // The first version of the file called rows `lines`, and still loads.
81
+ assert.deepStrictEqual(rl.normalizeLayout({ lines: [['more'], ['ddev']] }).rows[0][0], 'more');
82
+ const many = rl.normalizeLayout({ rows: Array.from({ length: 9 }, () => []) });
83
+ assert.strictEqual(many.rows.length, rl.MAX_ROWS);
84
+ });
85
+
86
+ // ------------------------------------------------------------------ actions
87
+
88
+ test('an action must be safe to hand to the screen runner, and only goes where a menu is', () => {
89
+ assert.strictEqual(rl.customRefusal(clean()), '');
90
+ assert.strictEqual(rl.customRefusal(clean({ command: 'git pull --ff-only && ddev drush cr -y' })), '');
91
+ for (const [over, why] of [
92
+ [{ command: 'echo $HOME' }, /\$ or a backtick/],
93
+ [{ command: 'echo `id`' }, /\$ or a backtick/],
94
+ [{ command: 'ddev drush cr\nrm -rf ~' }, /one line/],
95
+ [{ command: '' }, /steps|one line/],
96
+ [{ steps: ['ok', { command: 'bad $X' }] }, /step 2/],
97
+ [{ steps: [{ command: 'ok', on_error: 'echo `id`' }] }, /on_error/],
98
+ [{ label: 'say "hi"' }, /label/],
99
+ [{ id: 'Bad Id' }, /id/],
100
+ [{ button: 'DDEV!' }, /button/],
101
+ [{ button: 'backup' }, /single button/],
102
+ [{ when: ['ddev', 'always'] }, /when/],
103
+ [{ command: 'git switch {{opt.branch}}' }, /no option called branch/],
104
+ [{ options: [{ name: 'b', enforced: true }] }, /enforced but lists no values/],
105
+ [{ options: [{ name: 'b', regex: '(' }] }, /regex does not compile/],
106
+ [{ options: [{ name: 'b', value: 'x$y' }] }, /\$/],
107
+ [{ options: [{ name: 'b' }, { name: 'b' }] }, /unique/],
108
+ ]) {
109
+ assert.match(rl.customRefusal(clean(over)), why, JSON.stringify(over));
110
+ }
111
+ assert.deepStrictEqual(rl.normalizeLayout({ custom: [action({ command: 'echo $X' })] }).custom, [], 'refused entries never reach the layout');
112
+ });
113
+
114
+ test('when is a filter: every clause must match, ! negates, a workspace can be named', () => {
115
+ const ctx = { key: 'dev', isDdev: true, hasComposer: true, hasNode: false, hasGit: true };
116
+ assert.strictEqual(rl.whenMatches(['ddev', 'composer'], ctx), true);
117
+ assert.strictEqual(rl.whenMatches(['ddev', 'node'], ctx), false);
118
+ assert.strictEqual(rl.whenMatches(['ddev', '!workspace:products'], ctx), true);
119
+ assert.strictEqual(rl.whenMatches(['ddev', '!workspace:products'], { ...ctx, key: 'products' }), false);
120
+ assert.strictEqual(rl.whenMatches('any', {}), true);
121
+ assert.strictEqual(rl.whenMatches(undefined, {}), true);
122
+ assert.strictEqual(rl.describeWhen(['ddev', '!workspace:products']), 'DDEV projects and not the products workspace');
123
+ });
124
+
125
+ test('steps become one line: && stops at a failure, keepgoing runs on, on_error takes the step\'s place', () => {
126
+ const stop = rl.cleanCustom(action({ steps: ['a', 'b'] }));
127
+ assert.strictEqual(rl.actionCommandLine(stop), '{ { a ; } && { b ; } ; } && echo "ROW-ACTION succeeded" || echo "ROW-ACTION failed"');
128
+ const go = rl.cleanCustom(action({ keepgoing: true, steps: ['a', { command: 'b', on_error: 'c' }] }));
129
+ const line = rl.actionCommandLine(go);
130
+ assert.match(line, /^\{ \{ \{ a ; \} \|\| echo "ROW-ACTION step 1 failed" ; \} ; \{ \{ \{ b ; \} \|\| \{ echo "ROW-ACTION step 2 failed: running its on_error" ; c ; \} ; \} \|\| echo "ROW-ACTION step 2 failed" ; \} ; \}/);
131
+ for (const l of [rl.actionCommandLine(stop), line]) assert.ok(!l.includes('$'), l);
132
+ });
133
+
134
+ test('an option is checked, falls back to its default, and reaches the line single-quoted', () => {
135
+ const e = rl.cleanCustom(action({
136
+ command: 'git switch {{opt.branch}} && echo {{opt.note}}',
137
+ options: [
138
+ { name: 'branch', value: 'main', regex: '^[A-Za-z0-9._/-]+$', required: true },
139
+ { name: 'note', value: '' },
140
+ { name: 'env', values: ['dev', 'stage'], enforced: true, value: 'dev' },
141
+ ],
142
+ }));
143
+ assert.strictEqual(rl.customRefusal(e), '');
144
+ let r = rl.resolveOptions(e, {});
145
+ assert.deepStrictEqual(r.values, { branch: 'main', note: '', env: 'dev' }, 'defaults');
146
+ r = rl.resolveOptions(e, { opt_branch: 'feature/x', opt_note: "it's fine; rm -rf ~", opt_env: 'stage' });
147
+ assert.strictEqual(rl.actionCommandLine(e, r.values).split(' && echo "ROW-ACTION')[0],
148
+ "{ { git switch 'feature/x' && echo 'it'\\''s fine; rm -rf ~' ; } ; }");
149
+ assert.match(rl.resolveOptions(e, { opt_branch: 'bad branch!' }).error, /does not match/);
150
+ assert.match(rl.resolveOptions(e, { opt_env: 'prod' }).error, /must be one of/);
151
+ assert.match(rl.resolveOptions(e, { opt_note: 'x $(id)' }).error, /\$/);
152
+ const req = rl.cleanCustom(action({ command: 'echo {{opt.who}}', options: [{ name: 'who', required: true }] }));
153
+ assert.match(rl.resolveOptions(req, {}).error, /required/);
154
+ });
155
+
156
+ test('a run\'s result is read from the markers its line prints', () => {
157
+ assert.deepStrictEqual(rl.runStatus('x\nROW-ACTION succeeded\n', false), { state: 'succeeded', note: '' });
158
+ assert.deepStrictEqual(rl.runStatus('ROW-ACTION failed', false).state, 'failed');
159
+ assert.deepStrictEqual(rl.runStatus('ROW-ACTION step 2 failed\nROW-ACTION succeeded', false), { state: 'partly', note: 'step 2 failed' });
160
+ assert.strictEqual(rl.runStatus('ROW-ACTION step 1 failed: running its on_error\nROW-ACTION succeeded', false).state, 'succeeded', 'a handled step is not a failed one');
161
+ assert.strictEqual(rl.runStatus('working…', true).state, 'running');
162
+ assert.strictEqual(rl.runStatus('', false).state, 'unknown');
163
+ });
164
+
165
+ // ------------------------------------------------------------------ menus
166
+
167
+ test('a menu is split into its top-level entries; anything else in it means it is left alone', () => {
168
+ assert.deepStrictEqual(texts(DDEV_INNER).slice(0, 3), ['site1', 'Stop', 'Restart']);
169
+ assert.strictEqual(rl.splitTopLevelLi('<li>a</li> stray text <li>b</li>'), null);
170
+ assert.strictEqual(rl.splitTopLevelLi('<li>a</li><li>unclosed'), null);
171
+ const layout = rl.normalizeLayout({ menus: { ddev: { groups: ['build'] } } });
172
+ assert.strictEqual(rl.arrangeNavInner('ddev', '<li>a</li> text', { layout }), '<li>a</li> text');
173
+ });
174
+
175
+ test('with nothing saved a menu keeps its order and every entry', () => {
176
+ const out = rl.arrangeNavInner('ddev', DDEV_INNER, { layout: rl.normalizeLayout({}) });
177
+ assert.deepStrictEqual(texts(out).filter(Boolean), texts(DDEV_INNER).filter(Boolean));
178
+ });
179
+
180
+ test('a menu with nothing to apply is returned exactly as it was rendered, and is still recorded', () => {
181
+ const inner = '<li class="uk-nav-header" data-group="fastpath">x</li>\n <!-- kept -->\n <li><a>Only here</a></li>';
182
+ const out = rl.arrangeNavInner('fastpath-menu', inner, { layout: rl.normalizeLayout({}) });
183
+ assert.strictEqual(out, inner, 'no settings and no actions: not rebuilt at all');
184
+ assert.strictEqual(rl.seenMenu('fastpath-menu').groups.fastpath.entries['only here'], 'Only here', 'still listed for the Settings page');
185
+ // A menu with an action of yours in it is rebuilt, so the action can be added.
186
+ const withAction = rl.normalizeLayout({ custom: [action({ id: 'fp', label: 'FP', button: 'fastpath-menu', group: 'fastpath', when: 'any', command: 'echo fp' })] });
187
+ assert.match(rl.arrangeNavInner('fastpath-menu', inner, { layout: withAction, key: 'dev', project: 'p' }), /"id":"fp"/);
188
+ });
189
+
190
+ test('groups and the commands inside them follow the saved order; hidden ones go; nested entries survive', () => {
191
+ const layout = rl.normalizeLayout({ menus: { ddev: {
192
+ groups: ['database', 'build'],
193
+ order: { project: ['describe', 'start-stop'] },
194
+ hidden_entries: ['restart'],
195
+ } } });
196
+ const out = rl.arrangeNavInner('ddev', DDEV_INNER, { layout });
197
+ const headers = rl.splitTopLevelLi(out).filter((li) => li.includes('uk-nav-header')).map((li) => rl.plainText(li));
198
+ assert.deepStrictEqual(headers, ['database', 'build', 'site1', 'a note with no entries']);
199
+ const project = texts(out).slice(texts(out).indexOf('site1'));
200
+ assert.deepStrictEqual(project.slice(1, 3), ['Describe', 'Stop'], 'the order of the commands inside a group');
201
+ assert.ok(!/Restart/.test(out));
202
+ assert.match(out, /<ul class="uk-nav-sub"><li><a href>one<\/a><\/li><li><a href>two<\/a><\/li><\/ul>/);
203
+
204
+ const hide = rl.normalizeLayout({ menus: { ddev: { hidden_groups: ['build'], hidden_entries: ['export the database', 'checks'] } } });
205
+ const out2 = rl.arrangeNavInner('ddev', DDEV_INNER, { layout: hide });
206
+ assert.ok(!/Composer install|>build</.test(out2), 'a hidden group goes with its header');
207
+ assert.ok(!/>database</.test(out2), 'a group whose every entry is hidden loses its header too');
208
+ assert.match(out2, /a note with no entries/, 'a header that never had entries is a note, and stays');
209
+ });
210
+
211
+ test('a group and an entry are named by data-group / data-entry first, else by their text', () => {
212
+ assert.strictEqual(rl.groupKeyOf('<li class="uk-nav-header" data-group="project">my-site</li>'), 'project');
213
+ assert.strictEqual(rl.groupKeyOf('<li class="uk-nav-header">Git · main</li>'), 'git');
214
+ assert.strictEqual(rl.entryKey('<li data-entry="start-stop"><a>Start</a></li>'), 'start-stop');
215
+ assert.strictEqual(rl.entryKey('<li><a>Build the graph (running…)</a></li>'), 'build the graph');
216
+ });
217
+
218
+ test('actions go into their group only where they apply, once per button, and open the way they say', () => {
219
+ const layout = rl.normalizeLayout({ custom: [
220
+ action({ when: ['ddev', '!workspace:products'], confirm: true }),
221
+ action({ id: 'mine', label: 'Say hello', group: 'Mine', when: 'any', command: 'echo hello' }),
222
+ action({ id: 'switch', label: 'Switch branch', group: 'Mine', when: 'any', command: 'git switch {{opt.b}}', options: [{ name: 'b', value: 'main' }] }),
223
+ ] });
224
+ const onDdev = rl.arrangeNavInner('ddev', DDEV_INNER, { layout, key: 'dev', project: 'site1', isDdev: true });
225
+ assert.match(onDdev, /hx-post="\/actions\/row-custom-run" hx-vals='\{"workspace":"dev","projectName":"site1","id":"cr"\}'[^>]*class="arm-step" hx-trigger="confirmed-remove"/, 'confirm asks in a dialog first');
226
+ assert.match(onDdev, /<li class="uk-nav-header" data-group="mine">Mine<\/li><li><a [^>]*"id":"mine"/, 'a group that does not exist is made');
227
+ assert.match(onDdev, /hx-post="\/actions\/row-custom-options"[^>]*"id":"switch"[^>]*hx-target="#editor-modal-body"/, 'an action with options opens its form');
228
+ assert.ok(!/"id":"cr"/.test(rl.arrangeNavInner('ddev', DDEV_INNER, { layout, key: 'products', project: 'x', isDdev: true })), '!workspace:products');
229
+ assert.ok(!/"id":"cr"/.test(rl.arrangeNavInner('ddev', DDEV_INNER, { layout, key: 'dev', project: 'x', isDdev: false })));
230
+
231
+ const twoLists = '<div><ul class="uk-nav uk-dropdown-nav"><li><span>info</span></li></ul><ul class="uk-nav uk-dropdown-nav"><li><a href>go</a></li></ul></div>';
232
+ const out = rl.arrangeButtonHtml('ddev', twoLists, { layout, isDdev: true, key: 'dev', project: 'site1' });
233
+ assert.strictEqual((out.match(/"id":"mine"/g) || []).length, 1, 'one copy, in the list that has links');
234
+ });
235
+
236
+ test('rows render in order; hidden and inapplicable buttons draw nothing; rows past the second come out as extra', () => {
237
+ const btn = (id) => `<span class="b-${id}"><ul class="uk-nav uk-dropdown-nav"><li><a href>${id}</a></li></ul></span>`;
238
+ const buttons = Object.fromEntries(rl.BUILTIN_BUTTONS.map((b) => [b.id, btn(b.id)]));
239
+ buttons.clone = '';
240
+ const layout = rl.normalizeLayout({
241
+ rows: [['git', 'ddev'], ['more'], ['composer']],
242
+ hidden: ['tests'],
243
+ custom: [action({ id: 'deploy', label: 'Deploy', button: 'tools', group: 'Ship', when: 'git', command: 'git push' })],
244
+ });
245
+ const order = (html) => [...html.matchAll(/class="b-([a-z]+)"/g)].map((m) => m[1]);
246
+ let out = rl.rowLinesHtml(buttons, { layout, key: 'dev', project: 'p', hasGit: false });
247
+ assert.deepStrictEqual(order(out.first).slice(0, 2), ['git', 'ddev']);
248
+ assert.deepStrictEqual(out.extra.length, 1);
249
+ assert.deepStrictEqual(order(out.extra[0]), ['composer']);
250
+ assert.ok(!out.rows.join('').includes('b-tests'), 'hidden');
251
+ assert.ok(!/act-menu-tools/.test(out.second), 'no git here, so no TOOLS button');
252
+ out = rl.rowLinesHtml(buttons, { layout, key: 'dev', project: 'p', hasGit: true });
253
+ assert.match(out.second, /act-menu-tools[\s\S]*TOOLS[\s\S]*"id":"deploy"/);
254
+ });
255
+
256
+ test('a menu can be laid out in columns, each group in the column it was put in', () => {
257
+ const btn = `<div uk-dropdown class="ddev-dropdown-wide"><ul class="uk-nav uk-dropdown-nav ddev-nav-columns">${DDEV_INNER}</ul></div>`;
258
+ const layout = rl.normalizeLayout({ menus: { ddev: { columns: [['project', 'build'], ['database']] } } });
259
+ const out = rl.columnizeButtonHtml('ddev', rl.arrangeButtonHtml('ddev', btn, { layout }), layout);
260
+ const m = /<div class="menu-columns menu-columns-2">(<ul[^>]*>[\s\S]*<\/ul>)<\/div>/.exec(out);
261
+ assert.ok(m, out.slice(0, 300));
262
+ // Split at each column's own list: a nested sub-list inside an entry has its own </ul>.
263
+ const lists = m[1].split('<ul class="uk-nav uk-dropdown-nav">').slice(1);
264
+ assert.strictEqual(lists.length, 2);
265
+ assert.ok(!/ddev-nav-columns/.test(m[1]), 'the lists are not split again by DDEV\'s own CSS columns');
266
+ assert.match(lists[0], />site1<[\s\S]*>build</);
267
+ assert.match(lists[1], />database<[\s\S]*a note with no entries/, 'a group not placed yet goes to the last column');
268
+ // One column, a menu that lays out its own, or a list filled in by a request: left as it was.
269
+ assert.strictEqual(rl.columnizeButtonHtml('ddev', btn, rl.normalizeLayout({})), btn);
270
+ const composer = rl.normalizeLayout({ menus: { composer: { columns: [['daily'], ['inspect']] } } });
271
+ assert.strictEqual(rl.columnizeButtonHtml('composer', btn, composer), btn);
272
+ const loader = '<div><ul class="uk-nav uk-dropdown-nav" hx-get="/fragments/x"><li>loading…</li></ul></div>';
273
+ assert.strictEqual(rl.columnizeButtonHtml('ddev', loader, layout), loader);
274
+
275
+ rl.arrangeNavInner('ddev', DDEV_INNER, { layout: rl.normalizeLayout({}) });
276
+ const doc = rl.arrangementFromForm(rl.normalizeLayout({}), {
277
+ menu_present_ddev: '1', columns_ddev: '2', group_ddev: ['project', 'build', 'database'],
278
+ gcol_ddev: ['project\t0', 'build\t1', 'database\t7'],
279
+ });
280
+ assert.deepStrictEqual(rl.normalizeLayout(doc).menus.ddev.columns, [['project'], ['build', 'database']], 'a column that no longer exists hands its groups to the last');
281
+ assert.deepStrictEqual(rl.normalizeLayout(rl.arrangementFromForm(rl.normalizeLayout({}), { menu_present_ddev: '1', columns_ddev: '1' })).menus.ddev.columns, []);
282
+ rl.saveRowLayout(rl.normalizeLayout(doc));
283
+ const html = rl.rowActionsEditorHtml('', 0, { openButton: 'ddev' });
284
+ assert.match(html, /<details class="row-button-body" open>[\s\S]*name="columns_ddev"/, 'the card being worked in stays open');
285
+ assert.match(html, /data-col="1"[^>]*uk-sortable="group: cols-ddev/);
286
+ assert.match(html, /COMPOSER lays out its own columns/);
287
+ rl.resetRowLayout();
288
+ });
289
+
290
+ // ------------------------------------------------------------------ the file
291
+
292
+ test('saving refuses a file that changed since the form was drawn; reset goes back to the defaults', () => {
293
+ const mtime = rl.saveRowLayout(rl.normalizeLayout({ hidden: ['rag'] }));
294
+ assert.ok(fs.readFileSync(process.env.ROW_ACTIONS_FILE, 'utf8').startsWith('# Rows management'));
295
+ assert.deepStrictEqual(rl.loadRowLayoutFresh().hidden, ['rag']);
296
+ assert.throws(() => rl.saveRowLayout(rl.normalizeLayout({}), mtime - 1000), (e) => e.code === 'CHANGED');
297
+ rl.resetRowLayout(rl.layoutMtime());
298
+ assert.strictEqual(fs.existsSync(process.env.ROW_ACTIONS_FILE), false);
299
+ fs.writeFileSync(process.env.ROW_ACTIONS_FILE, 'rows: [ [unclosed');
300
+ const broken = rl.loadRowLayoutFresh();
301
+ assert.deepStrictEqual(broken.rows, rl.DEFAULT_ROWS, 'a file that does not parse falls back to the defaults');
302
+ assert.ok(broken.error);
303
+ fs.unlinkSync(process.env.ROW_ACTIONS_FILE);
304
+ rl.invalidateRowLayout();
305
+ });
306
+
307
+ test('Definition: a pasted file is checked before anything is written, and loads like rd jobs load', () => {
308
+ const current = rl.normalizeLayout({ custom: [action({ id: 'keep', label: 'Keep me', when: 'any', command: 'echo keep' })] });
309
+ assert.match(rl.checkDefinition('rows: [ [', 'replace', current).errors[0], /does not parse/);
310
+ assert.match(rl.checkDefinition('custom:\n - id: x\n label: X\n button: ddev\n command: echo $HOME\n', 'replace', current).errors[0], /Action 1 \(x\).*\$ or a backtick/);
311
+ const replace = rl.checkDefinition('rows:\n - [ddev, nope]\nhidden: [rag]\n', 'replace', current);
312
+ assert.deepStrictEqual(replace.errors, []);
313
+ assert.match(replace.notes.join(' '), /Not a button, so ignored: nope/);
314
+ assert.strictEqual(replace.next.custom.length, 0, 'replace takes the whole file');
315
+ const pasted = 'custom:\n - { id: keep, label: Changed, button: ddev, command: echo changed }\n - { id: new-one, label: New, button: tools, command: echo new }\n';
316
+ const skip = rl.checkDefinition(pasted, 'skip', current);
317
+ assert.match(skip.notes[0], /1 action\(s\) added, 0 updated, 1 skipped/);
318
+ assert.strictEqual(skip.next.custom.find((c) => c.id === 'keep').label, 'Keep me');
319
+ const update = rl.checkDefinition(pasted, 'update', current);
320
+ assert.strictEqual(update.next.custom.find((c) => c.id === 'keep').label, 'Changed');
321
+ assert.ok(rl.definitionText().includes('rows:'), 'with no file the definition shows the defaults');
322
+ });
323
+
324
+ test('a YAML alias bomb is refused at once, wherever it is, and an empty paste says it is empty', () => {
325
+ const levels = ['a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]'];
326
+ for (let i = 1; i < 9; i += 1) { const p = String.fromCharCode(96 + i); levels.push(`${String.fromCharCode(97 + i)}: &${String.fromCharCode(97 + i)} [${Array(9).fill(`*${p}`).join(',')}]`); }
327
+ const current = rl.normalizeLayout({});
328
+ for (const where of ['rows', 'hidden', 'menus', 'custom', 'lines']) {
329
+ const t0 = Date.now();
330
+ const r = rl.checkDefinition(`${levels.join('\n')}\n${where}: *i\n`, 'replace', current);
331
+ assert.ok(Date.now() - t0 < 1000, `${where}: took ${Date.now() - t0}ms`);
332
+ assert.match(r.errors[0] || '', /too large or too deeply nested/, where);
333
+ }
334
+ // Loading such a file is refused the same way: every page is rendered from it.
335
+ fs.writeFileSync(process.env.ROW_ACTIONS_FILE, `${levels.join('\n')}\nrows: *i\n`);
336
+ const t0 = Date.now();
337
+ const l = rl.loadRowLayoutFresh();
338
+ assert.ok(Date.now() - t0 < 1000, `loading took ${Date.now() - t0}ms`);
339
+ assert.deepStrictEqual(l.rows, rl.DEFAULT_ROWS);
340
+ assert.match(l.error, /too large or too deeply nested/);
341
+ fs.unlinkSync(process.env.ROW_ACTIONS_FILE);
342
+ rl.invalidateRowLayout();
343
+ // A list inside a list is skipped, not turned into text.
344
+ assert.deepStrictEqual(rl.normalizeLayout({ rows: [[['ddev', 'git'], 'more', 12, null]] }).rows[0][0], 'more');
345
+ for (const empty of ['', ' \n\n']) {
346
+ assert.match(rl.checkDefinition(empty, 'replace', current).errors[0], /The definition is empty/);
347
+ }
348
+ assert.strictEqual(rl.withinBudget({ rows: rl.DEFAULT_ROWS, custom: [] }), true);
349
+ });
350
+
351
+ test('what a menu showed is remembered, and the editor offers it: built-in commands hide, yours edit and delete', () => {
352
+ rl.arrangeNavInner('ddev', DDEV_INNER, { layout: rl.normalizeLayout({}) });
353
+ const groups = rl.seenMenu('ddev').groups;
354
+ assert.ok(groups.project && groups.build && groups.database, Object.keys(groups).join(','));
355
+ rl.saveRowLayout(rl.normalizeLayout({ custom: [action({ id: 'mine', label: 'Say hello', group: 'build', when: 'any', command: 'echo hi' })] }));
356
+ const html = rl.rowActionsEditorHtml();
357
+ assert.match(html, /name="hide_entry_ddev" value="start-stop"/);
358
+ assert.match(html, /name="entry_ddev" value="project&#9;start-stop"/, 'the order of each command is posted with its group');
359
+ assert.match(html, /row-entry-builtin[^>]*>built in</);
360
+ assert.match(html, /hx-post="\/actions\/row-custom-save" hx-vals='\{"id":"mine"/);
361
+ assert.match(html, /hx-post="\/actions\/row-custom-remove" hx-vals='\{"id":"mine"[^>]*hx-trigger="confirmed-remove">Delete/);
362
+ assert.ok(!/row-custom-remove[^>]*"id":"start-stop"/.test(html), 'a built-in command has no delete');
363
+ // Add a row and add a button open in the modal now; their forms are checked in rows-add-modal.test.js.
364
+ assert.match(html, /hx-get="\/fragments\/row\/add-row"[\s\S]{0,140}Add a row/);
365
+ assert.match(html, /<li><a href="#">Rows<\/a><\/li><li><a href="#">Activity<\/a><\/li><li><a href="#">Definition<\/a><\/li>/);
366
+ rl.resetRowLayout();
367
+ });
368
+
369
+ test('the Rows tab keeps your place: a save bar in view, open cards and scroll carried over, a warning before losing changes', () => {
370
+ const html = rl.rowActionsEditorHtml();
371
+ assert.match(html, /<div class="row-save-bar">\s*<span class="row-unsaved-note" role="status">Unsaved changes<\/span>\s*<button type="submit"[^>]*>[^<]*<span[^>]*><\/span> Save the rows<\/button>\s*<\/div>\s*<\/form>/,
372
+ 'Save sits in the sticky bar at the end of the Rows form');
373
+ const css = fs.readFileSync(path.join(__dirname, '..', '..', 'public', 'css', 'components', 'settings-editor-core-config-yml.css'), 'utf8');
374
+ assert.match(css, /\.row-save-bar \{ position: sticky; bottom: 0;/);
375
+ assert.match(css, /#row-arrange-form\[data-dirty="1"\] \.row-unsaved-note \{ display: inline; \}/);
376
+ const ui = fs.readFileSync(path.join(__dirname, '..', '..', 'public', 'ui.js'), 'utf8');
377
+ // htmx 4 event names: the old camel-case ones never fire, which is how the first version of this did nothing.
378
+ assert.match(ui, /addEventListener\('htmx:before:swap', \(e\) => \{\s*const from = [^\n]*closest\('#row-actions-editor'\)[\s\S]*?details\.row-button-body\[open\][\s\S]*?scrollY/, 'the open cards and the scroll position are remembered');
379
+ assert.match(ui, /addEventListener\('htmx:after:swap', \(e\) => \{\s*if \(!rowEditorPlace\) return;\s*if \(swappedEl\(e\)\.id !== 'row-actions-editor'\) \{ rowEditorPlace = null; return; \}[\s\S]*?d\.open = true;[\s\S]*?window\.scrollTo\(0, place\.y\)/, 'and restored only after the editor itself is swapped');
380
+ assert.ok(!/'htmx:(beforeSwap|afterSwap)'/.test(ui), 'no listener uses the htmx 1/2 event names, which htmx 4 never fires');
381
+ assert.match(ui, /beforeunload[\s\S]*#row-arrange-form\[data-dirty="1"\][\s\S]*e\.preventDefault\(\)/, 'leaving with unsaved changes asks first');
382
+ assert.match(ui, /\/\^\(add\|edit\)_\/\.test\(el\.name\)/, 'the action fields, which save on their own, do not mark the rows as changed');
383
+ });
384
+
385
+ test('a save writes only what differs from what a menu shows by default', () => {
386
+ rl.arrangeNavInner('ddev', DDEV_INNER, { layout: rl.normalizeLayout({}) });
387
+ rl.arrangeNavInner('ide', '<li class="uk-nav-header">link</li><li><a>Status</a></li><li><a>Logs</a></li>', { layout: rl.normalizeLayout({}) });
388
+ const untouched = (id) => {
389
+ const cat = rl.seenMenu(id).groups;
390
+ return {
391
+ [`menu_present_${id}`]: '1',
392
+ [`group_${id}`]: Object.keys(cat),
393
+ [`entry_${id}`]: Object.entries(cat).flatMap(([g, v]) => Object.keys(v.entries).map((e) => `${g}\t${e}`)),
394
+ };
395
+ };
396
+ // Every card posts its whole, unchanged order; the one change is a hidden command in DDEV.
397
+ const form = { ...untouched('ddev'), ...untouched('ide'), hide_entry_ddev: 'restart' };
398
+ const doc = rl.toDoc(rl.normalizeLayout(rl.arrangementFromForm(rl.normalizeLayout({}), form)));
399
+ assert.deepStrictEqual(Object.keys(doc.menus), ['ddev'], 'a menu nobody changed is not written');
400
+ assert.deepStrictEqual(doc.menus.ddev, { groups: [], hidden_groups: [], hidden_entries: ['restart'] }, 'nor the order of a menu that was only hidden from');
401
+
402
+ // A real change of order is kept — of the groups, and of the commands inside one.
403
+ const cat = rl.seenMenu('ddev').groups;
404
+ const groups = Object.keys(cat).reverse();
405
+ const project = Object.keys(cat.project.entries).reverse();
406
+ const moved = rl.toDoc(rl.normalizeLayout(rl.arrangementFromForm(rl.normalizeLayout({}), {
407
+ menu_present_ddev: '1', group_ddev: groups,
408
+ entry_ddev: [...project.map((e) => `project\t${e}`), ...Object.keys(cat.build.entries).map((e) => `build\t${e}`)],
409
+ })));
410
+ assert.deepStrictEqual(moved.menus.ddev.groups, groups);
411
+ assert.deepStrictEqual(moved.menus.ddev.order, { project }, 'only the group whose commands moved');
412
+ });
413
+
414
+ test('every command can be reordered without dragging: move arrows beside its handle', () => {
415
+ rl.arrangeNavInner('ddev', DDEV_INNER, { layout: rl.normalizeLayout({}) });
416
+ const html = rl.rowActionsEditorHtml();
417
+ const items = html.split('<li class="row-entry-item">').slice(1).map((x) => x.split('</li>')[0]);
418
+ assert.ok(items.length > 0);
419
+ for (const item of items) {
420
+ assert.match(item, /row-handle-entry/);
421
+ assert.match(item, /<span class="ws-move">[\s\S]*title="Move [^"]+ up"[\s\S]*title="Move [^"]+ down"/, 'each command has its own arrows');
422
+ }
423
+ });
424
+
425
+ test('the Rows form describes rows, the order inside groups, and hidden commands', () => {
426
+ rl.arrangeNavInner('ddev', DDEV_INNER, { layout: rl.normalizeLayout({}) });
427
+ const layout = rl.normalizeLayout({});
428
+ const doc = rl.arrangementFromForm(layout, {
429
+ button: ['more', 'ddev', 'git'], row_more: '0', row_ddev: '1', row_git: 'hidden',
430
+ menu_present_ddev: '1', group_ddev: ['build', 'project'], entry_ddev: ['project\tdescribe', 'project\tstart-stop', 'project\tnope'],
431
+ hide_entry_ddev: 'restart',
432
+ });
433
+ const next = rl.normalizeLayout(doc);
434
+ assert.strictEqual(next.rows[0][0], 'more');
435
+ assert.ok(next.rows[1].includes('ddev'));
436
+ assert.deepStrictEqual(next.hidden, ['git']);
437
+ assert.ok(next.rows[0].includes('ide'), 'a button the form did not carry keeps the place it had');
438
+ assert.deepStrictEqual(next.menus.ddev.groups.slice(0, 2), ['build', 'project']);
439
+ assert.deepStrictEqual(next.menus.ddev.order.project, ['describe', 'start-stop'], 'an entry the menu never had is dropped');
440
+ assert.deepStrictEqual(next.menus.ddev.hiddenEntries, ['restart']);
441
+ });
442
+
443
+ test('a run is recorded, and settled only once its log says how it ended', () => {
444
+ rl.recordRun({ at: new Date().toISOString(), id: 'mine', label: 'Say hello', workspace: 'dev', project: 'p', session: 'dev--p--custom-mine-test' });
445
+ assert.strictEqual(rl.readRuns().length, 1);
446
+ // No output yet — the run may still be going (a request refused as "still running" calls this too):
447
+ // it must stay open, not be written down as unknown mid-run.
448
+ rl.settleRun('dev--p--custom-mine-test');
449
+ assert.strictEqual(rl.readRuns()[0].status, undefined);
450
+ const src = fs.readFileSync(path.join(__dirname, '..', '..', 'row-layout.js'), 'utf8');
451
+ const fn = src.slice(src.indexOf('function settleRun('), src.indexOf('// ---', src.indexOf('function settleRun(')));
452
+ assert.match(fn, /if \(!\/ROW-ACTION \(succeeded\|failed\)\/\.test\(text\)\) return;/, 'settled only from a finished log');
453
+ });
454
+
455
+ test('the handlers: runs by id only, refuses a $ anywhere, keeps unknown buttons out, manages rows and actions', async () => {
456
+ const actions = require('../../actions/row-actions');
457
+ const call = async (route, form) => {
458
+ const out = {};
459
+ await actions[route]({ form, workspace: 'test', pathname: route, send: (html, code = 200) => { out.html = html; out.code = code; } });
460
+ return out;
461
+ };
462
+ let r = await call('/actions/row-custom-run', { projectName: 'anything', id: 'no-such-action' });
463
+ assert.strictEqual(r.code, 404);
464
+ r = await call('/actions/row-custom-run', { projectName: '../etc', id: 'x' });
465
+ assert.strictEqual(r.code, 400);
466
+ r = await call('/actions/row-custom-add', { for_button: 'ddev', add_label_ddev: 'Bad', add_steps_ddev: 'echo $HOME' });
467
+ assert.strictEqual(r.code, 400);
468
+ assert.match(r.html, /\$ or a backtick/);
469
+
470
+ r = await call('/actions/row-arrange-save', { button: ['git', 'evil', 'ddev'], row_git: '0', row_ddev: 'hidden', row_evil: '0' });
471
+ assert.strictEqual(r.code, 200);
472
+ let saved = rl.loadRowLayoutFresh();
473
+ assert.strictEqual(saved.rows[0][0], 'git');
474
+ assert.deepStrictEqual(saved.hidden, ['ddev']);
475
+ assert.ok(!saved.rows.flat().includes('evil'));
476
+
477
+ r = await call('/actions/row-rows', { op: 'add' });
478
+ assert.strictEqual(rl.loadRowLayoutFresh().rows.length, 3);
479
+ r = await call('/actions/row-rows', { op: 'remove', index: '1' });
480
+ saved = rl.loadRowLayoutFresh();
481
+ assert.strictEqual(saved.rows.length, 2);
482
+ assert.ok(saved.rows[0].includes('screens'), 'a removed row\'s buttons move to the row above');
483
+
484
+ r = await call('/actions/row-custom-add', { for_button: '__new__', add_button_new: 'tools', add_label_new: 'Say hello', add_steps_new: 'echo one\necho two', add_open_new: 'screen', add_confirm_new: '1' });
485
+ assert.strictEqual(r.code, 200, r.html);
486
+ let e = rl.loadRowLayoutFresh().custom.find((c) => c.id === 'say-hello');
487
+ assert.deepStrictEqual([e.button, e.steps.length, e.open, e.confirm], ['tools', 2, 'screen', true]);
488
+ r = await call('/actions/row-custom-save', { id: 'say-hello', edit_label_say_hello: 'x', 'edit_label_say-hello': 'Say hi', 'edit_group_say-hello': 'Ship', 'edit_when_say-hello': 'git !workspace:products', 'edit_steps_say-hello': 'echo hi', 'edit_open_say-hello': 'toast' });
489
+ assert.strictEqual(r.code, 200, r.html);
490
+ e = rl.loadRowLayoutFresh().custom.find((c) => c.id === 'say-hello');
491
+ assert.deepStrictEqual([e.label, e.group, e.when, e.steps.length, e.open, e.confirm], ['Say hi', 'Ship', ['git', '!workspace:products'], 1, 'toast', false]);
492
+ r = await call('/actions/row-custom-remove', { id: 'start-stop' });
493
+ assert.strictEqual(r.code, 404, 'a built-in command cannot be deleted');
494
+ r = await call('/actions/row-custom-remove', { id: 'say-hello' });
495
+ assert.strictEqual(rl.loadRowLayoutFresh().custom.length, 0);
496
+
497
+ r = await call('/actions/row-definition-check', { definition: 'custom:\n - { id: a, label: A, button: ddev, command: echo `id` }\n', mode: 'replace' });
498
+ assert.strictEqual(r.code, 400);
499
+ r = await call('/actions/row-definition-save', { definition: 'hidden: [rag]\n', mode: 'replace' });
500
+ assert.strictEqual(r.code, 200, r.html);
501
+ assert.deepStrictEqual(rl.loadRowLayoutFresh().hidden, ['rag']);
502
+ await call('/actions/row-layout-reset', {});
503
+ assert.deepStrictEqual(rl.loadRowLayoutFresh().rows, rl.DEFAULT_ROWS);
504
+ });
505
+
506
+ test('the row, the SCREENS fragment and the Commands page are wired to rows management', () => {
507
+ const src = fs.readFileSync(path.join(__dirname, '..', '..', 'server.js'), 'utf8');
508
+ const map = /rowLayout\.rowLinesHtml\(\{([\s\S]*?)\}, rowCtx, \{ preArranged: new Set\(\['ddev', 'more'\]\) \}\)/.exec(src);
509
+ assert.ok(map, 'the row draws its rows from rowLinesHtml');
510
+ // The GIT slot is registered by this test (a plugin's), so it reaches the layout through pluginRowMenus, not the map.
511
+ for (const b of rl.BUILTIN_BUTTONS) if (!['git', 'ide', 'graph'].includes(b.id)) assert.match(map[1], new RegExp(`\\b${b.id}: \\w+`), `${b.id} is not handed to the layout`);
512
+ assert.match(src, /<div class="pcard-tools"[^>]*>\s*\$\{rowLines\.first\}\$\{rowLines\.second\}/);
513
+ // The empty extra rows are filtered out, and what remains joins the same strip of tools on the card.
514
+ assert.match(src, /const extraRows = rowLines\.extra\.filter\(Boolean\);/);
515
+ assert.match(src, /\$\{extraRows\.join\(''\)\}/);
516
+ assert.match(src, /const ddevItems = rowLayout\.arrangeNavInner\('ddev', ddevMenuItems, rowCtx\);/);
517
+ assert.match(src, /return res\.end\(rowLayout\.arrangeNavInner\('screens', screensNav,/);
518
+ assert.match(src, /'\/settings\/buttons'[\s\S]{0,400}settingsPage\('buttons'/);
519
+ assert.match(src, /\.\.\.require\('\.\/actions\/row-actions'\)/);
520
+ const ui = fs.readFileSync(path.join(__dirname, '..', '..', 'public', 'ui.js'), 'utf8');
521
+ assert.match(ui, /function syncRowButtons\(\)/, 'a dragged button carries its new row into the form');
522
+ });
523
+
524
+ /* ------------------------------------------------------------------ *
525
+ * How much of a button is drawn
526
+ * ------------------------------------------------------------------ */
527
+
528
+ test('a button shows its icon, its text, or both — the same three the navigation gives', () => {
529
+ const server = fs.readFileSync(path.join(__dirname, '..', '..', 'server.js'), 'utf8');
530
+ const src = fs.readFileSync(path.join(__dirname, '..', '..', 'row-layout.js'), 'utf8');
531
+ // Deliberately the same word: a row of buttons and a rail of entries are the same question about room.
532
+ assert.match(src, /show:\s+\{ <id>: icon \| text \}/, 'and the file says so');
533
+ assert.match(src, /const how = \(layout\.show \|\| \{\}\)\[id\];/);
534
+ assert.match(src, /`<span class="row-show-\$\{how\}">\$\{placed\}<\/span>`/,
535
+ 'the button is WRAPPED, never rewritten — its markup is built by whoever added it');
536
+ assert.ok(server.length > 0);
537
+ });
538
+
539
+ test('only icon and text are values; anything else is no value at all', () => {
540
+ fs.writeFileSync(process.env.ROW_ACTIONS_FILE, 'rows:\n - [ddev]\nshow:\n ddev: icon\n screens: sideways\n nosuchbutton: text\n');
541
+ const l = rl.loadRowLayoutFresh();
542
+ assert.deepStrictEqual(l.show, { ddev: 'icon' }, 'a value it does not know, and a button it does not know, are dropped');
543
+ });
544
+
545
+ test('both is not written down: the file stays a record of what somebody changed', () => {
546
+ fs.writeFileSync(process.env.ROW_ACTIONS_FILE, 'rows:\n - [ddev]\n');
547
+ const layout = rl.loadRowLayoutFresh();
548
+ const doc = rl.arrangementFromForm(layout, { button: ['ddev'], row_ddev: '0', show: ['ddev:both'] });
549
+ assert.ok(!doc.show || !doc.show.ddev, 'both leaves no trace');
550
+
551
+ const on = rl.arrangementFromForm(layout, { button: ['ddev'], row_ddev: '0', show: ['ddev:icon'] });
552
+ assert.deepStrictEqual(on.show, { ddev: 'icon' });
553
+ });
554
+
555
+ test('posting both is what turns it back off', () => {
556
+ fs.writeFileSync(process.env.ROW_ACTIONS_FILE, 'rows:\n - [ddev]\nshow:\n ddev: icon\n');
557
+ const layout = rl.loadRowLayoutFresh();
558
+ assert.strictEqual(layout.show.ddev, 'icon');
559
+ const off = rl.arrangementFromForm(layout, { button: ['ddev'], row_ddev: '0', show: ['ddev:both'] });
560
+ assert.ok(!off.show || !off.show.ddev);
561
+ });
562
+
563
+ test('a button the form did not carry keeps the answer it had', () => {
564
+ fs.writeFileSync(process.env.ROW_ACTIONS_FILE, 'rows:\n - [ddev, screens]\nshow:\n screens: icon\n');
565
+ const layout = rl.loadRowLayoutFresh();
566
+ const doc = rl.arrangementFromForm(layout, { button: ['ddev'], row_ddev: '0', show: ['ddev:text'] });
567
+ assert.strictEqual(doc.show.screens, 'icon', 'the one that was not on the form is not forgotten');
568
+ assert.strictEqual(doc.show.ddev, 'text');
569
+ });
570
+
571
+ test('the wrapper is what the stylesheet hides through, and the label stays in the markup', () => {
572
+ fs.writeFileSync(process.env.ROW_ACTIONS_FILE, 'rows:\n - [ddev]\nshow:\n ddev: icon\n');
573
+ rl.loadRowLayoutFresh();
574
+ const out = rl.rowLinesHtml({ ddev: '<button class="uk-button"><span uk-icon="icon: x"></span> DDEV</button>' }, {});
575
+ assert.match(out.first, /class="row-show-icon"/);
576
+ assert.match(out.first, /DDEV/, 'a screen reader still reads the name: this is presentation only');
577
+ });
578
+
579
+ test('a plugin button registers like a built-in one, in its default row, once', () => {
580
+ rl.registerButton({ id: 'x-cloud', label: 'CLOUD', row: 0 });
581
+ rl.registerButton({ id: 'x-cloud', label: 'CLOUD', row: 0 });
582
+ assert.strictEqual(rl.DEFAULT_ROWS[0].filter((b) => b === 'x-cloud').length, 1);
583
+ const l = rl.normalizeLayout({ rows: [['ddev'], []] });
584
+ assert.ok(l.rows[0].includes('x-cloud'), 'a layout written before the plugin existed still places its button');
585
+ });
@@ -0,0 +1,29 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const root = path.join(__dirname, '..', '..');
7
+ const ui = fs.readFileSync(path.join(root, 'public', 'ui.js'), 'utf8');
8
+ const css = fs.readFileSync(path.join(root, 'public', 'css', 'components', 'ddev-menu.css'), 'utf8');
9
+
10
+ test('every long row menu folds, not only DDEV', () => {
11
+ assert.match(ui, /querySelectorAll\('\.act-menu \.uk-dropdown:not\(\[data-folded\]\)'\)/);
12
+ assert.match(ui, /if \(links < 8 && rows < 12\) return;/);
13
+ });
14
+
15
+ test('a group ends at a divider, a short group does not fold, and only a short first group opens', () => {
16
+ assert.match(ui, /!n\.classList\.contains\('uk-nav-divider'\)/);
17
+ assert.match(ui, /if \(rows\.length < 3\) return;/);
18
+ assert.match(ui, /const open = \(folds === 0 && rows\.length <= 6\) \|\| \/this project\/i\.test/);
19
+ });
20
+
21
+ test('a menu that opens near the top moves the page so the top bar does not cover it', () => {
22
+ assert.match(ui, /document\.addEventListener\('shown', \(e\) => \{/);
23
+ assert.match(ui, /window\.scrollBy\(\{ top: top - limit/);
24
+ });
25
+
26
+ test('the menus are one 18rem column, including COMPOSER and NODE', () => {
27
+ assert.match(css, /\.project-row \.act-menu \.uk-dropdown \{ min-width: 0; width: 18rem;/);
28
+ assert.match(css, /:is\(\.composer-columns, \.node-columns\) \{ display: block; \}/);
29
+ });