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

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 (632) 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/{PLUGINS.md → core/workspace-app/PLUGINS.md} +9 -1
  160. package/core/workspace-app/README.md +380 -0
  161. package/{actions → core/workspace-app/actions}/ddev.js +0 -1
  162. package/{actions → core/workspace-app/actions}/rag.js +0 -1
  163. package/{actions → core/workspace-app/actions}/testing.js +0 -1
  164. package/{assistant.js → core/workspace-app/assistant.js} +1 -10
  165. package/{git-info.js → core/workspace-app/git-info.js} +17 -1
  166. package/core/workspace-app/package.json +29 -0
  167. package/core/workspace-app/plugin-catalog.js +50 -0
  168. package/{plugins-page.js → core/workspace-app/plugins-page.js} +28 -2
  169. package/{server.js → core/workspace-app/server.js} +18 -0
  170. package/core/workspace-app/tests/features/actions-rail.feature +44 -0
  171. package/core/workspace-app/tests/features/ai-items.feature +23 -0
  172. package/core/workspace-app/tests/features/assistant-speech.feature +63 -0
  173. package/core/workspace-app/tests/features/checks.feature +40 -0
  174. package/core/workspace-app/tests/features/clone-project.feature +30 -0
  175. package/core/workspace-app/tests/features/command-authoring.feature +58 -0
  176. package/core/workspace-app/tests/features/dashboard.feature +57 -0
  177. package/core/workspace-app/tests/features/menus.feature +29 -0
  178. package/core/workspace-app/tests/features/plugins/jira/jira.feature +47 -0
  179. package/core/workspace-app/tests/features/plugins.feature +20 -0
  180. package/core/workspace-app/tests/features/project-actions.feature +41 -0
  181. package/core/workspace-app/tests/features/screens.feature +29 -0
  182. package/core/workspace-app/tests/features/settings.feature +46 -0
  183. package/core/workspace-app/tests/features/test-reports.feature +32 -0
  184. package/core/workspace-app/tests/features/theme.feature +40 -0
  185. package/core/workspace-app/tests/step-definitions/actions-post.steps.js +25 -0
  186. package/core/workspace-app/tests/step-definitions/assistant-speech.steps.js +157 -0
  187. package/core/workspace-app/tests/step-definitions/clone-project.steps.js +47 -0
  188. package/core/workspace-app/tests/step-definitions/preconditions.steps.js +168 -0
  189. package/core/workspace-app/tests/unit/_isolated-config.js +31 -0
  190. package/core/workspace-app/tests/unit/accessibility.test.js +124 -0
  191. package/core/workspace-app/tests/unit/actions-rail-ux.test.js +56 -0
  192. package/core/workspace-app/tests/unit/actions-rail.test.js +51 -0
  193. package/core/workspace-app/tests/unit/ai-inventory.test.js +64 -0
  194. package/core/workspace-app/tests/unit/app-docs.test.js +64 -0
  195. package/core/workspace-app/tests/unit/assistant-avatar.test.js +110 -0
  196. package/core/workspace-app/tests/unit/assistant-cast.test.js +105 -0
  197. package/core/workspace-app/tests/unit/assistant-send-animation.test.js +15 -0
  198. package/core/workspace-app/tests/unit/assistant-suggestions.test.js +112 -0
  199. package/core/workspace-app/tests/unit/assistant-switches.test.js +53 -0
  200. package/core/workspace-app/tests/unit/assistant-views.test.js +146 -0
  201. package/core/workspace-app/tests/unit/assistant-working-indicator.test.js +49 -0
  202. package/core/workspace-app/tests/unit/assistant.test.js +61 -0
  203. package/core/workspace-app/tests/unit/audio-player.test.js +147 -0
  204. package/core/workspace-app/tests/unit/audio-voice.test.js +634 -0
  205. package/core/workspace-app/tests/unit/auth-google.test.js +180 -0
  206. package/core/workspace-app/tests/unit/autostart-watch.test.js +76 -0
  207. package/core/workspace-app/tests/unit/browsers.test.js +76 -0
  208. package/core/workspace-app/tests/unit/build-distribution.test.js +125 -0
  209. package/core/workspace-app/tests/unit/build-order.test.js +153 -0
  210. package/core/workspace-app/tests/unit/builder-args.test.js +113 -0
  211. package/core/workspace-app/tests/unit/builders.test.js +158 -0
  212. package/core/workspace-app/tests/unit/card-state-toggle.test.js +29 -0
  213. package/core/workspace-app/tests/unit/changelog.test.js +223 -0
  214. package/core/workspace-app/tests/unit/chat-thread.test.js +92 -0
  215. package/core/workspace-app/tests/unit/checks-plan.test.js +205 -0
  216. package/core/workspace-app/tests/unit/claude-args.test.js +182 -0
  217. package/core/workspace-app/tests/unit/claude-code-here.test.js +15 -0
  218. package/core/workspace-app/tests/unit/claude-resume.test.js +147 -0
  219. package/core/workspace-app/tests/unit/claude-screen-options.test.js +84 -0
  220. package/core/workspace-app/tests/unit/claude-session.test.js +41 -0
  221. package/core/workspace-app/tests/unit/command-flags.test.js +45 -0
  222. package/core/workspace-app/tests/unit/command-palette.test.js +105 -0
  223. package/core/workspace-app/tests/unit/commands-center.test.js +338 -0
  224. package/core/workspace-app/tests/unit/composer-menu.test.js +187 -0
  225. package/core/workspace-app/tests/unit/config-yaml.test.js +116 -0
  226. package/core/workspace-app/tests/unit/confirm-dialog.test.js +69 -0
  227. package/core/workspace-app/tests/unit/create-card.test.js +85 -0
  228. package/core/workspace-app/tests/unit/ddev-addons.test.js +144 -0
  229. package/core/workspace-app/tests/unit/ddev-auth-ssh.test.js +40 -0
  230. package/core/workspace-app/tests/unit/ddev-custom-commands.test.js +56 -0
  231. package/core/workspace-app/tests/unit/ddev-diagnose.test.js +57 -0
  232. package/core/workspace-app/tests/unit/ddev-domains.test.js +179 -0
  233. package/core/workspace-app/tests/unit/ddev-hostnames.test.js +143 -0
  234. package/core/workspace-app/tests/unit/ddev-import.test.js +82 -0
  235. package/core/workspace-app/tests/unit/ddev-mail.test.js +135 -0
  236. package/core/workspace-app/tests/unit/ddev-menu-and-create.test.js +28 -0
  237. package/core/workspace-app/tests/unit/ddev-mutagen.test.js +37 -0
  238. package/core/workspace-app/tests/unit/ddev-options.test.js +51 -0
  239. package/core/workspace-app/tests/unit/ddev-share.test.js +40 -0
  240. package/core/workspace-app/tests/unit/ddev-shim.test.js +57 -0
  241. package/core/workspace-app/tests/unit/ddev-snapshots.test.js +142 -0
  242. package/core/workspace-app/tests/unit/ddev-start-daemons.test.js +62 -0
  243. package/core/workspace-app/tests/unit/ddev-xdebug.test.js +41 -0
  244. package/core/workspace-app/tests/unit/deep-chat-origin.test.js +38 -0
  245. package/core/workspace-app/tests/unit/density.test.js +52 -0
  246. package/core/workspace-app/tests/unit/elevenlabs.test.js +453 -0
  247. package/core/workspace-app/tests/unit/event-log.test.js +102 -0
  248. package/core/workspace-app/tests/unit/favicon.test.js +68 -0
  249. package/core/workspace-app/tests/unit/files-browser.test.js +224 -0
  250. package/core/workspace-app/tests/unit/git-info.test.js +26 -0
  251. package/core/workspace-app/tests/unit/grid-and-list.test.js +103 -0
  252. package/core/workspace-app/tests/unit/host-tool-wrapper.test.js +18 -0
  253. package/core/workspace-app/tests/unit/html-patterns.test.js +51 -0
  254. package/core/workspace-app/tests/unit/ide-infra.test.js +28 -0
  255. package/core/workspace-app/tests/unit/images-ai.test.js +201 -0
  256. package/core/workspace-app/tests/unit/images-create.test.js +59 -0
  257. package/core/workspace-app/tests/unit/images-providers.test.js +248 -0
  258. package/core/workspace-app/tests/unit/images-svg.test.js +213 -0
  259. package/core/workspace-app/tests/unit/images-workspace.test.js +144 -0
  260. package/core/workspace-app/tests/unit/jobs-fragment.test.js +66 -0
  261. package/core/workspace-app/tests/unit/media-ux.test.js +164 -0
  262. package/core/workspace-app/tests/unit/modal-fit.test.js +87 -0
  263. package/core/workspace-app/tests/unit/modules-load.test.js +23 -0
  264. package/core/workspace-app/tests/unit/narration.test.js +186 -0
  265. package/core/workspace-app/tests/unit/navigation-editor.test.js +260 -0
  266. package/core/workspace-app/tests/unit/navigation.test.js +254 -0
  267. package/core/workspace-app/tests/unit/new-folder.test.js +121 -0
  268. package/core/workspace-app/tests/unit/new-screen-form.test.js +149 -0
  269. package/core/workspace-app/tests/unit/node-menu.test.js +161 -0
  270. package/core/workspace-app/tests/unit/notes.test.js +128 -0
  271. package/core/workspace-app/tests/unit/page-structure.test.js +40 -0
  272. package/core/workspace-app/tests/unit/page-width.test.js +59 -0
  273. package/core/workspace-app/tests/unit/php-version.test.js +90 -0
  274. package/core/workspace-app/tests/unit/plan-card.test.js +44 -0
  275. package/core/workspace-app/tests/unit/plugin-catalog.test.js +31 -0
  276. package/core/workspace-app/tests/unit/plugin-system.test.js +251 -0
  277. package/core/workspace-app/tests/unit/plugins-page.test.js +28 -0
  278. package/core/workspace-app/tests/unit/plugins.test.js +58 -0
  279. package/core/workspace-app/tests/unit/preconditions.test.js +21 -0
  280. package/core/workspace-app/tests/unit/project-activity-filter.test.js +180 -0
  281. package/core/workspace-app/tests/unit/project-make.test.js +143 -0
  282. package/core/workspace-app/tests/unit/project-name.test.js +202 -0
  283. package/core/workspace-app/tests/unit/reader.test.js +62 -0
  284. package/core/workspace-app/tests/unit/release.test.js +92 -0
  285. package/core/workspace-app/tests/unit/remove-confirm.test.js +37 -0
  286. package/core/workspace-app/tests/unit/report-pdf.test.js +171 -0
  287. package/core/workspace-app/tests/unit/round-11.test.js +31 -0
  288. package/core/workspace-app/tests/unit/round-12.test.js +37 -0
  289. package/core/workspace-app/tests/unit/row-layout.test.js +585 -0
  290. package/core/workspace-app/tests/unit/row-menus-fold.test.js +29 -0
  291. package/core/workspace-app/tests/unit/rows-add-modal.test.js +68 -0
  292. package/core/workspace-app/tests/unit/screen-runner-queue.test.js +41 -0
  293. package/core/workspace-app/tests/unit/screen-window.test.js +116 -0
  294. package/core/workspace-app/tests/unit/screens-beside-running.test.js +19 -0
  295. package/core/workspace-app/tests/unit/screens-columns.test.js +79 -0
  296. package/core/workspace-app/tests/unit/screens-menu.test.js +84 -0
  297. package/core/workspace-app/tests/unit/screens-modal-overlay.test.js +14 -0
  298. package/core/workspace-app/tests/unit/screens-waiting.test.js +129 -0
  299. package/core/workspace-app/tests/unit/screens.test.js +93 -0
  300. package/core/workspace-app/tests/unit/second-storage.test.js +218 -0
  301. package/core/workspace-app/tests/unit/session-name.test.js +64 -0
  302. package/core/workspace-app/tests/unit/settings-file.test.js +173 -0
  303. package/core/workspace-app/tests/unit/settings-nested-list.test.js +54 -0
  304. package/core/workspace-app/tests/unit/settings-raw.test.js +177 -0
  305. package/core/workspace-app/tests/unit/shared-prefs.test.js +49 -0
  306. package/core/workspace-app/tests/unit/speech.test.js +290 -0
  307. package/core/workspace-app/tests/unit/static-caching.test.js +39 -0
  308. package/core/workspace-app/tests/unit/status-language.test.js +47 -0
  309. package/core/workspace-app/tests/unit/tab-modal.test.js +69 -0
  310. package/core/workspace-app/tests/unit/tab-title.test.js +37 -0
  311. package/core/workspace-app/tests/unit/template-literals.test.js +52 -0
  312. package/core/workspace-app/tests/unit/terminal-scrollback.test.js +61 -0
  313. package/core/workspace-app/tests/unit/terminal-size.test.js +103 -0
  314. package/core/workspace-app/tests/unit/terminal-style.test.js +84 -0
  315. package/core/workspace-app/tests/unit/terminal-windows.test.js +154 -0
  316. package/core/workspace-app/tests/unit/themes.test.js +453 -0
  317. package/core/workspace-app/tests/unit/tool-labels.test.js +34 -0
  318. package/core/workspace-app/tests/unit/topbar-icons.test.js +47 -0
  319. package/core/workspace-app/tests/unit/update-docker-gid.test.js +51 -0
  320. package/core/workspace-app/tests/unit/user-guide.test.js +60 -0
  321. package/core/workspace-app/tests/unit/version-check.test.js +98 -0
  322. package/core/workspace-app/tests/unit/version-link.test.js +50 -0
  323. package/core/workspace-app/tests/unit/video-borrow.test.js +220 -0
  324. package/core/workspace-app/tests/unit/video-edit-ask.test.js +93 -0
  325. package/core/workspace-app/tests/unit/video-edit-short.test.js +42 -0
  326. package/core/workspace-app/tests/unit/video-edit.test.js +436 -0
  327. package/core/workspace-app/tests/unit/video-transcript.test.js +237 -0
  328. package/core/workspace-app/tests/unit/waiting.test.js +48 -0
  329. package/core/workspace-app/tests/unit/wordpress-odoo-builders.test.js +38 -0
  330. package/core/workspace-app/tests/unit/workspace-bin.test.js +48 -0
  331. package/core/workspace-app/tests/unit/workspace-card-tools.test.js +30 -0
  332. package/core/workspace-app/tests/unit/workspace-delete.test.js +77 -0
  333. package/core/workspace-app/tests/unit/workspace-types.test.js +68 -0
  334. package/core/workspace-app/tests/unit/worktree-clone.test.js +250 -0
  335. package/package.json +38 -36
  336. /package/{WORKSPACES.md → core/workspace-app/WORKSPACES.md} +0 -0
  337. /package/{actions → core/workspace-app/actions}/ai-items.js +0 -0
  338. /package/{actions → core/workspace-app/actions}/audio.js +0 -0
  339. /package/{actions → core/workspace-app/actions}/build-order.js +0 -0
  340. /package/{actions → core/workspace-app/actions}/checks.js +0 -0
  341. /package/{actions → core/workspace-app/actions}/claude-session.js +0 -0
  342. /package/{actions → core/workspace-app/actions}/commands-center.js +0 -0
  343. /package/{actions → core/workspace-app/actions}/composer.js +0 -0
  344. /package/{actions → core/workspace-app/actions}/ddev-addons.js +0 -0
  345. /package/{actions → core/workspace-app/actions}/ddev-mail.js +0 -0
  346. /package/{actions → core/workspace-app/actions}/folders.js +0 -0
  347. /package/{actions → core/workspace-app/actions}/images-svg.js +0 -0
  348. /package/{actions → core/workspace-app/actions}/images.js +0 -0
  349. /package/{actions → core/workspace-app/actions}/navigation.js +0 -0
  350. /package/{actions → core/workspace-app/actions}/node.js +0 -0
  351. /package/{actions → core/workspace-app/actions}/notes.js +0 -0
  352. /package/{actions → core/workspace-app/actions}/plugins.js +0 -0
  353. /package/{actions → core/workspace-app/actions}/project-make.js +0 -0
  354. /package/{actions → core/workspace-app/actions}/row-actions.js +0 -0
  355. /package/{actions → core/workspace-app/actions}/screens.js +0 -0
  356. /package/{actions → core/workspace-app/actions}/video-edit.js +0 -0
  357. /package/{ai-inventory.js → core/workspace-app/ai-inventory.js} +0 -0
  358. /package/{ai-items.js → core/workspace-app/ai-items.js} +0 -0
  359. /package/{assistant-cast.js → core/workspace-app/assistant-cast.js} +0 -0
  360. /package/{assistant-suggestions.js → core/workspace-app/assistant-suggestions.js} +0 -0
  361. /package/{audio-voice.js → core/workspace-app/audio-voice.js} +0 -0
  362. /package/{auth-google.js → core/workspace-app/auth-google.js} +0 -0
  363. /package/{build-ai.js → core/workspace-app/build-ai.js} +0 -0
  364. /package/{build-order-page.js → core/workspace-app/build-order-page.js} +0 -0
  365. /package/{build-order.js → core/workspace-app/build-order.js} +0 -0
  366. /package/{builder-args.js → core/workspace-app/builder-args.js} +0 -0
  367. /package/{changelog.js → core/workspace-app/changelog.js} +0 -0
  368. /package/{chat-threads.js → core/workspace-app/chat-threads.js} +0 -0
  369. /package/{checks.js → core/workspace-app/checks.js} +0 -0
  370. /package/{claude-args.js → core/workspace-app/claude-args.js} +0 -0
  371. /package/{claude-sessions.js → core/workspace-app/claude-sessions.js} +0 -0
  372. /package/{command-author.js → core/workspace-app/command-author.js} +0 -0
  373. /package/{command-block.js → core/workspace-app/command-block.js} +0 -0
  374. /package/{commands → core/workspace-app/commands}/defaults.yml +0 -0
  375. /package/{commands-page.js → core/workspace-app/commands-page.js} +0 -0
  376. /package/{commands.js → core/workspace-app/commands.js} +0 -0
  377. /package/{cucumber.js → core/workspace-app/cucumber.js} +0 -0
  378. /package/{ddev-addons-page.js → core/workspace-app/ddev-addons-page.js} +0 -0
  379. /package/{ddev-addons.js → core/workspace-app/ddev-addons.js} +0 -0
  380. /package/{ddev-custom-commands.js → core/workspace-app/ddev-custom-commands.js} +0 -0
  381. /package/{ddev-domains.js → core/workspace-app/ddev-domains.js} +0 -0
  382. /package/{ddev-import.js → core/workspace-app/ddev-import.js} +0 -0
  383. /package/{ddev-mail-page.js → core/workspace-app/ddev-mail-page.js} +0 -0
  384. /package/{ddev-mail.js → core/workspace-app/ddev-mail.js} +0 -0
  385. /package/{ddev-options.js → core/workspace-app/ddev-options.js} +0 -0
  386. /package/{ddev-snapshots.js → core/workspace-app/ddev-snapshots.js} +0 -0
  387. /package/{ddev.js → core/workspace-app/ddev.js} +0 -0
  388. /package/{docs → core/workspace-app/docs}/DEVELOPER-GUIDE.md +0 -0
  389. /package/{docs → core/workspace-app/docs}/README.md +0 -0
  390. /package/{docs → core/workspace-app/docs}/REFERENCE.md +0 -0
  391. /package/{docs → core/workspace-app/docs}/USER-GUIDE.md +0 -0
  392. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-board.png +0 -0
  393. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-coffee.png +0 -0
  394. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-colours.png +0 -0
  395. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-icons.png +0 -0
  396. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-moves.png +0 -0
  397. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-pick-cookieman.png +0 -0
  398. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-settings-voices.png +0 -0
  399. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-status.png +0 -0
  400. /package/{docs → core/workspace-app/docs}/screenshots/create-card/create-card.png +0 -0
  401. /package/{docs → core/workspace-app/docs}/screenshots/create-card/start-ai.png +0 -0
  402. /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-address.png +0 -0
  403. /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-worktree-off.png +0 -0
  404. /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-worktree-on.png +0 -0
  405. /package/{docs → core/workspace-app/docs}/screenshots/guide-ddev-menu.png +0 -0
  406. /package/{docs → core/workspace-app/docs}/screenshots/guide-domains.png +0 -0
  407. /package/{docs → core/workspace-app/docs}/screenshots/guide-home.png +0 -0
  408. /package/{docs → core/workspace-app/docs}/screenshots/guide-ide-menu.png +0 -0
  409. /package/{docs → core/workspace-app/docs}/screenshots/guide-remove-refusal.png +0 -0
  410. /package/{docs → core/workspace-app/docs}/screenshots/guide-rows-worktree.png +0 -0
  411. /package/{docs → core/workspace-app/docs}/screenshots/guide-screens.png +0 -0
  412. /package/{docs → core/workspace-app/docs}/screenshots/guide-settings.png +0 -0
  413. /package/{docs → core/workspace-app/docs}/screenshots/guide-workspace-page.png +0 -0
  414. /package/{docs → core/workspace-app/docs}/screenshots/home/card-settings.png +0 -0
  415. /package/{docs → core/workspace-app/docs}/screenshots/home/drag-reorder.png +0 -0
  416. /package/{docs → core/workspace-app/docs}/screenshots/home/edit-card.png +0 -0
  417. /package/{docs → core/workspace-app/docs}/screenshots/images/claude-code-image.png +0 -0
  418. /package/{docs → core/workspace-app/docs}/screenshots/images/edit-with-ai.png +0 -0
  419. /package/{docs → core/workspace-app/docs}/screenshots/images/gallery.png +0 -0
  420. /package/{docs → core/workspace-app/docs}/screenshots/images/generate-description.png +0 -0
  421. /package/{docs → core/workspace-app/docs}/screenshots/images/generate-plan.png +0 -0
  422. /package/{docs → core/workspace-app/docs}/screenshots/images/providers.png +0 -0
  423. /package/{docs → core/workspace-app/docs}/screenshots/images/svg-form.png +0 -0
  424. /package/{docs → core/workspace-app/docs}/screenshots/images/svg-result.png +0 -0
  425. /package/{docs → core/workspace-app/docs}/screenshots/images/upload.png +0 -0
  426. /package/{docs → core/workspace-app/docs}/screenshots/new-folder/2-form.png +0 -0
  427. /package/{docs → core/workspace-app/docs}/screenshots/new-folder/8-ddev-options.png +0 -0
  428. /package/{docs → core/workspace-app/docs}/screenshots/new-folder/files-folder.png +0 -0
  429. /package/{docs → core/workspace-app/docs}/screenshots/projects/ddev-custom-commands.png +0 -0
  430. /package/{docs → core/workspace-app/docs}/screenshots/projects/state-chip-stop.png +0 -0
  431. /package/{docs → core/workspace-app/docs}/screenshots/readme/dashboard-dark.png +0 -0
  432. /package/{docs → core/workspace-app/docs}/screenshots/readme/dashboard-light.png +0 -0
  433. /package/{docs → core/workspace-app/docs}/screenshots/readme/dev-dark.png +0 -0
  434. /package/{docs → core/workspace-app/docs}/screenshots/readme/dev-light.png +0 -0
  435. /package/{docs → core/workspace-app/docs}/screenshots/readme/docs-dark.png +0 -0
  436. /package/{docs → core/workspace-app/docs}/screenshots/readme/docs-light.png +0 -0
  437. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/ai.svg +0 -0
  438. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/code.svg +0 -0
  439. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/docs.svg +0 -0
  440. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/services.svg +0 -0
  441. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/sites.svg +0 -0
  442. /package/{docs → core/workspace-app/docs}/screenshots/readme/test-report-dark.png +0 -0
  443. /package/{docs → core/workspace-app/docs}/screenshots/readme/test-report-light.png +0 -0
  444. /package/{docs → core/workspace-app/docs}/screenshots/readme/test-scenarios.png +0 -0
  445. /package/{docs → core/workspace-app/docs}/screenshots/readme/test-steps.png +0 -0
  446. /package/{docs → core/workspace-app/docs}/screenshots/readme/videos-dark.png +0 -0
  447. /package/{docs → core/workspace-app/docs}/screenshots/readme/videos-light.png +0 -0
  448. /package/{docs → core/workspace-app/docs}/screenshots/screens-menu.png +0 -0
  449. /package/{docs → core/workspace-app/docs}/screenshots/screens-new.png +0 -0
  450. /package/{docs → core/workspace-app/docs}/screenshots/settings/auth-google-lists.png +0 -0
  451. /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-1-row.png +0 -0
  452. /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-2-confirm.png +0 -0
  453. /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-3-type-name.png +0 -0
  454. /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-4-type-yes.png +0 -0
  455. /package/{event-log.js → core/workspace-app/event-log.js} +0 -0
  456. /package/{files-browser.js → core/workspace-app/files-browser.js} +0 -0
  457. /package/{folders.js → core/workspace-app/folders.js} +0 -0
  458. /package/{format.js → core/workspace-app/format.js} +0 -0
  459. /package/{github-auth.js → core/workspace-app/github-auth.js} +0 -0
  460. /package/{html.js → core/workspace-app/html.js} +0 -0
  461. /package/{icons.js → core/workspace-app/icons.js} +0 -0
  462. /package/{images-ai.js → core/workspace-app/images-ai.js} +0 -0
  463. /package/{images-create.js → core/workspace-app/images-create.js} +0 -0
  464. /package/{images-page.js → core/workspace-app/images-page.js} +0 -0
  465. /package/{images-records.js → core/workspace-app/images-records.js} +0 -0
  466. /package/{images-render.js → core/workspace-app/images-render.js} +0 -0
  467. /package/{images-svg-core.js → core/workspace-app/images-svg-core.js} +0 -0
  468. /package/{images-svg-sanitize.js → core/workspace-app/images-svg-sanitize.js} +0 -0
  469. /package/{images-svg.js → core/workspace-app/images-svg.js} +0 -0
  470. /package/{images.js → core/workspace-app/images.js} +0 -0
  471. /package/{jobs.js → core/workspace-app/jobs.js} +0 -0
  472. /package/{linger.js → core/workspace-app/linger.js} +0 -0
  473. /package/{lists.js → core/workspace-app/lists.js} +0 -0
  474. /package/{media-info.js → core/workspace-app/media-info.js} +0 -0
  475. /package/{navigation-page.js → core/workspace-app/navigation-page.js} +0 -0
  476. /package/{navigation.js → core/workspace-app/navigation.js} +0 -0
  477. /package/{notes.js → core/workspace-app/notes.js} +0 -0
  478. /package/{palette-index.js → core/workspace-app/palette-index.js} +0 -0
  479. /package/{playwright.config.js → core/workspace-app/playwright.config.js} +0 -0
  480. /package/{plugin-install.js → core/workspace-app/plugin-install.js} +0 -0
  481. /package/{plugin-loader.js → core/workspace-app/plugin-loader.js} +0 -0
  482. /package/{plugin-manifest.js → core/workspace-app/plugin-manifest.js} +0 -0
  483. /package/{plugin-settings.js → core/workspace-app/plugin-settings.js} +0 -0
  484. /package/{plugins → core/workspace-app/plugins}/workspace-notes/index.js +0 -0
  485. /package/{project-make.js → core/workspace-app/project-make.js} +0 -0
  486. /package/{project-name.js → core/workspace-app/project-name.js} +0 -0
  487. /package/{public → core/workspace-app/public}/assistant-avatar-moves.js +0 -0
  488. /package/{public → core/workspace-app/public}/assistant-avatar-ui.js +0 -0
  489. /package/{public → core/workspace-app/public}/assistant-avatar.js +0 -0
  490. /package/{public → core/workspace-app/public}/avatar/druplicon.vrm +0 -0
  491. /package/{public → core/workspace-app/public}/css/base.css +0 -0
  492. /package/{public → core/workspace-app/public}/css/components/ai-assistant.css +0 -0
  493. /package/{public → core/workspace-app/public}/css/components/assistant-avatar.css +0 -0
  494. /package/{public → core/workspace-app/public}/css/components/assistant-views.css +0 -0
  495. /package/{public → core/workspace-app/public}/css/components/build-order.css +0 -0
  496. /package/{public → core/workspace-app/public}/css/components/clone-as-a-git-worktree.css +0 -0
  497. /package/{public → core/workspace-app/public}/css/components/command-palette.css +0 -0
  498. /package/{public → core/workspace-app/public}/css/components/commands-center.css +0 -0
  499. /package/{public → core/workspace-app/public}/css/components/confirm-dialog.css +0 -0
  500. /package/{public → core/workspace-app/public}/css/components/create-card.css +0 -0
  501. /package/{public → core/workspace-app/public}/css/components/ddev-menu.css +0 -0
  502. /package/{public → core/workspace-app/public}/css/components/deep-chat.css +0 -0
  503. /package/{public → core/workspace-app/public}/css/components/density.css +0 -0
  504. /package/{public → core/workspace-app/public}/css/components/floating-assistant-workspace-pages.css +0 -0
  505. /package/{public → core/workspace-app/public}/css/components/folders.css +0 -0
  506. /package/{public → core/workspace-app/public}/css/components/full-height-left-assistant-sidebar-all-pages.css +0 -0
  507. /package/{public → core/workspace-app/public}/css/components/full-height-right-actions-rail-pages-that-have-actions.css +0 -0
  508. /package/{public → core/workspace-app/public}/css/components/grid-and-list.css +0 -0
  509. /package/{public → core/workspace-app/public}/css/components/help-page.css +0 -0
  510. /package/{public → core/workspace-app/public}/css/components/hero.css +0 -0
  511. /package/{public → core/workspace-app/public}/css/components/images.css +0 -0
  512. /package/{public → core/workspace-app/public}/css/components/layout-rhythm-and-spacing-polish.css +0 -0
  513. /package/{public → core/workspace-app/public}/css/components/mic-on-any-field-that-wants-typing.css +0 -0
  514. /package/{public → core/workspace-app/public}/css/components/modal-fit.css +0 -0
  515. /package/{public → core/workspace-app/public}/css/components/page-width.css +0 -0
  516. /package/{public → core/workspace-app/public}/css/components/plugins-page.css +0 -0
  517. /package/{public → core/workspace-app/public}/css/components/project-cards.css +0 -0
  518. /package/{public → core/workspace-app/public}/css/components/project-rows.css +0 -0
  519. /package/{public → core/workspace-app/public}/css/components/reordering-the-workspace-cards.css +0 -0
  520. /package/{public → core/workspace-app/public}/css/components/responsive-home-and-the-workspace-pages.css +0 -0
  521. /package/{public → core/workspace-app/public}/css/components/running-commands-as-toasts.css +0 -0
  522. /package/{public → core/workspace-app/public}/css/components/settings-editor-core-config-yml.css +0 -0
  523. /package/{public → core/workspace-app/public}/css/components/short-results-as-a-toast-at-the-top.css +0 -0
  524. /package/{public → core/workspace-app/public}/css/components/sound-player.css +0 -0
  525. /package/{public → core/workspace-app/public}/css/components/suggest-agents-skills-commands.css +0 -0
  526. /package/{public → core/workspace-app/public}/css/components/tab-modal.css +0 -0
  527. /package/{public → core/workspace-app/public}/css/components/terminals.css +0 -0
  528. /package/{public → core/workspace-app/public}/css/components/test-report-media-one-file-per-row-paged.css +0 -0
  529. /package/{public → core/workspace-app/public}/css/components/top-toolbar-logo-breadcrumb.css +0 -0
  530. /package/{public → core/workspace-app/public}/css/components/video-edit.css +0 -0
  531. /package/{public → core/workspace-app/public}/css/components/web-ide-open-with.css +0 -0
  532. /package/{public → core/workspace-app/public}/css/components/working-indicators.css +0 -0
  533. /package/{public → core/workspace-app/public}/css/components/workspace-cards.css +0 -0
  534. /package/{public → core/workspace-app/public}/favicon.ico +0 -0
  535. /package/{public → core/workspace-app/public}/favicon.png +0 -0
  536. /package/{public → core/workspace-app/public}/htmx.min.js +0 -0
  537. /package/{public → core/workspace-app/public}/logo.png +0 -0
  538. /package/{public → core/workspace-app/public}/media-player.js +0 -0
  539. /package/{public → core/workspace-app/public}/mic.js +0 -0
  540. /package/{public → core/workspace-app/public}/narration.js +0 -0
  541. /package/{public → core/workspace-app/public}/palette.js +0 -0
  542. /package/{public → core/workspace-app/public}/sortable.js +0 -0
  543. /package/{public → core/workspace-app/public}/suggest.js +0 -0
  544. /package/{public → core/workspace-app/public}/terminal.js +0 -0
  545. /package/{public → core/workspace-app/public}/themes/default/theme.css +0 -0
  546. /package/{public → core/workspace-app/public}/themes/ember/theme.css +0 -0
  547. /package/{public → core/workspace-app/public}/themes/press/theme.css +0 -0
  548. /package/{public → core/workspace-app/public}/themes/slate/theme.css +0 -0
  549. /package/{public → core/workspace-app/public}/touch-icon.png +0 -0
  550. /package/{public → core/workspace-app/public}/ui.js +0 -0
  551. /package/{public → core/workspace-app/public}/vendor/ace/LICENSE.txt +0 -0
  552. /package/{public → core/workspace-app/public}/vendor/ace/ace.js +0 -0
  553. /package/{public → core/workspace-app/public}/vendor/ace/ext-language_tools.js +0 -0
  554. /package/{public → core/workspace-app/public}/vendor/ace/ext-searchbox.js +0 -0
  555. /package/{public → core/workspace-app/public}/vendor/ace/mode-css.js +0 -0
  556. /package/{public → core/workspace-app/public}/vendor/ace/mode-html.js +0 -0
  557. /package/{public → core/workspace-app/public}/vendor/ace/mode-javascript.js +0 -0
  558. /package/{public → core/workspace-app/public}/vendor/ace/mode-markdown.js +0 -0
  559. /package/{public → core/workspace-app/public}/vendor/ace/mode-sh.js +0 -0
  560. /package/{public → core/workspace-app/public}/vendor/ace/mode-yaml.js +0 -0
  561. /package/{public → core/workspace-app/public}/vendor/ace/theme-textmate.js +0 -0
  562. /package/{public → core/workspace-app/public}/vendor/ace/theme-tomorrow_night.js +0 -0
  563. /package/{public → core/workspace-app/public}/vendor/ace/worker-yaml.js +0 -0
  564. /package/{public → core/workspace-app/public}/vendor/deep-chat.LICENSE +0 -0
  565. /package/{public → core/workspace-app/public}/vendor/deep-chat.bundle.js +0 -0
  566. /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-LICENSE.txt +0 -0
  567. /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
  568. /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
  569. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-LICENSE.txt +0 -0
  570. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
  571. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
  572. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
  573. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
  574. /package/{public → core/workspace-app/public}/vendor/fonts/jetbrains-mono-LICENSE.txt +0 -0
  575. /package/{public → core/workspace-app/public}/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  576. /package/{public → core/workspace-app/public}/vendor/fonts/ubuntu-mono-LICENSE.txt +0 -0
  577. /package/{public → core/workspace-app/public}/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
  578. /package/{public → core/workspace-app/public}/vendor/tabler-LICENSE.txt +0 -0
  579. /package/{public → core/workspace-app/public}/vendor/tabler-sprite.svg +0 -0
  580. /package/{public → core/workspace-app/public}/vendor/three/LICENSE +0 -0
  581. /package/{public → core/workspace-app/public}/vendor/three/addons/loaders/GLTFLoader.js +0 -0
  582. /package/{public → core/workspace-app/public}/vendor/three/addons/utils/BufferGeometryUtils.js +0 -0
  583. /package/{public → core/workspace-app/public}/vendor/three/addons/utils/SkeletonUtils.js +0 -0
  584. /package/{public → core/workspace-app/public}/vendor/three/three.core.min.js +0 -0
  585. /package/{public → core/workspace-app/public}/vendor/three/three.module.min.js +0 -0
  586. /package/{public → core/workspace-app/public}/vendor/three-vrm/LICENSE +0 -0
  587. /package/{public → core/workspace-app/public}/vendor/three-vrm/three-vrm.module.min.js +0 -0
  588. /package/{public → core/workspace-app/public}/vendor/uikit-icons.min.js +0 -0
  589. /package/{public → core/workspace-app/public}/vendor/uikit.min.css +0 -0
  590. /package/{public → core/workspace-app/public}/vendor/uikit.min.js +0 -0
  591. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/bookmark-outlined.svg +0 -0
  592. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/headset.svg +0 -0
  593. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/menu.svg +0 -0
  594. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/pause.svg +0 -0
  595. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/play.svg +0 -0
  596. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/share.svg +0 -0
  597. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/sound-off.svg +0 -0
  598. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/sound-on.svg +0 -0
  599. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/stop.svg +0 -0
  600. /package/{public → core/workspace-app/public}/vendor/xterm/addon-fit.js +0 -0
  601. /package/{public → core/workspace-app/public}/vendor/xterm/addon-unicode11.js +0 -0
  602. /package/{public → core/workspace-app/public}/vendor/xterm/addon-web-links.js +0 -0
  603. /package/{public → core/workspace-app/public}/vendor/xterm/xterm.css +0 -0
  604. /package/{public → core/workspace-app/public}/vendor/xterm/xterm.js +0 -0
  605. /package/{rag.js → core/workspace-app/rag.js} +0 -0
  606. /package/{reader.js → core/workspace-app/reader.js} +0 -0
  607. /package/{repo-blobs.js → core/workspace-app/repo-blobs.js} +0 -0
  608. /package/{report-pdf.js → core/workspace-app/report-pdf.js} +0 -0
  609. /package/{row-layout.js → core/workspace-app/row-layout.js} +0 -0
  610. /package/{screenrc → core/workspace-app/screenrc} +0 -0
  611. /package/{screens.js → core/workspace-app/screens.js} +0 -0
  612. /package/{scripts → core/workspace-app/scripts}/code-server-start.sh +0 -0
  613. /package/{scripts → core/workspace-app/scripts}/images-claude-finish.js +0 -0
  614. /package/{scripts → core/workspace-app/scripts}/screen-host.sh +0 -0
  615. /package/{scripts → core/workspace-app/scripts}/screen-runner.js +0 -0
  616. /package/{scripts → core/workspace-app/scripts}/screen-then.js +0 -0
  617. /package/{scripts → core/workspace-app/scripts}/update.sh +0 -0
  618. /package/{settings-file.js → core/workspace-app/settings-file.js} +0 -0
  619. /package/{speech.js → core/workspace-app/speech.js} +0 -0
  620. /package/{tab-modal.js → core/workspace-app/tab-modal.js} +0 -0
  621. /package/{test-reports.js → core/workspace-app/test-reports.js} +0 -0
  622. /package/{themes.js → core/workspace-app/themes.js} +0 -0
  623. /package/{transcripts.js → core/workspace-app/transcripts.js} +0 -0
  624. /package/{urls.js → core/workspace-app/urls.js} +0 -0
  625. /package/{validate.js → core/workspace-app/validate.js} +0 -0
  626. /package/{version-check.js → core/workspace-app/version-check.js} +0 -0
  627. /package/{video-edit.js → core/workspace-app/video-edit.js} +0 -0
  628. /package/{waiting.js → core/workspace-app/waiting.js} +0 -0
  629. /package/{workspace-card-tools.js → core/workspace-app/workspace-card-tools.js} +0 -0
  630. /package/{workspace-delete.js → core/workspace-app/workspace-delete.js} +0 -0
  631. /package/{workspace-types.js → core/workspace-app/workspace-types.js} +0 -0
  632. /package/{workspaces.js → core/workspace-app/workspaces.js} +0 -0
@@ -0,0 +1,213 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const http = require('http');
7
+
8
+ const { sanitizeSvg, removedSummary } = require('../../images-svg-sanitize');
9
+ const core = require('../../images-svg-core');
10
+ const svg = require('../../images-svg');
11
+
12
+ const GOOD = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 400">'
13
+ + '<g class="part"><path d="M10 10 L100 10" fill="none" stroke="#0074e0" stroke-width="4"/></g>'
14
+ + '<g class="part"><circle cx="200" cy="200" r="40" fill="none" stroke="#2b447c" stroke-width="4"/></g>'
15
+ + '<g class="part"><line x1="300" y1="100" x2="400" y2="300" stroke="#000000" stroke-width="4"/></g>'
16
+ + '<g class="part" transform="translate(20 0)"><rect x="500" y="100" width="80" height="80" fill="none" stroke="#0074e0" stroke-width="4"/></g></svg>';
17
+
18
+ test('the sanitiser strips scripts, handlers and every way out of the file, and keeps safe drawing', () => {
19
+ const r = sanitizeSvg(`<?xml version="1.0"?><svg viewBox="0 0 10 10" onload="x()"><script>alert(1)</script>
20
+ <style>@import url(https://e.example/x.css); .a{fill:url(#g)} .b{background:url(https://e.example/t.png)}</style>
21
+ <defs><linearGradient id="g"><stop offset="0" stop-color="#0074e0"/></linearGradient></defs>
22
+ <foreignObject><div>hi</div><rect/></foreignObject>
23
+ <a href="https://e.example"><path d="M0 0" onclick="x()" fill="url(#g)"/></a>
24
+ <use href="https://e.example/s.svg#a"/><use href="#g"/><image href="https://e.example/p.png"/>
25
+ <set attributeName="href" to="javascript:alert(1)"/><animate attributeName="opacity" from="0" to="1" dur="1s"/>
26
+ <circle cx="1" cy="1" r="1" style="fill:url(https://e.example/x)"/><text>A &amp; B</text></svg>`);
27
+ assert.doesNotMatch(r.svg, /script|onload|onclick|foreignObject|<image|e\.example|javascript:|@import|<a |<set/i);
28
+ assert.match(r.svg, /<linearGradient id="g">/);
29
+ assert.match(r.svg, /<path d="M0 0" fill="url\(#g\)"\/>/, 'a local url() survives; the handler does not');
30
+ assert.match(r.svg, /<use href="#g"\/>/);
31
+ assert.match(r.svg, /<animate attributeName="opacity"/);
32
+ assert.match(r.svg, /<text>A &amp; B<\/text>/);
33
+ assert.match(r.svg, /^<svg xmlns="http:\/\/www\.w3\.org\/2000\/svg" viewBox="0 0 10 10">/);
34
+ assert.equal(r.drawableLost, 1, 'the rect inside foreignObject was lost');
35
+ assert.deepEqual(r.removed.elements, { script: 1, foreignObject: 1, a: 1, image: 1, set: 1 });
36
+ assert.match(removedSummary(r), /1 script/);
37
+ assert.equal(sanitizeSvg('no drawing here').svg, '');
38
+ });
39
+
40
+ test('the substance check wants a viewBox, four shapes and four parts', () => {
41
+ assert.deepEqual(core.substance(GOOD), { ok: true, why: '', drawable: 4, parts: 4 });
42
+ const thin = core.substance('<svg viewBox="0 0 1 1" width="10"><g class="part"><rect/></g></svg>');
43
+ assert.equal(thin.ok, false);
44
+ assert.match(thin.why, /width or height/);
45
+ assert.match(thin.why, /1 drawable element, and it needs at least 4/);
46
+ assert.match(thin.why, /1 <g class="part"> group, and it needs at least 4/);
47
+ assert.match(core.substance('<svg><rect/></svg>').why, /no viewBox/);
48
+ assert.equal(core.fixRoot('<svg width="960" height="400px"><g/></svg>'), '<svg viewBox="0 0 960 400"><g/></svg>');
49
+ });
50
+
51
+ test('a transform on an element a CSS animation moves goes onto a wrapping <g>', () => {
52
+ const r = core.repairTransforms('<svg viewBox="0 0 1 1"><style>@media (prefers-reduced-motion: no-preference){.spin{animation:s 1s}}</style>'
53
+ + '<g class="spin" transform="rotate(5)"><rect/></g><rect class="spin" transform="scale(2)"/><rect class="still" transform="scale(3)"/></svg>');
54
+ assert.equal(r.moved, 2);
55
+ assert.match(r.svg, /<g transform="rotate\(5\)"><g class="spin"><rect\/><\/g><\/g>/);
56
+ assert.match(r.svg, /<g transform="scale\(2\)"><rect class="spin"\/><\/g>/);
57
+ assert.match(r.svg, /<rect class="still" transform="scale\(3\)"\/>/, 'an element nothing animates keeps its transform');
58
+ });
59
+
60
+ test('Draw the strokes: parts are numbered for the stagger, strokes traced by length, reduced motion respected', () => {
61
+ const done = core.finish(GOOD, { style: 'draw', motion: { duration: 1100, stagger: 110 } });
62
+ assert.equal(done.ok, true, done.why);
63
+ assert.equal(done.moved, 1, 'the part placed with a transform was wrapped');
64
+ assert.match(done.svg, /@media \(prefers-reduced-motion: no-preference\)/);
65
+ assert.match(done.svg, /animation:part-rise 1100ms/);
66
+ assert.match(done.svg, /calc\(var\(--i,0\)\*110ms\)/);
67
+ assert.deepEqual([...done.svg.matchAll(/style="--i:(\d)"/g)].map((m) => m[1]), ['0', '1', '2', '3']);
68
+ assert.equal((done.svg.match(/pathLength="1"/g) || []).length, 4);
69
+ assert.doesNotMatch(core.finish(GOOD, { style: 'still' }).svg, /part-rise/);
70
+ });
71
+
72
+ test('the prompt is built from the svg: settings, with the brand palette and the style rules', () => {
73
+ const cfg = svg.config();
74
+ assert.match(cfg.brief, /flat line illustrations/);
75
+ assert.deepEqual(cfg.palette, { primary: '#0074e0', secondary: '#2b447c', accent: '#e4dcff', dark: '#000000', light: '#ebe8e5' });
76
+ assert.deepEqual([cfg.max_attempts, cfg.request_timeout, cfg.time_budget], [3, 180, 240]);
77
+ assert.deepEqual([cfg.motion.duration, cfg.motion.stagger, cfg.motion.threshold, cfg.motion.replay], [1100, 110, 0.25, true]);
78
+ const { system, user } = core.buildPrompt({ prompt: 'a lighthouse', style: 'draw', palette: cfg.palette, brief: cfg.brief, shape: 'wide' });
79
+ assert.match(system, /The brief: Clean, flat line illustrations/);
80
+ assert.match(system, /viewBox="0 0 960 400"> root, with no width or height/);
81
+ assert.match(system, /primary #0074e0, secondary #2b447c, accent #e4dcff, dark #000000, light #ebe8e5/);
82
+ assert.match(system, /Stroke only: every shape has fill="none"/);
83
+ assert.match(system, /Answer with JSON only/);
84
+ assert.equal(user, 'Draw: a lighthouse');
85
+ assert.match(core.buildPrompt({ prompt: 'x', style: 'css', palette: cfg.palette, shape: 'square' }).system, /prefers-reduced-motion: no-preference/);
86
+ const p = svg.plan({ prompt: 'x', palette_primary: '#ff0000', palette_dark: 'red; x' });
87
+ assert.equal(p.palette.primary, '#ff0000', 'the form may change a colour for one run');
88
+ assert.equal(p.palette.dark, '#000000', 'a value that is not a colour is ignored');
89
+ });
90
+
91
+ test('the examples are data in settings, generic, and free of the names they were adapted from', () => {
92
+ const ex = svg.config().examples;
93
+ assert.ok(ex.length >= 6 && ex.length <= 10, `${ex.length} examples`);
94
+ const deny = /enlyte|mitchell|genex|coventry|partstrader|claim|insur|casualty|pharmac|nurse|injur|adjuster|workers.? comp|bill review|carrier|policyholder/i;
95
+ const raw = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'config', 'workspace.images.settings.yml'), 'utf8');
96
+ assert.doesNotMatch(raw, deny);
97
+ for (const e of ex) {
98
+ assert.ok(core.STYLES[e.style], e.title);
99
+ assert.ok(e.prompt.length > 40, e.title);
100
+ assert.doesNotMatch(`${e.title} ${e.prompt}`, deny);
101
+ }
102
+ const form = svg.formHtml();
103
+ assert.match(form, /<select class="uk-select uk-form-small" id="svg-example" data-svg-example>/);
104
+ assert.match(form, /data-prompt="Five rounded bars rising/);
105
+ });
106
+
107
+ // A stand-in for a chat model that answers from a list, recording each request.
108
+ async function standIn(replies) {
109
+ const seen = [];
110
+ const srv = http.createServer((req, res) => {
111
+ let body = '';
112
+ req.on('data', (d) => { body += d; });
113
+ req.on('end', () => {
114
+ seen.push(JSON.parse(body));
115
+ const text = replies[Math.min(seen.length - 1, replies.length - 1)];
116
+ res.writeHead(200, { 'Content-Type': 'application/json' });
117
+ res.end(JSON.stringify({ choices: [{ message: { content: text } }], usage: { prompt_tokens: 1000, completion_tokens: 1000 } }));
118
+ });
119
+ });
120
+ await new Promise((r) => srv.listen(0, r));
121
+ process.env.OPENAI_CHAT_API = `http://127.0.0.1:${srv.address().port}/v1/chat/completions`;
122
+ const https = require('https');
123
+ const real = https.request;
124
+ https.request = http.request;
125
+ return { seen, close: () => { https.request = real; srv.close(); delete process.env.OPENAI_CHAT_API; } };
126
+ }
127
+
128
+ test('a thin answer is retried with the reason, and the second is saved with its records', async () => {
129
+ const thin = JSON.stringify({ svg: '<svg viewBox="0 0 960 400"><g class="part"><rect/></g></svg>', summary: 's', alt: 'a' });
130
+ const good = JSON.stringify({ svg: GOOD, summary: 'Four shapes in a row.', alt: 'Four outlined shapes' });
131
+ const s = await standIn([thin, good]);
132
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-'));
133
+ try {
134
+ assert.equal((await svg.generateSvg({ prompt: 'shapes' })).status, 400, 'nothing without confirm');
135
+ const r = await svg.generateSvg({ prompt: 'shapes', provider: 'openai', style: 'draw', name: 'shapes', confirm: 'yes' }, { key: 'sk-test', dir });
136
+ assert.equal(r.ok, true, r.error);
137
+ assert.equal(r.attempts, 2);
138
+ assert.equal(r.cost.toFixed(6), (2 * (1000 * 0.75 / 1e6 + 1000 * 4.5 / 1e6)).toFixed(6));
139
+ const second = s.seen[1].messages;
140
+ assert.equal(second[2].role, 'assistant');
141
+ assert.match(second[3].content, /The drawing was not used: it has 1 drawable element/);
142
+ const side = JSON.parse(fs.readFileSync(path.join(dir, 'shapes.svg.json'), 'utf8'));
143
+ assert.deepEqual([side.style, side.attempts, side.provider, side.summary, side.alt],
144
+ ['Draw the strokes', 2, 'OpenAI', 'Four shapes in a row.', 'Four outlined shapes']);
145
+ assert.match(side.palette, /primary #0074e0/);
146
+ assert.match(fs.readFileSync(path.join(dir, 'shapes.md'), 'utf8'), /- \*\*Style:\*\* Draw the strokes/);
147
+ assert.match(fs.readFileSync(path.join(dir, 'shapes.svg'), 'utf8'), /part-rise/);
148
+ } finally {
149
+ s.close();
150
+ fs.rmSync(dir, { recursive: true, force: true });
151
+ }
152
+ });
153
+
154
+ test('a drawing that lost shapes to the sanitiser is drawn again; one that never passes is reported, not saved', async () => {
155
+ const lost = JSON.stringify({ svg: GOOD.replace('</svg>', '<foreignObject><rect/></foreignObject></svg>') });
156
+ const s = await standIn([lost]);
157
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-'));
158
+ try {
159
+ const r = await svg.generateSvg({ prompt: 'x', provider: 'openai', style: 'still', confirm: 'yes' }, { key: 'sk-test', dir });
160
+ assert.equal(r.ok, false);
161
+ assert.equal(s.seen.length, 3, 'max_attempts');
162
+ assert.match(s.seen[1].messages[3].content, /1 drawable element was lost/);
163
+ assert.match(r.error, /No usable drawing after 3 attempts/);
164
+ assert.deepEqual(fs.readdirSync(dir), []);
165
+ } finally {
166
+ s.close();
167
+ fs.rmSync(dir, { recursive: true, force: true });
168
+ }
169
+ });
170
+
171
+ test('Gemini and Anthropic need a real key; Claude Code draws in a session, retried by its finish step', async () => {
172
+ const noKey = await svg.generateSvg({ prompt: 'x', provider: 'anthropic', confirm: 'yes' });
173
+ assert.equal(noKey.status, 503);
174
+ assert.match(noKey.error, /ai_providers\.anthropic\.api_key/);
175
+ const calls = [];
176
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'svgcc-'));
177
+ try {
178
+ const r = await svg.generateSvg({ prompt: 'nodes; $(id)', provider: 'claude_code_cli', style: 'draw', name: 'nodes', confirm: 'yes' },
179
+ { dir, start: async (a, payload) => { calls.push(payload); return { ok: true }; } });
180
+ assert.equal(r.ok, true, r.error);
181
+ const { cmd } = calls[0];
182
+ assert.doesNotMatch(cmd, /nodes;|\$\(id\)/);
183
+ assert.match(cmd, /^for try in \$\(seq 1 3\); do claude -p "\$\(cat '[^']+'\)" /);
184
+ const promptFile = cmd.match(/cat '([^']+)'/)[1];
185
+ const jobFile = cmd.match(/'([^']+\.job\.json)'/)[1];
186
+ assert.match(fs.readFileSync(promptFile, 'utf8'), /Write exactly two files[\s\S]*1\. nodes\.svg/);
187
+ const finish = require('../../scripts/images-claude-finish');
188
+ fs.writeFileSync(path.join(dir, 'nodes.svg'), '<svg viewBox="0 0 960 400"><g class="part"><rect/></g></svg>');
189
+ assert.equal(finish.main(jobFile), 3, 'refused, drawn again');
190
+ assert.match(fs.readFileSync(promptFile, 'utf8'), /Your last drawing was refused\. The drawing was not used/);
191
+ fs.writeFileSync(path.join(dir, 'nodes.svg'), GOOD);
192
+ fs.writeFileSync(path.join(dir, 'nodes.claude.json'), JSON.stringify({ summary: 'Nodes.', alt: 'Nodes' }));
193
+ assert.equal(finish.main(jobFile), 0);
194
+ const side = JSON.parse(fs.readFileSync(path.join(dir, 'nodes.svg.json'), 'utf8'));
195
+ assert.deepEqual([side.provider, side.attempts, side.style, side.alt], ['claude_code_cli', 2, 'Draw the strokes', 'Nodes']);
196
+ assert.ok(!fs.existsSync(promptFile) && !fs.existsSync(jobFile));
197
+ fs.rmSync(path.join(path.dirname(path.dirname(promptFile)), 'images--claude-nodes.cwd'), { force: true });
198
+ } finally {
199
+ fs.rmSync(dir, { recursive: true, force: true });
200
+ }
201
+ });
202
+
203
+ test('the plan comes first, through the confirm dialog, and the viewer can replay and copy the markup', () => {
204
+ const p = svg.plan({ prompt: 'nodes', provider: 'openai', style: 'draw' });
205
+ const html = svg.planHtml(p);
206
+ assert.match(html, /hx-trigger="confirmed-remove"/);
207
+ assert.match(html, /&quot;confirm&quot;:&quot;yes&quot;/);
208
+ assert.match(html, /at most 3 attempts/);
209
+ const page = fs.readFileSync(path.join(__dirname, '..', '..', 'images-page.js'), 'utf8');
210
+ assert.match(page, /data-svg-replay/);
211
+ assert.match(page, /Copy SVG markup/);
212
+ assert.match(page, /<source media="\(prefers-reduced-motion: reduce\)"/);
213
+ });
@@ -0,0 +1,144 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const images = require('../../images');
8
+
9
+ const root = path.join(__dirname, '..', '..');
10
+ const repo = path.join(root, '..', '..');
11
+ const read = (p) => fs.readFileSync(path.join(root, p), 'utf8');
12
+
13
+ function png(w, h) {
14
+ const b = Buffer.alloc(33);
15
+ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]).copy(b, 0);
16
+ b.writeUInt32BE(13, 8); b.write('IHDR', 12); b.writeUInt32BE(w, 16); b.writeUInt32BE(h, 20);
17
+ return b;
18
+ }
19
+ function gif(w, h) { const b = Buffer.alloc(13); b.write('GIF89a'); b.writeUInt16LE(w, 6); b.writeUInt16LE(h, 8); return b; }
20
+ function jpeg(w, h) {
21
+ // SOI, an APP0 segment, then SOF0 with height and width.
22
+ return Buffer.concat([Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x04, 0x00, 0x00]),
23
+ Buffer.from([0xff, 0xc0, 0x00, 0x11, 0x08, h >> 8, h & 255, w >> 8, w & 255, 0x03])]);
24
+ }
25
+ function webp8x(w, h) {
26
+ const b = Buffer.alloc(30); b.write('RIFF'); b.write('WEBP', 8); b.write('VP8X', 12);
27
+ b.writeUIntLE(w - 1, 24, 3); b.writeUIntLE(h - 1, 27, 3); return b;
28
+ }
29
+
30
+ test('dimensions are read from each format\'s own header', () => {
31
+ const p = images._parsers;
32
+ assert.deepEqual(p.pngSize(png(1920, 1080)), { width: 1920, height: 1080 });
33
+ assert.deepEqual(p.gifSize(gif(64, 32)), { width: 64, height: 32 });
34
+ assert.deepEqual(p.jpegSize(jpeg(800, 600)), { width: 800, height: 600 });
35
+ assert.deepEqual(p.webpSize(webp8x(1600, 1000)), { width: 1600, height: 1000 });
36
+ assert.deepEqual(p.svgSize(Buffer.from('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 40"></svg>')), { width: 120, height: 40 });
37
+ assert.deepEqual(p.svgSize(Buffer.from('<svg width="24px" height="16" viewBox="0 0 1 1">')), { width: 24, height: 16 });
38
+ assert.equal(p.pngSize(Buffer.from('not a png at all, really not')), null);
39
+ });
40
+
41
+ test('a folder lists its folders and images, and nothing outside it', () => {
42
+ const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'img-'));
43
+ try {
44
+ fs.mkdirSync(path.join(ws, 'shots'));
45
+ fs.writeFileSync(path.join(ws, 'a.png'), png(10, 20));
46
+ fs.writeFileSync(path.join(ws, 'shots', 'b.gif'), gif(3, 4));
47
+ fs.writeFileSync(path.join(ws, 'notes.md'), '# not an image');
48
+ fs.writeFileSync(path.join(ws, '.hidden.png'), png(1, 1));
49
+ const top = images.listFolder(ws, '');
50
+ assert.deepEqual(top.images.map((i) => [i.name, i.width, i.height, i.type]), [['a.png', 10, 20, 'png']]);
51
+ assert.deepEqual(top.folders.map((f) => [f.name, f.count]), [['shots', 1]]);
52
+ assert.equal(images.listFolder(ws, 'shots').images[0].rel, 'shots/b.gif');
53
+ assert.equal(images.listFolder(ws, '../etc').ok, false);
54
+ assert.equal(images.safeIn(ws, '../../etc/passwd'), null);
55
+ } finally {
56
+ fs.rmSync(ws, { recursive: true, force: true });
57
+ }
58
+ });
59
+
60
+ test('names are made safe, and a new name never takes an existing one', () => {
61
+ const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'img-'));
62
+ try {
63
+ assert.equal(images.safeName('../My Shot (1).PNG'), 'My-Shot-1.PNG');
64
+ fs.writeFileSync(path.join(ws, 'shot.png'), 'x');
65
+ assert.equal(images.freeName(ws, 'shot', 'png'), 'shot-2.png');
66
+ assert.equal(images.versionName(ws, 'shot.png', 'webp'), 'shot-v2.webp');
67
+ fs.writeFileSync(path.join(ws, 'shot-v2.png'), 'x');
68
+ assert.equal(images.versionName(ws, 'shot-v2.png', 'png'), 'shot-v3.png');
69
+ } finally {
70
+ fs.rmSync(ws, { recursive: true, force: true });
71
+ }
72
+ });
73
+
74
+ test('"All media images" reads media and doc workspaces and the dashboard screenshots, never sites', () => {
75
+ assert.deepEqual(images.EVERYWHERE, ['images', 'docs', 'videos', 'audio', 'specs', 'designs']);
76
+ for (const site of ['dev', 'test', 'demos', 'sandboxes', 'projects', 'products']) assert.ok(!images.EVERYWHERE.includes(site), site);
77
+ assert.match(images.appShotsDir(), /core\/workspace-app\/docs\/screenshots$/);
78
+ const list = images.scanEverywhere({ fresh: true });
79
+ assert.ok(Array.isArray(list));
80
+ assert.ok(list.every((im) => images.IMAGE_RE.test(im.name) && im.src));
81
+ });
82
+
83
+ test('the workspace is registered before videos, as a media workspace with a gallery', () => {
84
+ const settings = fs.readFileSync(path.join(repo, 'core/config/settings.yml'), 'utf8');
85
+ assert.match(settings, /\n {2}- images\n {2}- videos\n/);
86
+ assert.match(fs.readFileSync(path.join(repo, 'core/config/workspace.images.settings.yml'), 'utf8'), /type: media/);
87
+ assert.match(read('workspaces.js'), /images: +\{ icon: 'tabler:photo', subtitle: 'Images, screenshots and generated pictures', noun: 'image', kind: 'files' \}/);
88
+ assert.match(read('workspace-types.js'), /images: 'media'/);
89
+ assert.match(read('files-browser.js'), /'images'/);
90
+ assert.match(read('.ddev/config.yaml'), /"\*\.images\.workspace"/);
91
+ for (const f of ['README.md', 'AGENTS.md', 'CLAUDE.md']) assert.ok(fs.existsSync(path.join(repo, 'images', f)), f);
92
+ const server = read('server.js');
93
+ assert.match(server, /if \(key === 'images'\) return require\('\.\/images-page'\)\.galleryHtml\(/);
94
+ assert.match(server, /'\.webp': 'image\/webp', '\.avif': 'image\/avif'/);
95
+ assert.match(server, /type === 'image\/svg\+xml' \? \{ 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; sandbox" \}/);
96
+ });
97
+
98
+ test('the gallery and the viewer draw what the list holds, and only the images workspace can rename or delete', () => {
99
+ const page = require('../../images-page');
100
+ const ws = { per: 'all', page: 1, q: '', sort: 'newest', kind: '', scope: 'here' };
101
+ const html = page.galleryHtml(ws, '', { folderDocs: '', openWith: '', viewSwitcher: '<div class="layout-controls"></div>' });
102
+ // The same building blocks the docs and videos lists use: heading with a count, summary, layout switcher, file list.
103
+ assert.match(html, /<div class="cards-head fx-head">/);
104
+ assert.match(html, /<h3 class="uk-margin-remove">Images <span class="uk-badge">\d+<\/span><\/h3>/);
105
+ assert.match(html, /<p class="uk-text-meta fx-summary">/);
106
+ assert.match(html, /<div class="layout-controls"><\/div>/);
107
+ assert.match(html, /<option value="here" selected>This workspace<\/option>/);
108
+ assert.match(html, /<option value="all">All media images<\/option>/);
109
+ assert.match(html, /name="kind" aria-label="Image type"/);
110
+ assert.match(read('lists.js'), /size: +\{ label: 'Largest first', filesOnly: true/);
111
+ const actions = read('actions/images.js');
112
+ assert.match(actions, /images\.safeIn\(workspaceDir\('images'\)/);
113
+ assert.match(actions, /if \(form\.confirm !== 'yes'\) return send/);
114
+ const ui = read('public/ui.js');
115
+ assert.match(ui, /#editor-modal\.uk-open \[data-img-viewer\]/);
116
+ });
117
+
118
+ test('each source has its own share of the scan, and a vector thumbnail is padded, not stretched', () => {
119
+ assert.equal(images.SCAN_MAX, 1000);
120
+ assert.match(read('images.js'), /const full = \(\) => out\.length - start >= SCAN_MAX;/);
121
+ assert.match(read('images-page.js'), /im\.type === 'svg' \? ' is-vector' : ''/);
122
+ const css = read('public/css/components/images.css');
123
+ assert.match(css, /\.img-row \.fx-thumb\.is-vector img \{ object-fit: contain; padding: 1\.25rem;/);
124
+ assert.match(css, /\.img-stage\.is-fit \{ overflow: hidden; \}/);
125
+ });
126
+
127
+ test('an image is a card of the shared file list: thumbnail with its size in the corner, name, meta, View and a menu', () => {
128
+ const page = require('../../images-page');
129
+ const src = read('images-page.js');
130
+ assert.match(src, /<div class="fx-row fx-file has-thumb img-row">/);
131
+ assert.match(src, /<span class="fx-duration fx-dims">/);
132
+ assert.match(src, /<a class="fx-row fx-folder"/);
133
+ assert.match(src, /<div class="file-list img-list">/);
134
+ assert.match(src, /class="uk-button uk-button-default uk-button-small fx-open" title="View/);
135
+ assert.match(read('server.js'), /viewSwitcher: viewSwitcherHtml\('files'\)/);
136
+ assert.equal(typeof page.galleryHtml, 'function');
137
+ });
138
+
139
+ test('a thumbnail fills the card shape and is cropped from the top, so a tall screenshot stays sharp', () => {
140
+ const src = read('images-page.js');
141
+ assert.match(src, /'-thumbnail', `\$\{THUMB_W\}x\$\{THUMB_H\}\^`, '-gravity', 'north',\n\s+'-extent', `\$\{THUMB_W\}x\$\{THUMB_H\}`/);
142
+ assert.match(src, /thumb\?v=2&src=/);
143
+ assert.match(read('images.js'), /folders\.push\(\{ folder: true, name: e\.name, rel: childRel, count, folders: subs,/);
144
+ });
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The job fragment's two newer faces: the phase progress bar and inline mode.
3
+ *
4
+ * Both are rendered from the job record alone, so they can be tested by seeding
5
+ * the registry directly — no process is spawned. The phase bar's one honesty
6
+ * rule is tested from both sides: a phase counts only when its marker is in the
7
+ * output, and a finished job reports full whatever the output said.
8
+ */
9
+
10
+ const { test } = require('node:test');
11
+ const assert = require('node:assert');
12
+
13
+ const { jobs, jobFragment } = require('../../jobs');
14
+
15
+ function seed(id, job) {
16
+ jobs.set(id, { title: 'T', key: null, phases: null, inline: false, buf: '', done: false, ok: null, ...job });
17
+ return id;
18
+ }
19
+
20
+ test('a job without phases renders no progress bar', () => {
21
+ const { html } = jobFragment(seed('t-plain', { buf: 'hello' }));
22
+ assert.ok(!html.includes('job-progress'));
23
+ });
24
+
25
+ test('the phase bar counts only markers that actually appeared', () => {
26
+ const phases = ['Exporting', 'Copying', 'Importing'];
27
+ const { html } = jobFragment(seed('t-phases', { phases, buf: 'Exporting the db\nCopying files' }));
28
+ assert.match(html, /value="2" max="3"/);
29
+ // The label names the phase the run is ON, not one it finished.
30
+ assert.ok(html.includes('Importing (2/3)'));
31
+ });
32
+
33
+ test('a finished successful job reports the bar full', () => {
34
+ const { html } = jobFragment(seed('t-done', { phases: ['A', 'B'], buf: 'A', done: true, ok: true }));
35
+ assert.match(html, /value="2" max="2"/);
36
+ assert.ok(html.includes('Done (2/2)'));
37
+ });
38
+
39
+ test('inline jobs carry the class that keeps them out of the toast stack, and a done note', () => {
40
+ const running = jobFragment(seed('t-inline', { inline: true, buf: 'x' })).html;
41
+ assert.ok(running.includes('job-inline'));
42
+ assert.ok(!running.includes('job-done-note'), 'no done note while running');
43
+ const done = jobFragment(seed('t-inline-done', { inline: true, buf: 'x', done: true, ok: true })).html;
44
+ assert.ok(done.includes('job-done-note'));
45
+ assert.ok(done.includes('Done — the process finished'));
46
+ const failed = jobFragment(seed('t-inline-fail', { inline: true, buf: 'x', done: true, ok: false })).html;
47
+ assert.ok(failed.includes('The process failed'));
48
+ });
49
+
50
+ test('a non-inline job never renders the done note', () => {
51
+ const { html } = jobFragment(seed('t-stack-done', { buf: 'x', done: true, ok: true }));
52
+ assert.ok(!html.includes('job-done-note'));
53
+ });
54
+
55
+ const { screenWrap } = require('../../jobs');
56
+
57
+ test('screenWrap builds the detach-tail-wait line and refuses bad names', () => {
58
+ const line = screenWrap({ session: 'jira-x-1', inner: 'echo hi', logFile: '/tmp/x.log' });
59
+ assert.match(line, /screen -c "[^"]*\/screenrc" -dmS jira-x-1/,
60
+ "under the dashboard's own screenrc, never the machine's");
61
+ assert.ok(line.includes('tail -F "/tmp/x.log"'));
62
+ // The waiter must match the session EXACTLY: a bare "[.]jira-x-1" also matches
63
+ // jira-x-12, so a job would wait on the wrong session's lifetime.
64
+ assert.ok(line.includes('grep -q "[.]jira-x-1[[:space:]]"'), 'the waiter dot-escapes and terminates the session name');
65
+ assert.throws(() => screenWrap({ session: 'bad name; rm', inner: 'x', logFile: '/tmp/y' }));
66
+ });
@@ -0,0 +1,164 @@
1
+ /**
2
+ * The player and the cards, the way a streaming site has them.
3
+ *
4
+ * Lengths are read from the files, so they are checked against ffprobe where it is installed and against
5
+ * hand-built headers where it is not. The rows, the chapters and the player markup come from the real server.
6
+ */
7
+
8
+ const { test, before, after } = require('node:test');
9
+ const assert = require('node:assert');
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+ const net = require('net');
14
+ const { spawn, execFileSync } = require('child_process');
15
+
16
+ const APP = path.join(__dirname, '../..');
17
+ const REPO = path.join(APP, '../..');
18
+ const { copyConfig } = require('./_isolated-config');
19
+ const mi = require('../../media-info');
20
+
21
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-media-'));
22
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-media-root-'));
23
+
24
+ function hasFfmpeg() {
25
+ try { execFileSync('ffmpeg', ['-version'], { stdio: 'ignore' }); return true; } catch (_) { return false; }
26
+ }
27
+ const ffmpeg = (...a) => execFileSync('ffmpeg', ['-y', '-loglevel', 'error', ...a]);
28
+
29
+ test('a length is said the way a player says it', () => {
30
+ assert.strictEqual(mi.clock(476.1), '7:56');
31
+ assert.strictEqual(mi.clock(59.6), '1:00');
32
+ assert.strictEqual(mi.clock(3725), '1:02:05');
33
+ assert.strictEqual(mi.clock(0), '');
34
+ assert.strictEqual(mi.clock(null), '');
35
+ });
36
+
37
+ test('a wav and a constant bit rate mp3 are read from their headers', () => {
38
+ const wav = Buffer.alloc(44 + 88200);
39
+ wav.write('RIFF', 0); wav.writeUInt32LE(36 + 88200, 4); wav.write('WAVEfmt ', 8); wav.writeUInt32LE(16, 16);
40
+ wav.writeUInt16LE(1, 20); wav.writeUInt16LE(1, 22); wav.writeUInt32LE(22050, 24); wav.writeUInt32LE(44100, 28);
41
+ wav.writeUInt16LE(2, 32); wav.writeUInt16LE(16, 34); wav.write('data', 36); wav.writeUInt32LE(88200, 40);
42
+ fs.writeFileSync(path.join(tmp, 'two.wav'), wav);
43
+ assert.strictEqual(mi.seconds(path.join(tmp, 'two.wav')), 2);
44
+ // MPEG-1 Layer III, 128 kbit/s, 44.1 kHz: 0xFFFB 0x90 0x00. 160000 bytes at 128 kbit/s is 10 seconds.
45
+ const mp3 = Buffer.alloc(160000, 0);
46
+ Buffer.from([0xff, 0xfb, 0x90, 0x00]).copy(mp3, 0);
47
+ fs.writeFileSync(path.join(tmp, 'ten.mp3'), mp3);
48
+ assert.strictEqual(mi.seconds(path.join(tmp, 'ten.mp3')), 10);
49
+ fs.writeFileSync(path.join(tmp, 'junk.mp3'), Buffer.alloc(500, 1));
50
+ assert.strictEqual(mi.seconds(path.join(tmp, 'junk.mp3')), null);
51
+ assert.strictEqual(mi.seconds(path.join(tmp, 'missing.mp4')), null);
52
+ assert.strictEqual(mi.seconds(path.join(tmp, 'two.txt')), null);
53
+ });
54
+
55
+ test('an mp4 length is found whether the encoder put its header first or last', { skip: !hasFfmpeg() }, () => {
56
+ for (const [name, flags] of [['front.mp4', ['-movflags', '+faststart']], ['end.mp4', []]]) {
57
+ const out = path.join(tmp, name);
58
+ ffmpeg('-f', 'lavfi', '-i', 'testsrc=duration=5:size=160x120:rate=10', '-f', 'lavfi', '-i', 'sine=duration=5', '-shortest', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-c:a', 'aac', ...flags, out);
59
+ const truth = parseFloat(execFileSync('ffprobe', ['-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', out]).toString());
60
+ assert.ok(Math.abs(mi.seconds(out) - truth) < 0.1, `${name}: ${mi.seconds(out)} against ${truth}`);
61
+ }
62
+ const mp3 = path.join(tmp, 'tone.mp3');
63
+ ffmpeg('-f', 'lavfi', '-i', 'sine=duration=4', '-codec:a', 'libmp3lame', '-b:a', '128k', mp3);
64
+ assert.ok(Math.abs(mi.seconds(mp3) - 4) < 0.2, String(mi.seconds(mp3)));
65
+ });
66
+
67
+ test('the player offers what people expect, and the cards say how long and whether there are words', () => {
68
+ const js = fs.readFileSync(path.join(APP, 'public/media-player.js'), 'utf8');
69
+ for (const bit of ["key === 'j'", "key === 'l'", "'ArrowLeft'", "'ArrowUp'", "key === 'm'", "requestFullscreen", "key === '<'", "/^[0-9]$/", "ws-media-speed", "media.play()", "data-media-step", "mouseover"]) {
70
+ assert.ok(js.includes(bit), bit);
71
+ }
72
+ assert.strictEqual(fs.readFileSync(path.join(APP, 'public/ui.js'), 'utf8').indexOf('data-sound-speed'), -1, 'the old handlers are gone from ui.js');
73
+ });
74
+
75
+ // ---- the running server ------------------------------------------------------------------------
76
+
77
+ let proc; let base;
78
+ const free = () => new Promise((resolve) => {
79
+ const s = net.createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); });
80
+ });
81
+ const SCRIPT = `# Demo
82
+
83
+ ## E01 Opening (00:00.0 to 00:02.0)
84
+
85
+ > Hello there.
86
+
87
+ ## E02 Close (00:02.0 to 00:05.0)
88
+
89
+ > Goodbye now.
90
+ `;
91
+
92
+ before(async () => {
93
+ fs.mkdirSync(path.join(root, 'core'), { recursive: true });
94
+ copyConfig(path.join(REPO, 'core/config'), path.join(root, 'core/config'));
95
+ fs.symlinkSync(path.join(REPO, 'core/scripts'), path.join(root, 'core/scripts'));
96
+ fs.symlinkSync(APP, path.join(root, 'core/workspace-app'));
97
+ for (const d of ['videos', 'docs', 'audio']) fs.mkdirSync(path.join(root, d), { recursive: true });
98
+ if (hasFfmpeg()) {
99
+ ffmpeg('-f', 'lavfi', '-i', 'testsrc=duration=5:size=160x120:rate=10', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-movflags', '+faststart', path.join(root, 'videos/demo.mp4'));
100
+ ffmpeg('-f', 'lavfi', '-i', 'sine=duration=3', '-codec:a', 'libmp3lame', '-b:a', '128k', path.join(root, 'audio/tone.mp3'));
101
+ } else {
102
+ fs.writeFileSync(path.join(root, 'videos/demo.mp4'), Buffer.alloc(100, 1));
103
+ fs.writeFileSync(path.join(root, 'audio/tone.mp3'), Buffer.alloc(100, 1));
104
+ }
105
+ fs.writeFileSync(path.join(root, 'videos/plain.mp4'), Buffer.alloc(100, 2));
106
+ fs.writeFileSync(path.join(root, 'docs/demo-voiceover-script.md'), SCRIPT);
107
+ const port = await free();
108
+ base = `http://127.0.0.1:${port}`;
109
+ proc = spawn(process.execPath, ['server.js'], { cwd: APP, env: { ...process.env, PORT: String(port), WORKSPACE_ROOT: root }, stdio: ['ignore', 'pipe', 'pipe'] });
110
+ await new Promise((resolve, reject) => {
111
+ const t = setTimeout(() => reject(new Error('server did not start')), 15000);
112
+ proc.stdout.on('data', (d) => { if (/listening/.test(String(d))) { clearTimeout(t); resolve(); } });
113
+ proc.on('exit', (c) => { clearTimeout(t); reject(new Error(`server exited ${c}`)); });
114
+ });
115
+ });
116
+
117
+ after(() => {
118
+ if (proc) proc.kill();
119
+ for (const d of [tmp, root]) fs.rmSync(d, { recursive: true, force: true });
120
+ });
121
+
122
+ const get = (p) => fetch(base + p);
123
+ const rowOf = (html, name) => { const at = html.indexOf(`title="Play ${name}`); return html.slice(Math.max(0, at - 2500), at + 1500); };
124
+
125
+ test('a recording\'s card carries its length, CC when it has a script, and a preview address', { skip: !hasFfmpeg() }, async () => {
126
+ const html = await (await get('/fragments/videos/items')).text();
127
+ const demo = rowOf(html, 'demo.mp4');
128
+ assert.match(demo, /<span class="fx-duration">0:05<\/span>/);
129
+ assert.match(demo, /<span class="fx-cc" title="Has a transcript">CC<\/span>/);
130
+ assert.match(demo, /data-preview="\/files\/videos\/demo\.mp4"/);
131
+ assert.match(demo, /<span class="fx-size">[^<]*· 0:05<\/span>/);
132
+ const plain = rowOf(html, 'plain.mp4');
133
+ assert.ok(!/fx-duration|fx-cc/.test(plain.slice(plain.lastIndexOf('<div class="fx-row'))), 'no length and no CC where there is neither');
134
+ const sound = await (await get('/fragments/audio/items')).text();
135
+ assert.match(sound, /<span class="fx-duration">0:03<\/span>/);
136
+ assert.ok(!/data-preview/.test(sound), 'a sound has no picture to preview');
137
+ });
138
+
139
+ test('the player is a stage with a speed menu, chapters from the script, and previous and next', async () => {
140
+ const html = await (await get('/fragments/videos/play/demo.mp4')).text();
141
+ assert.match(html, /class="uk-card uk-card-default uk-card-body uk-margin-top editor-card media-card"/);
142
+ assert.match(html, /<div class="media-stage"><video class="video-player"/);
143
+ assert.match(html, /data-speed-label[^>]*>1×<\/button>\s*<div uk-dropdown=/);
144
+ assert.strictEqual((html.match(/data-sound-speed=/g) || []).length, 19);
145
+ const chapters = JSON.parse(html.match(/data-chapters="([^"]*)"/)[1].replace(/&quot;/g, '"'));
146
+ assert.deepStrictEqual(chapters, [{ start: 0, end: 2, title: 'E01 Opening' }, { start: 2, end: 5, title: 'E02 Close' }]);
147
+ assert.match(html, /<section class="transcript-section" data-start="2" data-end="5">/);
148
+ assert.match(html, /data-media-step="-1" disabled/);
149
+ assert.match(html, /data-media-step="1" disabled/);
150
+ assert.match(html, /Space play/);
151
+ assert.match(html, /<p class="player-chapter" data-chapter hidden><\/p>/);
152
+ const sound = await (await get('/fragments/audio/play/tone.mp3')).text();
153
+ assert.match(sound, /media-stage is-sound/);
154
+ assert.ok(!/chapter-bar/.test(sound), 'a sound has no chapters');
155
+ const plain = await (await get('/fragments/videos/play/plain.mp4')).text();
156
+ assert.ok(!/chapter-bar/.test(plain), 'no script, no chapters');
157
+ });
158
+
159
+ test('the shell loads the player script', async () => {
160
+ const page = await (await get('/videos')).text();
161
+ assert.match(page, /<script src="\/media-player\.js[^"]*" defer><\/script>/);
162
+ const js = await get('/media-player.js');
163
+ assert.strictEqual(js.status, 200);
164
+ });