@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.
- package/LICENSE +339 -0
- package/README.md +28 -364
- package/core/bin/workspace.js +135 -0
- package/core/config/settings.labels.yml +514 -0
- package/core/config/settings.template.yml +292 -0
- package/core/config/style.yml +53 -0
- package/core/config/theme.default.yml +161 -0
- package/core/config/theme.dispatch.yml +166 -0
- package/core/config/theme.drupal.yml +175 -0
- package/core/config/theme.ember.yml +160 -0
- package/core/config/theme.github.yml +172 -0
- package/core/config/theme.gitlab.yml +168 -0
- package/core/config/theme.press.yml +161 -0
- package/core/config/theme.slate.yml +160 -0
- package/core/config/theme.upsun.yml +174 -0
- package/core/docs/architecture.svg +65 -0
- package/core/docs/getting-started.md +51 -0
- package/core/docs/plugins.md +27 -0
- package/core/docs/releases.md +23 -0
- package/core/docs/testing.md +15 -0
- package/core/docs/workspaces.md +25 -0
- package/core/scripts/args/arg-ai-items.sh +65 -0
- package/core/scripts/args/arg-autostart.sh +31 -0
- package/core/scripts/args/arg-avatar-agent.sh +28 -0
- package/core/scripts/args/arg-avatar-catalog.sh +12 -0
- package/core/scripts/args/arg-avatar-character.sh +68 -0
- package/core/scripts/args/arg-avatar-voice.sh +24 -0
- package/core/scripts/args/arg-avatar.sh +22 -0
- package/core/scripts/args/arg-checks.sh +36 -0
- package/core/scripts/args/arg-clone.sh +42 -0
- package/core/scripts/args/arg-commands.sh +51 -0
- package/core/scripts/args/arg-cucumber.sh +34 -0
- package/core/scripts/args/arg-distribution.sh +30 -0
- package/core/scripts/args/arg-drupal.sh +39 -0
- package/core/scripts/args/arg-drupal_cms.sh +18 -0
- package/core/scripts/args/arg-gitlab-ci-local.sh +25 -0
- package/core/scripts/args/arg-graphify.sh +36 -0
- package/core/scripts/args/arg-graphs.sh +12 -0
- package/core/scripts/args/arg-hostnames.sh +23 -0
- package/core/scripts/args/arg-milvus-backup.sh +8 -0
- package/core/scripts/args/arg-milvus-mcp.sh +12 -0
- package/core/scripts/args/arg-milvus.sh +32 -0
- package/core/scripts/args/arg-obsidian.sh +17 -0
- package/core/scripts/args/arg-odoo.sh +18 -0
- package/core/scripts/args/arg-penpot-backup.sh +8 -0
- package/core/scripts/args/arg-penpot-mcp.sh +15 -0
- package/core/scripts/args/arg-penpot-token.sh +17 -0
- package/core/scripts/args/arg-penpot.sh +44 -0
- package/core/scripts/args/arg-rag.sh +21 -0
- package/core/scripts/args/arg-ragify.sh +35 -0
- package/core/scripts/args/arg-reclaim.sh +18 -0
- package/core/scripts/args/arg-release.sh +21 -0
- package/core/scripts/args/arg-screen.sh +28 -0
- package/core/scripts/args/arg-site-template.sh +13 -0
- package/core/scripts/args/arg-spdd.sh +16 -0
- package/core/scripts/args/arg-specs.sh +22 -0
- package/core/scripts/args/arg-storage.sh +36 -0
- package/core/scripts/args/arg-testing.sh +36 -0
- package/core/scripts/args/arg-webship.sh +34 -0
- package/core/scripts/args/arg-webships-project.sh +22 -0
- package/core/scripts/args/arg-webships-site-template.sh +17 -0
- package/core/scripts/args/arg-webships.sh +34 -0
- package/core/scripts/args/arg-website-site-template.sh +17 -0
- package/core/scripts/args/arg-website.sh +25 -0
- package/core/scripts/args/arg-wordpress.sh +18 -0
- package/core/scripts/autostart-run.sh +162 -0
- package/core/scripts/autostart-stop.sh +30 -0
- package/core/scripts/autostart-watch.sh +43 -0
- package/core/scripts/autostart.sh +26 -0
- package/core/scripts/bootstrap-functions.sh +117 -0
- package/core/scripts/bootstrap-libraries.sh +10 -0
- package/core/scripts/bootstrap.sh +71 -0
- package/core/scripts/ddev-shim/ddev +122 -0
- package/core/scripts/docker-gid.sh +119 -0
- package/core/scripts/functions/arg-site-template.sh +28 -0
- package/core/scripts/functions/fun-ai-items-lock.sh +175 -0
- package/core/scripts/functions/fun-ai-items-sources.sh +298 -0
- package/core/scripts/functions/fun-ai-items.sh +621 -0
- package/core/scripts/functions/fun-ai-providers.sh +72 -0
- package/core/scripts/functions/fun-autostart.sh +429 -0
- package/core/scripts/functions/fun-avatar.sh +441 -0
- package/core/scripts/functions/fun-backup-item.sh +50 -0
- package/core/scripts/functions/fun-build-distribution.sh +226 -0
- package/core/scripts/functions/fun-build-site-template.sh +104 -0
- package/core/scripts/functions/fun-build-webships.sh +161 -0
- package/core/scripts/functions/fun-build-website.sh +199 -0
- package/core/scripts/functions/fun-checks.sh +243 -0
- package/core/scripts/functions/fun-clone-project.sh +289 -0
- package/core/scripts/functions/fun-commands.sh +521 -0
- package/core/scripts/functions/fun-database.sh +63 -0
- package/core/scripts/functions/fun-ddev-hostnames.sh +213 -0
- package/core/scripts/functions/fun-ddev-nodejs.sh +15 -0
- package/core/scripts/functions/fun-ddev-telemetry.sh +15 -0
- package/core/scripts/functions/fun-default-settings.sh +28 -0
- package/core/scripts/functions/fun-distribution-cucumber.sh +45 -0
- package/core/scripts/functions/fun-distribution-drupal.sh +25 -0
- package/core/scripts/functions/fun-distribution-drupal_cms.sh +35 -0
- package/core/scripts/functions/fun-distribution-webship.sh +28 -0
- package/core/scripts/functions/fun-distribution-webships.sh +33 -0
- package/core/scripts/functions/fun-distribution-website.sh +19 -0
- package/core/scripts/functions/fun-drush.sh +48 -0
- package/core/scripts/functions/fun-elevenlabs.sh +179 -0
- package/core/scripts/functions/fun-files.sh +19 -0
- package/core/scripts/functions/fun-git-worktrees.sh +142 -0
- package/core/scripts/functions/fun-gitlab-ci.sh +156 -0
- package/core/scripts/functions/fun-hostnames-cli.sh +88 -0
- package/core/scripts/functions/fun-milvus.sh +584 -0
- package/core/scripts/functions/fun-odoo.sh +121 -0
- package/core/scripts/functions/fun-penpot.sh +724 -0
- package/core/scripts/functions/fun-project-name.sh +20 -0
- package/core/scripts/functions/fun-rag.sh +253 -0
- package/core/scripts/functions/fun-ragify.sh +165 -0
- package/core/scripts/functions/fun-reclaim.sh +297 -0
- package/core/scripts/functions/fun-release.sh +159 -0
- package/core/scripts/functions/fun-remove-project.sh +319 -0
- package/core/scripts/functions/fun-screen.sh +96 -0
- package/core/scripts/functions/fun-spdd.sh +676 -0
- package/core/scripts/functions/fun-specs-share.sh +237 -0
- package/core/scripts/functions/fun-storage.sh +347 -0
- package/core/scripts/functions/fun-testing.sh +329 -0
- package/core/scripts/functions/fun-users.sh +87 -0
- package/core/scripts/functions/fun-version.sh +72 -0
- package/core/scripts/functions/fun-video-edit.sh +48 -0
- package/core/scripts/functions/fun-voice.sh +45 -0
- package/core/scripts/functions/fun-wordpress.sh +59 -0
- package/core/scripts/host-mounts.sh +161 -0
- package/core/scripts/install.sh +602 -0
- package/core/scripts/libs/__pycache__/video_edit.cpython-312.pyc +0 -0
- package/core/scripts/libs/argparse.sh +93 -0
- package/core/scripts/libs/bash-yaml.sh +52 -0
- package/core/scripts/libs/checks_plan.js +312 -0
- package/core/scripts/libs/elevenlabs.py +627 -0
- package/core/scripts/libs/progress-bar.sh +19 -0
- package/core/scripts/libs/rag_index.py +446 -0
- package/core/scripts/libs/video_edit.py +636 -0
- package/core/scripts/libs/voice.py +430 -0
- package/core/scripts/plugins/jira/host-install.sh +119 -0
- package/core/scripts/plugins/jira/host-mounts.txt +2 -0
- package/core/scripts/plugins/upsun/args/arg-upsun.sh +51 -0
- package/core/scripts/plugins/upsun/functions/fun-upsun.sh +304 -0
- package/core/scripts/plugins/upsun/host-install.sh +99 -0
- package/core/scripts/plugins/upsun/host-mounts.txt +1 -0
- package/core/scripts/plugins/upsun/upsun-ai.sh +133 -0
- package/core/scripts/plugins.sh +152 -0
- package/core/scripts/release.sh +69 -0
- package/core/scripts/storage.sh +39 -0
- package/core/scripts/sync-tooling.sh +246 -0
- package/core/scripts/tests/cmd-smoke-test.sh +109 -0
- package/core/scripts/trust-https.sh +257 -0
- package/core/workspace-app/.ddev/commands/web/test-dashboard +49 -0
- package/core/workspace-app/.ddev/config.yaml +400 -0
- package/core/workspace-app/.ddev/docker-compose.dood.yaml +45 -0
- package/core/workspace-app/.ddev/docker-compose.plugin-jira-twg.yaml +26 -0
- package/core/workspace-app/.ddev/docker-compose.plugin-upsun-upsun-cli.yaml +8 -0
- package/core/workspace-app/.ddev/docker-compose.screens.yaml +15 -0
- package/core/workspace-app/.ddev/nginx_full/nginx-site.conf +114 -0
- package/core/workspace-app/.ddev/web-build/Dockerfile.workspace-app +203 -0
- package/core/workspace-app/.ddev/web-build/ddev-shim-ddev +122 -0
- package/core/workspace-app/README.md +380 -0
- package/{actions → core/workspace-app/actions}/ddev.js +0 -1
- package/{actions → core/workspace-app/actions}/rag.js +0 -1
- package/{actions → core/workspace-app/actions}/testing.js +0 -1
- package/{assistant.js → core/workspace-app/assistant.js} +1 -10
- package/{git-info.js → core/workspace-app/git-info.js} +17 -1
- package/core/workspace-app/package.json +29 -0
- package/core/workspace-app/plugin-catalog.js +50 -0
- package/{plugins-page.js → core/workspace-app/plugins-page.js} +28 -2
- package/{server.js → core/workspace-app/server.js} +18 -0
- package/core/workspace-app/tests/features/actions-rail.feature +44 -0
- package/core/workspace-app/tests/features/ai-items.feature +23 -0
- package/core/workspace-app/tests/features/assistant-speech.feature +63 -0
- package/core/workspace-app/tests/features/checks.feature +39 -0
- package/core/workspace-app/tests/features/clone-project.feature +30 -0
- package/core/workspace-app/tests/features/command-authoring.feature +58 -0
- package/core/workspace-app/tests/features/dashboard.feature +57 -0
- package/core/workspace-app/tests/features/menus.feature +29 -0
- package/core/workspace-app/tests/features/plugins/jira/jira.feature +47 -0
- package/core/workspace-app/tests/features/plugins.feature +20 -0
- package/core/workspace-app/tests/features/project-actions.feature +40 -0
- package/core/workspace-app/tests/features/screens.feature +29 -0
- package/core/workspace-app/tests/features/settings.feature +46 -0
- package/core/workspace-app/tests/features/test-reports.feature +32 -0
- package/core/workspace-app/tests/features/theme.feature +40 -0
- package/core/workspace-app/tests/step-definitions/actions-post.steps.js +25 -0
- package/core/workspace-app/tests/step-definitions/assistant-speech.steps.js +152 -0
- package/core/workspace-app/tests/step-definitions/preconditions.steps.js +100 -0
- package/core/workspace-app/tests/unit/_isolated-config.js +31 -0
- package/core/workspace-app/tests/unit/accessibility.test.js +124 -0
- package/core/workspace-app/tests/unit/actions-rail-ux.test.js +56 -0
- package/core/workspace-app/tests/unit/actions-rail.test.js +51 -0
- package/core/workspace-app/tests/unit/ai-inventory.test.js +64 -0
- package/core/workspace-app/tests/unit/app-docs.test.js +64 -0
- package/core/workspace-app/tests/unit/assistant-avatar.test.js +110 -0
- package/core/workspace-app/tests/unit/assistant-cast.test.js +105 -0
- package/core/workspace-app/tests/unit/assistant-send-animation.test.js +15 -0
- package/core/workspace-app/tests/unit/assistant-suggestions.test.js +112 -0
- package/core/workspace-app/tests/unit/assistant-switches.test.js +53 -0
- package/core/workspace-app/tests/unit/assistant-views.test.js +146 -0
- package/core/workspace-app/tests/unit/assistant-working-indicator.test.js +49 -0
- package/core/workspace-app/tests/unit/assistant.test.js +61 -0
- package/core/workspace-app/tests/unit/audio-player.test.js +147 -0
- package/core/workspace-app/tests/unit/audio-voice.test.js +634 -0
- package/core/workspace-app/tests/unit/auth-google.test.js +180 -0
- package/core/workspace-app/tests/unit/autostart-watch.test.js +76 -0
- package/core/workspace-app/tests/unit/browsers.test.js +76 -0
- package/core/workspace-app/tests/unit/build-distribution.test.js +125 -0
- package/core/workspace-app/tests/unit/build-order.test.js +153 -0
- package/core/workspace-app/tests/unit/builder-args.test.js +113 -0
- package/core/workspace-app/tests/unit/builders.test.js +158 -0
- package/core/workspace-app/tests/unit/card-state-toggle.test.js +29 -0
- package/core/workspace-app/tests/unit/changelog.test.js +177 -0
- package/core/workspace-app/tests/unit/chat-thread.test.js +92 -0
- package/core/workspace-app/tests/unit/checks-plan.test.js +205 -0
- package/core/workspace-app/tests/unit/claude-args.test.js +182 -0
- package/core/workspace-app/tests/unit/claude-code-here.test.js +15 -0
- package/core/workspace-app/tests/unit/claude-resume.test.js +147 -0
- package/core/workspace-app/tests/unit/claude-screen-options.test.js +84 -0
- package/core/workspace-app/tests/unit/claude-session.test.js +41 -0
- package/core/workspace-app/tests/unit/command-flags.test.js +45 -0
- package/core/workspace-app/tests/unit/command-palette.test.js +105 -0
- package/core/workspace-app/tests/unit/commands-center.test.js +338 -0
- package/core/workspace-app/tests/unit/composer-menu.test.js +187 -0
- package/core/workspace-app/tests/unit/config-yaml.test.js +116 -0
- package/core/workspace-app/tests/unit/confirm-dialog.test.js +69 -0
- package/core/workspace-app/tests/unit/create-card.test.js +85 -0
- package/core/workspace-app/tests/unit/ddev-addons.test.js +144 -0
- package/core/workspace-app/tests/unit/ddev-auth-ssh.test.js +40 -0
- package/core/workspace-app/tests/unit/ddev-custom-commands.test.js +56 -0
- package/core/workspace-app/tests/unit/ddev-diagnose.test.js +57 -0
- package/core/workspace-app/tests/unit/ddev-domains.test.js +153 -0
- package/core/workspace-app/tests/unit/ddev-hostnames.test.js +143 -0
- package/core/workspace-app/tests/unit/ddev-import.test.js +82 -0
- package/core/workspace-app/tests/unit/ddev-mail.test.js +135 -0
- package/core/workspace-app/tests/unit/ddev-menu-and-create.test.js +28 -0
- package/core/workspace-app/tests/unit/ddev-mutagen.test.js +37 -0
- package/core/workspace-app/tests/unit/ddev-options.test.js +51 -0
- package/core/workspace-app/tests/unit/ddev-share.test.js +40 -0
- package/core/workspace-app/tests/unit/ddev-shim.test.js +57 -0
- package/core/workspace-app/tests/unit/ddev-snapshots.test.js +142 -0
- package/core/workspace-app/tests/unit/ddev-start-daemons.test.js +62 -0
- package/core/workspace-app/tests/unit/ddev-xdebug.test.js +41 -0
- package/core/workspace-app/tests/unit/deep-chat-origin.test.js +38 -0
- package/core/workspace-app/tests/unit/density.test.js +52 -0
- package/core/workspace-app/tests/unit/elevenlabs.test.js +453 -0
- package/core/workspace-app/tests/unit/event-log.test.js +102 -0
- package/core/workspace-app/tests/unit/favicon.test.js +68 -0
- package/core/workspace-app/tests/unit/files-browser.test.js +224 -0
- package/core/workspace-app/tests/unit/git-info.test.js +26 -0
- package/core/workspace-app/tests/unit/grid-and-list.test.js +103 -0
- package/core/workspace-app/tests/unit/host-tool-wrapper.test.js +18 -0
- package/core/workspace-app/tests/unit/html-patterns.test.js +51 -0
- package/core/workspace-app/tests/unit/ide-infra.test.js +28 -0
- package/core/workspace-app/tests/unit/images-ai.test.js +201 -0
- package/core/workspace-app/tests/unit/images-create.test.js +59 -0
- package/core/workspace-app/tests/unit/images-providers.test.js +248 -0
- package/core/workspace-app/tests/unit/images-svg.test.js +213 -0
- package/core/workspace-app/tests/unit/images-workspace.test.js +144 -0
- package/core/workspace-app/tests/unit/jobs-fragment.test.js +66 -0
- package/core/workspace-app/tests/unit/media-ux.test.js +164 -0
- package/core/workspace-app/tests/unit/modal-fit.test.js +87 -0
- package/core/workspace-app/tests/unit/modules-load.test.js +23 -0
- package/core/workspace-app/tests/unit/narration.test.js +186 -0
- package/core/workspace-app/tests/unit/navigation-editor.test.js +260 -0
- package/core/workspace-app/tests/unit/navigation.test.js +254 -0
- package/core/workspace-app/tests/unit/new-folder.test.js +121 -0
- package/core/workspace-app/tests/unit/new-screen-form.test.js +149 -0
- package/core/workspace-app/tests/unit/node-menu.test.js +161 -0
- package/core/workspace-app/tests/unit/notes.test.js +128 -0
- package/core/workspace-app/tests/unit/page-structure.test.js +40 -0
- package/core/workspace-app/tests/unit/page-width.test.js +59 -0
- package/core/workspace-app/tests/unit/php-version.test.js +90 -0
- package/core/workspace-app/tests/unit/plan-card.test.js +44 -0
- package/core/workspace-app/tests/unit/plugin-catalog.test.js +31 -0
- package/core/workspace-app/tests/unit/plugin-system.test.js +251 -0
- package/core/workspace-app/tests/unit/plugins-page.test.js +28 -0
- package/core/workspace-app/tests/unit/plugins.test.js +58 -0
- package/core/workspace-app/tests/unit/project-activity-filter.test.js +180 -0
- package/core/workspace-app/tests/unit/project-make.test.js +143 -0
- package/core/workspace-app/tests/unit/project-name.test.js +202 -0
- package/core/workspace-app/tests/unit/reader.test.js +62 -0
- package/core/workspace-app/tests/unit/release.test.js +92 -0
- package/core/workspace-app/tests/unit/remove-confirm.test.js +37 -0
- package/core/workspace-app/tests/unit/report-pdf.test.js +171 -0
- package/core/workspace-app/tests/unit/round-11.test.js +31 -0
- package/core/workspace-app/tests/unit/round-12.test.js +37 -0
- package/core/workspace-app/tests/unit/row-layout.test.js +585 -0
- package/core/workspace-app/tests/unit/row-menus-fold.test.js +29 -0
- package/core/workspace-app/tests/unit/rows-add-modal.test.js +68 -0
- package/core/workspace-app/tests/unit/screen-runner-queue.test.js +41 -0
- package/core/workspace-app/tests/unit/screen-window.test.js +116 -0
- package/core/workspace-app/tests/unit/screens-beside-running.test.js +19 -0
- package/core/workspace-app/tests/unit/screens-columns.test.js +79 -0
- package/core/workspace-app/tests/unit/screens-menu.test.js +84 -0
- package/core/workspace-app/tests/unit/screens-modal-overlay.test.js +14 -0
- package/core/workspace-app/tests/unit/screens-waiting.test.js +129 -0
- package/core/workspace-app/tests/unit/screens.test.js +74 -0
- package/core/workspace-app/tests/unit/second-storage.test.js +218 -0
- package/core/workspace-app/tests/unit/session-name.test.js +64 -0
- package/core/workspace-app/tests/unit/settings-file.test.js +173 -0
- package/core/workspace-app/tests/unit/settings-nested-list.test.js +54 -0
- package/core/workspace-app/tests/unit/settings-raw.test.js +177 -0
- package/core/workspace-app/tests/unit/shared-prefs.test.js +49 -0
- package/core/workspace-app/tests/unit/speech.test.js +290 -0
- package/core/workspace-app/tests/unit/static-caching.test.js +39 -0
- package/core/workspace-app/tests/unit/status-language.test.js +47 -0
- package/core/workspace-app/tests/unit/tab-modal.test.js +69 -0
- package/core/workspace-app/tests/unit/tab-title.test.js +37 -0
- package/core/workspace-app/tests/unit/template-literals.test.js +52 -0
- package/core/workspace-app/tests/unit/terminal-scrollback.test.js +61 -0
- package/core/workspace-app/tests/unit/terminal-size.test.js +103 -0
- package/core/workspace-app/tests/unit/terminal-style.test.js +84 -0
- package/core/workspace-app/tests/unit/terminal-windows.test.js +154 -0
- package/core/workspace-app/tests/unit/themes.test.js +453 -0
- package/core/workspace-app/tests/unit/tool-labels.test.js +34 -0
- package/core/workspace-app/tests/unit/topbar-icons.test.js +47 -0
- package/core/workspace-app/tests/unit/update-docker-gid.test.js +51 -0
- package/core/workspace-app/tests/unit/user-guide.test.js +60 -0
- package/core/workspace-app/tests/unit/version-check.test.js +98 -0
- package/core/workspace-app/tests/unit/version-link.test.js +50 -0
- package/core/workspace-app/tests/unit/video-borrow.test.js +198 -0
- package/core/workspace-app/tests/unit/video-edit-ask.test.js +93 -0
- package/core/workspace-app/tests/unit/video-edit-short.test.js +42 -0
- package/core/workspace-app/tests/unit/video-edit.test.js +417 -0
- package/core/workspace-app/tests/unit/video-transcript.test.js +237 -0
- package/core/workspace-app/tests/unit/waiting.test.js +48 -0
- package/core/workspace-app/tests/unit/wordpress-odoo-builders.test.js +38 -0
- package/core/workspace-app/tests/unit/workspace-bin.test.js +48 -0
- package/core/workspace-app/tests/unit/workspace-card-tools.test.js +30 -0
- package/core/workspace-app/tests/unit/workspace-delete.test.js +77 -0
- package/core/workspace-app/tests/unit/workspace-types.test.js +68 -0
- package/core/workspace-app/tests/unit/worktree-clone.test.js +250 -0
- package/package.json +38 -36
- /package/{PLUGINS.md → core/workspace-app/PLUGINS.md} +0 -0
- /package/{WORKSPACES.md → core/workspace-app/WORKSPACES.md} +0 -0
- /package/{actions → core/workspace-app/actions}/ai-items.js +0 -0
- /package/{actions → core/workspace-app/actions}/audio.js +0 -0
- /package/{actions → core/workspace-app/actions}/build-order.js +0 -0
- /package/{actions → core/workspace-app/actions}/checks.js +0 -0
- /package/{actions → core/workspace-app/actions}/claude-session.js +0 -0
- /package/{actions → core/workspace-app/actions}/commands-center.js +0 -0
- /package/{actions → core/workspace-app/actions}/composer.js +0 -0
- /package/{actions → core/workspace-app/actions}/ddev-addons.js +0 -0
- /package/{actions → core/workspace-app/actions}/ddev-mail.js +0 -0
- /package/{actions → core/workspace-app/actions}/folders.js +0 -0
- /package/{actions → core/workspace-app/actions}/images-svg.js +0 -0
- /package/{actions → core/workspace-app/actions}/images.js +0 -0
- /package/{actions → core/workspace-app/actions}/navigation.js +0 -0
- /package/{actions → core/workspace-app/actions}/node.js +0 -0
- /package/{actions → core/workspace-app/actions}/notes.js +0 -0
- /package/{actions → core/workspace-app/actions}/plugins.js +0 -0
- /package/{actions → core/workspace-app/actions}/project-make.js +0 -0
- /package/{actions → core/workspace-app/actions}/row-actions.js +0 -0
- /package/{actions → core/workspace-app/actions}/screens.js +0 -0
- /package/{actions → core/workspace-app/actions}/video-edit.js +0 -0
- /package/{ai-inventory.js → core/workspace-app/ai-inventory.js} +0 -0
- /package/{ai-items.js → core/workspace-app/ai-items.js} +0 -0
- /package/{assistant-cast.js → core/workspace-app/assistant-cast.js} +0 -0
- /package/{assistant-suggestions.js → core/workspace-app/assistant-suggestions.js} +0 -0
- /package/{audio-voice.js → core/workspace-app/audio-voice.js} +0 -0
- /package/{auth-google.js → core/workspace-app/auth-google.js} +0 -0
- /package/{build-ai.js → core/workspace-app/build-ai.js} +0 -0
- /package/{build-order-page.js → core/workspace-app/build-order-page.js} +0 -0
- /package/{build-order.js → core/workspace-app/build-order.js} +0 -0
- /package/{builder-args.js → core/workspace-app/builder-args.js} +0 -0
- /package/{changelog.js → core/workspace-app/changelog.js} +0 -0
- /package/{chat-threads.js → core/workspace-app/chat-threads.js} +0 -0
- /package/{checks.js → core/workspace-app/checks.js} +0 -0
- /package/{claude-args.js → core/workspace-app/claude-args.js} +0 -0
- /package/{claude-sessions.js → core/workspace-app/claude-sessions.js} +0 -0
- /package/{command-author.js → core/workspace-app/command-author.js} +0 -0
- /package/{command-block.js → core/workspace-app/command-block.js} +0 -0
- /package/{commands → core/workspace-app/commands}/defaults.yml +0 -0
- /package/{commands-page.js → core/workspace-app/commands-page.js} +0 -0
- /package/{commands.js → core/workspace-app/commands.js} +0 -0
- /package/{cucumber.js → core/workspace-app/cucumber.js} +0 -0
- /package/{ddev-addons-page.js → core/workspace-app/ddev-addons-page.js} +0 -0
- /package/{ddev-addons.js → core/workspace-app/ddev-addons.js} +0 -0
- /package/{ddev-custom-commands.js → core/workspace-app/ddev-custom-commands.js} +0 -0
- /package/{ddev-domains.js → core/workspace-app/ddev-domains.js} +0 -0
- /package/{ddev-import.js → core/workspace-app/ddev-import.js} +0 -0
- /package/{ddev-mail-page.js → core/workspace-app/ddev-mail-page.js} +0 -0
- /package/{ddev-mail.js → core/workspace-app/ddev-mail.js} +0 -0
- /package/{ddev-options.js → core/workspace-app/ddev-options.js} +0 -0
- /package/{ddev-snapshots.js → core/workspace-app/ddev-snapshots.js} +0 -0
- /package/{ddev.js → core/workspace-app/ddev.js} +0 -0
- /package/{docs → core/workspace-app/docs}/DEVELOPER-GUIDE.md +0 -0
- /package/{docs → core/workspace-app/docs}/README.md +0 -0
- /package/{docs → core/workspace-app/docs}/REFERENCE.md +0 -0
- /package/{docs → core/workspace-app/docs}/USER-GUIDE.md +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-board.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-coffee.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-colours.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-icons.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-moves.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-pick-cookieman.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-settings-voices.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-status.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/create-card/create-card.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/create-card/start-ai.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-address.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-worktree-off.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-worktree-on.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-ddev-menu.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-domains.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-home.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-ide-menu.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-remove-refusal.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-rows-worktree.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-screens.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-settings.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-workspace-page.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/home/card-settings.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/home/drag-reorder.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/home/edit-card.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/claude-code-image.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/edit-with-ai.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/gallery.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/generate-description.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/generate-plan.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/providers.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/svg-form.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/svg-result.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/upload.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/new-folder/2-form.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/new-folder/8-ddev-options.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/new-folder/files-folder.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/projects/ddev-custom-commands.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/projects/state-chip-stop.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/dashboard-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/dashboard-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/dev-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/dev-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/docs-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/docs-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/ai.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/code.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/docs.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/services.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/sites.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/test-report-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/test-report-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/test-scenarios.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/test-steps.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/videos-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/videos-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/screens-menu.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/screens-new.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/auth-google-lists.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-1-row.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-2-confirm.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-3-type-name.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-4-type-yes.png +0 -0
- /package/{event-log.js → core/workspace-app/event-log.js} +0 -0
- /package/{files-browser.js → core/workspace-app/files-browser.js} +0 -0
- /package/{folders.js → core/workspace-app/folders.js} +0 -0
- /package/{format.js → core/workspace-app/format.js} +0 -0
- /package/{github-auth.js → core/workspace-app/github-auth.js} +0 -0
- /package/{html.js → core/workspace-app/html.js} +0 -0
- /package/{icons.js → core/workspace-app/icons.js} +0 -0
- /package/{images-ai.js → core/workspace-app/images-ai.js} +0 -0
- /package/{images-create.js → core/workspace-app/images-create.js} +0 -0
- /package/{images-page.js → core/workspace-app/images-page.js} +0 -0
- /package/{images-records.js → core/workspace-app/images-records.js} +0 -0
- /package/{images-render.js → core/workspace-app/images-render.js} +0 -0
- /package/{images-svg-core.js → core/workspace-app/images-svg-core.js} +0 -0
- /package/{images-svg-sanitize.js → core/workspace-app/images-svg-sanitize.js} +0 -0
- /package/{images-svg.js → core/workspace-app/images-svg.js} +0 -0
- /package/{images.js → core/workspace-app/images.js} +0 -0
- /package/{jobs.js → core/workspace-app/jobs.js} +0 -0
- /package/{linger.js → core/workspace-app/linger.js} +0 -0
- /package/{lists.js → core/workspace-app/lists.js} +0 -0
- /package/{media-info.js → core/workspace-app/media-info.js} +0 -0
- /package/{navigation-page.js → core/workspace-app/navigation-page.js} +0 -0
- /package/{navigation.js → core/workspace-app/navigation.js} +0 -0
- /package/{notes.js → core/workspace-app/notes.js} +0 -0
- /package/{palette-index.js → core/workspace-app/palette-index.js} +0 -0
- /package/{playwright.config.js → core/workspace-app/playwright.config.js} +0 -0
- /package/{plugin-install.js → core/workspace-app/plugin-install.js} +0 -0
- /package/{plugin-loader.js → core/workspace-app/plugin-loader.js} +0 -0
- /package/{plugin-manifest.js → core/workspace-app/plugin-manifest.js} +0 -0
- /package/{plugin-settings.js → core/workspace-app/plugin-settings.js} +0 -0
- /package/{plugins → core/workspace-app/plugins}/workspace-notes/index.js +0 -0
- /package/{project-make.js → core/workspace-app/project-make.js} +0 -0
- /package/{project-name.js → core/workspace-app/project-name.js} +0 -0
- /package/{public → core/workspace-app/public}/assistant-avatar-moves.js +0 -0
- /package/{public → core/workspace-app/public}/assistant-avatar-ui.js +0 -0
- /package/{public → core/workspace-app/public}/assistant-avatar.js +0 -0
- /package/{public → core/workspace-app/public}/avatar/druplicon.vrm +0 -0
- /package/{public → core/workspace-app/public}/css/base.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/ai-assistant.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/assistant-avatar.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/assistant-views.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/build-order.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/clone-as-a-git-worktree.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/command-palette.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/commands-center.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/confirm-dialog.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/create-card.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/ddev-menu.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/deep-chat.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/density.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/floating-assistant-workspace-pages.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/folders.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/full-height-left-assistant-sidebar-all-pages.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/full-height-right-actions-rail-pages-that-have-actions.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/grid-and-list.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/help-page.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/hero.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/images.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/layout-rhythm-and-spacing-polish.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/mic-on-any-field-that-wants-typing.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/modal-fit.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/page-width.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/plugins-page.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/project-cards.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/project-rows.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/reordering-the-workspace-cards.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/responsive-home-and-the-workspace-pages.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/running-commands-as-toasts.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/settings-editor-core-config-yml.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/short-results-as-a-toast-at-the-top.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/sound-player.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/suggest-agents-skills-commands.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/tab-modal.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/terminals.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/test-report-media-one-file-per-row-paged.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/top-toolbar-logo-breadcrumb.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/video-edit.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/web-ide-open-with.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/working-indicators.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/workspace-cards.css +0 -0
- /package/{public → core/workspace-app/public}/favicon.ico +0 -0
- /package/{public → core/workspace-app/public}/favicon.png +0 -0
- /package/{public → core/workspace-app/public}/htmx.min.js +0 -0
- /package/{public → core/workspace-app/public}/logo.png +0 -0
- /package/{public → core/workspace-app/public}/media-player.js +0 -0
- /package/{public → core/workspace-app/public}/mic.js +0 -0
- /package/{public → core/workspace-app/public}/narration.js +0 -0
- /package/{public → core/workspace-app/public}/palette.js +0 -0
- /package/{public → core/workspace-app/public}/sortable.js +0 -0
- /package/{public → core/workspace-app/public}/suggest.js +0 -0
- /package/{public → core/workspace-app/public}/terminal.js +0 -0
- /package/{public → core/workspace-app/public}/themes/default/theme.css +0 -0
- /package/{public → core/workspace-app/public}/themes/ember/theme.css +0 -0
- /package/{public → core/workspace-app/public}/themes/press/theme.css +0 -0
- /package/{public → core/workspace-app/public}/themes/slate/theme.css +0 -0
- /package/{public → core/workspace-app/public}/touch-icon.png +0 -0
- /package/{public → core/workspace-app/public}/ui.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/ace.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/ext-language_tools.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/ext-searchbox.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-css.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-html.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-javascript.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-markdown.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-sh.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-yaml.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/theme-textmate.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/theme-tomorrow_night.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/worker-yaml.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/deep-chat.LICENSE +0 -0
- /package/{public → core/workspace-app/public}/vendor/deep-chat.bundle.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/jetbrains-mono-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/ubuntu-mono-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/tabler-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/tabler-sprite.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/LICENSE +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/addons/loaders/GLTFLoader.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/addons/utils/BufferGeometryUtils.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/addons/utils/SkeletonUtils.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/three.core.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/three.module.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three-vrm/LICENSE +0 -0
- /package/{public → core/workspace-app/public}/vendor/three-vrm/three-vrm.module.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/uikit-icons.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/uikit.min.css +0 -0
- /package/{public → core/workspace-app/public}/vendor/uikit.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/bookmark-outlined.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/headset.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/menu.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/pause.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/play.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/share.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/sound-off.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/sound-on.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/stop.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/addon-fit.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/addon-unicode11.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/addon-web-links.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/xterm.css +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/xterm.js +0 -0
- /package/{rag.js → core/workspace-app/rag.js} +0 -0
- /package/{reader.js → core/workspace-app/reader.js} +0 -0
- /package/{repo-blobs.js → core/workspace-app/repo-blobs.js} +0 -0
- /package/{report-pdf.js → core/workspace-app/report-pdf.js} +0 -0
- /package/{row-layout.js → core/workspace-app/row-layout.js} +0 -0
- /package/{screenrc → core/workspace-app/screenrc} +0 -0
- /package/{screens.js → core/workspace-app/screens.js} +0 -0
- /package/{scripts → core/workspace-app/scripts}/code-server-start.sh +0 -0
- /package/{scripts → core/workspace-app/scripts}/images-claude-finish.js +0 -0
- /package/{scripts → core/workspace-app/scripts}/screen-host.sh +0 -0
- /package/{scripts → core/workspace-app/scripts}/screen-runner.js +0 -0
- /package/{scripts → core/workspace-app/scripts}/screen-then.js +0 -0
- /package/{scripts → core/workspace-app/scripts}/update.sh +0 -0
- /package/{settings-file.js → core/workspace-app/settings-file.js} +0 -0
- /package/{speech.js → core/workspace-app/speech.js} +0 -0
- /package/{tab-modal.js → core/workspace-app/tab-modal.js} +0 -0
- /package/{test-reports.js → core/workspace-app/test-reports.js} +0 -0
- /package/{themes.js → core/workspace-app/themes.js} +0 -0
- /package/{transcripts.js → core/workspace-app/transcripts.js} +0 -0
- /package/{urls.js → core/workspace-app/urls.js} +0 -0
- /package/{validate.js → core/workspace-app/validate.js} +0 -0
- /package/{version-check.js → core/workspace-app/version-check.js} +0 -0
- /package/{video-edit.js → core/workspace-app/video-edit.js} +0 -0
- /package/{waiting.js → core/workspace-app/waiting.js} +0 -0
- /package/{workspace-card-tools.js → core/workspace-app/workspace-card-tools.js} +0 -0
- /package/{workspace-delete.js → core/workspace-app/workspace-delete.js} +0 -0
- /package/{workspace-types.js → core/workspace-app/workspace-types.js} +0 -0
- /package/{workspaces.js → core/workspace-app/workspaces.js} +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
|
|
8
|
+
const { sanitizeSvg, removedSummary } = require('../../images-svg-sanitize');
|
|
9
|
+
const core = require('../../images-svg-core');
|
|
10
|
+
const svg = require('../../images-svg');
|
|
11
|
+
|
|
12
|
+
const GOOD = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 400">'
|
|
13
|
+
+ '<g class="part"><path d="M10 10 L100 10" fill="none" stroke="#0074e0" stroke-width="4"/></g>'
|
|
14
|
+
+ '<g class="part"><circle cx="200" cy="200" r="40" fill="none" stroke="#2b447c" stroke-width="4"/></g>'
|
|
15
|
+
+ '<g class="part"><line x1="300" y1="100" x2="400" y2="300" stroke="#000000" stroke-width="4"/></g>'
|
|
16
|
+
+ '<g class="part" transform="translate(20 0)"><rect x="500" y="100" width="80" height="80" fill="none" stroke="#0074e0" stroke-width="4"/></g></svg>';
|
|
17
|
+
|
|
18
|
+
test('the sanitiser strips scripts, handlers and every way out of the file, and keeps safe drawing', () => {
|
|
19
|
+
const r = sanitizeSvg(`<?xml version="1.0"?><svg viewBox="0 0 10 10" onload="x()"><script>alert(1)</script>
|
|
20
|
+
<style>@import url(https://e.example/x.css); .a{fill:url(#g)} .b{background:url(https://e.example/t.png)}</style>
|
|
21
|
+
<defs><linearGradient id="g"><stop offset="0" stop-color="#0074e0"/></linearGradient></defs>
|
|
22
|
+
<foreignObject><div>hi</div><rect/></foreignObject>
|
|
23
|
+
<a href="https://e.example"><path d="M0 0" onclick="x()" fill="url(#g)"/></a>
|
|
24
|
+
<use href="https://e.example/s.svg#a"/><use href="#g"/><image href="https://e.example/p.png"/>
|
|
25
|
+
<set attributeName="href" to="javascript:alert(1)"/><animate attributeName="opacity" from="0" to="1" dur="1s"/>
|
|
26
|
+
<circle cx="1" cy="1" r="1" style="fill:url(https://e.example/x)"/><text>A & B</text></svg>`);
|
|
27
|
+
assert.doesNotMatch(r.svg, /script|onload|onclick|foreignObject|<image|e\.example|javascript:|@import|<a |<set/i);
|
|
28
|
+
assert.match(r.svg, /<linearGradient id="g">/);
|
|
29
|
+
assert.match(r.svg, /<path d="M0 0" fill="url\(#g\)"\/>/, 'a local url() survives; the handler does not');
|
|
30
|
+
assert.match(r.svg, /<use href="#g"\/>/);
|
|
31
|
+
assert.match(r.svg, /<animate attributeName="opacity"/);
|
|
32
|
+
assert.match(r.svg, /<text>A & B<\/text>/);
|
|
33
|
+
assert.match(r.svg, /^<svg xmlns="http:\/\/www\.w3\.org\/2000\/svg" viewBox="0 0 10 10">/);
|
|
34
|
+
assert.equal(r.drawableLost, 1, 'the rect inside foreignObject was lost');
|
|
35
|
+
assert.deepEqual(r.removed.elements, { script: 1, foreignObject: 1, a: 1, image: 1, set: 1 });
|
|
36
|
+
assert.match(removedSummary(r), /1 script/);
|
|
37
|
+
assert.equal(sanitizeSvg('no drawing here').svg, '');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('the substance check wants a viewBox, four shapes and four parts', () => {
|
|
41
|
+
assert.deepEqual(core.substance(GOOD), { ok: true, why: '', drawable: 4, parts: 4 });
|
|
42
|
+
const thin = core.substance('<svg viewBox="0 0 1 1" width="10"><g class="part"><rect/></g></svg>');
|
|
43
|
+
assert.equal(thin.ok, false);
|
|
44
|
+
assert.match(thin.why, /width or height/);
|
|
45
|
+
assert.match(thin.why, /1 drawable element, and it needs at least 4/);
|
|
46
|
+
assert.match(thin.why, /1 <g class="part"> group, and it needs at least 4/);
|
|
47
|
+
assert.match(core.substance('<svg><rect/></svg>').why, /no viewBox/);
|
|
48
|
+
assert.equal(core.fixRoot('<svg width="960" height="400px"><g/></svg>'), '<svg viewBox="0 0 960 400"><g/></svg>');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('a transform on an element a CSS animation moves goes onto a wrapping <g>', () => {
|
|
52
|
+
const r = core.repairTransforms('<svg viewBox="0 0 1 1"><style>@media (prefers-reduced-motion: no-preference){.spin{animation:s 1s}}</style>'
|
|
53
|
+
+ '<g class="spin" transform="rotate(5)"><rect/></g><rect class="spin" transform="scale(2)"/><rect class="still" transform="scale(3)"/></svg>');
|
|
54
|
+
assert.equal(r.moved, 2);
|
|
55
|
+
assert.match(r.svg, /<g transform="rotate\(5\)"><g class="spin"><rect\/><\/g><\/g>/);
|
|
56
|
+
assert.match(r.svg, /<g transform="scale\(2\)"><rect class="spin"\/><\/g>/);
|
|
57
|
+
assert.match(r.svg, /<rect class="still" transform="scale\(3\)"\/>/, 'an element nothing animates keeps its transform');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('Draw the strokes: parts are numbered for the stagger, strokes traced by length, reduced motion respected', () => {
|
|
61
|
+
const done = core.finish(GOOD, { style: 'draw', motion: { duration: 1100, stagger: 110 } });
|
|
62
|
+
assert.equal(done.ok, true, done.why);
|
|
63
|
+
assert.equal(done.moved, 1, 'the part placed with a transform was wrapped');
|
|
64
|
+
assert.match(done.svg, /@media \(prefers-reduced-motion: no-preference\)/);
|
|
65
|
+
assert.match(done.svg, /animation:part-rise 1100ms/);
|
|
66
|
+
assert.match(done.svg, /calc\(var\(--i,0\)\*110ms\)/);
|
|
67
|
+
assert.deepEqual([...done.svg.matchAll(/style="--i:(\d)"/g)].map((m) => m[1]), ['0', '1', '2', '3']);
|
|
68
|
+
assert.equal((done.svg.match(/pathLength="1"/g) || []).length, 4);
|
|
69
|
+
assert.doesNotMatch(core.finish(GOOD, { style: 'still' }).svg, /part-rise/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('the prompt is built from the svg: settings, with the brand palette and the style rules', () => {
|
|
73
|
+
const cfg = svg.config();
|
|
74
|
+
assert.match(cfg.brief, /flat line illustrations/);
|
|
75
|
+
assert.deepEqual(cfg.palette, { primary: '#0074e0', secondary: '#2b447c', accent: '#e4dcff', dark: '#000000', light: '#ebe8e5' });
|
|
76
|
+
assert.deepEqual([cfg.max_attempts, cfg.request_timeout, cfg.time_budget], [3, 180, 240]);
|
|
77
|
+
assert.deepEqual([cfg.motion.duration, cfg.motion.stagger, cfg.motion.threshold, cfg.motion.replay], [1100, 110, 0.25, true]);
|
|
78
|
+
const { system, user } = core.buildPrompt({ prompt: 'a lighthouse', style: 'draw', palette: cfg.palette, brief: cfg.brief, shape: 'wide' });
|
|
79
|
+
assert.match(system, /The brief: Clean, flat line illustrations/);
|
|
80
|
+
assert.match(system, /viewBox="0 0 960 400"> root, with no width or height/);
|
|
81
|
+
assert.match(system, /primary #0074e0, secondary #2b447c, accent #e4dcff, dark #000000, light #ebe8e5/);
|
|
82
|
+
assert.match(system, /Stroke only: every shape has fill="none"/);
|
|
83
|
+
assert.match(system, /Answer with JSON only/);
|
|
84
|
+
assert.equal(user, 'Draw: a lighthouse');
|
|
85
|
+
assert.match(core.buildPrompt({ prompt: 'x', style: 'css', palette: cfg.palette, shape: 'square' }).system, /prefers-reduced-motion: no-preference/);
|
|
86
|
+
const p = svg.plan({ prompt: 'x', palette_primary: '#ff0000', palette_dark: 'red; x' });
|
|
87
|
+
assert.equal(p.palette.primary, '#ff0000', 'the form may change a colour for one run');
|
|
88
|
+
assert.equal(p.palette.dark, '#000000', 'a value that is not a colour is ignored');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('the examples are data in settings, generic, and free of the names they were adapted from', () => {
|
|
92
|
+
const ex = svg.config().examples;
|
|
93
|
+
assert.ok(ex.length >= 6 && ex.length <= 10, `${ex.length} examples`);
|
|
94
|
+
const deny = /enlyte|mitchell|genex|coventry|partstrader|claim|insur|casualty|pharmac|nurse|injur|adjuster|workers.? comp|bill review|carrier|policyholder/i;
|
|
95
|
+
const raw = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'config', 'workspace.images.settings.yml'), 'utf8');
|
|
96
|
+
assert.doesNotMatch(raw, deny);
|
|
97
|
+
for (const e of ex) {
|
|
98
|
+
assert.ok(core.STYLES[e.style], e.title);
|
|
99
|
+
assert.ok(e.prompt.length > 40, e.title);
|
|
100
|
+
assert.doesNotMatch(`${e.title} ${e.prompt}`, deny);
|
|
101
|
+
}
|
|
102
|
+
const form = svg.formHtml();
|
|
103
|
+
assert.match(form, /<select class="uk-select uk-form-small" id="svg-example" data-svg-example>/);
|
|
104
|
+
assert.match(form, /data-prompt="Five rounded bars rising/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// A stand-in for a chat model that answers from a list, recording each request.
|
|
108
|
+
async function standIn(replies) {
|
|
109
|
+
const seen = [];
|
|
110
|
+
const srv = http.createServer((req, res) => {
|
|
111
|
+
let body = '';
|
|
112
|
+
req.on('data', (d) => { body += d; });
|
|
113
|
+
req.on('end', () => {
|
|
114
|
+
seen.push(JSON.parse(body));
|
|
115
|
+
const text = replies[Math.min(seen.length - 1, replies.length - 1)];
|
|
116
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
117
|
+
res.end(JSON.stringify({ choices: [{ message: { content: text } }], usage: { prompt_tokens: 1000, completion_tokens: 1000 } }));
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
await new Promise((r) => srv.listen(0, r));
|
|
121
|
+
process.env.OPENAI_CHAT_API = `http://127.0.0.1:${srv.address().port}/v1/chat/completions`;
|
|
122
|
+
const https = require('https');
|
|
123
|
+
const real = https.request;
|
|
124
|
+
https.request = http.request;
|
|
125
|
+
return { seen, close: () => { https.request = real; srv.close(); delete process.env.OPENAI_CHAT_API; } };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
test('a thin answer is retried with the reason, and the second is saved with its records', async () => {
|
|
129
|
+
const thin = JSON.stringify({ svg: '<svg viewBox="0 0 960 400"><g class="part"><rect/></g></svg>', summary: 's', alt: 'a' });
|
|
130
|
+
const good = JSON.stringify({ svg: GOOD, summary: 'Four shapes in a row.', alt: 'Four outlined shapes' });
|
|
131
|
+
const s = await standIn([thin, good]);
|
|
132
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-'));
|
|
133
|
+
try {
|
|
134
|
+
assert.equal((await svg.generateSvg({ prompt: 'shapes' })).status, 400, 'nothing without confirm');
|
|
135
|
+
const r = await svg.generateSvg({ prompt: 'shapes', provider: 'openai', style: 'draw', name: 'shapes', confirm: 'yes' }, { key: 'sk-test', dir });
|
|
136
|
+
assert.equal(r.ok, true, r.error);
|
|
137
|
+
assert.equal(r.attempts, 2);
|
|
138
|
+
assert.equal(r.cost.toFixed(6), (2 * (1000 * 0.75 / 1e6 + 1000 * 4.5 / 1e6)).toFixed(6));
|
|
139
|
+
const second = s.seen[1].messages;
|
|
140
|
+
assert.equal(second[2].role, 'assistant');
|
|
141
|
+
assert.match(second[3].content, /The drawing was not used: it has 1 drawable element/);
|
|
142
|
+
const side = JSON.parse(fs.readFileSync(path.join(dir, 'shapes.svg.json'), 'utf8'));
|
|
143
|
+
assert.deepEqual([side.style, side.attempts, side.provider, side.summary, side.alt],
|
|
144
|
+
['Draw the strokes', 2, 'OpenAI', 'Four shapes in a row.', 'Four outlined shapes']);
|
|
145
|
+
assert.match(side.palette, /primary #0074e0/);
|
|
146
|
+
assert.match(fs.readFileSync(path.join(dir, 'shapes.md'), 'utf8'), /- \*\*Style:\*\* Draw the strokes/);
|
|
147
|
+
assert.match(fs.readFileSync(path.join(dir, 'shapes.svg'), 'utf8'), /part-rise/);
|
|
148
|
+
} finally {
|
|
149
|
+
s.close();
|
|
150
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('a drawing that lost shapes to the sanitiser is drawn again; one that never passes is reported, not saved', async () => {
|
|
155
|
+
const lost = JSON.stringify({ svg: GOOD.replace('</svg>', '<foreignObject><rect/></foreignObject></svg>') });
|
|
156
|
+
const s = await standIn([lost]);
|
|
157
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-'));
|
|
158
|
+
try {
|
|
159
|
+
const r = await svg.generateSvg({ prompt: 'x', provider: 'openai', style: 'still', confirm: 'yes' }, { key: 'sk-test', dir });
|
|
160
|
+
assert.equal(r.ok, false);
|
|
161
|
+
assert.equal(s.seen.length, 3, 'max_attempts');
|
|
162
|
+
assert.match(s.seen[1].messages[3].content, /1 drawable element was lost/);
|
|
163
|
+
assert.match(r.error, /No usable drawing after 3 attempts/);
|
|
164
|
+
assert.deepEqual(fs.readdirSync(dir), []);
|
|
165
|
+
} finally {
|
|
166
|
+
s.close();
|
|
167
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('Gemini and Anthropic need a real key; Claude Code draws in a session, retried by its finish step', async () => {
|
|
172
|
+
const noKey = await svg.generateSvg({ prompt: 'x', provider: 'anthropic', confirm: 'yes' });
|
|
173
|
+
assert.equal(noKey.status, 503);
|
|
174
|
+
assert.match(noKey.error, /ai_providers\.anthropic\.api_key/);
|
|
175
|
+
const calls = [];
|
|
176
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'svgcc-'));
|
|
177
|
+
try {
|
|
178
|
+
const r = await svg.generateSvg({ prompt: 'nodes; $(id)', provider: 'claude_code_cli', style: 'draw', name: 'nodes', confirm: 'yes' },
|
|
179
|
+
{ dir, start: async (a, payload) => { calls.push(payload); return { ok: true }; } });
|
|
180
|
+
assert.equal(r.ok, true, r.error);
|
|
181
|
+
const { cmd } = calls[0];
|
|
182
|
+
assert.doesNotMatch(cmd, /nodes;|\$\(id\)/);
|
|
183
|
+
assert.match(cmd, /^for try in \$\(seq 1 3\); do claude -p "\$\(cat '[^']+'\)" /);
|
|
184
|
+
const promptFile = cmd.match(/cat '([^']+)'/)[1];
|
|
185
|
+
const jobFile = cmd.match(/'([^']+\.job\.json)'/)[1];
|
|
186
|
+
assert.match(fs.readFileSync(promptFile, 'utf8'), /Write exactly two files[\s\S]*1\. nodes\.svg/);
|
|
187
|
+
const finish = require('../../scripts/images-claude-finish');
|
|
188
|
+
fs.writeFileSync(path.join(dir, 'nodes.svg'), '<svg viewBox="0 0 960 400"><g class="part"><rect/></g></svg>');
|
|
189
|
+
assert.equal(finish.main(jobFile), 3, 'refused, drawn again');
|
|
190
|
+
assert.match(fs.readFileSync(promptFile, 'utf8'), /Your last drawing was refused\. The drawing was not used/);
|
|
191
|
+
fs.writeFileSync(path.join(dir, 'nodes.svg'), GOOD);
|
|
192
|
+
fs.writeFileSync(path.join(dir, 'nodes.claude.json'), JSON.stringify({ summary: 'Nodes.', alt: 'Nodes' }));
|
|
193
|
+
assert.equal(finish.main(jobFile), 0);
|
|
194
|
+
const side = JSON.parse(fs.readFileSync(path.join(dir, 'nodes.svg.json'), 'utf8'));
|
|
195
|
+
assert.deepEqual([side.provider, side.attempts, side.style, side.alt], ['claude_code_cli', 2, 'Draw the strokes', 'Nodes']);
|
|
196
|
+
assert.ok(!fs.existsSync(promptFile) && !fs.existsSync(jobFile));
|
|
197
|
+
fs.rmSync(path.join(path.dirname(path.dirname(promptFile)), 'images--claude-nodes.cwd'), { force: true });
|
|
198
|
+
} finally {
|
|
199
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('the plan comes first, through the confirm dialog, and the viewer can replay and copy the markup', () => {
|
|
204
|
+
const p = svg.plan({ prompt: 'nodes', provider: 'openai', style: 'draw' });
|
|
205
|
+
const html = svg.planHtml(p);
|
|
206
|
+
assert.match(html, /hx-trigger="confirmed-remove"/);
|
|
207
|
+
assert.match(html, /"confirm":"yes"/);
|
|
208
|
+
assert.match(html, /at most 3 attempts/);
|
|
209
|
+
const page = fs.readFileSync(path.join(__dirname, '..', '..', 'images-page.js'), 'utf8');
|
|
210
|
+
assert.match(page, /data-svg-replay/);
|
|
211
|
+
assert.match(page, /Copy SVG markup/);
|
|
212
|
+
assert.match(page, /<source media="\(prefers-reduced-motion: reduce\)"/);
|
|
213
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const images = require('../../images');
|
|
8
|
+
|
|
9
|
+
const root = path.join(__dirname, '..', '..');
|
|
10
|
+
const repo = path.join(root, '..', '..');
|
|
11
|
+
const read = (p) => fs.readFileSync(path.join(root, p), 'utf8');
|
|
12
|
+
|
|
13
|
+
function png(w, h) {
|
|
14
|
+
const b = Buffer.alloc(33);
|
|
15
|
+
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]).copy(b, 0);
|
|
16
|
+
b.writeUInt32BE(13, 8); b.write('IHDR', 12); b.writeUInt32BE(w, 16); b.writeUInt32BE(h, 20);
|
|
17
|
+
return b;
|
|
18
|
+
}
|
|
19
|
+
function gif(w, h) { const b = Buffer.alloc(13); b.write('GIF89a'); b.writeUInt16LE(w, 6); b.writeUInt16LE(h, 8); return b; }
|
|
20
|
+
function jpeg(w, h) {
|
|
21
|
+
// SOI, an APP0 segment, then SOF0 with height and width.
|
|
22
|
+
return Buffer.concat([Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x04, 0x00, 0x00]),
|
|
23
|
+
Buffer.from([0xff, 0xc0, 0x00, 0x11, 0x08, h >> 8, h & 255, w >> 8, w & 255, 0x03])]);
|
|
24
|
+
}
|
|
25
|
+
function webp8x(w, h) {
|
|
26
|
+
const b = Buffer.alloc(30); b.write('RIFF'); b.write('WEBP', 8); b.write('VP8X', 12);
|
|
27
|
+
b.writeUIntLE(w - 1, 24, 3); b.writeUIntLE(h - 1, 27, 3); return b;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test('dimensions are read from each format\'s own header', () => {
|
|
31
|
+
const p = images._parsers;
|
|
32
|
+
assert.deepEqual(p.pngSize(png(1920, 1080)), { width: 1920, height: 1080 });
|
|
33
|
+
assert.deepEqual(p.gifSize(gif(64, 32)), { width: 64, height: 32 });
|
|
34
|
+
assert.deepEqual(p.jpegSize(jpeg(800, 600)), { width: 800, height: 600 });
|
|
35
|
+
assert.deepEqual(p.webpSize(webp8x(1600, 1000)), { width: 1600, height: 1000 });
|
|
36
|
+
assert.deepEqual(p.svgSize(Buffer.from('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 40"></svg>')), { width: 120, height: 40 });
|
|
37
|
+
assert.deepEqual(p.svgSize(Buffer.from('<svg width="24px" height="16" viewBox="0 0 1 1">')), { width: 24, height: 16 });
|
|
38
|
+
assert.equal(p.pngSize(Buffer.from('not a png at all, really not')), null);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('a folder lists its folders and images, and nothing outside it', () => {
|
|
42
|
+
const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'img-'));
|
|
43
|
+
try {
|
|
44
|
+
fs.mkdirSync(path.join(ws, 'shots'));
|
|
45
|
+
fs.writeFileSync(path.join(ws, 'a.png'), png(10, 20));
|
|
46
|
+
fs.writeFileSync(path.join(ws, 'shots', 'b.gif'), gif(3, 4));
|
|
47
|
+
fs.writeFileSync(path.join(ws, 'notes.md'), '# not an image');
|
|
48
|
+
fs.writeFileSync(path.join(ws, '.hidden.png'), png(1, 1));
|
|
49
|
+
const top = images.listFolder(ws, '');
|
|
50
|
+
assert.deepEqual(top.images.map((i) => [i.name, i.width, i.height, i.type]), [['a.png', 10, 20, 'png']]);
|
|
51
|
+
assert.deepEqual(top.folders.map((f) => [f.name, f.count]), [['shots', 1]]);
|
|
52
|
+
assert.equal(images.listFolder(ws, 'shots').images[0].rel, 'shots/b.gif');
|
|
53
|
+
assert.equal(images.listFolder(ws, '../etc').ok, false);
|
|
54
|
+
assert.equal(images.safeIn(ws, '../../etc/passwd'), null);
|
|
55
|
+
} finally {
|
|
56
|
+
fs.rmSync(ws, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('names are made safe, and a new name never takes an existing one', () => {
|
|
61
|
+
const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'img-'));
|
|
62
|
+
try {
|
|
63
|
+
assert.equal(images.safeName('../My Shot (1).PNG'), 'My-Shot-1.PNG');
|
|
64
|
+
fs.writeFileSync(path.join(ws, 'shot.png'), 'x');
|
|
65
|
+
assert.equal(images.freeName(ws, 'shot', 'png'), 'shot-2.png');
|
|
66
|
+
assert.equal(images.versionName(ws, 'shot.png', 'webp'), 'shot-v2.webp');
|
|
67
|
+
fs.writeFileSync(path.join(ws, 'shot-v2.png'), 'x');
|
|
68
|
+
assert.equal(images.versionName(ws, 'shot-v2.png', 'png'), 'shot-v3.png');
|
|
69
|
+
} finally {
|
|
70
|
+
fs.rmSync(ws, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('"All media images" reads media and doc workspaces and the dashboard screenshots, never sites', () => {
|
|
75
|
+
assert.deepEqual(images.EVERYWHERE, ['images', 'docs', 'videos', 'audio', 'specs', 'designs']);
|
|
76
|
+
for (const site of ['dev', 'test', 'demos', 'sandboxes', 'projects', 'products']) assert.ok(!images.EVERYWHERE.includes(site), site);
|
|
77
|
+
assert.match(images.appShotsDir(), /core\/workspace-app\/docs\/screenshots$/);
|
|
78
|
+
const list = images.scanEverywhere({ fresh: true });
|
|
79
|
+
assert.ok(Array.isArray(list));
|
|
80
|
+
assert.ok(list.every((im) => images.IMAGE_RE.test(im.name) && im.src));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('the workspace is registered before videos, as a media workspace with a gallery', () => {
|
|
84
|
+
const settings = fs.readFileSync(path.join(repo, 'core/config/settings.yml'), 'utf8');
|
|
85
|
+
assert.match(settings, /\n {2}- images\n {2}- videos\n/);
|
|
86
|
+
assert.match(fs.readFileSync(path.join(repo, 'core/config/workspace.images.settings.yml'), 'utf8'), /type: media/);
|
|
87
|
+
assert.match(read('workspaces.js'), /images: +\{ icon: 'tabler:photo', subtitle: 'Images, screenshots and generated pictures', noun: 'image', kind: 'files' \}/);
|
|
88
|
+
assert.match(read('workspace-types.js'), /images: 'media'/);
|
|
89
|
+
assert.match(read('files-browser.js'), /'images'/);
|
|
90
|
+
assert.match(read('.ddev/config.yaml'), /"\*\.images\.workspace"/);
|
|
91
|
+
for (const f of ['README.md', 'AGENTS.md', 'CLAUDE.md']) assert.ok(fs.existsSync(path.join(repo, 'images', f)), f);
|
|
92
|
+
const server = read('server.js');
|
|
93
|
+
assert.match(server, /if \(key === 'images'\) return require\('\.\/images-page'\)\.galleryHtml\(/);
|
|
94
|
+
assert.match(server, /'\.webp': 'image\/webp', '\.avif': 'image\/avif'/);
|
|
95
|
+
assert.match(server, /type === 'image\/svg\+xml' \? \{ 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; sandbox" \}/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('the gallery and the viewer draw what the list holds, and only the images workspace can rename or delete', () => {
|
|
99
|
+
const page = require('../../images-page');
|
|
100
|
+
const ws = { per: 'all', page: 1, q: '', sort: 'newest', kind: '', scope: 'here' };
|
|
101
|
+
const html = page.galleryHtml(ws, '', { folderDocs: '', openWith: '', viewSwitcher: '<div class="layout-controls"></div>' });
|
|
102
|
+
// The same building blocks the docs and videos lists use: heading with a count, summary, layout switcher, file list.
|
|
103
|
+
assert.match(html, /<div class="cards-head fx-head">/);
|
|
104
|
+
assert.match(html, /<h3 class="uk-margin-remove">Images <span class="uk-badge">\d+<\/span><\/h3>/);
|
|
105
|
+
assert.match(html, /<p class="uk-text-meta fx-summary">/);
|
|
106
|
+
assert.match(html, /<div class="layout-controls"><\/div>/);
|
|
107
|
+
assert.match(html, /<option value="here" selected>This workspace<\/option>/);
|
|
108
|
+
assert.match(html, /<option value="all">All media images<\/option>/);
|
|
109
|
+
assert.match(html, /name="kind" aria-label="Image type"/);
|
|
110
|
+
assert.match(read('lists.js'), /size: +\{ label: 'Largest first', filesOnly: true/);
|
|
111
|
+
const actions = read('actions/images.js');
|
|
112
|
+
assert.match(actions, /images\.safeIn\(workspaceDir\('images'\)/);
|
|
113
|
+
assert.match(actions, /if \(form\.confirm !== 'yes'\) return send/);
|
|
114
|
+
const ui = read('public/ui.js');
|
|
115
|
+
assert.match(ui, /#editor-modal\.uk-open \[data-img-viewer\]/);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('each source has its own share of the scan, and a vector thumbnail is padded, not stretched', () => {
|
|
119
|
+
assert.equal(images.SCAN_MAX, 1000);
|
|
120
|
+
assert.match(read('images.js'), /const full = \(\) => out\.length - start >= SCAN_MAX;/);
|
|
121
|
+
assert.match(read('images-page.js'), /im\.type === 'svg' \? ' is-vector' : ''/);
|
|
122
|
+
const css = read('public/css/components/images.css');
|
|
123
|
+
assert.match(css, /\.img-row \.fx-thumb\.is-vector img \{ object-fit: contain; padding: 1\.25rem;/);
|
|
124
|
+
assert.match(css, /\.img-stage\.is-fit \{ overflow: hidden; \}/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('an image is a card of the shared file list: thumbnail with its size in the corner, name, meta, View and a menu', () => {
|
|
128
|
+
const page = require('../../images-page');
|
|
129
|
+
const src = read('images-page.js');
|
|
130
|
+
assert.match(src, /<div class="fx-row fx-file has-thumb img-row">/);
|
|
131
|
+
assert.match(src, /<span class="fx-duration fx-dims">/);
|
|
132
|
+
assert.match(src, /<a class="fx-row fx-folder"/);
|
|
133
|
+
assert.match(src, /<div class="file-list img-list">/);
|
|
134
|
+
assert.match(src, /class="uk-button uk-button-default uk-button-small fx-open" title="View/);
|
|
135
|
+
assert.match(read('server.js'), /viewSwitcher: viewSwitcherHtml\('files'\)/);
|
|
136
|
+
assert.equal(typeof page.galleryHtml, 'function');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('a thumbnail fills the card shape and is cropped from the top, so a tall screenshot stays sharp', () => {
|
|
140
|
+
const src = read('images-page.js');
|
|
141
|
+
assert.match(src, /'-thumbnail', `\$\{THUMB_W\}x\$\{THUMB_H\}\^`, '-gravity', 'north',\n\s+'-extent', `\$\{THUMB_W\}x\$\{THUMB_H\}`/);
|
|
142
|
+
assert.match(src, /thumb\?v=2&src=/);
|
|
143
|
+
assert.match(read('images.js'), /folders\.push\(\{ folder: true, name: e\.name, rel: childRel, count, folders: subs,/);
|
|
144
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The job fragment's two newer faces: the phase progress bar and inline mode.
|
|
3
|
+
*
|
|
4
|
+
* Both are rendered from the job record alone, so they can be tested by seeding
|
|
5
|
+
* the registry directly — no process is spawned. The phase bar's one honesty
|
|
6
|
+
* rule is tested from both sides: a phase counts only when its marker is in the
|
|
7
|
+
* output, and a finished job reports full whatever the output said.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { test } = require('node:test');
|
|
11
|
+
const assert = require('node:assert');
|
|
12
|
+
|
|
13
|
+
const { jobs, jobFragment } = require('../../jobs');
|
|
14
|
+
|
|
15
|
+
function seed(id, job) {
|
|
16
|
+
jobs.set(id, { title: 'T', key: null, phases: null, inline: false, buf: '', done: false, ok: null, ...job });
|
|
17
|
+
return id;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('a job without phases renders no progress bar', () => {
|
|
21
|
+
const { html } = jobFragment(seed('t-plain', { buf: 'hello' }));
|
|
22
|
+
assert.ok(!html.includes('job-progress'));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('the phase bar counts only markers that actually appeared', () => {
|
|
26
|
+
const phases = ['Exporting', 'Copying', 'Importing'];
|
|
27
|
+
const { html } = jobFragment(seed('t-phases', { phases, buf: 'Exporting the db\nCopying files' }));
|
|
28
|
+
assert.match(html, /value="2" max="3"/);
|
|
29
|
+
// The label names the phase the run is ON, not one it finished.
|
|
30
|
+
assert.ok(html.includes('Importing (2/3)'));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('a finished successful job reports the bar full', () => {
|
|
34
|
+
const { html } = jobFragment(seed('t-done', { phases: ['A', 'B'], buf: 'A', done: true, ok: true }));
|
|
35
|
+
assert.match(html, /value="2" max="2"/);
|
|
36
|
+
assert.ok(html.includes('Done (2/2)'));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('inline jobs carry the class that keeps them out of the toast stack, and a done note', () => {
|
|
40
|
+
const running = jobFragment(seed('t-inline', { inline: true, buf: 'x' })).html;
|
|
41
|
+
assert.ok(running.includes('job-inline'));
|
|
42
|
+
assert.ok(!running.includes('job-done-note'), 'no done note while running');
|
|
43
|
+
const done = jobFragment(seed('t-inline-done', { inline: true, buf: 'x', done: true, ok: true })).html;
|
|
44
|
+
assert.ok(done.includes('job-done-note'));
|
|
45
|
+
assert.ok(done.includes('Done — the process finished'));
|
|
46
|
+
const failed = jobFragment(seed('t-inline-fail', { inline: true, buf: 'x', done: true, ok: false })).html;
|
|
47
|
+
assert.ok(failed.includes('The process failed'));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('a non-inline job never renders the done note', () => {
|
|
51
|
+
const { html } = jobFragment(seed('t-stack-done', { buf: 'x', done: true, ok: true }));
|
|
52
|
+
assert.ok(!html.includes('job-done-note'));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const { screenWrap } = require('../../jobs');
|
|
56
|
+
|
|
57
|
+
test('screenWrap builds the detach-tail-wait line and refuses bad names', () => {
|
|
58
|
+
const line = screenWrap({ session: 'jira-x-1', inner: 'echo hi', logFile: '/tmp/x.log' });
|
|
59
|
+
assert.match(line, /screen -c "[^"]*\/screenrc" -dmS jira-x-1/,
|
|
60
|
+
"under the dashboard's own screenrc, never the machine's");
|
|
61
|
+
assert.ok(line.includes('tail -F "/tmp/x.log"'));
|
|
62
|
+
// The waiter must match the session EXACTLY: a bare "[.]jira-x-1" also matches
|
|
63
|
+
// jira-x-12, so a job would wait on the wrong session's lifetime.
|
|
64
|
+
assert.ok(line.includes('grep -q "[.]jira-x-1[[:space:]]"'), 'the waiter dot-escapes and terminates the session name');
|
|
65
|
+
assert.throws(() => screenWrap({ session: 'bad name; rm', inner: 'x', logFile: '/tmp/y' }));
|
|
66
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The player and the cards, the way a streaming site has them.
|
|
3
|
+
*
|
|
4
|
+
* Lengths are read from the files, so they are checked against ffprobe where it is installed and against
|
|
5
|
+
* hand-built headers where it is not. The rows, the chapters and the player markup come from the real server.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { test, before, after } = require('node:test');
|
|
9
|
+
const assert = require('node:assert');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const net = require('net');
|
|
14
|
+
const { spawn, execFileSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
const APP = path.join(__dirname, '../..');
|
|
17
|
+
const REPO = path.join(APP, '../..');
|
|
18
|
+
const { copyConfig } = require('./_isolated-config');
|
|
19
|
+
const mi = require('../../media-info');
|
|
20
|
+
|
|
21
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-media-'));
|
|
22
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-media-root-'));
|
|
23
|
+
|
|
24
|
+
function hasFfmpeg() {
|
|
25
|
+
try { execFileSync('ffmpeg', ['-version'], { stdio: 'ignore' }); return true; } catch (_) { return false; }
|
|
26
|
+
}
|
|
27
|
+
const ffmpeg = (...a) => execFileSync('ffmpeg', ['-y', '-loglevel', 'error', ...a]);
|
|
28
|
+
|
|
29
|
+
test('a length is said the way a player says it', () => {
|
|
30
|
+
assert.strictEqual(mi.clock(476.1), '7:56');
|
|
31
|
+
assert.strictEqual(mi.clock(59.6), '1:00');
|
|
32
|
+
assert.strictEqual(mi.clock(3725), '1:02:05');
|
|
33
|
+
assert.strictEqual(mi.clock(0), '');
|
|
34
|
+
assert.strictEqual(mi.clock(null), '');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('a wav and a constant bit rate mp3 are read from their headers', () => {
|
|
38
|
+
const wav = Buffer.alloc(44 + 88200);
|
|
39
|
+
wav.write('RIFF', 0); wav.writeUInt32LE(36 + 88200, 4); wav.write('WAVEfmt ', 8); wav.writeUInt32LE(16, 16);
|
|
40
|
+
wav.writeUInt16LE(1, 20); wav.writeUInt16LE(1, 22); wav.writeUInt32LE(22050, 24); wav.writeUInt32LE(44100, 28);
|
|
41
|
+
wav.writeUInt16LE(2, 32); wav.writeUInt16LE(16, 34); wav.write('data', 36); wav.writeUInt32LE(88200, 40);
|
|
42
|
+
fs.writeFileSync(path.join(tmp, 'two.wav'), wav);
|
|
43
|
+
assert.strictEqual(mi.seconds(path.join(tmp, 'two.wav')), 2);
|
|
44
|
+
// MPEG-1 Layer III, 128 kbit/s, 44.1 kHz: 0xFFFB 0x90 0x00. 160000 bytes at 128 kbit/s is 10 seconds.
|
|
45
|
+
const mp3 = Buffer.alloc(160000, 0);
|
|
46
|
+
Buffer.from([0xff, 0xfb, 0x90, 0x00]).copy(mp3, 0);
|
|
47
|
+
fs.writeFileSync(path.join(tmp, 'ten.mp3'), mp3);
|
|
48
|
+
assert.strictEqual(mi.seconds(path.join(tmp, 'ten.mp3')), 10);
|
|
49
|
+
fs.writeFileSync(path.join(tmp, 'junk.mp3'), Buffer.alloc(500, 1));
|
|
50
|
+
assert.strictEqual(mi.seconds(path.join(tmp, 'junk.mp3')), null);
|
|
51
|
+
assert.strictEqual(mi.seconds(path.join(tmp, 'missing.mp4')), null);
|
|
52
|
+
assert.strictEqual(mi.seconds(path.join(tmp, 'two.txt')), null);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('an mp4 length is found whether the encoder put its header first or last', { skip: !hasFfmpeg() }, () => {
|
|
56
|
+
for (const [name, flags] of [['front.mp4', ['-movflags', '+faststart']], ['end.mp4', []]]) {
|
|
57
|
+
const out = path.join(tmp, name);
|
|
58
|
+
ffmpeg('-f', 'lavfi', '-i', 'testsrc=duration=5:size=160x120:rate=10', '-f', 'lavfi', '-i', 'sine=duration=5', '-shortest', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-c:a', 'aac', ...flags, out);
|
|
59
|
+
const truth = parseFloat(execFileSync('ffprobe', ['-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', out]).toString());
|
|
60
|
+
assert.ok(Math.abs(mi.seconds(out) - truth) < 0.1, `${name}: ${mi.seconds(out)} against ${truth}`);
|
|
61
|
+
}
|
|
62
|
+
const mp3 = path.join(tmp, 'tone.mp3');
|
|
63
|
+
ffmpeg('-f', 'lavfi', '-i', 'sine=duration=4', '-codec:a', 'libmp3lame', '-b:a', '128k', mp3);
|
|
64
|
+
assert.ok(Math.abs(mi.seconds(mp3) - 4) < 0.2, String(mi.seconds(mp3)));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('the player offers what people expect, and the cards say how long and whether there are words', () => {
|
|
68
|
+
const js = fs.readFileSync(path.join(APP, 'public/media-player.js'), 'utf8');
|
|
69
|
+
for (const bit of ["key === 'j'", "key === 'l'", "'ArrowLeft'", "'ArrowUp'", "key === 'm'", "requestFullscreen", "key === '<'", "/^[0-9]$/", "ws-media-speed", "media.play()", "data-media-step", "mouseover"]) {
|
|
70
|
+
assert.ok(js.includes(bit), bit);
|
|
71
|
+
}
|
|
72
|
+
assert.strictEqual(fs.readFileSync(path.join(APP, 'public/ui.js'), 'utf8').indexOf('data-sound-speed'), -1, 'the old handlers are gone from ui.js');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ---- the running server ------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
let proc; let base;
|
|
78
|
+
const free = () => new Promise((resolve) => {
|
|
79
|
+
const s = net.createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); });
|
|
80
|
+
});
|
|
81
|
+
const SCRIPT = `# Demo
|
|
82
|
+
|
|
83
|
+
## E01 Opening (00:00.0 to 00:02.0)
|
|
84
|
+
|
|
85
|
+
> Hello there.
|
|
86
|
+
|
|
87
|
+
## E02 Close (00:02.0 to 00:05.0)
|
|
88
|
+
|
|
89
|
+
> Goodbye now.
|
|
90
|
+
`;
|
|
91
|
+
|
|
92
|
+
before(async () => {
|
|
93
|
+
fs.mkdirSync(path.join(root, 'core'), { recursive: true });
|
|
94
|
+
copyConfig(path.join(REPO, 'core/config'), path.join(root, 'core/config'));
|
|
95
|
+
fs.symlinkSync(path.join(REPO, 'core/scripts'), path.join(root, 'core/scripts'));
|
|
96
|
+
fs.symlinkSync(APP, path.join(root, 'core/workspace-app'));
|
|
97
|
+
for (const d of ['videos', 'docs', 'audio']) fs.mkdirSync(path.join(root, d), { recursive: true });
|
|
98
|
+
if (hasFfmpeg()) {
|
|
99
|
+
ffmpeg('-f', 'lavfi', '-i', 'testsrc=duration=5:size=160x120:rate=10', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-movflags', '+faststart', path.join(root, 'videos/demo.mp4'));
|
|
100
|
+
ffmpeg('-f', 'lavfi', '-i', 'sine=duration=3', '-codec:a', 'libmp3lame', '-b:a', '128k', path.join(root, 'audio/tone.mp3'));
|
|
101
|
+
} else {
|
|
102
|
+
fs.writeFileSync(path.join(root, 'videos/demo.mp4'), Buffer.alloc(100, 1));
|
|
103
|
+
fs.writeFileSync(path.join(root, 'audio/tone.mp3'), Buffer.alloc(100, 1));
|
|
104
|
+
}
|
|
105
|
+
fs.writeFileSync(path.join(root, 'videos/plain.mp4'), Buffer.alloc(100, 2));
|
|
106
|
+
fs.writeFileSync(path.join(root, 'docs/demo-voiceover-script.md'), SCRIPT);
|
|
107
|
+
const port = await free();
|
|
108
|
+
base = `http://127.0.0.1:${port}`;
|
|
109
|
+
proc = spawn(process.execPath, ['server.js'], { cwd: APP, env: { ...process.env, PORT: String(port), WORKSPACE_ROOT: root }, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
110
|
+
await new Promise((resolve, reject) => {
|
|
111
|
+
const t = setTimeout(() => reject(new Error('server did not start')), 15000);
|
|
112
|
+
proc.stdout.on('data', (d) => { if (/listening/.test(String(d))) { clearTimeout(t); resolve(); } });
|
|
113
|
+
proc.on('exit', (c) => { clearTimeout(t); reject(new Error(`server exited ${c}`)); });
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
after(() => {
|
|
118
|
+
if (proc) proc.kill();
|
|
119
|
+
for (const d of [tmp, root]) fs.rmSync(d, { recursive: true, force: true });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const get = (p) => fetch(base + p);
|
|
123
|
+
const rowOf = (html, name) => { const at = html.indexOf(`title="Play ${name}`); return html.slice(Math.max(0, at - 2500), at + 1500); };
|
|
124
|
+
|
|
125
|
+
test('a recording\'s card carries its length, CC when it has a script, and a preview address', { skip: !hasFfmpeg() }, async () => {
|
|
126
|
+
const html = await (await get('/fragments/videos/items')).text();
|
|
127
|
+
const demo = rowOf(html, 'demo.mp4');
|
|
128
|
+
assert.match(demo, /<span class="fx-duration">0:05<\/span>/);
|
|
129
|
+
assert.match(demo, /<span class="fx-cc" title="Has a transcript">CC<\/span>/);
|
|
130
|
+
assert.match(demo, /data-preview="\/files\/videos\/demo\.mp4"/);
|
|
131
|
+
assert.match(demo, /<span class="fx-size">[^<]*· 0:05<\/span>/);
|
|
132
|
+
const plain = rowOf(html, 'plain.mp4');
|
|
133
|
+
assert.ok(!/fx-duration|fx-cc/.test(plain.slice(plain.lastIndexOf('<div class="fx-row'))), 'no length and no CC where there is neither');
|
|
134
|
+
const sound = await (await get('/fragments/audio/items')).text();
|
|
135
|
+
assert.match(sound, /<span class="fx-duration">0:03<\/span>/);
|
|
136
|
+
assert.ok(!/data-preview/.test(sound), 'a sound has no picture to preview');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('the player is a stage with a speed menu, chapters from the script, and previous and next', async () => {
|
|
140
|
+
const html = await (await get('/fragments/videos/play/demo.mp4')).text();
|
|
141
|
+
assert.match(html, /class="uk-card uk-card-default uk-card-body uk-margin-top editor-card media-card"/);
|
|
142
|
+
assert.match(html, /<div class="media-stage"><video class="video-player"/);
|
|
143
|
+
assert.match(html, /data-speed-label[^>]*>1×<\/button>\s*<div uk-dropdown=/);
|
|
144
|
+
assert.strictEqual((html.match(/data-sound-speed=/g) || []).length, 19);
|
|
145
|
+
const chapters = JSON.parse(html.match(/data-chapters="([^"]*)"/)[1].replace(/"/g, '"'));
|
|
146
|
+
assert.deepStrictEqual(chapters, [{ start: 0, end: 2, title: 'E01 Opening' }, { start: 2, end: 5, title: 'E02 Close' }]);
|
|
147
|
+
assert.match(html, /<section class="transcript-section" data-start="2" data-end="5">/);
|
|
148
|
+
assert.match(html, /data-media-step="-1" disabled/);
|
|
149
|
+
assert.match(html, /data-media-step="1" disabled/);
|
|
150
|
+
assert.match(html, /Space play/);
|
|
151
|
+
assert.match(html, /<p class="player-chapter" data-chapter hidden><\/p>/);
|
|
152
|
+
const sound = await (await get('/fragments/audio/play/tone.mp3')).text();
|
|
153
|
+
assert.match(sound, /media-stage is-sound/);
|
|
154
|
+
assert.ok(!/chapter-bar/.test(sound), 'a sound has no chapters');
|
|
155
|
+
const plain = await (await get('/fragments/videos/play/plain.mp4')).text();
|
|
156
|
+
assert.ok(!/chapter-bar/.test(plain), 'no script, no chapters');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('the shell loads the player script', async () => {
|
|
160
|
+
const page = await (await get('/videos')).text();
|
|
161
|
+
assert.match(page, /<script src="\/media-player\.js[^"]*" defer><\/script>/);
|
|
162
|
+
const js = await get('/media-player.js');
|
|
163
|
+
assert.strictEqual(js.status, 200);
|
|
164
|
+
});
|