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