@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,201 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const http = require('http');
7
+
8
+ const ai = require('../../images-ai');
9
+
10
+ const root = path.join(__dirname, '..', '..');
11
+ const read = (p) => fs.readFileSync(path.join(root, p), 'utf8');
12
+ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64');
13
+
14
+ test('the plan checks every choice and estimates the cost before anything is sent', () => {
15
+ const p = ai.plan({ prompt: 'A calm blue lake', model: 'gpt-image-2.5-sunburst', shape: 'landscape', quality: 'medium', count: '3', name: 'Lake 1' });
16
+ assert.deepEqual([p.model, p.size, p.quality, p.n, p.name], ['gpt-image-2.5-sunburst', '1536x1024', 'medium', 3, 'Lake-1']);
17
+ assert.equal(p.estimate.toFixed(3), ((0.053 + ai.DESCRIBE_ESTIMATE) * 3).toFixed(3));
18
+ const odd = ai.plan({ prompt: 'x', model: 'dall-e-9', shape: 'huge', quality: 'max', count: '99' });
19
+ assert.deepEqual([odd.model, odd.size, odd.quality, odd.n], ['gpt-image-2.5-flare', '1024x1024', 'low', 4]);
20
+ assert.equal(ai.plan({ prompt: ' ' }).ok, false);
21
+ assert.match(ai.planHtml(p), /hx-trigger="confirmed-remove"/);
22
+ assert.match(ai.planHtml(p), /"confirm":"yes"/);
23
+ });
24
+
25
+ test('nothing is generated without confirm=yes, nor without a real key', async () => {
26
+ assert.equal((await ai.generate({ prompt: 'x' })).status, 400);
27
+ const noKey = await ai.generate({ prompt: 'x', confirm: 'yes' }, { key: '' });
28
+ assert.equal(noKey.ok, false);
29
+ assert.ok([503].includes(noKey.status) || /settings\.yml/.test(noKey.error));
30
+ });
31
+
32
+ test('a run saves each image with a sidecar and reports what it cost, against a stand-in for OpenAI', async () => {
33
+ let seen = null;
34
+ const srv = http.createServer((req, res) => {
35
+ let body = '';
36
+ req.on('data', (d) => { body += d; });
37
+ req.on('end', () => {
38
+ seen = { auth: req.headers.authorization, body: JSON.parse(body), url: req.url };
39
+ res.writeHead(200, { 'Content-Type': 'application/json' });
40
+ res.end(JSON.stringify({ data: [{ b64_json: PNG.toString('base64') }], usage: { output_tokens: 400, input_tokens: 20 } }));
41
+ });
42
+ });
43
+ await new Promise((r) => srv.listen(0, r));
44
+ process.env.OPENAI_IMAGES_API = `http://127.0.0.1:${srv.address().port}/v1/images/generations`;
45
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gen-'));
46
+ try {
47
+ // The stand-in is plain http, so the request module is swapped for this one run.
48
+ const https = require('https');
49
+ const realRequest = https.request;
50
+ https.request = http.request;
51
+ const r = await ai.generate({ prompt: 'A small test square', quality: 'low', count: '1', name: 'test', confirm: 'yes' }, { key: 'sk-test-key', dir });
52
+ https.request = realRequest;
53
+ assert.equal(r.ok, true, r.error);
54
+ assert.deepEqual(r.made, ['generated/test.png']);
55
+ assert.equal(seen.auth, 'Bearer sk-test-key');
56
+ assert.deepEqual([seen.body.model, seen.body.size, seen.body.quality, seen.body.n], ['gpt-image-2.5-flare', '1024x1024', 'low', 1]);
57
+ assert.ok(fs.readFileSync(path.join(dir, 'test.png')).equals(PNG));
58
+ const side = JSON.parse(fs.readFileSync(path.join(dir, 'test.png.json'), 'utf8'));
59
+ assert.equal(side.prompt, 'A small test square');
60
+ assert.equal(side.model, 'gpt-image-2.5-flare');
61
+ assert.equal(r.cost.toFixed(6), (400 * 30 / 1e6 + 20 * 5 / 1e6).toFixed(6));
62
+ } finally {
63
+ delete process.env.OPENAI_IMAGES_API;
64
+ srv.close();
65
+ fs.rmSync(dir, { recursive: true, force: true });
66
+ }
67
+ });
68
+
69
+ test('the key never reaches the page, a URL or a log line', () => {
70
+ const src = read('images-ai.js');
71
+ assert.match(read('images-providers/openai.js'), /Authorization: `Bearer \$\{key\}`/);
72
+ assert.doesNotMatch(src, /console\.(log|error)/);
73
+ assert.match(read('images-providers/http.js'), /replace\(\/sk-\[A-Za-z0-9_-\]\+\/g, 'sk-…'\)/);
74
+ assert.doesNotMatch(ai.formHtml(), /sk-[A-Za-z0-9]{8}/);
75
+ assert.match(read('images-page.js'), /btn\('secondary', 'bolt', 'Generate with AI'/);
76
+ assert.match(read('actions/images.js'), /if \(form\.confirm !== 'yes'\) \{\n\s+const p = ai\.plan\(form\);/);
77
+ });
78
+
79
+ // A stand-in for OpenAI that records each request and answers per path.
80
+ async function standIn(answer) {
81
+ const seen = [];
82
+ const srv = http.createServer((req, res) => {
83
+ const chunks = [];
84
+ req.on('data', (d) => chunks.push(d));
85
+ req.on('end', () => {
86
+ const raw = Buffer.concat(chunks);
87
+ seen.push({ url: req.url, auth: req.headers.authorization, type: req.headers['content-type'], raw });
88
+ const [status, body] = answer(req.url, raw);
89
+ res.writeHead(status, { 'Content-Type': 'application/json' });
90
+ res.end(JSON.stringify(body));
91
+ });
92
+ });
93
+ await new Promise((r) => srv.listen(0, r));
94
+ const base = `http://127.0.0.1:${srv.address().port}`;
95
+ process.env.OPENAI_IMAGES_API = `${base}/v1/images/generations`;
96
+ process.env.OPENAI_EDITS_API = `${base}/v1/images/edits`;
97
+ process.env.OPENAI_CHAT_API = `${base}/v1/chat/completions`;
98
+ const https = require('https');
99
+ const real = https.request;
100
+ https.request = http.request;
101
+ return { seen, close: () => { https.request = real; srv.close(); delete process.env.OPENAI_IMAGES_API; delete process.env.OPENAI_EDITS_API; delete process.env.OPENAI_CHAT_API; } };
102
+ }
103
+
104
+ const DESCRIBED = { choices: [{ message: { content: JSON.stringify({ summary: 'A small blue square on white.', alt: 'Blue square' }) } }], usage: { prompt_tokens: 100, completion_tokens: 50 } };
105
+
106
+ test('a generated image keeps its prompt in a readable .md, with an AI summary and alt text from a vision call', async () => {
107
+ const s = await standIn((url) => (url.includes('chat') ? [200, DESCRIBED]
108
+ : [200, { data: [{ b64_json: PNG.toString('base64') }], usage: { output_tokens: 400, input_tokens: 20 } }]));
109
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gen-'));
110
+ try {
111
+ const r = await ai.generate({ prompt: 'A small test square', quality: 'low', count: '1', name: 'sq', confirm: 'yes' }, { key: 'sk-test-key', dir });
112
+ assert.equal(r.ok, true, r.error);
113
+ const chat = s.seen.find((x) => x.url.includes('chat'));
114
+ const body = JSON.parse(chat.raw.toString());
115
+ assert.equal(chat.auth, 'Bearer sk-test-key');
116
+ assert.equal(body.model, ai.VISION_MODEL);
117
+ assert.deepEqual(body.response_format, { type: 'json_object' });
118
+ const img = body.messages[1].content.find((c) => c.type === 'image_url');
119
+ assert.match(img.image_url.url, /^data:image\/png;base64,/);
120
+ assert.equal(img.image_url.detail, 'low');
121
+ const side = JSON.parse(fs.readFileSync(path.join(dir, 'sq.png.json'), 'utf8'));
122
+ assert.deepEqual([side.summary, side.alt, side.described_by], ['A small blue square on white.', 'Blue square', ai.VISION_MODEL]);
123
+ const md = fs.readFileSync(path.join(dir, 'sq.md'), 'utf8');
124
+ assert.match(md, /^<!-- images: how this image was made -->\n# sq\n\n!\[Blue square\]\(\.\/sq\.png\)/);
125
+ assert.match(md, /A small blue square on white\./);
126
+ assert.match(md, /- \*\*Prompt:\*\* A small test square/);
127
+ assert.equal(r.cost.toFixed(6), (400 * 30 / 1e6 + 20 * 5 / 1e6 + 100 * 0.2 / 1e6 + 50 * 1.25 / 1e6).toFixed(6));
128
+ // The .md is not an image, so no list shows it.
129
+ assert.equal(require('../../images').IMAGE_RE.test('sq.md'), false);
130
+ } finally {
131
+ s.close();
132
+ fs.rmSync(dir, { recursive: true, force: true });
133
+ }
134
+ });
135
+
136
+ test('when the description call fails the image is kept, marked "not written", and Describe it is offered', async () => {
137
+ const s = await standIn((url) => (url.includes('chat') ? [500, { error: { message: 'busy' } }]
138
+ : [200, { data: [{ b64_json: PNG.toString('base64') }], usage: { output_tokens: 10 } }]));
139
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gen-'));
140
+ try {
141
+ const r = await ai.generate({ prompt: 'x', count: '1', name: 'nodesc', confirm: 'yes' }, { key: 'sk-test-key', dir });
142
+ assert.equal(r.ok, true);
143
+ assert.deepEqual(r.undescribed, ['nodesc.png']);
144
+ assert.equal(JSON.parse(fs.readFileSync(path.join(dir, 'nodesc.png.json'), 'utf8')).description, 'not written');
145
+ const page = read('images-page.js');
146
+ assert.match(page, /hx-post="\/actions\/image-describe" hx-trigger="confirmed-remove"/);
147
+ assert.match(page, /made\.description === 'not written' \? 'The description was not written\.'/);
148
+ assert.equal((await ai.describeExisting({ file: 'x.png' })).status, 400);
149
+ } finally {
150
+ s.close();
151
+ fs.rmSync(dir, { recursive: true, force: true });
152
+ }
153
+ });
154
+
155
+ test('Copy Markdown uses the alt text, and the plan prices the description', () => {
156
+ assert.match(read('images-page.js'), /data-copy="\$\{esc\(`!\[\$\{String\(\(made && made\.alt\) \|\| im\.name/);
157
+ const p = ai.plan({ prompt: 'x', quality: 'low', count: '2' });
158
+ assert.equal(p.estimate.toFixed(3), ((0.006 + ai.DESCRIBE_ESTIMATE) * 2).toFixed(3));
159
+ assert.match(ai.planHtml(p), /for the description/);
160
+ });
161
+
162
+ test('Edit with AI sends the source as multipart, never overwrites, records its source, and refuses an SVG', async () => {
163
+ const s = await standIn((url) => (url.includes('chat') ? [200, DESCRIBED]
164
+ : [200, { data: [{ b64_json: PNG.toString('base64') }], usage: { output_tokens: 300, input_tokens: 50, input_tokens_details: { image_tokens: 40, text_tokens: 10 } } }]));
165
+ const imagesRoot = require('../../workspaces').workspaceDir('images');
166
+ const dir = fs.mkdtempSync(path.join(imagesRoot, 'test-ai-edit-'));
167
+ const relDir = path.basename(dir);
168
+ try {
169
+ fs.writeFileSync(path.join(dir, 'pic.png'), PNG);
170
+ fs.writeFileSync(path.join(dir, 'pic-ai-v1.png'), PNG);
171
+ assert.equal((await ai.editImage({ src: 'images', file: `${relDir}/pic.png`, prompt: 'warmer' }, { key: 'sk-test-key' })).status, 400);
172
+ const r = await ai.editImage({ src: 'images', file: `${relDir}/pic.png`, prompt: 'make it warmer', quality: 'low', confirm: 'yes' }, { key: 'sk-test-key' });
173
+ assert.equal(r.ok, true, r.error);
174
+ assert.equal(r.name, 'pic-ai-v2.png');
175
+ const edit = s.seen.find((x) => x.url.includes('edits'));
176
+ assert.match(edit.type, /^multipart\/form-data; boundary=/);
177
+ const raw = edit.raw.toString('latin1');
178
+ assert.match(raw, /name="prompt"\r\n\r\nmake it warmer\r\n/);
179
+ assert.match(raw, /name="image"; filename="pic\.png"\r\nContent-Type: image\/png/);
180
+ assert.equal(edit.auth, 'Bearer sk-test-key');
181
+ const side = JSON.parse(fs.readFileSync(path.join(dir, 'pic-ai-v2.png.json'), 'utf8'));
182
+ assert.deepEqual(side.source, { workspace: 'images', path: `${relDir}/pic.png` });
183
+ assert.equal(side.edit_prompt, 'make it warmer');
184
+ assert.equal(side.alt, 'Blue square');
185
+ assert.ok(fs.readFileSync(path.join(dir, 'pic.png')).equals(PNG), 'the source is unchanged');
186
+ fs.writeFileSync(path.join(dir, 'logo.svg'), '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"/>');
187
+ assert.match(ai.editPlan({ src: 'images', file: `${relDir}/logo.svg`, prompt: 'x' }).error, /SVG cannot be edited/);
188
+ assert.equal(ai.aiVersionName(dir, 'pic.png', 'png'), 'pic-ai-v3.png');
189
+ assert.equal(ai.nearestSize(1600, 1000), '1536x1024');
190
+ assert.match(ai.editPlanHtml(ai.editPlan({ src: 'images', file: `${relDir}/pic.png`, prompt: 'x' })), /hx-trigger="confirmed-remove"/);
191
+ } finally {
192
+ s.close();
193
+ fs.rmSync(dir, { recursive: true, force: true });
194
+ }
195
+ });
196
+
197
+ test('an edit of an image from another workspace is saved into images/edited/', () => {
198
+ const p = ai.editPlan({ src: 'docs', file: 'nothing-here.png', prompt: 'x' });
199
+ assert.equal(p.ok, false);
200
+ assert.match(read('images-ai.js'), /const dir = src === 'images' \? path\.dirname\(where\.abs\) : path\.join\(workspaceDir\('images'\), 'edited'\);/);
201
+ });
@@ -0,0 +1,59 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const create = require('../../images-create');
7
+
8
+ const root = path.join(__dirname, '..', '..');
9
+ const read = (p) => fs.readFileSync(path.join(root, p), 'utf8');
10
+
11
+ test('an upload is an image by its bytes, not by its name', () => {
12
+ assert.equal(create.sniff(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])), 'png');
13
+ assert.equal(create.sniff(Buffer.from([0xff, 0xd8, 0xff, 0xe0])), 'jpg');
14
+ assert.equal(create.sniff(Buffer.from('GIF89a......')), 'gif');
15
+ assert.equal(create.sniff(Buffer.from('RIFF....WEBPVP8 ')), 'webp');
16
+ assert.equal(create.sniff(Buffer.from('....ftypavif....')), 'avif');
17
+ assert.equal(create.sniff(Buffer.from('<?xml version="1.0"?>\n<svg xmlns="http://www.w3.org/2000/svg"></svg>')), 'svg');
18
+ assert.equal(create.sniff(Buffer.from('#!/bin/sh\nrm -rf /')), '');
19
+ assert.equal(create.sniff(Buffer.from('<html><svg></svg></html>')), '');
20
+ assert.equal(create.MAX_UPLOAD, 25 * 1024 * 1024);
21
+ });
22
+
23
+ test('an upload or a screenshot only ever lands inside the images workspace', () => {
24
+ assert.equal(create.targetDir('../dev'), null);
25
+ assert.equal(create.targetDir('a/../../etc'), null);
26
+ assert.equal(create.targetDir('.hidden'), null);
27
+ assert.equal(create.targetDir('ok;rm -rf'), null);
28
+ });
29
+
30
+ test('an edit is ImageMagick argv from fixed lists and checked numbers, saved under a new name', () => {
31
+ const dir = '/tmp/x';
32
+ const src = '/tmp/x/shot.png';
33
+ const crop = create.editArgs(src, dir, { crop: '16:9', width: '800', format: 'webp', quality: '70' }, { width: 1600, height: 1000 });
34
+ assert.deepEqual(crop.args, [`${src}[0]`, '-auto-orient', '-gravity', 'center', '-crop', '1600x900+0+0', '+repage',
35
+ '-resize', '800x', '-quality', '70', 'webp:/tmp/x/shot-v2.webp']);
36
+ assert.equal(crop.changed, true);
37
+ const tall = create.editArgs(src, dir, { crop: '1:1' }, { width: 1600, height: 1000 });
38
+ assert.ok(tall.args.includes('1000x1000+0+0'));
39
+ const junk = create.editArgs(src, dir, { width: '9999999', format: 'exe', crop: '; rm', quality: '500' }, { width: 10, height: 10 });
40
+ assert.equal(junk.changed, false);
41
+ assert.ok(!junk.args.some((a) => /rm|exe|9999999/.test(a)));
42
+ const jpg = create.editArgs(src, dir, { format: 'jpg', quality: '500' }, null);
43
+ assert.ok(jpg.args.includes('-quality') && jpg.args.includes('85'));
44
+ });
45
+
46
+ test('upload, screenshot and edit are wired, with the upload on its own binary route', () => {
47
+ const server = read('server.js');
48
+ assert.match(server, /pathname === '\/actions\/image-upload' && isValidWorkspace\('images'\)\) \{\n\s+if \(foreignOrigin\(req\)\)/);
49
+ const actions = read('actions/images.js');
50
+ assert.match(actions, /'\/actions\/image-screenshot': image_screenshot/);
51
+ assert.match(actions, /'\/actions\/image-edit': image_edit/);
52
+ const page = read('images-page.js');
53
+ assert.match(page, /btn\('primary', 'upload', 'Upload images'/);
54
+ assert.match(page, /btn\('default', 'tabler:camera', 'Screenshot a page'/);
55
+ const ui = read('public/ui.js');
56
+ assert.match(ui, /fetch\(`\/actions\/image-upload\?path=\$\{encodeURIComponent\(folder\)\}&name=\$\{encodeURIComponent\(f\.name\)\}`/);
57
+ assert.match(create.screenshotFormHtml(), /name="width"/);
58
+ assert.match(create.editFormHtml('a.png'), /Save as a new file/);
59
+ });
@@ -0,0 +1,248 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const http = require('http');
7
+
8
+ const providers = require('../../images-providers');
9
+ const ai = require('../../images-ai');
10
+
11
+ const root = path.join(__dirname, '..', '..');
12
+ const read = (p) => fs.readFileSync(path.join(root, p), 'utf8');
13
+ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64');
14
+
15
+ // A stand-in for every provider: it records each request and answers per path.
16
+ async function standIn(answer) {
17
+ const seen = [];
18
+ const srv = http.createServer((req, res) => {
19
+ const chunks = [];
20
+ req.on('data', (d) => chunks.push(d));
21
+ req.on('end', () => {
22
+ const raw = Buffer.concat(chunks);
23
+ seen.push({ url: req.url, headers: req.headers, raw, json: (() => { try { return JSON.parse(raw); } catch (_) { return null; } })() });
24
+ const [status, body] = answer(req.url, raw);
25
+ res.writeHead(status, { 'Content-Type': 'application/json' });
26
+ res.end(JSON.stringify(body));
27
+ });
28
+ });
29
+ await new Promise((r) => srv.listen(0, r));
30
+ const base = `http://127.0.0.1:${srv.address().port}`;
31
+ const env = {
32
+ GEMINI_INTERACTIONS_API: `${base}/gemini/interactions`,
33
+ XAI_IMAGES_API: `${base}/xai/images/generations`,
34
+ XAI_EDITS_API: `${base}/xai/images/edits`,
35
+ STABILITY_CORE_API: `${base}/stability/core`,
36
+ ANTHROPIC_MESSAGES_API: `${base}/anthropic/messages`,
37
+ OPENAI_CHAT_API: `${base}/openai/chat`,
38
+ };
39
+ Object.assign(process.env, env);
40
+ const https = require('https');
41
+ const real = https.request;
42
+ https.request = http.request;
43
+ return { seen, close: () => { https.request = real; srv.close(); for (const k of Object.keys(env)) delete process.env[k]; } };
44
+ }
45
+
46
+ test('every provider keeps the registry contract', () => {
47
+ assert.deepEqual(providers.ORDER, ['openai', 'gemini', 'xai', 'stability', 'anthropic', 'claude-code']);
48
+ for (const p of providers.list()) {
49
+ assert.equal(typeof p.id, 'string');
50
+ assert.equal(typeof p.label, 'string');
51
+ assert.ok(Array.isArray(p.kinds) && p.kinds.length, p.id);
52
+ const a = p.available();
53
+ assert.equal(typeof a.ok, 'boolean', p.id);
54
+ if (!a.ok) assert.match(a.why, /settings\.yml|claude CLI/, p.id);
55
+ for (const kind of p.kinds) {
56
+ if (kind === 'generate' || kind === 'edit') {
57
+ assert.ok(p.models[p.defaultModel], `${p.id} default model`);
58
+ assert.ok(Object.keys(p.sizes).length, `${p.id} sizes`);
59
+ assert.ok(p.qualities === null || p.qualities.length, `${p.id} qualities`);
60
+ }
61
+ if (kind === 'generate') assert.ok(p.async || typeof p.generate === 'function', `${p.id} generate`);
62
+ if (kind === 'edit') assert.ok(p.async || typeof p.edit === 'function', `${p.id} edit`);
63
+ if (kind === 'svg') assert.ok(p.async || typeof p.svg === 'function', `${p.id} svg`);
64
+ }
65
+ }
66
+ assert.equal(providers.pick('nope', 'generate').id, 'openai');
67
+ assert.equal(providers.pick('stability', 'generate').id, 'stability');
68
+ assert.equal(providers.pick('stability', 'edit').id, 'openai', 'Stability does not edit');
69
+ assert.deepEqual(providers.list('svg').map((p) => p.id), ['openai', 'gemini', 'anthropic', 'claude_code_cli']);
70
+ });
71
+
72
+ test('a provider with a placeholder key is unavailable, shown disabled with the key to set, and nothing is sent', async () => {
73
+ for (const id of ['gemini', 'xai', 'stability', 'anthropic']) {
74
+ const p = providers.get(id);
75
+ assert.equal(p.key(), '', `${id} reads the shipped placeholder as no key`);
76
+ assert.deepEqual(p.available(), { ok: false, why: `set ai_providers.${id}.api_key in settings.yml` });
77
+ }
78
+ const html = ai.providerSelectHtml('generate', 'gemini');
79
+ assert.match(html, /<option value="gemini" disabled>Google Gemini \(set ai_providers\.gemini\.api_key in settings\.yml\)<\/option>/);
80
+ const s = await standIn(() => [200, {}]);
81
+ try {
82
+ assert.equal((await ai.generate({ prompt: 'x', provider: 'stability' })).status, 400, 'no confirm, no call');
83
+ const r = await ai.generate({ prompt: 'x', provider: 'stability', confirm: 'yes' });
84
+ assert.equal(r.status, 503);
85
+ assert.match(r.error, /ai_providers\.stability\.api_key/);
86
+ assert.equal(s.seen.length, 0);
87
+ } finally {
88
+ s.close();
89
+ }
90
+ });
91
+
92
+ test('Gemini: the Interactions request, the key in x-goog-api-key, and the image read from the output steps', async () => {
93
+ const s = await standIn(() => [200, { steps: [{ type: 'thought' }, { type: 'model_output', content: [{ type: 'text', text: 'here' }, { type: 'image', mime_type: 'image/png', data: PNG.toString('base64') }] }] }]);
94
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gem-'));
95
+ try {
96
+ const r = await ai.generate({ prompt: 'A lake', provider: 'gemini', shape: 'landscape', quality: 'medium', count: '1', name: 'lake', confirm: 'yes' },
97
+ { key: 'AIzaTESTKEY', dir, describe: false });
98
+ assert.equal(r.ok, true, r.error);
99
+ assert.equal(r.cost, null, 'no published figure, so no made-up cost');
100
+ const call = s.seen[0];
101
+ assert.equal(call.url, '/gemini/interactions');
102
+ assert.equal(call.headers['x-goog-api-key'], 'AIzaTESTKEY');
103
+ assert.equal(call.headers.authorization, undefined);
104
+ assert.deepEqual(call.json, { model: 'gemini-nano-banana-2.1', input: [{ type: 'text', text: 'A lake' }],
105
+ response_format: { type: 'image', aspect_ratio: '16:9', image_size: '2K' } });
106
+ assert.ok(fs.readFileSync(path.join(dir, 'lake.png')).equals(PNG));
107
+ assert.equal(JSON.parse(fs.readFileSync(path.join(dir, 'lake.png.json'), 'utf8')).provider, 'Google Gemini');
108
+ } finally {
109
+ s.close();
110
+ fs.rmSync(dir, { recursive: true, force: true });
111
+ }
112
+ });
113
+
114
+ test('xAI: generate and edit with a Bearer key, base64 asked for, the edit image as a data URL', async () => {
115
+ const s = await standIn(() => [200, { data: [{ b64_json: PNG.toString('base64') }] }]);
116
+ try {
117
+ const xai = providers.get('xai');
118
+ const g = await xai.generate({ model: 'grok-imagine-image-2.0', prompt: 'A fox', n: 2 }, 'xai-TESTKEY');
119
+ assert.equal(g.images.length, 1);
120
+ assert.equal(s.seen[0].url, '/xai/images/generations');
121
+ assert.equal(s.seen[0].headers.authorization, 'Bearer xai-TESTKEY');
122
+ assert.deepEqual(s.seen[0].json, { model: 'grok-imagine-image-2.0', prompt: 'A fox', n: 2, response_format: 'b64_json' });
123
+ const e = await xai.edit({ model: 'grok-imagine-image-2.0', prompt: 'warmer' }, { name: 'a.png', type: 'image/png', data: PNG }, 'xai-TESTKEY');
124
+ assert.ok(e.image.equals(PNG));
125
+ assert.equal(s.seen[1].url, '/xai/images/edits');
126
+ assert.deepEqual(s.seen[1].json.image, { type: 'image_url', url: `data:image/png;base64,${PNG.toString('base64')}` });
127
+ } finally {
128
+ s.close();
129
+ }
130
+ });
131
+
132
+ test('Stability: multipart with prompt, aspect ratio and PNG, JSON asked for, one call per image', async () => {
133
+ const s = await standIn(() => [200, { image: PNG.toString('base64'), finish_reason: 'SUCCESS' }]);
134
+ try {
135
+ const st = providers.get('stability');
136
+ const r = await st.generate({ prompt: 'A hill', size: '9:16', n: 2 }, 'sk-STABTEST');
137
+ assert.equal(r.images.length, 2);
138
+ assert.equal(r.cost.toFixed(2), '0.06');
139
+ const call = s.seen[0];
140
+ assert.equal(call.headers.authorization, 'Bearer sk-STABTEST');
141
+ assert.equal(call.headers.accept, 'application/json');
142
+ assert.match(call.headers['content-type'], /^multipart\/form-data; boundary=/);
143
+ const raw = call.raw.toString('latin1');
144
+ assert.match(raw, /name="prompt"\r\n\r\nA hill\r\n/);
145
+ assert.match(raw, /name="aspect_ratio"\r\n\r\n9:16\r\n/);
146
+ assert.match(raw, /name="output_format"\r\n\r\npng\r\n/);
147
+ } finally {
148
+ s.close();
149
+ }
150
+ });
151
+
152
+ test('Anthropic and OpenAI SVG source: the key in its own header, the text read back', async () => {
153
+ const s = await standIn((url) => (url.includes('anthropic')
154
+ ? [200, { content: [{ type: 'text', text: '{"svg":"<svg/>"}' }] }]
155
+ : [200, { choices: [{ message: { content: '{"svg":"<svg/>"}' } }], usage: { prompt_tokens: 1000, completion_tokens: 2000 } }]));
156
+ try {
157
+ const a = await providers.get('anthropic').svg({ system: 'sys', messages: [{ role: 'user', content: 'draw' }], model: 'claude-haiku-5-5' }, 'sk-ant-TEST');
158
+ assert.equal(a.text, '{"svg":"<svg/>"}');
159
+ const call = s.seen[0];
160
+ assert.equal(call.headers['x-api-key'], 'sk-ant-TEST');
161
+ assert.equal(call.headers['anthropic-version'], '2023-06-01');
162
+ assert.deepEqual([call.json.model, call.json.system, call.json.messages[0].content], ['claude-haiku-5-5', 'sys', 'draw']);
163
+ const o = await providers.get('openai').svg({ system: 'sys', messages: [{ role: 'user', content: 'draw' }], model: 'gpt-5.4-mini' }, 'sk-TEST');
164
+ assert.equal(o.cost.toFixed(6), (1000 * 0.75 / 1e6 + 2000 * 4.5 / 1e6).toFixed(6));
165
+ assert.deepEqual(s.seen[1].json.response_format, { type: 'json_object' });
166
+ } finally {
167
+ s.close();
168
+ }
169
+ });
170
+
171
+ test('an error from a provider is shown with any key masked', async () => {
172
+ const s = await standIn(() => [401, { error: { message: 'Incorrect API key provided: sk-abcdef123456 (Bearer sk-abcdef123456)' } }]);
173
+ try {
174
+ await assert.rejects(providers.get('xai').generate({ prompt: 'x', n: 1 }, 'sk-abcdef123456'), (e) => {
175
+ assert.doesNotMatch(e.message, /abcdef123456/);
176
+ assert.match(e.message, /sk-…/);
177
+ return true;
178
+ });
179
+ } finally {
180
+ s.close();
181
+ }
182
+ });
183
+
184
+ test('Claude Code: a session from a 0600 prompt file, no prompt text on the shell line, limited tools', async () => {
185
+ const cc = providers.get('claude_code_cli');
186
+ const calls = [];
187
+ const start = async (action, payload) => { calls.push({ action, payload }); return { ok: true }; };
188
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-'));
189
+ const secret = "a lighthouse; rm -rf ~ $(whoami) `id` 'quoted'";
190
+ try {
191
+ const r = await cc.startSession({ kind: 'generate', name: 'lighthouse', dir, prompt: secret, pixels: [1024, 1024], palette: 'blue #0074e0' }, start);
192
+ assert.equal(r.ok, true, r.error);
193
+ const { cmd, session } = calls[0].payload;
194
+ assert.equal(calls[0].action, 'start');
195
+ assert.equal(session, 'images--claude-lighthouse');
196
+ assert.doesNotMatch(cmd, /lighthouse;|whoami|rm -rf|quoted/, 'the prompt never reaches the line');
197
+ assert.match(cmd, /^claude -p "\$\(cat '[^']+\.txt'\)" /);
198
+ assert.match(cmd, /--allowedTools Write Read --disallowedTools Bash WebFetch WebSearch/);
199
+ assert.match(cmd, /&& node '[^']+images-claude-finish\.js' '[^']+\.job\.json'$/);
200
+ const promptFile = cmd.match(/cat '([^']+)'/)[1];
201
+ assert.equal(fs.statSync(promptFile).mode & 0o777, 0o600);
202
+ const prompt = fs.readFileSync(promptFile, 'utf8');
203
+ assert.match(prompt, /a lighthouse; rm -rf/);
204
+ assert.match(prompt, /lighthouse\.svg: one self-contained SVG with viewBox="0 0 1024 1024" and no width or height/);
205
+ assert.match(prompt, /Do not run any commands/);
206
+ const job = cmd.match(/'([^']+\.job\.json)'$/)[1];
207
+ assert.equal(fs.statSync(job).mode & 0o777, 0o600);
208
+ fs.rmSync(promptFile, { force: true });
209
+ fs.rmSync(job, { force: true });
210
+ fs.rmSync(path.join(path.dirname(path.dirname(promptFile)), `${session}.cwd`), { force: true });
211
+ } finally {
212
+ fs.rmSync(dir, { recursive: true, force: true });
213
+ }
214
+ });
215
+
216
+ test('Claude Code in the forms: no charge, watched on Screens, the confirm dialog still asked, SVG-only for edits', () => {
217
+ const p = ai.plan({ prompt: 'a lighthouse', provider: 'claude_code_cli', count: '3' });
218
+ assert.deepEqual([p.provider, p.n, p.estimate, p.quality], ['claude_code_cli', 1, 0, null]);
219
+ const html = ai.planHtml(p);
220
+ assert.match(html, /No API charge: it uses your own Claude Code login\./);
221
+ assert.match(html, /watch it on Screens/);
222
+ assert.match(html, /hx-trigger="confirmed-remove"/);
223
+ assert.deepEqual(ai.editProviders(true).map((x) => x.id), ['claude_code_cli']);
224
+ assert.ok(!ai.editProviders(false).some((x) => x.id === 'claude_code_cli'));
225
+ assert.match(read('actions/images.js'), /if \(r\.async\) return send\(startedHtml\(r\.session/);
226
+ });
227
+
228
+ test('the finish step sanitises the SVG and writes its records', () => {
229
+ const finish = require('../../scripts/images-claude-finish');
230
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccf-'));
231
+ try {
232
+ fs.writeFileSync(path.join(dir, 'pic.svg'), '<svg viewBox="0 0 10 10" onload="x()"><script>x()</script><rect width="5" height="5" fill="#0074e0"/></svg>');
233
+ fs.writeFileSync(path.join(dir, 'pic.claude.json'), JSON.stringify({ summary: 'A blue square.', alt: 'Blue square' }));
234
+ const job = path.join(dir, 'pic.job.json');
235
+ fs.writeFileSync(job, JSON.stringify({ kind: 'generate', name: 'pic', dir, prompt: 'a square', pixels: [10, 10], png: false }));
236
+ assert.equal(finish.main(job), 0);
237
+ const svg = fs.readFileSync(path.join(dir, 'pic.svg'), 'utf8');
238
+ assert.doesNotMatch(svg, /script|onload/);
239
+ assert.match(svg, /<rect /);
240
+ const side = JSON.parse(fs.readFileSync(path.join(dir, 'pic.svg.json'), 'utf8'));
241
+ assert.deepEqual([side.provider, side.summary, side.alt, side.prompt], ['claude_code_cli', 'A blue square.', 'Blue square', 'a square']);
242
+ assert.ok(!fs.existsSync(path.join(dir, 'pic.claude.json')));
243
+ assert.ok(!fs.existsSync(job));
244
+ assert.match(fs.readFileSync(path.join(dir, 'pic.md'), 'utf8'), /- \*\*Provider:\*\* claude_code_cli/);
245
+ } finally {
246
+ fs.rmSync(dir, { recursive: true, force: true });
247
+ }
248
+ });