@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,436 @@
1
+ /**
2
+ * Edit a video into a new version of itself.
3
+ *
4
+ * The real server and the real `videos/cmd-tools-video-edit.sh` run here, on a six second video made for the
5
+ * test, so nothing depends on what is installed on the machine beyond ffmpeg (and Chrome, for the logo scenes).
6
+ */
7
+
8
+ const { test, before, after } = require('node:test');
9
+ const assert = require('node:assert');
10
+ const fs = require('fs');
11
+ const net = require('net');
12
+ const os = require('os');
13
+ const path = require('path');
14
+ const { spawn, execFile, spawnSync } = require('child_process');
15
+ const { promisify } = require('util');
16
+ const run = promisify(execFile);
17
+
18
+ const APP = path.join(__dirname, '../..');
19
+ const REPO = path.join(APP, '../..');
20
+ const { copyConfig } = require('./_isolated-config');
21
+
22
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-vedit-root-'));
23
+ process.env.WORKSPACE_ROOT = root;
24
+ const videoEdit = require('../../video-edit');
25
+ const voice = require('../../audio-voice');
26
+
27
+ const have = (tool) => spawnSync('bash', ['-c', `command -v ${tool}`]).status === 0;
28
+ const CAN_RUN = have('ffmpeg') && have('ffprobe');
29
+ const CAN_CARD = CAN_RUN && (have('google-chrome') || have('chromium') || have('chromium-browser'));
30
+
31
+ const SCRIPT = `# Demo
32
+
33
+ ## How to use this script
34
+
35
+ Not a timed section.
36
+
37
+ ## S01 Opening (00:00.0 to 00:03.0)
38
+
39
+ > The opening.
40
+
41
+ ## S02 Close (00:03.0 to 00:06.0)
42
+
43
+ > The close.
44
+ `;
45
+ const ENGINES = [
46
+ { id: 'piper', label: 'Piper', available: true, free: true, hostOnly: true, voices: [{ id: 'en_US-lessac-high', installed: true, label: 'American' }] },
47
+ { id: 'elevenlabs', label: 'ElevenLabs', available: true, can_list_voices: true, default_model: 'eleven_v4', models: [{ id: 'eleven_v4', label: 'v4' }, { id: 'eleven_flash_v2_5', label: 'Flash' }],
48
+ voices: [{ id: 'voiceAAAAAAAAAAAAAAAA', label: 'Alice' }] },
49
+ ];
50
+
51
+ let home;
52
+ const servers = [];
53
+
54
+ const free = () => new Promise((resolve) => {
55
+ const s = net.createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); });
56
+ });
57
+
58
+ before(async () => {
59
+ home = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-vedit-home-'));
60
+ fs.mkdirSync(path.join(home, 'bin'), { recursive: true });
61
+ for (const tool of ['ffmpeg', 'ffprobe']) {
62
+ const found = (process.env.PATH || '').split(path.delimiter).map((d) => path.join(d, tool)).find((f) => fs.existsSync(f));
63
+ if (found) fs.symlinkSync(found, path.join(home, 'bin', tool));
64
+ }
65
+ fs.mkdirSync(path.join(home, '.claude/skills/voice-narration/scripts'), { recursive: true });
66
+ fs.writeFileSync(path.join(home, '.claude/skills/voice-narration/scripts/claude-say'),
67
+ '#!/usr/bin/env bash\necho "claude-say voices:"\necho " 1) en_US-lessac-high American English"\n');
68
+ fs.mkdirSync(path.join(root, 'core'), { recursive: true });
69
+ copyConfig(path.join(REPO, 'core/config'), path.join(root, 'core/config'));
70
+ fs.symlinkSync(path.join(REPO, 'core/scripts'), path.join(root, 'core/scripts'));
71
+ fs.symlinkSync(APP, path.join(root, 'core/workspace-app'));
72
+ for (const d of ['audio/sounds', 'videos', 'docs']) fs.mkdirSync(path.join(root, d), { recursive: true });
73
+ fs.copyFileSync(path.join(REPO, 'videos/cmd-tools-video-edit.sh'), path.join(root, 'videos/cmd-tools-video-edit.sh'));
74
+ fs.copyFileSync(path.join(REPO, 'audio/cmd-tools-voice.sh'), path.join(root, 'audio/cmd-tools-voice.sh'));
75
+ fs.copyFileSync(path.join(REPO, 'videos/cmd-tools-elevenlabs.sh'), path.join(root, 'videos/cmd-tools-elevenlabs.sh'));
76
+ fs.writeFileSync(path.join(root, 'docs/demo-voiceover-script.md'), SCRIPT);
77
+ if (CAN_RUN) {
78
+ const env = { PATH: `${path.join(home, 'bin')}:/usr/bin:/bin`, HOME: home };
79
+ await run('ffmpeg', ['-y', '-loglevel', 'error', '-f', 'lavfi', '-i', 'testsrc2=size=320x180:rate=15', '-f', 'lavfi', '-i', 'sine=frequency=440',
80
+ '-t', '6', '-c:v', 'libx264', '-preset', 'ultrafast', '-pix_fmt', 'yuv420p', '-c:a', 'aac', path.join(root, 'videos/demo.mp4')], { env });
81
+ await run('ffmpeg', ['-y', '-loglevel', 'error', '-f', 'lavfi', '-i', 'sine=frequency=220:duration=10', '-c:a', 'libmp3lame', path.join(root, 'audio/sounds/bed.mp3')], { env });
82
+ } else {
83
+ // No ffmpeg on this machine: the command tests are skipped, and the page tests read only the files' names.
84
+ fs.writeFileSync(path.join(root, 'videos/demo.mp4'), Buffer.alloc(100, 1));
85
+ fs.writeFileSync(path.join(root, 'audio/sounds/bed.mp3'), Buffer.alloc(100, 1));
86
+ }
87
+ // The poster is the row's thumbnail whichever way the video was made.
88
+ fs.writeFileSync(path.join(root, 'videos/demo-poster.jpg'), Buffer.alloc(50, 2));
89
+ });
90
+
91
+ /**
92
+ * Stop the servers this file started, then remove a tree, trying again for a moment. The server runs `ddev list` with
93
+ * HOME inside the tree, and a ddev still finishing after the server is gone puts `~/.ddev` back while the tree is
94
+ * being removed: a single rmSync then fails with ENOTEMPTY and the tree is left in /tmp.
95
+ */
96
+ async function stopAndRemove(dirs) {
97
+ await Promise.all(servers.map((s) => new Promise((resolve) => {
98
+ if (s.exitCode !== null || s.signalCode !== null) return resolve();
99
+ s.once('exit', resolve);
100
+ s.kill();
101
+ })));
102
+ for (const dir of dirs) {
103
+ for (let i = 0; i < 50; i += 1) {
104
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) { /* something was still writing */ }
105
+ await new Promise((r) => setTimeout(r, 100));
106
+ if (!fs.existsSync(dir)) break;
107
+ }
108
+ }
109
+ }
110
+
111
+ after(() => stopAndRemove([root, home]));
112
+
113
+ const env = (extra = {}) => ({ PATH: `/usr/bin:/bin:${path.dirname(process.execPath)}:${path.join(home, 'bin')}`, HOME: home, WORKSPACE_ROOT: root, ...extra });
114
+ const cli = (args) => run('bash', [path.join(root, 'videos/cmd-tools-video-edit.sh'), ...args], { env: env() });
115
+ const refused = (args) => cli(args).then(() => assert.fail('should have been refused'), (e) => e);
116
+ const seconds = async (f) => parseFloat((await run('ffprobe', ['-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', f], { env: env() })).stdout);
117
+ const videos = (n) => path.join(root, 'videos', n);
118
+ const heading = (text, id) => (text.match(new RegExp(`^## ${id} .*\\((\\S+) to (\\S+)\\)`, 'm')) || []).slice(1);
119
+
120
+ async function serve() {
121
+ const port = await free();
122
+ const proc = spawn(process.execPath, ['server.js'], { cwd: APP, env: { ...env({ ELEVENLABS_API_KEY: '' }), PORT: String(port) }, stdio: ['ignore', 'pipe', 'pipe'] });
123
+ servers.push(proc);
124
+ await new Promise((resolve, reject) => {
125
+ const t = setTimeout(() => reject(new Error('server did not start')), 15000);
126
+ proc.stdout.on('data', (d) => { if (/listening/.test(String(d))) { clearTimeout(t); resolve(); } });
127
+ proc.on('exit', (c) => { clearTimeout(t); reject(new Error(`server exited ${c}`)); });
128
+ });
129
+ return `http://127.0.0.1:${port}`;
130
+ }
131
+ const post = (base, p, form) => fetch(base + p, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(form) });
132
+
133
+ // ---- what a form may ask for ----------------------------------------------------------------------------
134
+
135
+ test('a new version is named after the video, and never takes a name that is there', () => {
136
+ assert.strictEqual(videoEdit.nextName('demo'), 'demo-v2.mp4');
137
+ assert.strictEqual(videoEdit.nextName('demo-v2'), 'demo-v2.mp4', 'a version is numbered from the video it came from');
138
+ fs.writeFileSync(videos('demo-v2.mp4'), 'x');
139
+ assert.strictEqual(videoEdit.nextName('demo'), 'demo-v3.mp4');
140
+ assert.strictEqual(videoEdit.nextName('demo-v2'), 'demo-v3.mp4', 'a version of a version is the next number, not -v2-v2');
141
+ fs.rmSync(videos('demo-v2.mp4'));
142
+ });
143
+
144
+ test('a request is checked before anything runs, and free text never reaches the arguments', () => {
145
+ const plan = (form) => videoEdit.plan({ file: 'demo.mp4', ...form }, ENGINES);
146
+ assert.match(plan({ file: 'nope.mp4', mode: 'clone' }).error, /Choose a video/);
147
+ assert.deepStrictEqual(plan({ mode: 'clone' }).args, ['--clone']);
148
+ assert.strictEqual(plan({ mode: 'clone' }).out, 'demo-v2.mp4');
149
+ assert.strictEqual(plan({ mode: 'clone', name: 'Client cut #1' }).out, 'client-cut-1.mp4');
150
+ // logo scenes and music
151
+ assert.match(plan({ mode: 'scenes' }).error, /Choose a logo scene or some music/);
152
+ assert.match(plan({ mode: 'scenes', intro_card: '30' }).error, /1 to 10 seconds/);
153
+ assert.match(plan({ mode: 'scenes', intro_music: '../../etc/passwd' }).error, /Choose music from the audio workspace/);
154
+ const scenes = plan({ mode: 'scenes', intro_card: '3', intro_title: 'Hello $(rm -rf /)', outro_card: '2', outro_music: 'sounds/bed.mp3', level: '0.8' });
155
+ assert.deepStrictEqual(scenes.args, ['--intro-card', '3', '--outro-card', '2', '--outro-music', path.join(root, 'audio/sounds/bed.mp3'), '--music-volume', '0.8']);
156
+ assert.strictEqual(scenes.strings.intro_title, 'Hello $(rm -rf /)', 'the words travel in a file');
157
+ assert.ok(!scenes.args.join(' ').includes('rm -rf'));
158
+ // pace
159
+ assert.match(plan({ mode: 'pace' }).error, /Choose a speed, a length or a trim/);
160
+ assert.match(plan({ mode: 'pace', speed: '2', length: '1:00' }).error, /not both/);
161
+ assert.match(plan({ mode: 'pace', speed: '5' }).error, /0\.25 to 4/);
162
+ assert.match(plan({ mode: 'pace', length: 'soon' }).error, /seconds or minutes:seconds/);
163
+ assert.match(plan({ mode: 'pace', trim_start: '0:40', trim_end: '0:05' }).error, /ends before it starts/);
164
+ assert.deepStrictEqual(plan({ mode: 'pace', speed: '1.5', trim_end: '0:04' }).args, ['--speed', '1.5', '--trim', '0-0:04']);
165
+ assert.deepStrictEqual(plan({ mode: 'pace', length: '1:30' }).args, ['--length', '1:30']);
166
+ // sections
167
+ assert.match(plan({ mode: 'sections' }).error, /Choose sections to remove, borrow or add/);
168
+ assert.match(plan({ mode: 'sections', rm_S09: 'yes' }).error, /not in the script/);
169
+ assert.match(plan({ mode: 'sections', rm_S01: 'yes', rm_S02: 'yes' }).error, /every section/);
170
+ assert.match(plan({ mode: 'sections', add_title: 'Only a title' }).error, /title and the words/);
171
+ assert.match(plan({ mode: 'sections', add_title: 'T', add_text: 'W', add_after: 'S77', add_voice: 'piper:en_US-lessac-high' }).error, /where the new section goes/);
172
+ assert.match(plan({ mode: 'sections', add_title: 'T', add_text: 'W', add_voice: 'piper:nobody' }).error, /Piper voices/);
173
+ assert.match(plan({ mode: 'sections', add_title: 'T', add_text: 'W', add_voice: 'elevenlabs:other' }).error, /not on this account/);
174
+ const remove = plan({ mode: 'sections', rm_S02: 'yes' });
175
+ assert.deepStrictEqual(remove.args, ['--remove', 'S02']);
176
+ assert.strictEqual(remove.spends, false);
177
+ const free = plan({ mode: 'sections', add_title: 'Extra', add_text: 'Words $HOME `x`', add_after: 'S01', add_voice: 'piper:en_US-lessac-high' });
178
+ assert.deepStrictEqual(free.args, ['--add-after', 'S01', '--engine', 'piper', '--voice', 'en_US-lessac-high']);
179
+ assert.strictEqual(free.spends, false, 'Piper is free');
180
+ const paid = plan({ mode: 'sections', add_title: 'Extra', add_text: 'Twelve chars', add_voice: 'elevenlabs:voiceAAAAAAAAAAAAAAAA' });
181
+ assert.strictEqual(paid.spends, true);
182
+ assert.strictEqual(paid.characters, 12);
183
+ assert.deepStrictEqual(paid.args.slice(-2), ['--model', 'eleven_v4']);
184
+ const flash = plan({ mode: 'sections', add_title: 'T', add_text: 'W', add_voice: 'elevenlabs:voiceAAAAAAAAAAAAAAAA', add_model: 'eleven_flash_v2_5' });
185
+ assert.deepStrictEqual(flash.args.slice(-2), ['--model', 'eleven_flash_v2_5'], 'the model chosen for an added section');
186
+ const typed = plan({ mode: 'sections', add_title: 'T', add_text: 'W', add_voice: 'elevenlabs:voiceAAAAAAAAAAAAAAAA', add_model_other: 'eleven_brand_new' });
187
+ assert.deepStrictEqual(typed.args.slice(-2), ['--model', 'eleven_brand_new']);
188
+ assert.match(plan({ mode: 'sections', add_title: 'T', add_text: 'W', add_voice: 'elevenlabs:voiceAAAAAAAAAAAAAAAA', add_model: 'nope' }).error, /Choose one of the models/);
189
+ assert.match(plan({ mode: 'sections', add_title: 'T', add_text: 'W', add_voice: 'elevenlabs:voiceAAAAAAAAAAAAAAAA', add_model_other: 'a b' }).error, /not a model id/);
190
+ });
191
+
192
+ test('the line the host runs holds no shell expansion, and only a paid section carries --confirm', () => {
193
+ const p = videoEdit.plan({ file: 'demo.mp4', mode: 'sections', add_title: 'Extra $(x)', add_text: 'Words `y` $HOME', add_voice: 'elevenlabs:voiceAAAAAAAAAAAAAAAA' }, ENGINES);
194
+ const strings = path.join(root, '.screens/voice-text/e-test/strings.json');
195
+ const plain = videoEdit.commandLine(p, strings, false);
196
+ assert.ok(!/[`$]/.test(plain), 'no backtick and no dollar sign on the line');
197
+ assert.ok(!plain.includes('Words') && !plain.includes('Extra'), 'the text is in the file, not on the line');
198
+ assert.ok(!plain.includes('--confirm'));
199
+ assert.match(plain, /"--strings-file" ".*strings\.json"/);
200
+ assert.match(plain, /"--script" ".*demo-voiceover-script\.md" "--script-out" ".*demo-v2-voiceover-script\.md"/);
201
+ assert.match(plain, /rm -rf ".*e-test"/, 'the text is removed afterwards, success or not');
202
+ assert.ok(videoEdit.commandLine(p, strings, true).includes('"--confirm"'));
203
+ assert.ok(!videoEdit.commandLine(videoEdit.plan({ file: 'demo.mp4', mode: 'clone' }, ENGINES), null, true).includes('--confirm'));
204
+ });
205
+
206
+ // ---- the modal and the page -------------------------------------------------------------------------------
207
+
208
+ test('the modal has a tab for each kind of change, and fields that belong to the tab', () => {
209
+ const form = (mode) => videoEdit.formHtml('demo.mp4', mode, { engines: ENGINES });
210
+ for (const [mode, label] of [['voice', 'Voice'], ['scenes', 'Logo and music'], ['pace', 'Speed and length'], ['sections', 'Sections'], ['clone', 'Clone']]) {
211
+ const html = form(mode);
212
+ assert.match(html, new RegExp(`hx-get="/fragments/videos/edit\\?file=demo\\.mp4&amp;mode=${mode}"`), `${mode} tab`);
213
+ assert.ok(html.includes(label), label);
214
+ assert.ok(html.includes('<li class="uk-active"><a href="#" aria-current="true" hx-get="/fragments/videos/edit?file=demo.mp4&amp;mode=' + mode), `${mode} is the active tab`);
215
+ assert.ok(html.includes('<div id="voice-result"'), `${mode} keeps its result in the modal`);
216
+ }
217
+ const voiceTab = form('voice');
218
+ assert.match(voiceTab, /<option value="demo\.mp4" selected>/, 'the video is chosen');
219
+ assert.match(voiceTab, /<option value="demo-voiceover-script\.md" selected>/, 'and so is its script');
220
+ assert.match(form('scenes'), /name="intro_card"[\s\S]*name="outro_card"[\s\S]*<option value="sounds\/bed\.mp3">/);
221
+ assert.match(form('pace'), /name="speed"[\s\S]*name="length"[\s\S]*name="trim_start"[\s\S]*name="trim_end"/);
222
+ const sections = form('sections');
223
+ assert.match(sections, /name="rm_S01"[\s\S]*name="rm_S02"/);
224
+ assert.match(sections, /<option value="after-S01"|<option value="S01">After S01 Opening/);
225
+ assert.match(sections, /<optgroup label="ElevenLabs — uses credits">[\s\S]*<optgroup label="Piper — free, on this machine">/);
226
+ assert.match(form('clone'), /Makes a copy of the video, its poster and its script/);
227
+ assert.match(form('nonsense'), /Voice over a video|Edit a video/, 'an unknown tab is the voice tab');
228
+ assert.match(videoEdit.formHtml('', 'pace', { engines: [] }), /<option value="demo\.mp4" selected>/, 'no video given: the newest');
229
+ });
230
+
231
+ test('each video row offers Edit, and the rail offers Edit a video', async () => {
232
+ const base = await serve();
233
+ const page = await (await fetch(`${base}/videos`)).text();
234
+ assert.match(page, /hx-get="\/fragments\/videos\/edit\?mode=voice" hx-target="#editor-modal-body" hx-swap="innerHTML"/, 'the rail');
235
+ assert.ok(page.includes('Edit a video'));
236
+ assert.ok(page.includes('tabler-sprite.svg#cut"'), 'Edit a video has its own icon, apart from Notes');
237
+ const rows = await (await fetch(`${base}/fragments/videos/items`)).text();
238
+ assert.match(rows, /class="uk-button uk-button-default uk-button-small fx-open fx-edit"/, 'the row button');
239
+ assert.match(rows, /hx-get="\/fragments\/videos\/edit\?file=demo\.mp4&amp;mode=voice"/);
240
+ for (const mode of ['voice', 'scenes', 'pace', 'sections', 'clone']) assert.ok(rows.includes(`mode=${mode}"`), `${mode} is in the row's menu`);
241
+ const audio = await (await fetch(`${base}/fragments/audio/items`)).text();
242
+ assert.ok(!audio.includes('fx-edit'), 'only videos are edited');
243
+ const modal = await fetch(`${base}/fragments/videos/edit?file=demo.mp4&mode=pace`);
244
+ assert.strictEqual(modal.status, 200);
245
+ assert.match(await modal.text(), /name="trim_start"/);
246
+ });
247
+
248
+ // ---- the action --------------------------------------------------------------------------------------------
249
+
250
+ const screens = () => path.join(root, '.screens');
251
+ function hostRuns(asked) {
252
+ fs.mkdirSync(path.join(screens(), 'queue'), { recursive: true });
253
+ fs.mkdirSync(path.join(screens(), 'results'), { recursive: true });
254
+ return setInterval(() => {
255
+ fs.writeFileSync(path.join(screens(), 'host-sessions.json'), JSON.stringify({ at: Date.now(), host: 'test', sessions: [] }));
256
+ for (const f of fs.readdirSync(path.join(screens(), 'queue'))) {
257
+ if (!f.endsWith('.json')) continue;
258
+ const req = JSON.parse(fs.readFileSync(path.join(screens(), 'queue', f), 'utf8'));
259
+ fs.unlinkSync(path.join(screens(), 'queue', f));
260
+ asked.push(req);
261
+ fs.writeFileSync(path.join(screens(), 'results', `${req.id}.json`), JSON.stringify({ ok: true }));
262
+ }
263
+ }, 80);
264
+ }
265
+
266
+ test('the action refuses what the form should never send, and asks before a paid section', async () => {
267
+ const asked = [];
268
+ const runner = hostRuns(asked);
269
+ try {
270
+ const base = await serve();
271
+ const send = (form) => post(base, '/actions/video-edit', { workspace: 'videos', file: 'demo.mp4', ...form });
272
+ const other = await post(base, '/actions/video-edit', { workspace: 'dev', file: 'demo.mp4', mode: 'clone' });
273
+ assert.strictEqual(other.status, 400);
274
+ const none = await send({ mode: 'scenes' });
275
+ assert.strictEqual(none.status, 400);
276
+ assert.match(await none.text(), /Choose a logo scene or some music/);
277
+ assert.strictEqual((await send({ mode: 'clone', file: '../../etc/passwd' })).status, 400);
278
+ assert.strictEqual(asked.length, 0, 'nothing was started by any of those');
279
+ const ask = await send({ mode: 'sections', add_title: 'Extra', add_text: 'Spoken words', add_voice: 'elevenlabs:voiceAAAAAAAAAAAAAAAA' });
280
+ assert.strictEqual(ask.status, 400, 'with no key here the paid voice is not on offer');
281
+ // a free section runs, and its progress says it is an edit
282
+ const ok = await send({ mode: 'clone', name: 'press-test' });
283
+ assert.strictEqual(ok.status, 200);
284
+ const html = await ok.text();
285
+ assert.match(html, /class="voice-progress" hx-get="\/fragments\/audio\/generation\?kind=video&amp;file=press-test\.mp4&amp;session=videos--press-test--edit&amp;t0=\d+&amp;engine=&amp;edit=1"/);
286
+ assert.match(html, /Making <strong>press-test\.mp4<\/strong> <span/, 'no engine named: it is an edit');
287
+ assert.strictEqual(asked.length, 1);
288
+ assert.match(asked[0].cmd, /bash cmd-tools-video-edit\.sh "-a" "edit" "--video" ".*videos\/demo\.mp4" "--out" ".*videos\/press-test\.mp4" "--clone"/);
289
+ assert.ok(!asked[0].cmd.includes('--confirm'));
290
+ } finally { clearInterval(runner); }
291
+ });
292
+
293
+ test('when the new version is there the modal shows it and offers to edit it again', async () => {
294
+ const base = await serve();
295
+ const q = { kind: 'video', file: 'demo-v9.mp4', session: 'videos--demo-v9--edit', t0: String(Date.now() - 20000), engine: '', edit: '1' };
296
+ fs.writeFileSync(path.join(screens(), 'host-sessions.json'), JSON.stringify({ at: Date.now(), host: 'test', sessions: [{ session: 'videos--demo-v9--edit' }] }));
297
+ const running = await (await fetch(`${base}/fragments/audio/generation?${new URLSearchParams(q)}`)).text();
298
+ assert.match(running, /Making <strong>demo-v9\.mp4<\/strong> <span/);
299
+ assert.match(running, /edit=1/, 'the next poll still knows it is an edit');
300
+ fs.writeFileSync(videos('demo-v9.mp4'), Buffer.alloc(100, 1));
301
+ fs.writeFileSync(path.join(screens(), 'host-sessions.json'), JSON.stringify({ at: Date.now(), host: 'test', sessions: [] }));
302
+ const done = await fetch(`${base}/fragments/audio/generation?${new URLSearchParams(q)}`);
303
+ assert.strictEqual(done.headers.get('hx-trigger'), 'refresh-projects');
304
+ const html = await done.text();
305
+ assert.match(html, /<strong>demo-v9\.mp4<\/strong> is ready, in <code>videos\/<\/code>/);
306
+ assert.match(html, /<video class="video-player"[^>]*src="\/files\/videos\/demo-v9\.mp4">/);
307
+ assert.match(html, /hx-get="\/fragments\/videos\/edit\?file=demo-v9\.mp4&amp;mode=pace"[^>]*>Edit this version</);
308
+ assert.ok(!/Make another/.test(html));
309
+ fs.rmSync(videos('demo-v9.mp4'));
310
+ });
311
+
312
+ // ---- the command ---------------------------------------------------------------------------------------------
313
+
314
+ test('the command refuses an edit that changes nothing, a name that is taken and a section that is not there', { skip: !CAN_RUN }, async () => {
315
+ const base = ['-a', 'edit', '--video', videos('demo.mp4'), '--script', path.join(root, 'docs/demo-voiceover-script.md')];
316
+ assert.match((await refused([...base, '--out', videos('x1.mp4')])).stderr, /Nothing to change/);
317
+ assert.match((await refused([...base, '--out', videos('x2.mp4'), '--remove', 'S99'])).stderr, /no section "S99". It has: S01, S02/);
318
+ assert.match((await refused([...base, '--out', videos('x3.mp4'), '--speed', '9'])).stderr, /Speed goes from 0\.25x to 4x/);
319
+ assert.match((await refused([...base, '--out', videos('x4.mp4'), '--length', '1'])).stderr, /would play at 6\.00x/);
320
+ assert.match((await refused([...base, '--out', videos('demo.mp4'), '--clone'])).stderr, /already exists/);
321
+ assert.ok(!fs.existsSync(videos('x1.mp4')) && !fs.existsSync(videos('x2.mp4')));
322
+ });
323
+
324
+ test('speed changes the length and the script follows; the original is untouched', { skip: !CAN_RUN }, async () => {
325
+ const before = fs.readFileSync(videos('demo.mp4'));
326
+ const script = path.join(root, 'docs/demo-voiceover-script.md');
327
+ const { stdout } = await cli(['-a', 'edit', '--video', videos('demo.mp4'), '--out', videos('fast.mp4'), '--script', script, '--script-out', path.join(root, 'docs/fast-voiceover-script.md'), '--speed', '2']);
328
+ assert.match(stdout, /Wrote .*fast\.mp4 \(00:03\.0\)/);
329
+ assert.ok(Math.abs(await seconds(videos('fast.mp4')) - 3) < 0.25, 'twice as fast is half as long');
330
+ const text = fs.readFileSync(path.join(root, 'docs/fast-voiceover-script.md'), 'utf8');
331
+ assert.deepStrictEqual(heading(text, 'S01'), ['00:00.0', '00:01.5']);
332
+ assert.deepStrictEqual(heading(text, 'S02'), ['00:01.5', '00:03.0']);
333
+ assert.match(text, /## How to use this script\n\nNot a timed section\./, 'what is not a timed section stays');
334
+ assert.ok(before.equals(fs.readFileSync(videos('demo.mp4'))), 'the source is byte for byte what it was');
335
+ assert.ok(fs.existsSync(videos('fast-poster.jpg')), 'the poster comes along');
336
+ const record = JSON.parse(fs.readFileSync(videos('fast-edit.json'), 'utf8'));
337
+ assert.deepStrictEqual([record.kind, record.source, record.changes], ['video-edit', 'demo.mp4', ['at 2.00x speed']]);
338
+ });
339
+
340
+ test('a length, a trim and a section removed each leave a script that matches the picture', { skip: !CAN_RUN }, async () => {
341
+ const script = path.join(root, 'docs/demo-voiceover-script.md');
342
+ const edit = (name, more) => cli(['-a', 'edit', '--video', videos('demo.mp4'), '--out', videos(`${name}.mp4`), '--script', script, '--script-out', path.join(root, `docs/${name}-voiceover-script.md`), ...more]);
343
+ await edit('long', ['--length', '0:12']);
344
+ assert.ok(Math.abs(await seconds(videos('long.mp4')) - 12) < 0.3, 'made exactly that long');
345
+ assert.deepStrictEqual(heading(fs.readFileSync(path.join(root, 'docs/long-voiceover-script.md'), 'utf8'), 'S02'), ['00:06.0', '00:12.0']);
346
+ await edit('cut', ['--remove', 'S01']);
347
+ assert.ok(Math.abs(await seconds(videos('cut.mp4')) - 3) < 0.25);
348
+ const cut = fs.readFileSync(path.join(root, 'docs/cut-voiceover-script.md'), 'utf8');
349
+ assert.ok(!/## S01/.test(cut) && !cut.includes('The opening.'), 'the section and its words are gone');
350
+ assert.deepStrictEqual(heading(cut, 'S02'), ['00:00.0', '00:03.0'], 'the next one moved up to the start');
351
+ await edit('trim', ['--trim', '0:02-0:05']);
352
+ assert.ok(Math.abs(await seconds(videos('trim.mp4')) - 3) < 0.25);
353
+ const trim = fs.readFileSync(path.join(root, 'docs/trim-voiceover-script.md'), 'utf8');
354
+ assert.deepStrictEqual(heading(trim, 'S01'), ['00:00.0', '00:01.0'], 'a section cut in part is shortened');
355
+ assert.deepStrictEqual(heading(trim, 'S02'), ['00:01.0', '00:03.0']);
356
+ });
357
+
358
+ test('a plain clone is a byte for byte copy with its poster and script', { skip: !CAN_RUN }, async () => {
359
+ await cli(['-a', 'edit', '--video', videos('demo.mp4'), '--out', videos('copy.mp4'), '--script', path.join(root, 'docs/demo-voiceover-script.md'), '--script-out', path.join(root, 'docs/copy-voiceover-script.md'), '--clone']);
360
+ assert.ok(fs.readFileSync(videos('demo.mp4')).equals(fs.readFileSync(videos('copy.mp4'))));
361
+ assert.strictEqual(fs.readFileSync(path.join(root, 'docs/copy-voiceover-script.md'), 'utf8'), SCRIPT);
362
+ assert.ok(fs.existsSync(videos('copy-poster.jpg')));
363
+ });
364
+
365
+ test('the Webship logo scenes and music add their length, and the script moves later by the opening', { skip: !CAN_CARD }, async () => {
366
+ const strings = path.join(root, 'strings.json');
367
+ fs.writeFileSync(strings, JSON.stringify({ intro_title: 'Hello $(x) `y`', intro_subtitle: 'A "quoted" line', outro_title: 'Thank you' }));
368
+ const script = path.join(root, 'docs/demo-voiceover-script.md');
369
+ await cli(['-a', 'edit', '--video', videos('demo.mp4'), '--out', videos('scenes.mp4'), '--script', script, '--script-out', path.join(root, 'docs/scenes-voiceover-script.md'),
370
+ '--intro-card', '2', '--outro-card', '2', '--intro-music', path.join(root, 'audio/sounds/bed.mp3'), '--outro-music', path.join(root, 'audio/sounds/bed.mp3'), '--strings-file', strings]);
371
+ assert.ok(Math.abs(await seconds(videos('scenes.mp4')) - 10) < 0.3, 'six seconds and two scenes of two');
372
+ const text = fs.readFileSync(path.join(root, 'docs/scenes-voiceover-script.md'), 'utf8');
373
+ assert.deepStrictEqual(heading(text, 'S01'), ['00:02.0', '00:05.0']);
374
+ assert.deepStrictEqual(heading(text, 'S02'), ['00:05.0', '00:08.0']);
375
+ const { stdout } = await run('ffprobe', ['-v', 'error', '-show_entries', 'stream=codec_type,width,height', '-of', 'csv=p=0', videos('scenes.mp4')], { env: env() });
376
+ assert.match(stdout, /video,320,180/, 'the scenes are drawn at the size of the video');
377
+ assert.match(stdout, /audio/);
378
+ const record = JSON.parse(fs.readFileSync(videos('scenes-edit.json'), 'utf8'));
379
+ assert.deepStrictEqual(record.changes, ['opens with the Webship logo', 'closes with the Webship logo', 'intro music', 'outro music']);
380
+ });
381
+
382
+ test('a section added after another lands where it was asked, and an ElevenLabs one stops at its plan', { skip: !CAN_RUN }, async () => {
383
+ const py = async (code) => JSON.parse((await run('python3', ['-B', '-I', '-c', `import sys, json; sys.path.insert(0, ${JSON.stringify(path.join(REPO, 'core/scripts/libs'))})\nimport video_edit as v\n${code}`])).stdout);
384
+ const out = await py(`
385
+ pieces = v.source_pieces(6.0, [], 1.0)
386
+ a, b = v.Piece('source', 0.0, 3.0), v.Piece('source', 3.0, 6.0)
387
+ card = v.Piece('card', seconds=2.0, title='Extra')
388
+ pieces = [a, card, b]
389
+ v.lay_out(pieces)
390
+ text = v.retime_script(${JSON.stringify(SCRIPT)}, pieces, [{'after': 'S01', 'id': 'N01', 'title': 'Extra', 'text': 'Added words.', 'card': card}])
391
+ print(json.dumps(text))`);
392
+ assert.deepStrictEqual(heading(out, 'S01'), ['00:00.0', '00:03.0']);
393
+ assert.deepStrictEqual(heading(out, 'N01'), ['00:03.0', '00:05.0']);
394
+ assert.deepStrictEqual(heading(out, 'S02'), ['00:05.0', '00:08.0']);
395
+ assert.ok(out.indexOf('## N01') > out.indexOf('## S01') && out.indexOf('## N01') < out.indexOf('## S02'));
396
+ assert.match(out, /## N01 Extra[^\n]*\n> Added words\./);
397
+ const plan = await refused(['-a', 'edit', '--video', videos('demo.mp4'), '--out', videos('el.mp4'), '--add-title', 'X', '--add-text', 'Hello there', '--engine', 'elevenlabs', '--voice', 'abc']);
398
+ assert.strictEqual(plan.code, 4, 'plan only');
399
+ assert.match(plan.stdout, /would send 11 characters to ElevenLabs/);
400
+ assert.ok(!fs.existsSync(videos('el.mp4')), 'and nothing was made');
401
+ });
402
+
403
+ test('the tests work on a copy of the config, whatever the machine\'s own settings hold', () => {
404
+ const used = fs.readFileSync(path.join(root, 'core/config/settings.yml'), 'utf8');
405
+ assert.ok(!/api_key: sk_/.test(used.slice(used.indexOf('elevenlabs:'))));
406
+ assert.ok(voice.ID_RE.test('voiceAAAAAAAAAAAAAAAA'));
407
+ });
408
+
409
+ // ---- what the videos list holds ---------------------------------------------------------------------------
410
+
411
+ test('the videos list holds only videos: a script beside one is read from its card, not listed', async () => {
412
+ const w = (name, body = 'x') => fs.writeFileSync(videos(name), body);
413
+ w('walk.mp4', 'v');
414
+ w('walk-voiceover-script.md', '# Walk\n\n## W01 Start (00:00.0 to 00:02.0)\n\n> Words in a script saved beside its video.\n');
415
+ w('stray-notes.md', '# notes');
416
+ w('stray.png', 'p');
417
+ const base = await serve();
418
+ const rows = await (await fetch(`${base}/fragments/videos/items`)).text();
419
+ const names = [...rows.matchAll(/class="fx-name" title="([^"]*)"/g)].map((m) => m[1]);
420
+ assert.ok(names.includes('demo.mp4') && names.includes('walk.mp4'), 'the videos are listed');
421
+ assert.deepStrictEqual(names.filter((n) => !/\.(mp4|webm|mov)$/.test(n)), [], 'and nothing else is');
422
+ assert.ok(!rows.includes('walk-voiceover-script') || rows.includes('Read the voiceover script: walk-voiceover-script.md'), 'the script is only named by its button');
423
+ assert.ok(!/item-row-body|stray-notes/.test(rows), 'no markdown card for a script or a note');
424
+ const walk = rows.slice(rows.indexOf('title="walk.mp4"'));
425
+ assert.match(walk.slice(0, walk.indexOf('fx-actions') + 900), /fx-transcript/, 'the script is one click from its card');
426
+ assert.match(rows, /<img src="\/files\/videos\/demo-poster\.jpg"/, 'a poster is still the thumbnail');
427
+ assert.ok(!rows.includes('stray.png'), 'a picture that is nobody\'s poster is not a video');
428
+ const words = await fetch(`${base}/fragments/videos/transcript/walk.mp4`);
429
+ assert.strictEqual(words.status, 200);
430
+ const html = await words.text();
431
+ assert.match(html, /Words in a script saved beside its video\./);
432
+ assert.match(html, /href="\/files\/videos\/walk-voiceover-script\.md"/, 'it links to where the script is');
433
+ const docsScript = await (await fetch(`${base}/fragments/videos/transcript/demo.mp4`)).text();
434
+ assert.match(docsScript, /href="\/files\/docs\/demo-voiceover-script\.md"/, 'one in docs/ still links there');
435
+ for (const n of ['walk.mp4', 'walk-voiceover-script.md', 'stray-notes.md', 'stray.png']) fs.rmSync(videos(n), { force: true });
436
+ });