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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (630) hide show
  1. package/LICENSE +339 -0
  2. package/README.md +28 -364
  3. package/core/bin/workspace.js +135 -0
  4. package/core/config/settings.labels.yml +514 -0
  5. package/core/config/settings.template.yml +292 -0
  6. package/core/config/style.yml +53 -0
  7. package/core/config/theme.default.yml +161 -0
  8. package/core/config/theme.dispatch.yml +166 -0
  9. package/core/config/theme.drupal.yml +175 -0
  10. package/core/config/theme.ember.yml +160 -0
  11. package/core/config/theme.github.yml +172 -0
  12. package/core/config/theme.gitlab.yml +168 -0
  13. package/core/config/theme.press.yml +161 -0
  14. package/core/config/theme.slate.yml +160 -0
  15. package/core/config/theme.upsun.yml +174 -0
  16. package/core/docs/architecture.svg +65 -0
  17. package/core/docs/getting-started.md +51 -0
  18. package/core/docs/plugins.md +27 -0
  19. package/core/docs/releases.md +23 -0
  20. package/core/docs/testing.md +15 -0
  21. package/core/docs/workspaces.md +25 -0
  22. package/core/scripts/args/arg-ai-items.sh +65 -0
  23. package/core/scripts/args/arg-autostart.sh +31 -0
  24. package/core/scripts/args/arg-avatar-agent.sh +28 -0
  25. package/core/scripts/args/arg-avatar-catalog.sh +12 -0
  26. package/core/scripts/args/arg-avatar-character.sh +68 -0
  27. package/core/scripts/args/arg-avatar-voice.sh +24 -0
  28. package/core/scripts/args/arg-avatar.sh +22 -0
  29. package/core/scripts/args/arg-checks.sh +36 -0
  30. package/core/scripts/args/arg-clone.sh +42 -0
  31. package/core/scripts/args/arg-commands.sh +51 -0
  32. package/core/scripts/args/arg-cucumber.sh +34 -0
  33. package/core/scripts/args/arg-distribution.sh +30 -0
  34. package/core/scripts/args/arg-drupal.sh +39 -0
  35. package/core/scripts/args/arg-drupal_cms.sh +18 -0
  36. package/core/scripts/args/arg-gitlab-ci-local.sh +25 -0
  37. package/core/scripts/args/arg-graphify.sh +36 -0
  38. package/core/scripts/args/arg-graphs.sh +12 -0
  39. package/core/scripts/args/arg-hostnames.sh +23 -0
  40. package/core/scripts/args/arg-milvus-backup.sh +8 -0
  41. package/core/scripts/args/arg-milvus-mcp.sh +12 -0
  42. package/core/scripts/args/arg-milvus.sh +32 -0
  43. package/core/scripts/args/arg-obsidian.sh +17 -0
  44. package/core/scripts/args/arg-odoo.sh +18 -0
  45. package/core/scripts/args/arg-penpot-backup.sh +8 -0
  46. package/core/scripts/args/arg-penpot-mcp.sh +15 -0
  47. package/core/scripts/args/arg-penpot-token.sh +17 -0
  48. package/core/scripts/args/arg-penpot.sh +44 -0
  49. package/core/scripts/args/arg-rag.sh +21 -0
  50. package/core/scripts/args/arg-ragify.sh +35 -0
  51. package/core/scripts/args/arg-reclaim.sh +18 -0
  52. package/core/scripts/args/arg-release.sh +21 -0
  53. package/core/scripts/args/arg-screen.sh +28 -0
  54. package/core/scripts/args/arg-site-template.sh +13 -0
  55. package/core/scripts/args/arg-spdd.sh +16 -0
  56. package/core/scripts/args/arg-specs.sh +22 -0
  57. package/core/scripts/args/arg-storage.sh +36 -0
  58. package/core/scripts/args/arg-testing.sh +36 -0
  59. package/core/scripts/args/arg-webship.sh +34 -0
  60. package/core/scripts/args/arg-webships-project.sh +22 -0
  61. package/core/scripts/args/arg-webships-site-template.sh +17 -0
  62. package/core/scripts/args/arg-webships.sh +34 -0
  63. package/core/scripts/args/arg-website-site-template.sh +17 -0
  64. package/core/scripts/args/arg-website.sh +25 -0
  65. package/core/scripts/args/arg-wordpress.sh +18 -0
  66. package/core/scripts/autostart-run.sh +162 -0
  67. package/core/scripts/autostart-stop.sh +30 -0
  68. package/core/scripts/autostart-watch.sh +43 -0
  69. package/core/scripts/autostart.sh +26 -0
  70. package/core/scripts/bootstrap-functions.sh +117 -0
  71. package/core/scripts/bootstrap-libraries.sh +10 -0
  72. package/core/scripts/bootstrap.sh +71 -0
  73. package/core/scripts/ddev-shim/ddev +122 -0
  74. package/core/scripts/docker-gid.sh +119 -0
  75. package/core/scripts/functions/arg-site-template.sh +28 -0
  76. package/core/scripts/functions/fun-ai-items-lock.sh +175 -0
  77. package/core/scripts/functions/fun-ai-items-sources.sh +298 -0
  78. package/core/scripts/functions/fun-ai-items.sh +621 -0
  79. package/core/scripts/functions/fun-ai-providers.sh +72 -0
  80. package/core/scripts/functions/fun-autostart.sh +429 -0
  81. package/core/scripts/functions/fun-avatar.sh +441 -0
  82. package/core/scripts/functions/fun-backup-item.sh +50 -0
  83. package/core/scripts/functions/fun-build-distribution.sh +226 -0
  84. package/core/scripts/functions/fun-build-site-template.sh +104 -0
  85. package/core/scripts/functions/fun-build-webships.sh +161 -0
  86. package/core/scripts/functions/fun-build-website.sh +199 -0
  87. package/core/scripts/functions/fun-checks.sh +243 -0
  88. package/core/scripts/functions/fun-clone-project.sh +289 -0
  89. package/core/scripts/functions/fun-commands.sh +521 -0
  90. package/core/scripts/functions/fun-database.sh +63 -0
  91. package/core/scripts/functions/fun-ddev-hostnames.sh +213 -0
  92. package/core/scripts/functions/fun-ddev-nodejs.sh +15 -0
  93. package/core/scripts/functions/fun-ddev-telemetry.sh +15 -0
  94. package/core/scripts/functions/fun-default-settings.sh +28 -0
  95. package/core/scripts/functions/fun-distribution-cucumber.sh +45 -0
  96. package/core/scripts/functions/fun-distribution-drupal.sh +25 -0
  97. package/core/scripts/functions/fun-distribution-drupal_cms.sh +35 -0
  98. package/core/scripts/functions/fun-distribution-webship.sh +28 -0
  99. package/core/scripts/functions/fun-distribution-webships.sh +33 -0
  100. package/core/scripts/functions/fun-distribution-website.sh +19 -0
  101. package/core/scripts/functions/fun-drush.sh +48 -0
  102. package/core/scripts/functions/fun-elevenlabs.sh +179 -0
  103. package/core/scripts/functions/fun-files.sh +19 -0
  104. package/core/scripts/functions/fun-git-worktrees.sh +142 -0
  105. package/core/scripts/functions/fun-gitlab-ci.sh +156 -0
  106. package/core/scripts/functions/fun-hostnames-cli.sh +88 -0
  107. package/core/scripts/functions/fun-milvus.sh +584 -0
  108. package/core/scripts/functions/fun-odoo.sh +121 -0
  109. package/core/scripts/functions/fun-penpot.sh +724 -0
  110. package/core/scripts/functions/fun-project-name.sh +20 -0
  111. package/core/scripts/functions/fun-rag.sh +253 -0
  112. package/core/scripts/functions/fun-ragify.sh +165 -0
  113. package/core/scripts/functions/fun-reclaim.sh +297 -0
  114. package/core/scripts/functions/fun-release.sh +159 -0
  115. package/core/scripts/functions/fun-remove-project.sh +319 -0
  116. package/core/scripts/functions/fun-screen.sh +96 -0
  117. package/core/scripts/functions/fun-spdd.sh +676 -0
  118. package/core/scripts/functions/fun-specs-share.sh +237 -0
  119. package/core/scripts/functions/fun-storage.sh +347 -0
  120. package/core/scripts/functions/fun-testing.sh +329 -0
  121. package/core/scripts/functions/fun-users.sh +87 -0
  122. package/core/scripts/functions/fun-version.sh +72 -0
  123. package/core/scripts/functions/fun-video-edit.sh +48 -0
  124. package/core/scripts/functions/fun-voice.sh +45 -0
  125. package/core/scripts/functions/fun-wordpress.sh +59 -0
  126. package/core/scripts/host-mounts.sh +161 -0
  127. package/core/scripts/install.sh +602 -0
  128. package/core/scripts/libs/__pycache__/video_edit.cpython-312.pyc +0 -0
  129. package/core/scripts/libs/argparse.sh +93 -0
  130. package/core/scripts/libs/bash-yaml.sh +52 -0
  131. package/core/scripts/libs/checks_plan.js +312 -0
  132. package/core/scripts/libs/elevenlabs.py +627 -0
  133. package/core/scripts/libs/progress-bar.sh +19 -0
  134. package/core/scripts/libs/rag_index.py +446 -0
  135. package/core/scripts/libs/video_edit.py +636 -0
  136. package/core/scripts/libs/voice.py +430 -0
  137. package/core/scripts/plugins/jira/host-install.sh +119 -0
  138. package/core/scripts/plugins/jira/host-mounts.txt +2 -0
  139. package/core/scripts/plugins/upsun/args/arg-upsun.sh +51 -0
  140. package/core/scripts/plugins/upsun/functions/fun-upsun.sh +304 -0
  141. package/core/scripts/plugins/upsun/host-install.sh +99 -0
  142. package/core/scripts/plugins/upsun/host-mounts.txt +1 -0
  143. package/core/scripts/plugins/upsun/upsun-ai.sh +133 -0
  144. package/core/scripts/plugins.sh +152 -0
  145. package/core/scripts/release.sh +69 -0
  146. package/core/scripts/storage.sh +39 -0
  147. package/core/scripts/sync-tooling.sh +246 -0
  148. package/core/scripts/tests/cmd-smoke-test.sh +109 -0
  149. package/core/scripts/trust-https.sh +257 -0
  150. package/core/workspace-app/.ddev/commands/web/test-dashboard +49 -0
  151. package/core/workspace-app/.ddev/config.yaml +400 -0
  152. package/core/workspace-app/.ddev/docker-compose.dood.yaml +45 -0
  153. package/core/workspace-app/.ddev/docker-compose.plugin-jira-twg.yaml +26 -0
  154. package/core/workspace-app/.ddev/docker-compose.plugin-upsun-upsun-cli.yaml +8 -0
  155. package/core/workspace-app/.ddev/docker-compose.screens.yaml +15 -0
  156. package/core/workspace-app/.ddev/nginx_full/nginx-site.conf +114 -0
  157. package/core/workspace-app/.ddev/web-build/Dockerfile.workspace-app +203 -0
  158. package/core/workspace-app/.ddev/web-build/ddev-shim-ddev +122 -0
  159. package/core/workspace-app/README.md +380 -0
  160. package/{actions → core/workspace-app/actions}/ddev.js +0 -1
  161. package/{actions → core/workspace-app/actions}/rag.js +0 -1
  162. package/{actions → core/workspace-app/actions}/testing.js +0 -1
  163. package/{assistant.js → core/workspace-app/assistant.js} +1 -10
  164. package/{git-info.js → core/workspace-app/git-info.js} +17 -1
  165. package/core/workspace-app/package.json +29 -0
  166. package/core/workspace-app/plugin-catalog.js +50 -0
  167. package/{plugins-page.js → core/workspace-app/plugins-page.js} +28 -2
  168. package/{server.js → core/workspace-app/server.js} +18 -0
  169. package/core/workspace-app/tests/features/actions-rail.feature +44 -0
  170. package/core/workspace-app/tests/features/ai-items.feature +23 -0
  171. package/core/workspace-app/tests/features/assistant-speech.feature +63 -0
  172. package/core/workspace-app/tests/features/checks.feature +39 -0
  173. package/core/workspace-app/tests/features/clone-project.feature +30 -0
  174. package/core/workspace-app/tests/features/command-authoring.feature +58 -0
  175. package/core/workspace-app/tests/features/dashboard.feature +57 -0
  176. package/core/workspace-app/tests/features/menus.feature +29 -0
  177. package/core/workspace-app/tests/features/plugins/jira/jira.feature +47 -0
  178. package/core/workspace-app/tests/features/plugins.feature +20 -0
  179. package/core/workspace-app/tests/features/project-actions.feature +40 -0
  180. package/core/workspace-app/tests/features/screens.feature +29 -0
  181. package/core/workspace-app/tests/features/settings.feature +46 -0
  182. package/core/workspace-app/tests/features/test-reports.feature +32 -0
  183. package/core/workspace-app/tests/features/theme.feature +40 -0
  184. package/core/workspace-app/tests/step-definitions/actions-post.steps.js +25 -0
  185. package/core/workspace-app/tests/step-definitions/assistant-speech.steps.js +152 -0
  186. package/core/workspace-app/tests/step-definitions/preconditions.steps.js +100 -0
  187. package/core/workspace-app/tests/unit/_isolated-config.js +31 -0
  188. package/core/workspace-app/tests/unit/accessibility.test.js +124 -0
  189. package/core/workspace-app/tests/unit/actions-rail-ux.test.js +56 -0
  190. package/core/workspace-app/tests/unit/actions-rail.test.js +51 -0
  191. package/core/workspace-app/tests/unit/ai-inventory.test.js +64 -0
  192. package/core/workspace-app/tests/unit/app-docs.test.js +64 -0
  193. package/core/workspace-app/tests/unit/assistant-avatar.test.js +110 -0
  194. package/core/workspace-app/tests/unit/assistant-cast.test.js +105 -0
  195. package/core/workspace-app/tests/unit/assistant-send-animation.test.js +15 -0
  196. package/core/workspace-app/tests/unit/assistant-suggestions.test.js +112 -0
  197. package/core/workspace-app/tests/unit/assistant-switches.test.js +53 -0
  198. package/core/workspace-app/tests/unit/assistant-views.test.js +146 -0
  199. package/core/workspace-app/tests/unit/assistant-working-indicator.test.js +49 -0
  200. package/core/workspace-app/tests/unit/assistant.test.js +61 -0
  201. package/core/workspace-app/tests/unit/audio-player.test.js +147 -0
  202. package/core/workspace-app/tests/unit/audio-voice.test.js +634 -0
  203. package/core/workspace-app/tests/unit/auth-google.test.js +180 -0
  204. package/core/workspace-app/tests/unit/autostart-watch.test.js +76 -0
  205. package/core/workspace-app/tests/unit/browsers.test.js +76 -0
  206. package/core/workspace-app/tests/unit/build-distribution.test.js +125 -0
  207. package/core/workspace-app/tests/unit/build-order.test.js +153 -0
  208. package/core/workspace-app/tests/unit/builder-args.test.js +113 -0
  209. package/core/workspace-app/tests/unit/builders.test.js +158 -0
  210. package/core/workspace-app/tests/unit/card-state-toggle.test.js +29 -0
  211. package/core/workspace-app/tests/unit/changelog.test.js +177 -0
  212. package/core/workspace-app/tests/unit/chat-thread.test.js +92 -0
  213. package/core/workspace-app/tests/unit/checks-plan.test.js +205 -0
  214. package/core/workspace-app/tests/unit/claude-args.test.js +182 -0
  215. package/core/workspace-app/tests/unit/claude-code-here.test.js +15 -0
  216. package/core/workspace-app/tests/unit/claude-resume.test.js +147 -0
  217. package/core/workspace-app/tests/unit/claude-screen-options.test.js +84 -0
  218. package/core/workspace-app/tests/unit/claude-session.test.js +41 -0
  219. package/core/workspace-app/tests/unit/command-flags.test.js +45 -0
  220. package/core/workspace-app/tests/unit/command-palette.test.js +105 -0
  221. package/core/workspace-app/tests/unit/commands-center.test.js +338 -0
  222. package/core/workspace-app/tests/unit/composer-menu.test.js +187 -0
  223. package/core/workspace-app/tests/unit/config-yaml.test.js +116 -0
  224. package/core/workspace-app/tests/unit/confirm-dialog.test.js +69 -0
  225. package/core/workspace-app/tests/unit/create-card.test.js +85 -0
  226. package/core/workspace-app/tests/unit/ddev-addons.test.js +144 -0
  227. package/core/workspace-app/tests/unit/ddev-auth-ssh.test.js +40 -0
  228. package/core/workspace-app/tests/unit/ddev-custom-commands.test.js +56 -0
  229. package/core/workspace-app/tests/unit/ddev-diagnose.test.js +57 -0
  230. package/core/workspace-app/tests/unit/ddev-domains.test.js +153 -0
  231. package/core/workspace-app/tests/unit/ddev-hostnames.test.js +143 -0
  232. package/core/workspace-app/tests/unit/ddev-import.test.js +82 -0
  233. package/core/workspace-app/tests/unit/ddev-mail.test.js +135 -0
  234. package/core/workspace-app/tests/unit/ddev-menu-and-create.test.js +28 -0
  235. package/core/workspace-app/tests/unit/ddev-mutagen.test.js +37 -0
  236. package/core/workspace-app/tests/unit/ddev-options.test.js +51 -0
  237. package/core/workspace-app/tests/unit/ddev-share.test.js +40 -0
  238. package/core/workspace-app/tests/unit/ddev-shim.test.js +57 -0
  239. package/core/workspace-app/tests/unit/ddev-snapshots.test.js +142 -0
  240. package/core/workspace-app/tests/unit/ddev-start-daemons.test.js +62 -0
  241. package/core/workspace-app/tests/unit/ddev-xdebug.test.js +41 -0
  242. package/core/workspace-app/tests/unit/deep-chat-origin.test.js +38 -0
  243. package/core/workspace-app/tests/unit/density.test.js +52 -0
  244. package/core/workspace-app/tests/unit/elevenlabs.test.js +453 -0
  245. package/core/workspace-app/tests/unit/event-log.test.js +102 -0
  246. package/core/workspace-app/tests/unit/favicon.test.js +68 -0
  247. package/core/workspace-app/tests/unit/files-browser.test.js +224 -0
  248. package/core/workspace-app/tests/unit/git-info.test.js +26 -0
  249. package/core/workspace-app/tests/unit/grid-and-list.test.js +103 -0
  250. package/core/workspace-app/tests/unit/host-tool-wrapper.test.js +18 -0
  251. package/core/workspace-app/tests/unit/html-patterns.test.js +51 -0
  252. package/core/workspace-app/tests/unit/ide-infra.test.js +28 -0
  253. package/core/workspace-app/tests/unit/images-ai.test.js +201 -0
  254. package/core/workspace-app/tests/unit/images-create.test.js +59 -0
  255. package/core/workspace-app/tests/unit/images-providers.test.js +248 -0
  256. package/core/workspace-app/tests/unit/images-svg.test.js +213 -0
  257. package/core/workspace-app/tests/unit/images-workspace.test.js +144 -0
  258. package/core/workspace-app/tests/unit/jobs-fragment.test.js +66 -0
  259. package/core/workspace-app/tests/unit/media-ux.test.js +164 -0
  260. package/core/workspace-app/tests/unit/modal-fit.test.js +87 -0
  261. package/core/workspace-app/tests/unit/modules-load.test.js +23 -0
  262. package/core/workspace-app/tests/unit/narration.test.js +186 -0
  263. package/core/workspace-app/tests/unit/navigation-editor.test.js +260 -0
  264. package/core/workspace-app/tests/unit/navigation.test.js +254 -0
  265. package/core/workspace-app/tests/unit/new-folder.test.js +121 -0
  266. package/core/workspace-app/tests/unit/new-screen-form.test.js +149 -0
  267. package/core/workspace-app/tests/unit/node-menu.test.js +161 -0
  268. package/core/workspace-app/tests/unit/notes.test.js +128 -0
  269. package/core/workspace-app/tests/unit/page-structure.test.js +40 -0
  270. package/core/workspace-app/tests/unit/page-width.test.js +59 -0
  271. package/core/workspace-app/tests/unit/php-version.test.js +90 -0
  272. package/core/workspace-app/tests/unit/plan-card.test.js +44 -0
  273. package/core/workspace-app/tests/unit/plugin-catalog.test.js +31 -0
  274. package/core/workspace-app/tests/unit/plugin-system.test.js +251 -0
  275. package/core/workspace-app/tests/unit/plugins-page.test.js +28 -0
  276. package/core/workspace-app/tests/unit/plugins.test.js +58 -0
  277. package/core/workspace-app/tests/unit/project-activity-filter.test.js +180 -0
  278. package/core/workspace-app/tests/unit/project-make.test.js +143 -0
  279. package/core/workspace-app/tests/unit/project-name.test.js +202 -0
  280. package/core/workspace-app/tests/unit/reader.test.js +62 -0
  281. package/core/workspace-app/tests/unit/release.test.js +92 -0
  282. package/core/workspace-app/tests/unit/remove-confirm.test.js +37 -0
  283. package/core/workspace-app/tests/unit/report-pdf.test.js +171 -0
  284. package/core/workspace-app/tests/unit/round-11.test.js +31 -0
  285. package/core/workspace-app/tests/unit/round-12.test.js +37 -0
  286. package/core/workspace-app/tests/unit/row-layout.test.js +585 -0
  287. package/core/workspace-app/tests/unit/row-menus-fold.test.js +29 -0
  288. package/core/workspace-app/tests/unit/rows-add-modal.test.js +68 -0
  289. package/core/workspace-app/tests/unit/screen-runner-queue.test.js +41 -0
  290. package/core/workspace-app/tests/unit/screen-window.test.js +116 -0
  291. package/core/workspace-app/tests/unit/screens-beside-running.test.js +19 -0
  292. package/core/workspace-app/tests/unit/screens-columns.test.js +79 -0
  293. package/core/workspace-app/tests/unit/screens-menu.test.js +84 -0
  294. package/core/workspace-app/tests/unit/screens-modal-overlay.test.js +14 -0
  295. package/core/workspace-app/tests/unit/screens-waiting.test.js +129 -0
  296. package/core/workspace-app/tests/unit/screens.test.js +74 -0
  297. package/core/workspace-app/tests/unit/second-storage.test.js +218 -0
  298. package/core/workspace-app/tests/unit/session-name.test.js +64 -0
  299. package/core/workspace-app/tests/unit/settings-file.test.js +173 -0
  300. package/core/workspace-app/tests/unit/settings-nested-list.test.js +54 -0
  301. package/core/workspace-app/tests/unit/settings-raw.test.js +177 -0
  302. package/core/workspace-app/tests/unit/shared-prefs.test.js +49 -0
  303. package/core/workspace-app/tests/unit/speech.test.js +290 -0
  304. package/core/workspace-app/tests/unit/static-caching.test.js +39 -0
  305. package/core/workspace-app/tests/unit/status-language.test.js +47 -0
  306. package/core/workspace-app/tests/unit/tab-modal.test.js +69 -0
  307. package/core/workspace-app/tests/unit/tab-title.test.js +37 -0
  308. package/core/workspace-app/tests/unit/template-literals.test.js +52 -0
  309. package/core/workspace-app/tests/unit/terminal-scrollback.test.js +61 -0
  310. package/core/workspace-app/tests/unit/terminal-size.test.js +103 -0
  311. package/core/workspace-app/tests/unit/terminal-style.test.js +84 -0
  312. package/core/workspace-app/tests/unit/terminal-windows.test.js +154 -0
  313. package/core/workspace-app/tests/unit/themes.test.js +453 -0
  314. package/core/workspace-app/tests/unit/tool-labels.test.js +34 -0
  315. package/core/workspace-app/tests/unit/topbar-icons.test.js +47 -0
  316. package/core/workspace-app/tests/unit/update-docker-gid.test.js +51 -0
  317. package/core/workspace-app/tests/unit/user-guide.test.js +60 -0
  318. package/core/workspace-app/tests/unit/version-check.test.js +98 -0
  319. package/core/workspace-app/tests/unit/version-link.test.js +50 -0
  320. package/core/workspace-app/tests/unit/video-borrow.test.js +198 -0
  321. package/core/workspace-app/tests/unit/video-edit-ask.test.js +93 -0
  322. package/core/workspace-app/tests/unit/video-edit-short.test.js +42 -0
  323. package/core/workspace-app/tests/unit/video-edit.test.js +417 -0
  324. package/core/workspace-app/tests/unit/video-transcript.test.js +237 -0
  325. package/core/workspace-app/tests/unit/waiting.test.js +48 -0
  326. package/core/workspace-app/tests/unit/wordpress-odoo-builders.test.js +38 -0
  327. package/core/workspace-app/tests/unit/workspace-bin.test.js +48 -0
  328. package/core/workspace-app/tests/unit/workspace-card-tools.test.js +30 -0
  329. package/core/workspace-app/tests/unit/workspace-delete.test.js +77 -0
  330. package/core/workspace-app/tests/unit/workspace-types.test.js +68 -0
  331. package/core/workspace-app/tests/unit/worktree-clone.test.js +250 -0
  332. package/package.json +38 -36
  333. /package/{PLUGINS.md → core/workspace-app/PLUGINS.md} +0 -0
  334. /package/{WORKSPACES.md → core/workspace-app/WORKSPACES.md} +0 -0
  335. /package/{actions → core/workspace-app/actions}/ai-items.js +0 -0
  336. /package/{actions → core/workspace-app/actions}/audio.js +0 -0
  337. /package/{actions → core/workspace-app/actions}/build-order.js +0 -0
  338. /package/{actions → core/workspace-app/actions}/checks.js +0 -0
  339. /package/{actions → core/workspace-app/actions}/claude-session.js +0 -0
  340. /package/{actions → core/workspace-app/actions}/commands-center.js +0 -0
  341. /package/{actions → core/workspace-app/actions}/composer.js +0 -0
  342. /package/{actions → core/workspace-app/actions}/ddev-addons.js +0 -0
  343. /package/{actions → core/workspace-app/actions}/ddev-mail.js +0 -0
  344. /package/{actions → core/workspace-app/actions}/folders.js +0 -0
  345. /package/{actions → core/workspace-app/actions}/images-svg.js +0 -0
  346. /package/{actions → core/workspace-app/actions}/images.js +0 -0
  347. /package/{actions → core/workspace-app/actions}/navigation.js +0 -0
  348. /package/{actions → core/workspace-app/actions}/node.js +0 -0
  349. /package/{actions → core/workspace-app/actions}/notes.js +0 -0
  350. /package/{actions → core/workspace-app/actions}/plugins.js +0 -0
  351. /package/{actions → core/workspace-app/actions}/project-make.js +0 -0
  352. /package/{actions → core/workspace-app/actions}/row-actions.js +0 -0
  353. /package/{actions → core/workspace-app/actions}/screens.js +0 -0
  354. /package/{actions → core/workspace-app/actions}/video-edit.js +0 -0
  355. /package/{ai-inventory.js → core/workspace-app/ai-inventory.js} +0 -0
  356. /package/{ai-items.js → core/workspace-app/ai-items.js} +0 -0
  357. /package/{assistant-cast.js → core/workspace-app/assistant-cast.js} +0 -0
  358. /package/{assistant-suggestions.js → core/workspace-app/assistant-suggestions.js} +0 -0
  359. /package/{audio-voice.js → core/workspace-app/audio-voice.js} +0 -0
  360. /package/{auth-google.js → core/workspace-app/auth-google.js} +0 -0
  361. /package/{build-ai.js → core/workspace-app/build-ai.js} +0 -0
  362. /package/{build-order-page.js → core/workspace-app/build-order-page.js} +0 -0
  363. /package/{build-order.js → core/workspace-app/build-order.js} +0 -0
  364. /package/{builder-args.js → core/workspace-app/builder-args.js} +0 -0
  365. /package/{changelog.js → core/workspace-app/changelog.js} +0 -0
  366. /package/{chat-threads.js → core/workspace-app/chat-threads.js} +0 -0
  367. /package/{checks.js → core/workspace-app/checks.js} +0 -0
  368. /package/{claude-args.js → core/workspace-app/claude-args.js} +0 -0
  369. /package/{claude-sessions.js → core/workspace-app/claude-sessions.js} +0 -0
  370. /package/{command-author.js → core/workspace-app/command-author.js} +0 -0
  371. /package/{command-block.js → core/workspace-app/command-block.js} +0 -0
  372. /package/{commands → core/workspace-app/commands}/defaults.yml +0 -0
  373. /package/{commands-page.js → core/workspace-app/commands-page.js} +0 -0
  374. /package/{commands.js → core/workspace-app/commands.js} +0 -0
  375. /package/{cucumber.js → core/workspace-app/cucumber.js} +0 -0
  376. /package/{ddev-addons-page.js → core/workspace-app/ddev-addons-page.js} +0 -0
  377. /package/{ddev-addons.js → core/workspace-app/ddev-addons.js} +0 -0
  378. /package/{ddev-custom-commands.js → core/workspace-app/ddev-custom-commands.js} +0 -0
  379. /package/{ddev-domains.js → core/workspace-app/ddev-domains.js} +0 -0
  380. /package/{ddev-import.js → core/workspace-app/ddev-import.js} +0 -0
  381. /package/{ddev-mail-page.js → core/workspace-app/ddev-mail-page.js} +0 -0
  382. /package/{ddev-mail.js → core/workspace-app/ddev-mail.js} +0 -0
  383. /package/{ddev-options.js → core/workspace-app/ddev-options.js} +0 -0
  384. /package/{ddev-snapshots.js → core/workspace-app/ddev-snapshots.js} +0 -0
  385. /package/{ddev.js → core/workspace-app/ddev.js} +0 -0
  386. /package/{docs → core/workspace-app/docs}/DEVELOPER-GUIDE.md +0 -0
  387. /package/{docs → core/workspace-app/docs}/README.md +0 -0
  388. /package/{docs → core/workspace-app/docs}/REFERENCE.md +0 -0
  389. /package/{docs → core/workspace-app/docs}/USER-GUIDE.md +0 -0
  390. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-board.png +0 -0
  391. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-coffee.png +0 -0
  392. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-colours.png +0 -0
  393. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-icons.png +0 -0
  394. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-moves.png +0 -0
  395. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-pick-cookieman.png +0 -0
  396. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-settings-voices.png +0 -0
  397. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-status.png +0 -0
  398. /package/{docs → core/workspace-app/docs}/screenshots/create-card/create-card.png +0 -0
  399. /package/{docs → core/workspace-app/docs}/screenshots/create-card/start-ai.png +0 -0
  400. /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-address.png +0 -0
  401. /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-worktree-off.png +0 -0
  402. /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-worktree-on.png +0 -0
  403. /package/{docs → core/workspace-app/docs}/screenshots/guide-ddev-menu.png +0 -0
  404. /package/{docs → core/workspace-app/docs}/screenshots/guide-domains.png +0 -0
  405. /package/{docs → core/workspace-app/docs}/screenshots/guide-home.png +0 -0
  406. /package/{docs → core/workspace-app/docs}/screenshots/guide-ide-menu.png +0 -0
  407. /package/{docs → core/workspace-app/docs}/screenshots/guide-remove-refusal.png +0 -0
  408. /package/{docs → core/workspace-app/docs}/screenshots/guide-rows-worktree.png +0 -0
  409. /package/{docs → core/workspace-app/docs}/screenshots/guide-screens.png +0 -0
  410. /package/{docs → core/workspace-app/docs}/screenshots/guide-settings.png +0 -0
  411. /package/{docs → core/workspace-app/docs}/screenshots/guide-workspace-page.png +0 -0
  412. /package/{docs → core/workspace-app/docs}/screenshots/home/card-settings.png +0 -0
  413. /package/{docs → core/workspace-app/docs}/screenshots/home/drag-reorder.png +0 -0
  414. /package/{docs → core/workspace-app/docs}/screenshots/home/edit-card.png +0 -0
  415. /package/{docs → core/workspace-app/docs}/screenshots/images/claude-code-image.png +0 -0
  416. /package/{docs → core/workspace-app/docs}/screenshots/images/edit-with-ai.png +0 -0
  417. /package/{docs → core/workspace-app/docs}/screenshots/images/gallery.png +0 -0
  418. /package/{docs → core/workspace-app/docs}/screenshots/images/generate-description.png +0 -0
  419. /package/{docs → core/workspace-app/docs}/screenshots/images/generate-plan.png +0 -0
  420. /package/{docs → core/workspace-app/docs}/screenshots/images/providers.png +0 -0
  421. /package/{docs → core/workspace-app/docs}/screenshots/images/svg-form.png +0 -0
  422. /package/{docs → core/workspace-app/docs}/screenshots/images/svg-result.png +0 -0
  423. /package/{docs → core/workspace-app/docs}/screenshots/images/upload.png +0 -0
  424. /package/{docs → core/workspace-app/docs}/screenshots/new-folder/2-form.png +0 -0
  425. /package/{docs → core/workspace-app/docs}/screenshots/new-folder/8-ddev-options.png +0 -0
  426. /package/{docs → core/workspace-app/docs}/screenshots/new-folder/files-folder.png +0 -0
  427. /package/{docs → core/workspace-app/docs}/screenshots/projects/ddev-custom-commands.png +0 -0
  428. /package/{docs → core/workspace-app/docs}/screenshots/projects/state-chip-stop.png +0 -0
  429. /package/{docs → core/workspace-app/docs}/screenshots/readme/dashboard-dark.png +0 -0
  430. /package/{docs → core/workspace-app/docs}/screenshots/readme/dashboard-light.png +0 -0
  431. /package/{docs → core/workspace-app/docs}/screenshots/readme/dev-dark.png +0 -0
  432. /package/{docs → core/workspace-app/docs}/screenshots/readme/dev-light.png +0 -0
  433. /package/{docs → core/workspace-app/docs}/screenshots/readme/docs-dark.png +0 -0
  434. /package/{docs → core/workspace-app/docs}/screenshots/readme/docs-light.png +0 -0
  435. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/ai.svg +0 -0
  436. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/code.svg +0 -0
  437. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/docs.svg +0 -0
  438. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/services.svg +0 -0
  439. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/sites.svg +0 -0
  440. /package/{docs → core/workspace-app/docs}/screenshots/readme/test-report-dark.png +0 -0
  441. /package/{docs → core/workspace-app/docs}/screenshots/readme/test-report-light.png +0 -0
  442. /package/{docs → core/workspace-app/docs}/screenshots/readme/test-scenarios.png +0 -0
  443. /package/{docs → core/workspace-app/docs}/screenshots/readme/test-steps.png +0 -0
  444. /package/{docs → core/workspace-app/docs}/screenshots/readme/videos-dark.png +0 -0
  445. /package/{docs → core/workspace-app/docs}/screenshots/readme/videos-light.png +0 -0
  446. /package/{docs → core/workspace-app/docs}/screenshots/screens-menu.png +0 -0
  447. /package/{docs → core/workspace-app/docs}/screenshots/screens-new.png +0 -0
  448. /package/{docs → core/workspace-app/docs}/screenshots/settings/auth-google-lists.png +0 -0
  449. /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-1-row.png +0 -0
  450. /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-2-confirm.png +0 -0
  451. /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-3-type-name.png +0 -0
  452. /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-4-type-yes.png +0 -0
  453. /package/{event-log.js → core/workspace-app/event-log.js} +0 -0
  454. /package/{files-browser.js → core/workspace-app/files-browser.js} +0 -0
  455. /package/{folders.js → core/workspace-app/folders.js} +0 -0
  456. /package/{format.js → core/workspace-app/format.js} +0 -0
  457. /package/{github-auth.js → core/workspace-app/github-auth.js} +0 -0
  458. /package/{html.js → core/workspace-app/html.js} +0 -0
  459. /package/{icons.js → core/workspace-app/icons.js} +0 -0
  460. /package/{images-ai.js → core/workspace-app/images-ai.js} +0 -0
  461. /package/{images-create.js → core/workspace-app/images-create.js} +0 -0
  462. /package/{images-page.js → core/workspace-app/images-page.js} +0 -0
  463. /package/{images-records.js → core/workspace-app/images-records.js} +0 -0
  464. /package/{images-render.js → core/workspace-app/images-render.js} +0 -0
  465. /package/{images-svg-core.js → core/workspace-app/images-svg-core.js} +0 -0
  466. /package/{images-svg-sanitize.js → core/workspace-app/images-svg-sanitize.js} +0 -0
  467. /package/{images-svg.js → core/workspace-app/images-svg.js} +0 -0
  468. /package/{images.js → core/workspace-app/images.js} +0 -0
  469. /package/{jobs.js → core/workspace-app/jobs.js} +0 -0
  470. /package/{linger.js → core/workspace-app/linger.js} +0 -0
  471. /package/{lists.js → core/workspace-app/lists.js} +0 -0
  472. /package/{media-info.js → core/workspace-app/media-info.js} +0 -0
  473. /package/{navigation-page.js → core/workspace-app/navigation-page.js} +0 -0
  474. /package/{navigation.js → core/workspace-app/navigation.js} +0 -0
  475. /package/{notes.js → core/workspace-app/notes.js} +0 -0
  476. /package/{palette-index.js → core/workspace-app/palette-index.js} +0 -0
  477. /package/{playwright.config.js → core/workspace-app/playwright.config.js} +0 -0
  478. /package/{plugin-install.js → core/workspace-app/plugin-install.js} +0 -0
  479. /package/{plugin-loader.js → core/workspace-app/plugin-loader.js} +0 -0
  480. /package/{plugin-manifest.js → core/workspace-app/plugin-manifest.js} +0 -0
  481. /package/{plugin-settings.js → core/workspace-app/plugin-settings.js} +0 -0
  482. /package/{plugins → core/workspace-app/plugins}/workspace-notes/index.js +0 -0
  483. /package/{project-make.js → core/workspace-app/project-make.js} +0 -0
  484. /package/{project-name.js → core/workspace-app/project-name.js} +0 -0
  485. /package/{public → core/workspace-app/public}/assistant-avatar-moves.js +0 -0
  486. /package/{public → core/workspace-app/public}/assistant-avatar-ui.js +0 -0
  487. /package/{public → core/workspace-app/public}/assistant-avatar.js +0 -0
  488. /package/{public → core/workspace-app/public}/avatar/druplicon.vrm +0 -0
  489. /package/{public → core/workspace-app/public}/css/base.css +0 -0
  490. /package/{public → core/workspace-app/public}/css/components/ai-assistant.css +0 -0
  491. /package/{public → core/workspace-app/public}/css/components/assistant-avatar.css +0 -0
  492. /package/{public → core/workspace-app/public}/css/components/assistant-views.css +0 -0
  493. /package/{public → core/workspace-app/public}/css/components/build-order.css +0 -0
  494. /package/{public → core/workspace-app/public}/css/components/clone-as-a-git-worktree.css +0 -0
  495. /package/{public → core/workspace-app/public}/css/components/command-palette.css +0 -0
  496. /package/{public → core/workspace-app/public}/css/components/commands-center.css +0 -0
  497. /package/{public → core/workspace-app/public}/css/components/confirm-dialog.css +0 -0
  498. /package/{public → core/workspace-app/public}/css/components/create-card.css +0 -0
  499. /package/{public → core/workspace-app/public}/css/components/ddev-menu.css +0 -0
  500. /package/{public → core/workspace-app/public}/css/components/deep-chat.css +0 -0
  501. /package/{public → core/workspace-app/public}/css/components/density.css +0 -0
  502. /package/{public → core/workspace-app/public}/css/components/floating-assistant-workspace-pages.css +0 -0
  503. /package/{public → core/workspace-app/public}/css/components/folders.css +0 -0
  504. /package/{public → core/workspace-app/public}/css/components/full-height-left-assistant-sidebar-all-pages.css +0 -0
  505. /package/{public → core/workspace-app/public}/css/components/full-height-right-actions-rail-pages-that-have-actions.css +0 -0
  506. /package/{public → core/workspace-app/public}/css/components/grid-and-list.css +0 -0
  507. /package/{public → core/workspace-app/public}/css/components/help-page.css +0 -0
  508. /package/{public → core/workspace-app/public}/css/components/hero.css +0 -0
  509. /package/{public → core/workspace-app/public}/css/components/images.css +0 -0
  510. /package/{public → core/workspace-app/public}/css/components/layout-rhythm-and-spacing-polish.css +0 -0
  511. /package/{public → core/workspace-app/public}/css/components/mic-on-any-field-that-wants-typing.css +0 -0
  512. /package/{public → core/workspace-app/public}/css/components/modal-fit.css +0 -0
  513. /package/{public → core/workspace-app/public}/css/components/page-width.css +0 -0
  514. /package/{public → core/workspace-app/public}/css/components/plugins-page.css +0 -0
  515. /package/{public → core/workspace-app/public}/css/components/project-cards.css +0 -0
  516. /package/{public → core/workspace-app/public}/css/components/project-rows.css +0 -0
  517. /package/{public → core/workspace-app/public}/css/components/reordering-the-workspace-cards.css +0 -0
  518. /package/{public → core/workspace-app/public}/css/components/responsive-home-and-the-workspace-pages.css +0 -0
  519. /package/{public → core/workspace-app/public}/css/components/running-commands-as-toasts.css +0 -0
  520. /package/{public → core/workspace-app/public}/css/components/settings-editor-core-config-yml.css +0 -0
  521. /package/{public → core/workspace-app/public}/css/components/short-results-as-a-toast-at-the-top.css +0 -0
  522. /package/{public → core/workspace-app/public}/css/components/sound-player.css +0 -0
  523. /package/{public → core/workspace-app/public}/css/components/suggest-agents-skills-commands.css +0 -0
  524. /package/{public → core/workspace-app/public}/css/components/tab-modal.css +0 -0
  525. /package/{public → core/workspace-app/public}/css/components/terminals.css +0 -0
  526. /package/{public → core/workspace-app/public}/css/components/test-report-media-one-file-per-row-paged.css +0 -0
  527. /package/{public → core/workspace-app/public}/css/components/top-toolbar-logo-breadcrumb.css +0 -0
  528. /package/{public → core/workspace-app/public}/css/components/video-edit.css +0 -0
  529. /package/{public → core/workspace-app/public}/css/components/web-ide-open-with.css +0 -0
  530. /package/{public → core/workspace-app/public}/css/components/working-indicators.css +0 -0
  531. /package/{public → core/workspace-app/public}/css/components/workspace-cards.css +0 -0
  532. /package/{public → core/workspace-app/public}/favicon.ico +0 -0
  533. /package/{public → core/workspace-app/public}/favicon.png +0 -0
  534. /package/{public → core/workspace-app/public}/htmx.min.js +0 -0
  535. /package/{public → core/workspace-app/public}/logo.png +0 -0
  536. /package/{public → core/workspace-app/public}/media-player.js +0 -0
  537. /package/{public → core/workspace-app/public}/mic.js +0 -0
  538. /package/{public → core/workspace-app/public}/narration.js +0 -0
  539. /package/{public → core/workspace-app/public}/palette.js +0 -0
  540. /package/{public → core/workspace-app/public}/sortable.js +0 -0
  541. /package/{public → core/workspace-app/public}/suggest.js +0 -0
  542. /package/{public → core/workspace-app/public}/terminal.js +0 -0
  543. /package/{public → core/workspace-app/public}/themes/default/theme.css +0 -0
  544. /package/{public → core/workspace-app/public}/themes/ember/theme.css +0 -0
  545. /package/{public → core/workspace-app/public}/themes/press/theme.css +0 -0
  546. /package/{public → core/workspace-app/public}/themes/slate/theme.css +0 -0
  547. /package/{public → core/workspace-app/public}/touch-icon.png +0 -0
  548. /package/{public → core/workspace-app/public}/ui.js +0 -0
  549. /package/{public → core/workspace-app/public}/vendor/ace/LICENSE.txt +0 -0
  550. /package/{public → core/workspace-app/public}/vendor/ace/ace.js +0 -0
  551. /package/{public → core/workspace-app/public}/vendor/ace/ext-language_tools.js +0 -0
  552. /package/{public → core/workspace-app/public}/vendor/ace/ext-searchbox.js +0 -0
  553. /package/{public → core/workspace-app/public}/vendor/ace/mode-css.js +0 -0
  554. /package/{public → core/workspace-app/public}/vendor/ace/mode-html.js +0 -0
  555. /package/{public → core/workspace-app/public}/vendor/ace/mode-javascript.js +0 -0
  556. /package/{public → core/workspace-app/public}/vendor/ace/mode-markdown.js +0 -0
  557. /package/{public → core/workspace-app/public}/vendor/ace/mode-sh.js +0 -0
  558. /package/{public → core/workspace-app/public}/vendor/ace/mode-yaml.js +0 -0
  559. /package/{public → core/workspace-app/public}/vendor/ace/theme-textmate.js +0 -0
  560. /package/{public → core/workspace-app/public}/vendor/ace/theme-tomorrow_night.js +0 -0
  561. /package/{public → core/workspace-app/public}/vendor/ace/worker-yaml.js +0 -0
  562. /package/{public → core/workspace-app/public}/vendor/deep-chat.LICENSE +0 -0
  563. /package/{public → core/workspace-app/public}/vendor/deep-chat.bundle.js +0 -0
  564. /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-LICENSE.txt +0 -0
  565. /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
  566. /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
  567. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-LICENSE.txt +0 -0
  568. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
  569. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
  570. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
  571. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
  572. /package/{public → core/workspace-app/public}/vendor/fonts/jetbrains-mono-LICENSE.txt +0 -0
  573. /package/{public → core/workspace-app/public}/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  574. /package/{public → core/workspace-app/public}/vendor/fonts/ubuntu-mono-LICENSE.txt +0 -0
  575. /package/{public → core/workspace-app/public}/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
  576. /package/{public → core/workspace-app/public}/vendor/tabler-LICENSE.txt +0 -0
  577. /package/{public → core/workspace-app/public}/vendor/tabler-sprite.svg +0 -0
  578. /package/{public → core/workspace-app/public}/vendor/three/LICENSE +0 -0
  579. /package/{public → core/workspace-app/public}/vendor/three/addons/loaders/GLTFLoader.js +0 -0
  580. /package/{public → core/workspace-app/public}/vendor/three/addons/utils/BufferGeometryUtils.js +0 -0
  581. /package/{public → core/workspace-app/public}/vendor/three/addons/utils/SkeletonUtils.js +0 -0
  582. /package/{public → core/workspace-app/public}/vendor/three/three.core.min.js +0 -0
  583. /package/{public → core/workspace-app/public}/vendor/three/three.module.min.js +0 -0
  584. /package/{public → core/workspace-app/public}/vendor/three-vrm/LICENSE +0 -0
  585. /package/{public → core/workspace-app/public}/vendor/three-vrm/three-vrm.module.min.js +0 -0
  586. /package/{public → core/workspace-app/public}/vendor/uikit-icons.min.js +0 -0
  587. /package/{public → core/workspace-app/public}/vendor/uikit.min.css +0 -0
  588. /package/{public → core/workspace-app/public}/vendor/uikit.min.js +0 -0
  589. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/bookmark-outlined.svg +0 -0
  590. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/headset.svg +0 -0
  591. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/menu.svg +0 -0
  592. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/pause.svg +0 -0
  593. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/play.svg +0 -0
  594. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/share.svg +0 -0
  595. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/sound-off.svg +0 -0
  596. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/sound-on.svg +0 -0
  597. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/stop.svg +0 -0
  598. /package/{public → core/workspace-app/public}/vendor/xterm/addon-fit.js +0 -0
  599. /package/{public → core/workspace-app/public}/vendor/xterm/addon-unicode11.js +0 -0
  600. /package/{public → core/workspace-app/public}/vendor/xterm/addon-web-links.js +0 -0
  601. /package/{public → core/workspace-app/public}/vendor/xterm/xterm.css +0 -0
  602. /package/{public → core/workspace-app/public}/vendor/xterm/xterm.js +0 -0
  603. /package/{rag.js → core/workspace-app/rag.js} +0 -0
  604. /package/{reader.js → core/workspace-app/reader.js} +0 -0
  605. /package/{repo-blobs.js → core/workspace-app/repo-blobs.js} +0 -0
  606. /package/{report-pdf.js → core/workspace-app/report-pdf.js} +0 -0
  607. /package/{row-layout.js → core/workspace-app/row-layout.js} +0 -0
  608. /package/{screenrc → core/workspace-app/screenrc} +0 -0
  609. /package/{screens.js → core/workspace-app/screens.js} +0 -0
  610. /package/{scripts → core/workspace-app/scripts}/code-server-start.sh +0 -0
  611. /package/{scripts → core/workspace-app/scripts}/images-claude-finish.js +0 -0
  612. /package/{scripts → core/workspace-app/scripts}/screen-host.sh +0 -0
  613. /package/{scripts → core/workspace-app/scripts}/screen-runner.js +0 -0
  614. /package/{scripts → core/workspace-app/scripts}/screen-then.js +0 -0
  615. /package/{scripts → core/workspace-app/scripts}/update.sh +0 -0
  616. /package/{settings-file.js → core/workspace-app/settings-file.js} +0 -0
  617. /package/{speech.js → core/workspace-app/speech.js} +0 -0
  618. /package/{tab-modal.js → core/workspace-app/tab-modal.js} +0 -0
  619. /package/{test-reports.js → core/workspace-app/test-reports.js} +0 -0
  620. /package/{themes.js → core/workspace-app/themes.js} +0 -0
  621. /package/{transcripts.js → core/workspace-app/transcripts.js} +0 -0
  622. /package/{urls.js → core/workspace-app/urls.js} +0 -0
  623. /package/{validate.js → core/workspace-app/validate.js} +0 -0
  624. /package/{version-check.js → core/workspace-app/version-check.js} +0 -0
  625. /package/{video-edit.js → core/workspace-app/video-edit.js} +0 -0
  626. /package/{waiting.js → core/workspace-app/waiting.js} +0 -0
  627. /package/{workspace-card-tools.js → core/workspace-app/workspace-card-tools.js} +0 -0
  628. /package/{workspace-delete.js → core/workspace-app/workspace-delete.js} +0 -0
  629. /package/{workspace-types.js → core/workspace-app/workspace-types.js} +0 -0
  630. /package/{workspaces.js → core/workspace-app/workspaces.js} +0 -0
@@ -0,0 +1,636 @@
1
+ #!/usr/bin/env python3
2
+ """Edit a video into a NEW version of itself: cut, speed up or slow down, add a section, drop a section,
3
+ open and close with the Webship logo, and put music under either end. Standard library plus ffmpeg.
4
+
5
+ The video it starts from is never changed. A change is described as a timeline of pieces (a stretch of the
6
+ source, or a card with the logo), one ffmpeg run builds it, and the timed voiceover script beside the video is
7
+ rewritten so every section still points at the same words in the new one.
8
+ Exit codes: 0 ok, 1 error, 2 usage, 3 no key, 4 plan only.
9
+ """
10
+
11
+ import argparse
12
+ import datetime
13
+ import json
14
+ import os
15
+ import re
16
+ import shutil
17
+ import subprocess
18
+ import sys
19
+ import tempfile
20
+
21
+ sys.dont_write_bytecode = True
22
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
23
+ import elevenlabs as el # noqa: E402
24
+
25
+ OK, ERROR, USAGE, PLAN_ONLY = el.OK, el.ERROR, el.USAGE, el.PLAN_ONLY
26
+ MIN_SPEED, MAX_SPEED = 0.25, 4.0
27
+ CARD_SECONDS = (1.0, 10.0)
28
+ SECTION_PAD = 0.8
29
+ FADE_IN, FADE_OUT = 0.6, 1.8
30
+ BRAND_BG = '#2b447c'
31
+ CHROME = ('google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser')
32
+
33
+
34
+ def workspace_root():
35
+ return os.environ.get('WORKSPACE_ROOT') or os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', '..'))
36
+
37
+
38
+ def public_dir():
39
+ return os.path.join(workspace_root(), 'core', 'workspace-app', 'public')
40
+
41
+
42
+ # ---- the timeline: pieces in order, and where a moment of the source ends up --------------------------
43
+
44
+ class Piece:
45
+ """A stretch of the source video (`kind` source), or a card shown for `seconds` (`kind` card)."""
46
+
47
+ def __init__(self, kind, start=0.0, end=0.0, speed=1.0, seconds=0.0, title='', subtitle='', audio=None, src=0):
48
+ # src 0 is the video being edited; 1.. are videos a section was borrowed from (their own clock)
49
+ self.kind, self.start, self.end, self.speed, self.src = kind, start, end, speed, src
50
+ self.seconds, self.title, self.subtitle, self.audio = seconds, title, subtitle, audio
51
+ self.out_start = 0.0
52
+
53
+ @property
54
+ def length(self):
55
+ return (self.end - self.start) / self.speed if self.kind == 'source' else self.seconds
56
+
57
+
58
+ def lay_out(pieces):
59
+ at = 0.0
60
+ for piece in pieces:
61
+ piece.out_start = at
62
+ at += piece.length
63
+ return at
64
+
65
+
66
+ def source_pieces(total, cuts, speed):
67
+ """The source with the `cuts` (start, end) taken out, every kept stretch at `speed`."""
68
+ kept, at = [], 0.0
69
+ for start, end in sorted(cuts):
70
+ start, end = max(0.0, start), min(total, end)
71
+ if end <= start:
72
+ continue
73
+ if start > at:
74
+ kept.append((at, start))
75
+ at = max(at, end)
76
+ if at < total:
77
+ kept.append((at, total))
78
+ return [Piece('source', a, b, speed) for a, b in kept if b - a > 0.04]
79
+
80
+
81
+ # ---- the script: timed sections the way the voiceover tools read them -------------------------------
82
+
83
+ def section_heading(sid, title, start, end):
84
+ return '## %s %s (%s to %s)' % (sid, title, el.mmss(start), el.mmss(end))
85
+
86
+
87
+ def parse_blocks(text):
88
+ """The script as a preamble and a list of sections {id, title, start, end, lines}."""
89
+ pre, sections, current = [], [], None
90
+ for line in text.splitlines():
91
+ found = el.HEADING.match(line)
92
+ if found:
93
+ current = {'id': found.group(1), 'title': found.group(2), 'start': el.seconds_of(found.group(3)),
94
+ 'end': el.seconds_of(found.group(4)), 'lines': []}
95
+ sections.append(current)
96
+ elif line.startswith('## '):
97
+ current = {'id': None, 'lines': [line]}
98
+ sections.append(current)
99
+ elif current is None:
100
+ pre.append(line)
101
+ else:
102
+ current['lines'].append(line)
103
+ return pre, sections
104
+
105
+
106
+ def retime_script(text, pieces, added=()):
107
+ """The script for the new video. A section cut away is dropped; one cut in part is shortened; the rest
108
+ follow their words to where the pieces put them. `added` sections (dicts with after, id, title, lines)
109
+ go in where their card now is. Anything that is not a timed section is kept where it was."""
110
+ pre, sections = parse_blocks(text)
111
+ out = list(pre)
112
+ placed = {}
113
+ for a in added:
114
+ placed.setdefault(a['after'], []).append(a)
115
+ for a in placed.pop('start', []):
116
+ out += card_section_lines(a, pieces)
117
+ for section in sections:
118
+ if section['id'] is None:
119
+ out += section['lines']
120
+ continue
121
+ inside = [p for p in pieces if p.kind == 'source' and p.src == 0 and p.end > section['start'] + 1e-6 and p.start < section['end'] - 1e-6]
122
+ if inside:
123
+ start = min(p.out_start + (max(p.start, section['start']) - p.start) / p.speed for p in inside)
124
+ end = max(p.out_start + (min(p.end, section['end']) - p.start) / p.speed for p in inside)
125
+ out += [section_heading(section['id'], section['title'], start, end)] + section['lines']
126
+ for a in placed.pop(section['id'], []):
127
+ out += card_section_lines(a, pieces)
128
+ for a in placed.pop('end', []):
129
+ out += card_section_lines(a, pieces)
130
+ for rest in placed.values():
131
+ for a in rest:
132
+ out += card_section_lines(a, pieces)
133
+ text = '\n'.join(out).rstrip('\n') + '\n'
134
+ return re.sub(r'\n{3,}', '\n\n', text)
135
+
136
+
137
+ def card_section_lines(added, pieces):
138
+ span = added.get('pieces') or [added['card']]
139
+ start, end = span[0].out_start, span[-1].out_start + span[-1].length
140
+ body = list(added['lines']) if added.get('lines') else ['> ' + added['text']]
141
+ while body and not body[-1].strip():
142
+ body.pop()
143
+ return ['', section_heading(added['id'], added['title'], start, end)] + body + ['']
144
+
145
+
146
+ def free_section_id(sections, prefix='N'):
147
+ taken = {s['id'] for s in sections if s['id']}
148
+ number = 1
149
+ while '%s%02d' % (prefix, number) in taken:
150
+ number += 1
151
+ return '%s%02d' % (prefix, number)
152
+
153
+
154
+ # ---- reading the video ------------------------------------------------------------------------------
155
+
156
+ def probe(path):
157
+ done = subprocess.run(['ffprobe', '-v', 'error', '-show_entries', 'stream=codec_type,width,height,r_frame_rate',
158
+ '-show_entries', 'format=duration', '-of', 'json', path], capture_output=True, text=True, timeout=60)
159
+ try:
160
+ data = json.loads(done.stdout)
161
+ except ValueError:
162
+ raise el.Stop('Cannot read the video: %s' % path)
163
+ video = next((s for s in data.get('streams', []) if s.get('codec_type') == 'video'), None)
164
+ if not video:
165
+ raise el.Stop('There is no picture in %s.' % path)
166
+ num, _, den = (video.get('r_frame_rate') or '30/1').partition('/')
167
+ fps = float(num) / float(den or 1) if float(den or 1) else 30.0
168
+ return {'width': int(video['width']) // 2 * 2, 'height': int(video['height']) // 2 * 2, 'fps': round(fps, 3) if 1 <= fps <= 120 else 30.0,
169
+ 'duration': float(data['format']['duration']), 'has_audio': any(s.get('codec_type') == 'audio' for s in data.get('streams', []))}
170
+
171
+
172
+ def parse_range(value):
173
+ found = re.match(r'^\s*([\d:.]+)\s*-\s*([\d:.]+)\s*$', value or '')
174
+ if not found:
175
+ raise el.Stop('A range is "start-end", in seconds or minutes:seconds, like 0:05-0:40.', USAGE)
176
+ return el.seconds_of(found.group(1)), el.seconds_of(found.group(2))
177
+
178
+
179
+ # ---- cards: the Webship logo, and words, as a picture ------------------------------------------------
180
+
181
+ def chrome():
182
+ for name in CHROME:
183
+ found = shutil.which(name)
184
+ if found:
185
+ return found
186
+ raise el.Stop('A card needs Chrome or Chromium on this machine, and none was found.')
187
+
188
+
189
+ def card_html(title, subtitle, width, height):
190
+ pub = public_dir()
191
+ fonts = os.path.join(pub, 'vendor', 'fonts')
192
+ logo = os.path.join(pub, 'logo.png')
193
+ if not os.path.isfile(logo):
194
+ raise el.Stop('The workspace logo is missing: %s' % logo)
195
+ esc = lambda s: s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;') # noqa: E731
196
+ scale = height / 720.0
197
+ return ('<!doctype html><meta charset="utf-8"><style>'
198
+ '@font-face{font-family:Figtree;font-weight:600;src:url("file://%(f)s/figtree-latin-600-normal.woff2")}'
199
+ '@font-face{font-family:Figtree;font-weight:400;src:url("file://%(f)s/figtree-latin-400-normal.woff2")}'
200
+ 'html,body{margin:0;width:%(w)dpx;height:%(h)dpx;background:%(bg)s;display:flex;flex-direction:column;align-items:center;'
201
+ 'justify-content:center;font-family:Figtree,sans-serif;text-align:center;overflow:hidden}'
202
+ 'img{width:%(logo)dpx}h1{color:#fff;font-weight:600;font-size:%(t)dpx;margin:%(gap)dpx 8%% 0;line-height:1.25}'
203
+ 'p{color:#deedff;font-size:%(s)dpx;margin:%(gap2)dpx 8%% 0;line-height:1.35}</style>'
204
+ '<img src="file://%(logo_file)s">%(h1)s%(p)s'
205
+ % {'f': fonts, 'w': width, 'h': height, 'bg': BRAND_BG, 'logo': int(420 * scale), 't': int(44 * scale), 's': int(26 * scale),
206
+ 'gap': int(40 * scale), 'gap2': int(10 * scale), 'logo_file': logo,
207
+ 'h1': '<h1>%s</h1>' % esc(title) if title else '', 'p': '<p>%s</p>' % esc(subtitle) if subtitle else ''})
208
+
209
+
210
+ def render_card(path, title, subtitle, width, height):
211
+ page = path + '.html'
212
+ with open(page, 'w', encoding='utf-8') as handle:
213
+ handle.write(card_html(title, subtitle, width, height))
214
+ profile = tempfile.mkdtemp(prefix='ws-card-profile-')
215
+ try:
216
+ done = subprocess.run([chrome(), '--headless=new', '--no-sandbox', '--disable-gpu', '--hide-scrollbars', '--allow-file-access-from-files',
217
+ '--user-data-dir=' + profile, '--window-size=%d,%d' % (width, height), '--screenshot=' + path, 'file://' + page],
218
+ capture_output=True, text=True, timeout=120)
219
+ finally:
220
+ shutil.rmtree(profile, ignore_errors=True)
221
+ if not os.path.isfile(path):
222
+ raise el.Stop('The card was not drawn: %s' % ((done.stderr or '').strip().splitlines()[-1:] or 'no output'))
223
+
224
+
225
+ # ---- building it ------------------------------------------------------------------------------------
226
+
227
+ def atempo_chain(speed):
228
+ """atempo takes 0.5 to 2 in older builds; chain it so any speed in range works."""
229
+ chain, rest = [], speed
230
+ while rest > 2.0:
231
+ chain.append(2.0)
232
+ rest /= 2.0
233
+ while rest < 0.5:
234
+ chain.append(0.5)
235
+ rest /= 0.5
236
+ chain.append(rest)
237
+ return ','.join('atempo=%.5f' % f for f in chain)
238
+
239
+
240
+ def build_filters(pieces, info, music_in, music_out, volume, cards):
241
+ """The ffmpeg inputs and filter graph for the timeline. `cards` maps a card piece to its picture."""
242
+ extras = info.get('extra', [])
243
+ inputs, chains, labels = [], [], []
244
+ count = [0]
245
+
246
+ def add(*flags):
247
+ inputs.extend(flags)
248
+ count[0] += 1
249
+ return count[0] - 1
250
+
251
+ w, h, fps = info['width'], info['height'], info['fps']
252
+ fmt = 'aformat=sample_rates=44100:channel_layouts=stereo'
253
+ for i, piece in enumerate(pieces):
254
+ if piece.kind == 'source':
255
+ origin = info if piece.src == 0 else extras[piece.src - 1]
256
+ has_audio = origin['has_audio']
257
+ # every stretch is its own seeked input of the file: one input split into many trims makes ffmpeg
258
+ # hold the whole rest of the video in memory while the timeline waits on an earlier piece
259
+ n = add('-ss', '%.3f' % piece.start, '-t', '%.3f' % (piece.end - piece.start), '-i', origin['path'])
260
+ # a borrowed picture keeps its own shape: fitted inside this video's frame, never stretched
261
+ fit = ('scale=%d:%d' % (w, h)) if piece.src == 0 else \
262
+ ('scale=%d:%d:force_original_aspect_ratio=decrease,pad=%d:%d:(ow-iw)/2:(oh-ih)/2:color=black' % (w, h, w, h))
263
+ chains.append('[%d:v]setpts=(PTS-STARTPTS)/%.5f,%s,fps=%.3f,format=yuv420p,setsar=1[v%d]'
264
+ % (n, piece.speed, fit, fps, i))
265
+ if has_audio:
266
+ tempo = ',' + atempo_chain(piece.speed) if abs(piece.speed - 1.0) > 1e-6 else ''
267
+ chains.append('[%d:a]asetpts=PTS-STARTPTS%s,%s,apad=whole_dur=%.3f,atrim=end=%.3f,asetpts=PTS-STARTPTS[a%d]'
268
+ % (n, tempo, fmt, piece.length, piece.length, i))
269
+ else:
270
+ chains.append('anullsrc=r=44100:cl=stereo,atrim=duration=%.3f,%s[a%d]' % (piece.length, fmt, i))
271
+ else:
272
+ n = add('-loop', '1', '-framerate', '%.3f' % fps, '-t', '%.3f' % piece.seconds, '-i', cards[id(piece)])
273
+ chains.append('[%d:v]scale=%d:%d,fps=%.3f,format=yuv420p,setsar=1[v%d]' % (n, w, h, fps, i))
274
+ if piece.audio:
275
+ m = add('-i', piece.audio)
276
+ chains.append('[%d:a]%s,apad=whole_dur=%.3f,atrim=end=%.3f,asetpts=PTS-STARTPTS[a%d]' % (m, fmt, piece.seconds, piece.seconds, i))
277
+ else:
278
+ chains.append('anullsrc=r=44100:cl=stereo,atrim=duration=%.3f,%s[a%d]' % (piece.seconds, fmt, i))
279
+ labels.append('[v%d][a%d]' % (i, i))
280
+ total = sum(p.length for p in pieces)
281
+ chains.append('%sconcat=n=%d:v=1:a=1[vout][amain]' % (''.join(labels), len(pieces)))
282
+ mixes, names = [], ['[amain]']
283
+ for tag, music, at_end in (('in', music_in, False), ('out', music_out, True)):
284
+ if not music:
285
+ continue
286
+ path, length = music
287
+ length = min(length, total)
288
+ n = add('-i', path)
289
+ delay = max(0.0, total - length) if at_end else 0.0
290
+ mixes.append('[%d:a]%s,atrim=end=%.3f,asetpts=PTS-STARTPTS,volume=%.3f,afade=t=in:st=0:d=%.2f,afade=t=out:st=%.3f:d=%.2f,adelay=%d:all=1[m%s]'
291
+ % (n, fmt, length, volume, min(FADE_OUT if at_end else FADE_IN, length / 2), max(0.0, length - FADE_OUT), min(FADE_OUT, length),
292
+ int(delay * 1000), tag))
293
+ names.append('[m%s]' % tag)
294
+ if mixes:
295
+ chains += mixes
296
+ chains.append('%samix=inputs=%d:duration=first:normalize=0[aout]' % (''.join(names), len(names)))
297
+ else:
298
+ chains.append('[amain]anull[aout]')
299
+ return inputs, ';'.join(chains), total
300
+
301
+
302
+ def speaker(engine, voice, model, confirm):
303
+ """A function that speaks a text and returns the mp3 bytes."""
304
+ import voice as vo # noqa: E402
305
+ if engine == 'piper':
306
+ installed = [v['id'] for v in vo.piper_voices() if v['installed']]
307
+ voice = voice or (installed[0] if installed else '')
308
+ if not vo.piper_bin() or voice not in installed:
309
+ raise el.Stop(vo.NOT_INSTALLED if not vo.piper_bin() else 'The Piper voice "%s" is not installed. Installed: %s.' % (voice, ', '.join(installed) or 'none'), USAGE)
310
+ return lambda text: vo.piper_synthesize(text, voice, '', 'mp3')[0]
311
+ el.key()
312
+ if not voice:
313
+ raise el.Stop('Choose an ElevenLabs voice for the new section.', USAGE)
314
+ voice_id = el.resolve_voice(voice)
315
+ return lambda text: el.synthesize(text, voice_id, model or el.DEFAULT_MODEL, el.DEFAULT_FORMAT)[0]
316
+
317
+
318
+ # ---- the command ------------------------------------------------------------------------------------
319
+
320
+ def cmd_info(args):
321
+ info = probe(args.video)
322
+ el.emit(args, dict(info, path=args.video), ['%dx%d at %s fps, %s long, %s' % (info['width'], info['height'], info['fps'], el.mmss(info['duration']),
323
+ 'with sound' if info['has_audio'] else 'silent')])
324
+ return OK
325
+
326
+
327
+ def plan_edit(args, info, script_text):
328
+ """The pieces the options ask for, and a description of each change for the record."""
329
+ total = info['duration']
330
+ cuts, changes = [], []
331
+ if args.trim:
332
+ start, end = parse_range(args.trim)
333
+ if not 0 <= start < end <= total + 0.5:
334
+ raise el.Stop('The trim %s is outside the video, which is %s long.' % (args.trim, el.mmss(total)), USAGE)
335
+ cuts += [(0.0, start), (end, total)]
336
+ changes.append('trimmed to %s-%s' % (el.mmss(start), el.mmss(end)))
337
+ if args.remove:
338
+ if not script_text:
339
+ raise el.Stop('Removing a section needs the video\'s timed voiceover script.', USAGE)
340
+ _, sections = parse_blocks(script_text)
341
+ by_id = {s['id']: s for s in sections if s['id']}
342
+ for sid in [x for x in args.remove.split(',') if x.strip()]:
343
+ if sid.strip() not in by_id:
344
+ raise el.Stop('The script has no section "%s". It has: %s.' % (sid.strip(), ', '.join(by_id) or 'none'), USAGE)
345
+ cuts.append((by_id[sid.strip()]['start'], by_id[sid.strip()]['end']))
346
+ changes.append('removed section %s' % sid.strip())
347
+ speed = args.speed or 1.0
348
+ pieces = source_pieces(total, cuts, 1.0)
349
+ if not pieces:
350
+ raise el.Stop('Nothing is left of the video after those cuts.', USAGE)
351
+ if args.length:
352
+ kept = sum(p.end - p.start for p in pieces)
353
+ target = el.seconds_of(args.length) if ':' in args.length else float(args.length)
354
+ speed = kept / target
355
+ if not MIN_SPEED <= speed <= MAX_SPEED:
356
+ raise el.Stop('To fit %s the picture would play at %.2fx; the range is %.2fx to %.0fx. Longest possible: %s, shortest: %s.'
357
+ % (el.mmss(target), speed, MIN_SPEED, MAX_SPEED, el.mmss(kept / MIN_SPEED), el.mmss(kept / MAX_SPEED)), USAGE)
358
+ if not MIN_SPEED <= speed <= MAX_SPEED:
359
+ raise el.Stop('Speed goes from %.2fx to %.0fx.' % (MIN_SPEED, MAX_SPEED), USAGE)
360
+ for piece in pieces:
361
+ piece.speed = speed
362
+ if abs(speed - 1.0) > 1e-6:
363
+ changes.append('at %.2fx speed' % speed)
364
+ return pieces, changes
365
+
366
+
367
+ def insert_pieces(pieces, script_text, where, new, taken_at_start=0):
368
+ """Put `new` pieces at `where`: start, end, or after the section of the edited video with that id. The source
369
+ piece a section ends inside is split so the new pieces land exactly after it. Returns the index they went to."""
370
+ if where == 'start':
371
+ pieces[taken_at_start:taken_at_start] = new
372
+ return taken_at_start
373
+ if where == 'end' or not script_text:
374
+ pieces.extend(new)
375
+ return len(pieces) - len(new)
376
+ end = next(s['end'] for s in parse_blocks(script_text)[1] if s['id'] == where)
377
+ at = max((i + 1 for i, p in enumerate(pieces) if p.kind == 'source' and p.src == 0 and p.start < end - 1e-6), default=len(pieces))
378
+ for i, p in enumerate(pieces):
379
+ if p.kind == 'source' and p.src == 0 and p.start + 1e-6 < end < p.end - 1e-6:
380
+ pieces[i:i + 1] = [Piece('source', p.start, end, p.speed), Piece('source', end, p.end, p.speed)]
381
+ at = i + 1
382
+ break
383
+ # several things going after one section keep the order they were asked for
384
+ while at < len(pieces) and (pieces[at].kind == 'card' or pieces[at].src > 0):
385
+ at += 1
386
+ pieces[at:at] = new
387
+ return at
388
+
389
+
390
+ def parse_borrow(spec):
391
+ """`VIDEO|SECTION-or-START-END|AFTER|SCRIPT`: the last two parts are optional."""
392
+ parts = [x.strip() for x in (spec or '').split('|')]
393
+ if len(parts) < 2 or not parts[0] or not parts[1] or len(parts) > 4:
394
+ raise el.Stop('--borrow is VIDEO|SECTION|AFTER|SCRIPT, like clip.mp4|T03|end|clip-voiceover-script.md (the last two are optional). '
395
+ 'SECTION is a section id of that video\'s script, or a range like 0:10-0:30.', USAGE)
396
+ parts += [''] * (4 - len(parts))
397
+ return {'video': parts[0], 'what': parts[1], 'after': parts[2] or 'end', 'script': parts[3]}
398
+
399
+
400
+ def plan_borrows(args, script_text, sources):
401
+ """Borrowed stretches from other videos: a list of {pieces, after, section, title, lines, from} and the
402
+ `sources` list (each {path, has_audio, ...}) the pieces' `src` numbers point into."""
403
+ out = []
404
+ own = [s['id'] for s in parse_blocks(script_text)[1] if s['id']] if script_text else []
405
+ for raw in args.borrow or []:
406
+ spec = parse_borrow(raw)
407
+ if os.path.abspath(spec['video']) == os.path.abspath(args.video):
408
+ raise el.Stop('A section borrowed from the video itself is already there: copy it by editing the video, or borrow from another.', USAGE)
409
+ if not os.path.isfile(spec['video']):
410
+ raise el.Stop('The video to borrow from is missing: %s' % spec['video'], USAGE)
411
+ if spec['after'] not in ('start', 'end') and spec['after'] not in own:
412
+ raise el.Stop('Cannot put it after "%s": %s' % (spec['after'], 'this video has sections ' + ', '.join(own) if own else
413
+ 'this video has no timed script, so only start or end'), USAGE)
414
+ index = next((i for i, s in enumerate(sources) if s['path'] == spec['video']), None)
415
+ if index is None:
416
+ info = probe(spec['video'])
417
+ info['path'] = spec['video']
418
+ sources.append(info)
419
+ index = len(sources) - 1
420
+ info = sources[index]
421
+ title, lines, label = '', [], spec['what']
422
+ if re.match(r'^[\d:.]+\s*-\s*[\d:.]+$', spec['what']):
423
+ start, end = parse_range(spec['what'])
424
+ title = 'Borrowed %s-%s' % (el.mmss(start), el.mmss(end))
425
+ else:
426
+ if not spec['script']:
427
+ raise el.Stop('A section id needs the script of %s: add it as the fourth part of --borrow, or borrow a range like 0:10-0:30.'
428
+ % os.path.basename(spec['video']), USAGE)
429
+ try:
430
+ with open(spec['script'], encoding='utf-8') as handle:
431
+ theirs = parse_blocks(handle.read())[1]
432
+ except OSError as error:
433
+ raise el.Stop('Cannot read the script of the video to borrow from: %s' % error, USAGE)
434
+ found = next((s for s in theirs if s['id'] == spec['what']), None)
435
+ if not found:
436
+ raise el.Stop('%s has no section "%s". It has: %s.' % (os.path.basename(spec['script']), spec['what'],
437
+ ', '.join(s['id'] for s in theirs if s['id']) or 'none'), USAGE)
438
+ start, end, title, lines = found['start'], found['end'], found['title'], found['lines']
439
+ if not 0 <= start < end <= info['duration'] + 0.5:
440
+ raise el.Stop('%s-%s is outside %s, which is %s long.' % (el.mmss(start), el.mmss(end), os.path.basename(spec['video']), el.mmss(info['duration'])), USAGE)
441
+ out.append({'pieces': [Piece('source', start, min(end, info['duration']), 1.0, src=index + 1)], 'after': spec['after'], 'what': label,
442
+ 'title': title, 'lines': lines, 'from': os.path.basename(spec['video'])})
443
+ return out
444
+
445
+
446
+ STRING_OPTIONS = ('add_title', 'add_subtitle', 'add_text', 'intro_title', 'intro_subtitle', 'outro_title', 'outro_subtitle')
447
+
448
+
449
+ def read_strings(args):
450
+ if not args.strings_file:
451
+ return
452
+ try:
453
+ with open(args.strings_file, encoding='utf-8') as handle:
454
+ given = json.load(handle)
455
+ except (OSError, ValueError) as error:
456
+ raise el.Stop('Cannot read the strings file: %s' % error, USAGE)
457
+ for name in STRING_OPTIONS:
458
+ if isinstance(given.get(name), str):
459
+ setattr(args, name, given[name].strip())
460
+
461
+
462
+ def cmd_edit(args):
463
+ read_strings(args)
464
+ el.need_tools()
465
+ info = probe(args.video)
466
+ info['path'] = args.video
467
+ info['extra'] = []
468
+ script_text = ''
469
+ if args.script:
470
+ try:
471
+ with open(args.script, encoding='utf-8') as handle:
472
+ script_text = handle.read()
473
+ except OSError as error:
474
+ raise el.Stop('Cannot read the script: %s' % error, USAGE)
475
+ pieces, changes = plan_edit(args, info, script_text)
476
+ added = []
477
+ work = tempfile.mkdtemp(prefix='ws-video-edit-')
478
+ try:
479
+ # an added section: a card with the Webship logo and a title, spoken by the chosen voice
480
+ if args.add_text:
481
+ if not args.add_title:
482
+ raise el.Stop('A new section needs a title.', USAGE)
483
+ where = args.add_after or 'end'
484
+ if script_text and where not in ('start', 'end') and where not in {s['id'] for s in parse_blocks(script_text)[1] if s['id']}:
485
+ raise el.Stop('The script has no section "%s" to add after.' % where, USAGE)
486
+ engine = args.engine or 'piper'
487
+ if engine == 'elevenlabs' and not args.confirm:
488
+ chars = len(args.add_text)
489
+ el.emit(args, {'plan': 'new section', 'characters_to_send': chars, 'voice': args.voice, 'confirmed': False},
490
+ ['Plan only, nothing was sent.', 'The new section "%s" would send %d characters to ElevenLabs (voice %s).' % (args.add_title, chars, args.voice),
491
+ 'Run again with --confirm to make the video.'])
492
+ return PLAN_ONLY
493
+ say = speaker(engine, args.voice, args.model, args.confirm)
494
+ speech = os.path.join(work, 'section.mp3')
495
+ with open(speech, 'wb') as handle:
496
+ handle.write(say(args.add_text))
497
+ spoken = el.duration(speech) or 0.0
498
+ card = Piece('card', seconds=round(spoken + SECTION_PAD, 2), title=args.add_title, subtitle=args.add_subtitle or '', audio=speech)
499
+ insert_pieces(pieces, script_text, where, [card])
500
+ sections = parse_blocks(script_text)[1] if script_text else []
501
+ added.append({'after': where if script_text else 'end', 'id': free_section_id(sections), 'title': args.add_title, 'text': args.add_text, 'card': card})
502
+ changes.append('added section "%s"' % args.add_title)
503
+ sections_now = parse_blocks(script_text)[1] if script_text else []
504
+ borrowed = plan_borrows(args, script_text, info['extra'])
505
+ start_slot = 0
506
+ for item in borrowed:
507
+ at = insert_pieces(pieces, script_text, item['after'], item['pieces'], start_slot)
508
+ if item['after'] == 'start':
509
+ start_slot = at + len(item['pieces'])
510
+ taken = sections_now + [{'id': a['id']} for a in added]
511
+ item['id'] = free_section_id(taken, 'B')
512
+ if script_text:
513
+ added.append({'after': item['after'], 'id': item['id'],
514
+ 'title': item['title'], 'lines': item['lines'] or ['> (borrowed from %s)' % item['from']],
515
+ 'text': '', 'pieces': item['pieces']})
516
+ changes.append('borrowed %s "%s" from %s' % (item['what'], item['title'], item['from']))
517
+ for side, seconds, title, subtitle in (('start', args.intro_card, args.intro_title, args.intro_subtitle), ('end', args.outro_card, args.outro_title, args.outro_subtitle)):
518
+ if seconds:
519
+ if not CARD_SECONDS[0] <= seconds <= CARD_SECONDS[1]:
520
+ raise el.Stop('A logo scene lasts %.0f to %.0f seconds.' % CARD_SECONDS, USAGE)
521
+ card = Piece('card', seconds=seconds, title=title or '', subtitle=subtitle or '')
522
+ pieces.insert(0, card) if side == 'start' else pieces.append(card)
523
+ changes.append('%s with the Webship logo' % ('opens' if side == 'start' else 'closes'))
524
+ # a stretch with no length would make ffmpeg read to the end of its file (-t 0 means no limit there)
525
+ pieces[:] = [p for p in pieces if p.kind == 'card' or p.end - p.start > 0.04]
526
+ total = lay_out(pieces)
527
+ music = {}
528
+ for side, path, card, seconds in (('in', args.intro_music, args.intro_card, args.intro_seconds), ('out', args.outro_music, args.outro_card, args.outro_seconds)):
529
+ if path:
530
+ if not os.path.isfile(path):
531
+ raise el.Stop('The music file is missing: %s' % path, USAGE)
532
+ music[side] = (path, card or seconds)
533
+ changes.append('%s music' % ('intro' if side == 'in' else 'outro'))
534
+ out = args.out
535
+ if os.path.exists(out) and not args.force:
536
+ raise el.Stop('%s already exists. Pass --force to replace it.' % out, USAGE)
537
+ copy_only = not changes
538
+ if copy_only:
539
+ if not args.clone:
540
+ raise el.Stop('Nothing to change: pick a change, or use --clone for a plain copy.', USAGE)
541
+ changes.append('copied for re-work')
542
+ if copy_only:
543
+ tmp = os.path.join(os.path.dirname(os.path.abspath(out)), '.%s.part' % os.path.basename(out))
544
+ shutil.copyfile(args.video, tmp)
545
+ os.replace(tmp, out)
546
+ else:
547
+ cards = {}
548
+ for i, piece in enumerate(pieces):
549
+ if piece.kind == 'card':
550
+ picture = os.path.join(work, 'card%d.png' % i)
551
+ render_card(picture, piece.title, piece.subtitle, info['width'], info['height'])
552
+ cards[id(piece)] = picture
553
+ inputs, graph, total = build_filters(pieces, info, music.get('in'), music.get('out'), args.music_volume, cards)
554
+ part = os.path.join(os.path.dirname(os.path.abspath(out)), '.%s.part' % os.path.basename(out))
555
+ el.run(['ffmpeg', '-y', '-loglevel', 'error'] + inputs + ['-filter_complex', graph, '-map', '[vout]', '-map', '[aout]',
556
+ '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '20', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-b:a', '192k',
557
+ '-movflags', '+faststart', '-f', 'mp4', part])
558
+ os.replace(part, out)
559
+ poster = os.path.splitext(args.video)[0] + '-poster.jpg'
560
+ new_poster = os.path.splitext(out)[0] + '-poster.jpg'
561
+ if os.path.exists(poster) and (args.force or not os.path.exists(new_poster)):
562
+ shutil.copyfile(poster, new_poster)
563
+ script_out = None
564
+ if script_text:
565
+ script_out = args.script_out or os.path.splitext(out)[0] + '-voiceover-script.md'
566
+ new_text = retime_script(script_text, pieces, added) if not copy_only else script_text
567
+ if args.force or not os.path.exists(script_out):
568
+ with open(script_out, 'w', encoding='utf-8') as handle:
569
+ handle.write(new_text)
570
+ length = el.duration(out)
571
+ record = {'kind': 'video-edit', 'borrowed': [{'from': b['from'], 'section': b['what'], 'title': b['title']} for b in borrowed], 'created_utc': datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
572
+ 'source': os.path.basename(args.video), 'changes': changes, 'seconds': round(length, 2) if length else None,
573
+ 'script': os.path.basename(script_out) if script_out else None}
574
+ with open(os.path.splitext(out)[0] + '-edit.json', 'w') as handle:
575
+ json.dump(record, handle, indent=2)
576
+ el.emit(args, dict(record, out=out), ['Wrote %s (%s)' % (out, el.mmss(length) if length else '?')] + [' ' + c for c in changes] +
577
+ (['Script: %s' % script_out] if script_out else []))
578
+ return OK
579
+ finally:
580
+ shutil.rmtree(work, ignore_errors=True)
581
+
582
+
583
+ def build_parser():
584
+ parser = argparse.ArgumentParser(prog='video_edit.py', description='Edit a video into a new version of itself.')
585
+ sub = parser.add_subparsers(dest='command', required=True)
586
+ info = sub.add_parser('info', help='size, frame rate and length of a video')
587
+ info.add_argument('--video', type=el.text_arg, required=True)
588
+ info.add_argument('--json', action='store_true')
589
+ edit = sub.add_parser('edit', help='a new version with the changes asked for; with none, a plain copy')
590
+ edit.add_argument('--video', type=el.text_arg, required=True)
591
+ edit.add_argument('--out', type=el.text_arg, required=True)
592
+ edit.add_argument('--script', type=el.text_arg, default='', help='the timed voiceover script; it is rewritten for the new version')
593
+ edit.add_argument('--script-out', type=el.text_arg, default='')
594
+ edit.add_argument('--strings-file', type=el.text_arg, default='', help='a JSON file holding the free text (titles, subtitles, add_text), so none of it is on a command line')
595
+ edit.add_argument('--clone', action='store_true', help='a plain copy for re-work')
596
+ edit.add_argument('--speed', type=float, default=None, help='%.2f to %.0f; 2 plays twice as fast' % (MIN_SPEED, MAX_SPEED))
597
+ edit.add_argument('--length', type=el.text_arg, default='', help='make it this long: seconds or minutes:seconds')
598
+ edit.add_argument('--trim', type=el.text_arg, default='', help='keep only start-end')
599
+ edit.add_argument('--remove', type=el.text_arg, default='', help='section ids to cut out, comma separated')
600
+ edit.add_argument('--add-title', type=el.text_arg, default='')
601
+ edit.add_argument('--add-subtitle', type=el.text_arg, default='')
602
+ edit.add_argument('--add-text', type=el.text_arg, default='', help='what the new section says')
603
+ edit.add_argument('--add-after', type=el.text_arg, default='', help='a section id, start or end')
604
+ edit.add_argument('--borrow', action='append', type=el.text_arg, default=[],
605
+ help='VIDEO|SECTION-or-START-END|AFTER|SCRIPT: a stretch of another video, put after a section, start or end (repeatable)')
606
+ edit.add_argument('--engine', choices=('piper', 'elevenlabs'), default='')
607
+ edit.add_argument('--voice', type=el.text_arg, default='')
608
+ edit.add_argument('--model', type=el.text_arg, default=os.environ.get('ELEVENLABS_MODEL') or el.DEFAULT_MODEL)
609
+ edit.add_argument('--intro-card', type=float, default=0, help='seconds of the Webship logo before the video')
610
+ edit.add_argument('--intro-title', type=el.text_arg, default='')
611
+ edit.add_argument('--intro-subtitle', type=el.text_arg, default='')
612
+ edit.add_argument('--outro-card', type=float, default=0, help='seconds of the Webship logo after the video')
613
+ edit.add_argument('--outro-title', type=el.text_arg, default='')
614
+ edit.add_argument('--outro-subtitle', type=el.text_arg, default='')
615
+ edit.add_argument('--intro-music', type=el.text_arg, default='')
616
+ edit.add_argument('--intro-seconds', type=float, default=8.0, help='how long the intro music plays when there is no logo scene')
617
+ edit.add_argument('--outro-music', type=el.text_arg, default='')
618
+ edit.add_argument('--outro-seconds', type=float, default=8.0)
619
+ edit.add_argument('--music-volume', type=float, default=0.5)
620
+ edit.add_argument('--confirm', action='store_true', help='ElevenLabs only: actually send the new section\'s text')
621
+ edit.add_argument('--force', action='store_true')
622
+ edit.add_argument('--json', action='store_true')
623
+ return parser
624
+
625
+
626
+ def main(argv):
627
+ args = build_parser().parse_args(argv)
628
+ try:
629
+ return {'info': cmd_info, 'edit': cmd_edit}[args.command](args)
630
+ except el.Stop as stop:
631
+ print(stop, file=sys.stderr)
632
+ return stop.code
633
+
634
+
635
+ if __name__ == '__main__':
636
+ sys.exit(main(sys.argv[1:]))