@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,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The COMPOSER menu on a project row.
|
|
3
|
+
*
|
|
4
|
+
* What must never drift: it appears only where there is a composer.json; every command runs in a
|
|
5
|
+
* screen session through DDEV (`ddev composer`, or the dashboard's DDEV container), never a host
|
|
6
|
+
* composer; the handler runs only its tables, a real package name and a script the folder declares;
|
|
7
|
+
* a second click never stops a run that is still going; and COMPOSER sits right after SCREENS.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const assert = require('node:assert');
|
|
11
|
+
const { test } = require('node:test');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const composer = require('../../actions/composer');
|
|
17
|
+
const { COMPOSER_RUN_ACTIONS, COMPOSER_PACKAGE_ACTIONS, SCRIPT_ROUTE, hasComposerJson, packageRefusal,
|
|
18
|
+
composerScripts, composerScreenCommand, sessionFinished } = composer;
|
|
19
|
+
const { LINGER_MARK, lingerTail } = require('../../linger');
|
|
20
|
+
// The GIT plugin's session line, for the comparison below: the same tail, the same no-prompt no-pager git.
|
|
21
|
+
const gitScreenCommand = (label, args) => `GIT_TERMINAL_PROMPT=0 git --no-pager ${args.join(' ')}; ${lingerTail(label)}`;
|
|
22
|
+
|
|
23
|
+
test('the menu offers install, update and the common commands, each with a handler', () => {
|
|
24
|
+
const labels = Object.values(COMPOSER_RUN_ACTIONS).map((a) => a.label);
|
|
25
|
+
for (const want of ['composer install', 'composer update', 'composer validate', 'composer outdated --direct', 'composer audit']) {
|
|
26
|
+
assert.ok(labels.includes(want), `missing: ${want}`);
|
|
27
|
+
}
|
|
28
|
+
for (const [route, act] of [...Object.entries(COMPOSER_RUN_ACTIONS), ...Object.entries(COMPOSER_PACKAGE_ACTIONS)]) {
|
|
29
|
+
assert.match(route, /^\/actions\/composer-[a-z-]+$/);
|
|
30
|
+
// Labels go into the session's closing echo, so they must stay shell-safe constants.
|
|
31
|
+
assert.match(act.label, /^[\w .()-]+$/, `label is not shell-safe: ${act.label}`);
|
|
32
|
+
assert.strictEqual(typeof composer[route], 'function', `${route} has no handler exported`);
|
|
33
|
+
}
|
|
34
|
+
assert.strictEqual(typeof composer[SCRIPT_ROUTE], 'function');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('the everyday commands come first', () => {
|
|
38
|
+
const { COMPOSER_GROUPS } = composer;
|
|
39
|
+
assert.deepStrictEqual(COMPOSER_GROUPS, ['Daily', 'Inspect', 'Maintenance']);
|
|
40
|
+
const labels = Object.values(COMPOSER_RUN_ACTIONS).map((a) => a.label);
|
|
41
|
+
assert.deepStrictEqual(labels.slice(0, 4), ['composer install', 'composer update', 'composer outdated --direct', 'composer validate']);
|
|
42
|
+
// The groups follow the table's order, so a Daily entry can never sit below an Inspect one.
|
|
43
|
+
const order = Object.values(COMPOSER_RUN_ACTIONS).map((a) => COMPOSER_GROUPS.indexOf(a.group));
|
|
44
|
+
assert.ok(order.every((g) => g >= 0), 'every entry belongs to a shown group');
|
|
45
|
+
assert.deepStrictEqual(order, [...order].sort((x, y) => x - y), 'entries must be listed group by group, Daily first');
|
|
46
|
+
assert.deepStrictEqual(Object.values(COMPOSER_PACKAGE_ACTIONS).slice(0, 3).map((a) => a.button), ['require', 'update -W', 'remove']);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('a command runs through DDEV, never a host composer, and stays open to be read', () => {
|
|
50
|
+
const act = COMPOSER_RUN_ACTIONS['/actions/composer-update'];
|
|
51
|
+
const ddev = composerScreenCommand(act.label, act.args, { isDdev: true, projectDir: '/home/u/workspace/dev/site1' });
|
|
52
|
+
assert.ok(ddev.startsWith('ddev composer --no-interaction --ansi update;'), ddev);
|
|
53
|
+
const plain = composerScreenCommand(act.label, act.args, { isDdev: false, projectDir: '/home/u/workspace/libraries/ace' });
|
|
54
|
+
assert.match(plain, /^cd '[^']+\/core\/workspace-app' && ddev exec -d '\/home\/u\/workspace\/libraries\/ace' composer --no-interaction --ansi update;/, plain);
|
|
55
|
+
for (const line of [ddev, plain]) {
|
|
56
|
+
assert.ok(!/(^|[;&|]\s*)composer\b/.test(line), `a bare host composer: ${line}`);
|
|
57
|
+
assert.ok(line.includes(LINGER_MARK), 'the session must wait to be read');
|
|
58
|
+
// The runner wraps this in an outer `bash -lc "…"`: any $ would be expanded there, too early.
|
|
59
|
+
assert.ok(!line.includes('$'), `a $ would be expanded by the runner's outer shell: ${line}`);
|
|
60
|
+
}
|
|
61
|
+
// The shared tail must not have changed what a git session runs.
|
|
62
|
+
assert.match(gitScreenCommand('git status', ['status']),
|
|
63
|
+
/^GIT_TERMINAL_PROMPT=0 git --no-pager status; echo; echo "git status finished\. Press Enter to close this session, or leave it: it closes by itself in 15 minutes\."; timeout --foreground 900 /);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('a package must look like a package; only require takes a constraint', () => {
|
|
67
|
+
const req = COMPOSER_PACKAGE_ACTIONS['/actions/composer-require'];
|
|
68
|
+
const rm = COMPOSER_PACKAGE_ACTIONS['/actions/composer-remove'];
|
|
69
|
+
assert.strictEqual(packageRefusal('drupal/token', rm), '');
|
|
70
|
+
assert.strictEqual(packageRefusal('drupal/token:^1.15', req), '');
|
|
71
|
+
assert.strictEqual(packageRefusal('drupal/core-recommended:>=11.2 <11.4', req), '');
|
|
72
|
+
assert.notStrictEqual(packageRefusal('drupal/token:^1.15', rm), '', 'remove takes no constraint');
|
|
73
|
+
for (const bad of ['', 'token', 'Drupal/Token', "drupal/token'; rm -rf ~; '", 'drupal/token:$(id)', 'drupal/token:`id`', 'drupal/token;id', 'drupal/token:1\\2', 'drupal/token:"1"']) {
|
|
74
|
+
assert.notStrictEqual(packageRefusal(bad, req), '', `must be refused: ${bad}`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('custom scripts come from composer.json with their aliases; hooks and unsafe names do not', () => {
|
|
79
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'composermenu-'));
|
|
80
|
+
try {
|
|
81
|
+
fs.writeFileSync(path.join(tmp, 'composer.json'), JSON.stringify({
|
|
82
|
+
scripts: {
|
|
83
|
+
'post-update-cmd': ['@php -r "1;"'],
|
|
84
|
+
'pre-install-cmd': 'x',
|
|
85
|
+
'drupal-libraries-sync': 'node sync.js',
|
|
86
|
+
'phpcs:fix': 'phpcbf',
|
|
87
|
+
'bad name; id': 'x',
|
|
88
|
+
},
|
|
89
|
+
'scripts-aliases': { 'drupal-libraries-sync': ['dls', 'bad alias!'] },
|
|
90
|
+
'scripts-descriptions': { 'phpcs:fix': 'Fix coding standards' },
|
|
91
|
+
}));
|
|
92
|
+
assert.strictEqual(hasComposerJson(tmp), true);
|
|
93
|
+
const found = composerScripts(tmp);
|
|
94
|
+
assert.deepStrictEqual(found.map((s) => s.name), ['drupal-libraries-sync', 'phpcs:fix']);
|
|
95
|
+
assert.deepStrictEqual(found[0].aliases, ['dls']);
|
|
96
|
+
assert.strictEqual(found[1].description, 'Fix coding standards');
|
|
97
|
+
fs.writeFileSync(path.join(tmp, 'composer.json'), '{ not json');
|
|
98
|
+
assert.deepStrictEqual(composerScripts(tmp), []);
|
|
99
|
+
assert.strictEqual(hasComposerJson(path.join(tmp, 'missing')), false);
|
|
100
|
+
} finally { fs.rmSync(tmp, { recursive: true, force: true }); }
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('the handler refuses anything outside its tables, bad input, and a folder without composer.json', async () => {
|
|
104
|
+
const call = async (pathname, form) => {
|
|
105
|
+
const out = {};
|
|
106
|
+
await composer[SCRIPT_ROUTE]({ form, send: (html, code = 200) => { out.html = html; out.code = code; }, workspace: 'test', pathname });
|
|
107
|
+
return out;
|
|
108
|
+
};
|
|
109
|
+
let r = await call('/actions/composer-exec', { projectName: 'anything' });
|
|
110
|
+
assert.strictEqual(r.code, 404, 'a route outside the tables must not run');
|
|
111
|
+
r = await call('/actions/composer-install', { projectName: '../etc' });
|
|
112
|
+
assert.strictEqual(r.code, 400);
|
|
113
|
+
r = await call('/actions/composer-require', { projectName: 'anything', package: 'drupal/x; id' });
|
|
114
|
+
assert.strictEqual(r.code, 400);
|
|
115
|
+
assert.match(r.html, /not a package name/);
|
|
116
|
+
r = await call(SCRIPT_ROUTE, { projectName: 'anything', script: 'x; id' });
|
|
117
|
+
assert.strictEqual(r.code, 400);
|
|
118
|
+
r = await call('/actions/composer-install', { projectName: 'no-such-project-for-composer-menu-test' });
|
|
119
|
+
assert.strictEqual(r.code, 400);
|
|
120
|
+
assert.match(r.html, /has no composer\.json/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('a second click replaces only a session that has finished', () => {
|
|
124
|
+
assert.strictEqual(sessionFinished(`Installing…\ncomposer install finished. ${LINGER_MARK}, or leave it: it closes by itself in 15 minutes.\n\n`), true);
|
|
125
|
+
assert.strictEqual(sessionFinished('Updating dependencies\n - Upgrading drupal/core (11.2.0 => 11.2.1)\n'), false, 'a run still going must be left alone');
|
|
126
|
+
assert.strictEqual(sessionFinished(''), false, 'no log is not proof it finished');
|
|
127
|
+
const src = fs.readFileSync(path.join(__dirname, '..', '..', 'actions', 'composer.js'), 'utf8');
|
|
128
|
+
assert.match(src, /if \(!sessionFinished\(sessionLogTail\(started\.session\)\)\) return \{ \.\.\.started, stillRunning: true \};\s*\n\s*await hostRunnerRequest\('stop'/,
|
|
129
|
+
'the stop must come only after the finished check');
|
|
130
|
+
assert.ok(!/<pre\b|terminal-pre/.test(src), 'composer output must not be rendered in the page');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('only a full workspace installs; everywhere else composer.json is only added to', () => {
|
|
134
|
+
const full = composer.composerActionsFor('full');
|
|
135
|
+
assert.ok(full.run['/actions/composer-install'] && full.run['/actions/composer-update'] && full.scripts);
|
|
136
|
+
// Manifest is the default: an unset or unknown mode must never install.
|
|
137
|
+
for (const mode of ['manifest', undefined, '', 'Full', 'anything']) {
|
|
138
|
+
const m = composer.composerActionsFor(mode);
|
|
139
|
+
assert.deepStrictEqual(Object.keys(m.run), ['/actions/composer-validate'], `mode ${mode} runs more than validate`);
|
|
140
|
+
assert.strictEqual(m.scripts, false, `mode ${mode} offers scripts`);
|
|
141
|
+
assert.deepStrictEqual(Object.keys(m.packages).sort(),
|
|
142
|
+
['/actions/composer-remove', '/actions/composer-require', '/actions/composer-require-dev']);
|
|
143
|
+
for (const a of Object.values(m.packages)) {
|
|
144
|
+
assert.ok(a.args.includes('--no-update'), `${a.label} would install: ${a.args.join(' ')}`);
|
|
145
|
+
assert.match(a.label, /^[\w .()-]+$/, `label is not shell-safe: ${a.label}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const { loadWorkspaces } = require('../../workspaces');
|
|
149
|
+
const ws = loadWorkspaces();
|
|
150
|
+
for (const key of ['dev', 'test', 'demos', 'sandboxes', 'projects']) {
|
|
151
|
+
if (ws[key]) assert.strictEqual(ws[key].composerMode, 'full', `${key} is a site workspace and should be full`);
|
|
152
|
+
}
|
|
153
|
+
for (const key of ['products', 'modules', 'themes', 'recipes', 'profiles']) {
|
|
154
|
+
if (ws[key]) assert.strictEqual(ws[key].composerMode, 'manifest', `${key} holds package clones and must not install`);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('the handler refuses install in a manifest workspace before touching the folder', async () => {
|
|
159
|
+
const { loadWorkspaces } = require('../../workspaces');
|
|
160
|
+
const manifestWs = Object.keys(loadWorkspaces()).find((k) => loadWorkspaces()[k].composerMode === 'manifest');
|
|
161
|
+
if (!manifestWs) return; // a tree without one
|
|
162
|
+
for (const route of ['/actions/composer-install', '/actions/composer-update', '/actions/composer-update-package', SCRIPT_ROUTE]) {
|
|
163
|
+
const out = {};
|
|
164
|
+
await composer[route]({ form: { projectName: 'anything', script: 'x', package: 'drupal/token' },
|
|
165
|
+
send: (html, code = 200) => { out.html = html; out.code = code; }, workspace: manifestWs, pathname: route });
|
|
166
|
+
assert.strictEqual(out.code, 403, `${route} must be refused in ${manifestWs}`);
|
|
167
|
+
assert.match(out.html, /only edits composer\.json/);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('the row shows COMPOSER right after SCREENS, only where there is a composer.json', () => {
|
|
172
|
+
const src = fs.readFileSync(path.join(__dirname, '..', '..', 'server.js'), 'utf8');
|
|
173
|
+
assert.match(src, /\.\.\.require\('\.\/actions\/composer'\)/, 'the composer routes are not merged');
|
|
174
|
+
// Where the buttons sit is Commands → Buttons now (row-layout.js); by default COMPOSER comes right after SCREENS.
|
|
175
|
+
const { DEFAULT_LINES } = require('../../row-layout');
|
|
176
|
+
assert.strictEqual(DEFAULT_LINES[1][DEFAULT_LINES[1].indexOf('screens') + 1], 'composer', 'COMPOSER must come right after SCREENS by default');
|
|
177
|
+
assert.strictEqual((src.match(/\bcomposer: composerMenu\b/g) || []).length, 1, 'handed to the layout once per row');
|
|
178
|
+
const fn = src.slice(src.indexOf('function composerMenuHtml'), src.indexOf('function openWithMenuHtml'));
|
|
179
|
+
assert.match(fn, /if \(!hasComposerJson\(projectPath\)\) return '';/, 'no composer.json, no button');
|
|
180
|
+
assert.match(fn, /hx-include="closest \.composer-package"/);
|
|
181
|
+
assert.match(fn, /SCRIPT_ROUTE/, 'the scripts from composer.json must be offered');
|
|
182
|
+
// The full menu is too long for one column: commands on the left, a package and the scripts on the right.
|
|
183
|
+
assert.match(fn, /const body = manifest\s*\n(\s*\/\/[^\n]*\n)*\s*\? `<ul class="uk-nav uk-dropdown-nav">\$\{head\}\s*\n\s*<li class="uk-nav-divider"><\/li>\$\{packageBlock\}\$\{groups\}<\/ul>`/,
|
|
184
|
+
'the short manifest menu stays one column, with the package field before validate');
|
|
185
|
+
assert.match(fn, /<div class="composer-columns">\s*\n\s*<ul class="uk-nav uk-dropdown-nav composer-col-commands">\$\{head\}\$\{groups\}<\/ul>\s*\n\s*<ul class="uk-nav uk-dropdown-nav composer-col-inputs">\$\{packageBlock\}\$\{scriptsBlock\}<\/ul>/,
|
|
186
|
+
'the full menu must be two columns');
|
|
187
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const yaml = require('js-yaml');
|
|
6
|
+
|
|
7
|
+
const CONFIG = path.join(__dirname, '..', '..', '..', 'config');
|
|
8
|
+
|
|
9
|
+
// Every .yml in core/config must actually be YAML.
|
|
10
|
+
//
|
|
11
|
+
// core/config/norms.yml was NOT, for as long as it had existed: one entry began with a
|
|
12
|
+
// backtick (a reserved indicator) and two carried a colon-space (read as a key separator).
|
|
13
|
+
// Nothing broke, because the only reader `cat`s it into a prompt — but it sat beside
|
|
14
|
+
// settings.yml and the workspace.*.settings.yml files, which ARE parsed, and anything that
|
|
15
|
+
// walked the directory would have thrown or silently produced nothing. `parse_yaml` gave
|
|
16
|
+
// zero variables from it. Found by a testing round rather than by a failure, which is the
|
|
17
|
+
// wrong way round; this makes the next one fail loudly instead.
|
|
18
|
+
test('every YAML file in core/config parses', () => {
|
|
19
|
+
const files = fs.readdirSync(CONFIG).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
20
|
+
assert.ok(files.length > 5, `expected the config directory, found ${files.length} files`);
|
|
21
|
+
const broken = [];
|
|
22
|
+
for (const f of files) {
|
|
23
|
+
try { yaml.load(fs.readFileSync(path.join(CONFIG, f), 'utf8')); } catch (e) {
|
|
24
|
+
broken.push(`${f}: ${String(e.message).split('\n')[0]}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
assert.deepEqual(broken, [], 'these are not valid YAML');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('norms.yml carries the three lists cmd-spdd.sh states it does', () => {
|
|
31
|
+
const d = yaml.load(fs.readFileSync(path.join(CONFIG, 'norms.yml'), 'utf8'));
|
|
32
|
+
for (const key of ['norms', 'safeguards', 'canvas_required_sections']) {
|
|
33
|
+
assert.ok(Array.isArray(d[key]) && d[key].length, `norms.yml has no ${key} list`);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('the workspace registry and every workspace settings file parse', () => {
|
|
38
|
+
const settings = yaml.load(fs.readFileSync(path.join(CONFIG, 'settings.yml'), 'utf8'));
|
|
39
|
+
assert.ok(Array.isArray(settings.workspaces) && settings.workspaces.length, 'no workspaces list');
|
|
40
|
+
const missing = settings.workspaces.filter(
|
|
41
|
+
(w) => !fs.existsSync(path.join(CONFIG, `workspace.${w}.settings.yml`)));
|
|
42
|
+
// A registered workspace with no settings file renders as a card that configures nothing.
|
|
43
|
+
assert.deepEqual(missing, [], 'registered workspaces with no settings file');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// The release a machine runs comes from core/workspace-app/.app-version, written by
|
|
47
|
+
// install.sh, sync-tooling.sh --pull and update.sh. package.json's "version" is npm
|
|
48
|
+
// metadata for an app that is never published, and it has said 1.0.0 since the first
|
|
49
|
+
// commit — which is worse than no answer, because it looks like one. Nothing may read it
|
|
50
|
+
// for display, or two numbers start disagreeing in public.
|
|
51
|
+
test('nothing displays package.json version — .app-version is the only source', () => {
|
|
52
|
+
const app = path.join(__dirname, '..', '..');
|
|
53
|
+
const readers = [];
|
|
54
|
+
for (const f of ['server.js', 'workspaces.js', 'screens.js', 'assistant.js']) {
|
|
55
|
+
const src = fs.readFileSync(path.join(app, f), 'utf8');
|
|
56
|
+
// require('./package.json').version, or reading the file and pulling .version out
|
|
57
|
+
if (/package\.json'\s*\)\s*\.version|packageJson\.version/.test(src)) readers.push(f);
|
|
58
|
+
}
|
|
59
|
+
assert.deepEqual(readers, [], 'these read package.json version — use appVersion() instead');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('appVersion reads the stamp, and says nothing when there is none', () => {
|
|
63
|
+
const { appVersion } = require(path.join(__dirname, '..', '..', 'workspaces'));
|
|
64
|
+
const v = appVersion();
|
|
65
|
+
assert.equal(typeof v, 'string');
|
|
66
|
+
// Either a real tag, or empty. Never a placeholder, never a guess.
|
|
67
|
+
if (v) assert.match(v, /^[0-9]+\.[0-9]+\.[0-9]+/, `unexpected version stamp: ${v}`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
/* ---------- the assistant's name and placeholder, at both levels ---------- */
|
|
72
|
+
|
|
73
|
+
test('settings.yml names the assistant and says what its composer shows', () => {
|
|
74
|
+
const text = fs.readFileSync(path.join(CONFIG, 'settings.yml'), 'utf8');
|
|
75
|
+
assert.match(text, /^\s{2}name: AI Assistant$/m, 'the default name, for the home dashboard');
|
|
76
|
+
assert.match(text, /^\s{2}placeholder: /m, 'and what the composer says when empty');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('every workspace can be given its own, from the dashboard', () => {
|
|
80
|
+
// The settings editor writes values onto lines that already exist — it does not add keys — so a
|
|
81
|
+
// workspace whose file has no `assistant:` block offers no field to edit and the setting looks
|
|
82
|
+
// like it does not exist. The shipped value is `default`, which means "inherit", so twenty-four
|
|
83
|
+
// files carrying the key do not pin the name against a rename in settings.yml.
|
|
84
|
+
const settings = yaml.load(fs.readFileSync(path.join(CONFIG, 'settings.yml'), 'utf8'));
|
|
85
|
+
const without = [];
|
|
86
|
+
for (const w of settings.workspaces) {
|
|
87
|
+
const f = path.join(CONFIG, `workspace.${w}.settings.yml`);
|
|
88
|
+
if (!fs.existsSync(f)) continue;
|
|
89
|
+
const own = yaml.load(fs.readFileSync(f, 'utf8')) || {};
|
|
90
|
+
if (!own.assistant || own.assistant.name === undefined || own.assistant.placeholder === undefined) without.push(w);
|
|
91
|
+
}
|
|
92
|
+
assert.deepEqual(without, [], 'these workspaces cannot name their assistant from the dashboard');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('both settings are explained in the editor, not shown as bare keys', () => {
|
|
96
|
+
// Without a label a row renders as the raw key — here, `name` and `placeholder` sitting under a
|
|
97
|
+
// workspace's own `doc: name:` and `presentation:`, which is two rows called name on one page.
|
|
98
|
+
const labels = yaml.load(fs.readFileSync(path.join(CONFIG, 'settings.labels.yml'), 'utf8'));
|
|
99
|
+
for (const k of ['assistant.name', 'assistant.placeholder']) {
|
|
100
|
+
assert.ok(labels[k] && labels[k].label, `${k} has no label`);
|
|
101
|
+
assert.ok(labels[k].info, `${k} says nothing about what it does`);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('the composer is never handed an empty placeholder', () => {
|
|
106
|
+
// deep-chat reads it as `placeholder.text || "Ask me anything!"`, so an empty string does not
|
|
107
|
+
// remove its placeholder, it restores deep-chat's. Whatever is passed must be a real string, and
|
|
108
|
+
// the hiding is done in CSS — which also keeps the aria-label that textbox needs.
|
|
109
|
+
const ui = fs.readFileSync(path.join(__dirname, '..', '..', 'public', 'ui.js'), 'utf8');
|
|
110
|
+
assert.match(ui, /placeholder: \{ text: cfg\.placeholder \|\| `Message the \$\{cfg\.name \|\| 'AI Assistant'\}` \}/,
|
|
111
|
+
'an empty text here brings back "Ask me anything!"');
|
|
112
|
+
assert.match(ui, /#text-input:empty::before \{ content: "" !important; \}/,
|
|
113
|
+
'the placeholder is hidden by blanking what is drawn, not by blanking what it is called');
|
|
114
|
+
assert.match(ui, /\.deep-chat-intro \{ display: none !important; \}/,
|
|
115
|
+
'and the empty intro bubble is hidden, not left as a blank pill at the top of every transcript');
|
|
116
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The confirm dialog (ui.js + the `#confirm-modal` in server.js's pageShell). Every important action —
|
|
3
|
+
* remove, delete, reset, a command that asks before it runs — is a `.arm-step` and must go through it.
|
|
4
|
+
*
|
|
5
|
+
* It replaced a two-step arm whose first click swapped the label for "⚠️ Sure?": a double click did the
|
|
6
|
+
* whole thing, and nothing ever said what was about to happen. What the guards here protect:
|
|
7
|
+
* the click must not navigate (these are links as well as buttons, and a link with an empty href
|
|
8
|
+
* reloads the page and loses the toast the request answers with), only Yes may fire the request, and
|
|
9
|
+
* every other way of closing the dialog — the X, Escape, the backdrop — must count as No.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const assert = require('node:assert');
|
|
13
|
+
const { test } = require('node:test');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const app = path.join(__dirname, '..', '..');
|
|
18
|
+
const ui = fs.readFileSync(path.join(app, 'public', 'ui.js'), 'utf8');
|
|
19
|
+
const server = fs.readFileSync(path.join(app, 'server.js'), 'utf8');
|
|
20
|
+
const open = ui.slice(ui.indexOf("const btn = e.target.closest('.arm-step');"), ui.indexOf('.confirm-yes'));
|
|
21
|
+
|
|
22
|
+
test('the dialog is in every page, and says what it asks', () => {
|
|
23
|
+
const modal = server.slice(server.indexOf('id="confirm-modal"'), server.indexOf('id="confirm-modal"') + 900);
|
|
24
|
+
assert.ok(modal.length > 100, 'no #confirm-modal in pageShell');
|
|
25
|
+
assert.match(modal, /role="alertdialog"/, 'an alertdialog, so it is announced');
|
|
26
|
+
assert.match(modal, /stack: true/, 'it must stack: an action can be armed inside the editor modal');
|
|
27
|
+
assert.match(modal, /Wait!/);
|
|
28
|
+
assert.match(modal, /Are you sure you want to proceed with this action\?/);
|
|
29
|
+
assert.match(modal, /confirm-no[^>]*>No</, 'No');
|
|
30
|
+
assert.match(modal, /confirm-yes[^>]*>Yes</, 'Yes');
|
|
31
|
+
assert.ok(modal.indexOf('confirm-no') < modal.indexOf('confirm-yes'), 'No comes first: the safe answer is nearest');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('arming opens the dialog, names the action, and navigates nowhere', () => {
|
|
35
|
+
assert.ok(open.length > 0, 'the .arm-step handler was not found');
|
|
36
|
+
assert.match(open, /e\.preventDefault\(\);/, 'the click must not navigate');
|
|
37
|
+
assert.match(open, /getAttribute\('title'\)/, 'an icon-only button says what it is in its title');
|
|
38
|
+
assert.match(open, /getElementById\('confirm-what'\)/, 'what is about to happen is written into the dialog');
|
|
39
|
+
assert.match(open, /UIkit\.modal\('#confirm-modal'\)\.show\(\)/);
|
|
40
|
+
assert.match(open, /window\.confirm\(/, 'with no UIkit on the page, the browser dialog rather than acting unasked');
|
|
41
|
+
assert.ok(!/htmx\.trigger/.test(open), 'opening the dialog must never fire the request by itself');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('only Yes fires the request, and every other way out is No', () => {
|
|
45
|
+
const rest = ui.slice(ui.indexOf('.confirm-yes'), ui.indexOf('The field microphones'));
|
|
46
|
+
assert.match(rest, /^\.confirm-yes'\)\) \{[\s\S]*?runConfirmed\(btn\)/, 'Yes runs it');
|
|
47
|
+
assert.match(rest, /closest\('\.confirm-no'\)\) \{\s*\n\s*pendingConfirm = null;/, 'No forgets it');
|
|
48
|
+
assert.match(rest, /addEventListener\('hidden'[\s\S]*?confirm-modal[\s\S]*?pendingConfirm = null;/,
|
|
49
|
+
'the X, Escape and the backdrop are No as well');
|
|
50
|
+
assert.match(ui, /function runConfirmed\(btn\) \{\s*\n\s*pendingConfirm = null;/,
|
|
51
|
+
'the pending action is forgotten before it runs, so one Yes cannot fire twice');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('the destructive actions are all armed', () => {
|
|
55
|
+
for (const [file, action] of [
|
|
56
|
+
['server.js', '/actions/delete-item'],
|
|
57
|
+
['server.js', '/actions/ddev-drush-sql-drop'],
|
|
58
|
+
['server.js', '/actions/rag-remove'],
|
|
59
|
+
['commands-page.js', '/actions/cc-remove'],
|
|
60
|
+
['commands-page.js', '/actions/cc-reset'],
|
|
61
|
+
]) {
|
|
62
|
+
const src = fs.readFileSync(path.join(app, file), 'utf8');
|
|
63
|
+
const i = src.indexOf(`hx-post="${action}"`);
|
|
64
|
+
assert.ok(i > 0, `${action} is gone from ${file}`);
|
|
65
|
+
const line = src.slice(src.lastIndexOf('\n', i - 1), src.indexOf('\n', i) + 400);
|
|
66
|
+
assert.match(line, /arm-step/, `${action} must ask first`);
|
|
67
|
+
assert.match(line, /hx-trigger="confirmed-remove"/, `${action} must fire only on the dialog's Yes`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
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 root = path.join(__dirname, '..', '..');
|
|
8
|
+
const server = fs.readFileSync(path.join(root, 'server.js'), 'utf8');
|
|
9
|
+
|
|
10
|
+
test('a project workspace asks "What would you like to create?" in its build card, with New folder in it', () => {
|
|
11
|
+
assert.match(server, /<h2 class="uk-h3 uk-margin-remove">What would you like to create\?<\/h2>\$\{folderBtn\}/);
|
|
12
|
+
assert.match(server, /class="uk-button uk-button-default uk-button-small create-folder-btn"/);
|
|
13
|
+
assert.doesNotMatch(server, /Build a new \$\{esc\(meta\.noun\)\}/);
|
|
14
|
+
// Files workspaces ask the same question inside their card: the create actions as a row, New folder on the right.
|
|
15
|
+
assert.doesNotMatch(server, /class="ws-create"/);
|
|
16
|
+
assert.match(server, /const folder = create\.find\(\(it\) => it\.id === 'new-folder'\);/);
|
|
17
|
+
assert.match(server, /<div class="create-actions">\$\{rest\.map\(\(it\) => it\.html\)\.join\(''\)\}<\/div>/);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('the build form offers Claude Code after the build, with its prompt shown only when ticked', () => {
|
|
21
|
+
assert.match(server, /name="ai_after" value="1"> Start AI<\/label>/);
|
|
22
|
+
assert.match(server, /name="ai_prompt"[^>]*><\/textarea>/);
|
|
23
|
+
assert.doesNotMatch(server, /name="ai_prompt"[^>]*placeholder=/);
|
|
24
|
+
const css = fs.readFileSync(path.join(root, 'public/css/components/create-card.css'), 'utf8');
|
|
25
|
+
assert.match(css, /:has\(input\[name="ai_after"\]:checked\) \.build-ai-prompt \{ display: block; \}/);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('the build ends with the step that starts Claude Code, so a failed build starts nothing', () => {
|
|
29
|
+
assert.match(server, /cmdLine \+= ` && \$\{after\.step\}`;/);
|
|
30
|
+
assert.match(server, /installed: flags\.some\(\(f\) => f === '--install' \|\| f === '-i'\)/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('the step writes a private prompt and payload, and the shell line holds only paths', () => {
|
|
34
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-'));
|
|
35
|
+
const old = process.env.WORKSPACE_ROOT;
|
|
36
|
+
try {
|
|
37
|
+
const ai = require('../../build-ai');
|
|
38
|
+
const { ROOT } = require('../../workspaces');
|
|
39
|
+
const r = ai.afterBuildStep({ ws: 'dev', dir: path.join(ROOT, 'dev'), project: 'v11x00b9', label: 'Webship 11', installed: true,
|
|
40
|
+
text: 'Run $(rm -rf /) and `whoami`' });
|
|
41
|
+
assert.equal(r.session, 'dev--v11x00b9--claude');
|
|
42
|
+
assert.doesNotMatch(r.step, /rm -rf|whoami/);
|
|
43
|
+
assert.match(r.step, /^node '.*scripts\/screen-then\.js' '.*\.screens\/prompts\/dev--v11x00b9--claude\.start\.json'$/);
|
|
44
|
+
// The step runs on the host, so the helper is found from the workspace root, never the container's own path.
|
|
45
|
+
assert.ok(r.step.startsWith(`node '${path.join(ROOT, 'core', 'workspace-app', 'scripts', 'screen-then.js')}'`));
|
|
46
|
+
const payloadFile = r.step.match(/'([^']+\.start\.json)'$/)[1];
|
|
47
|
+
const payload = JSON.parse(fs.readFileSync(payloadFile, 'utf8'));
|
|
48
|
+
assert.match(payload.cmd, /^exec env WEBSHIP_WORKSPACE='dev' WEBSHIP_PROJECT='v11x00b9' .* claude "\$\(cat '.*dev--v11x00b9--claude\.txt'\)"$/);
|
|
49
|
+
assert.equal(payload.dir, path.join(ROOT, 'dev', 'v11x00b9'));
|
|
50
|
+
const prompt = fs.readFileSync(payload.cmd.match(/cat '([^']+)'/)[1], 'utf8');
|
|
51
|
+
assert.match(prompt, /^The project dev\/v11x00b9 was just built here with Webship 11 and installed\.\n\nRun \$\(rm -rf \/\)/);
|
|
52
|
+
assert.equal(fs.statSync(payloadFile).mode & 0o777, 0o600);
|
|
53
|
+
fs.unlinkSync(payloadFile);
|
|
54
|
+
fs.unlinkSync(payload.cmd.match(/cat '([^']+)'/)[1]);
|
|
55
|
+
} finally {
|
|
56
|
+
if (old === undefined) delete process.env.WORKSPACE_ROOT; else process.env.WORKSPACE_ROOT = old;
|
|
57
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('Start AI offers the Claude Code options New screen offers, with the local agents as a list and no placeholders', () => {
|
|
62
|
+
assert.match(server, /claudeArgsHtml\(\{ target: `ws:\$\{bm\[1\]\}`, agentSelect: true \}\)\)\.replace\(\/ placeholder="\[\^"\]\*"\/g, ''\)/);
|
|
63
|
+
assert.match(server, /const \{ words, problems \} = claudeArgs\.flagsFromForm\(options, form\);\n if \(problems\.length\)/);
|
|
64
|
+
const screens = fs.readFileSync(path.join(root, 'screens.js'), 'utf8');
|
|
65
|
+
assert.match(screens, /claudeArgFieldHtml\(o, valueOf\(o\), suggestionsFor\(o\), !!p\.agentSelect && o\.name === '--agent'\)/);
|
|
66
|
+
assert.match(screens, /<option value="">None<\/option>/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('the chosen Claude Code options go on the claude line after the build', () => {
|
|
70
|
+
const ai = require('../../build-ai');
|
|
71
|
+
const { ROOT } = require('../../workspaces');
|
|
72
|
+
const r = ai.afterBuildStep({ ws: 'dev', dir: path.join(ROOT, 'dev'), project: 'v11x00b8', flags: ['--agent', 'webship-ai-expert', '--permission-mode', 'plan'] });
|
|
73
|
+
const payloadFile = r.step.match(/'([^']+\.start\.json)'$/)[1];
|
|
74
|
+
const payload = JSON.parse(fs.readFileSync(payloadFile, 'utf8'));
|
|
75
|
+
assert.match(payload.cmd, / claude --agent webship-ai-expert --permission-mode plan "\$\(cat '/);
|
|
76
|
+
fs.unlinkSync(payloadFile);
|
|
77
|
+
fs.unlinkSync(payload.cmd.match(/cat '([^']+)'/)[1]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('the Start AI prompt is an Ace editor, loaded only when Start AI is ticked, with @ suggestions', () => {
|
|
81
|
+
assert.match(server, /name="ai_prompt" rows="5" maxlength="4000" aria-label="What the AI should do"\n\s+data-ace-on-demand="markdown" data-mentions="\/fragments\/ai-suggest\/\$\{esc\(key\)\}"><\/textarea>/);
|
|
82
|
+
const ui = fs.readFileSync(path.join(root, 'public/ui.js'), 'utf8');
|
|
83
|
+
assert.match(ui, /matches\('input\[name="ai_after"\]'\)/);
|
|
84
|
+
assert.match(ui, /ta\.dataset\.ace = ta\.dataset\.aceOnDemand;\n attachAce\(form\);/);
|
|
85
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DDEV add-ons: what a project has, and what it could.
|
|
3
|
+
*
|
|
4
|
+
* An add-on is a repository DDEV copies into a project — a service (`ddev-redis`), a tool
|
|
5
|
+
* (`ddev-adminer`), a whole development setup (`ddev-drupal-contrib`). There are 287 in the registry
|
|
6
|
+
* and no way to reach any of them from here except a terminal.
|
|
7
|
+
*
|
|
8
|
+
* The one thing this feature must not get wrong: `ddev add-on get` installs from a URL or a local
|
|
9
|
+
* path as happily as from a repository, so it is a download-and-run from anywhere. A name that
|
|
10
|
+
* arrives from a browser is checked against the registry LISTING, not merely against a pattern.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { test } = require('node:test');
|
|
14
|
+
const assert = require('node:assert');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const addons = require('../../ddev-addons');
|
|
20
|
+
|
|
21
|
+
const APP = path.join(__dirname, '../..');
|
|
22
|
+
const server = fs.readFileSync(path.join(APP, 'server.js'), 'utf8');
|
|
23
|
+
const page = fs.readFileSync(path.join(APP, 'ddev-addons-page.js'), 'utf8');
|
|
24
|
+
const actions = fs.readFileSync(path.join(APP, 'actions/ddev-addons.js'), 'utf8');
|
|
25
|
+
|
|
26
|
+
/** A project with the metadata DDEV writes when it installs one. */
|
|
27
|
+
function project(installed = {}) {
|
|
28
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-addon-'));
|
|
29
|
+
fs.mkdirSync(path.join(dir, '.ddev', 'addon-metadata'), { recursive: true });
|
|
30
|
+
fs.writeFileSync(path.join(dir, '.ddev', 'config.yaml'), 'name: demo\ntype: drupal\n');
|
|
31
|
+
for (const [name, meta] of Object.entries(installed)) {
|
|
32
|
+
const d = path.join(dir, '.ddev', 'addon-metadata', name);
|
|
33
|
+
fs.mkdirSync(d, { recursive: true });
|
|
34
|
+
// The real shape, copied from an actual install of ddev/ddev-adminer.
|
|
35
|
+
fs.writeFileSync(path.join(d, 'manifest.yaml'),
|
|
36
|
+
`name: ${name}\nrepository: ${meta.repository}\nversion: ${meta.version}\n`
|
|
37
|
+
+ `install_date: "${meta.date}"\nproject_files:\n - docker-compose.${name}.yaml\n - commands/host/${name}\nglobal_files: []\n`);
|
|
38
|
+
}
|
|
39
|
+
return dir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test('installed add-ons are read from what DDEV recorded', () => {
|
|
43
|
+
// `.ddev/addon-metadata/<name>/manifest.yaml`, so listing them is a readdir and a project row
|
|
44
|
+
// costs no subprocess.
|
|
45
|
+
const dir = project({
|
|
46
|
+
adminer: { repository: 'ddev/ddev-adminer', version: 'v1.4.1', date: '2026-09-23T13:02:17+03:00' },
|
|
47
|
+
redis: { repository: 'ddev/ddev-redis', version: 'v2.2.0', date: '2026-09-23T14:10:00+03:00' },
|
|
48
|
+
});
|
|
49
|
+
const list = addons.installedAddons(dir);
|
|
50
|
+
assert.deepEqual(list.map((a) => a.name), ['redis', 'adminer'], 'newest first');
|
|
51
|
+
assert.equal(list[1].repository, 'ddev/ddev-adminer');
|
|
52
|
+
assert.equal(list[1].version, 'v1.4.1');
|
|
53
|
+
assert.deepEqual(list[1].files, ['docker-compose.adminer.yaml', 'commands/host/adminer'],
|
|
54
|
+
'what it owns, which is what removing it takes away');
|
|
55
|
+
assert.deepEqual(addons.installedAddons(project()), [], 'a project with none says none');
|
|
56
|
+
assert.deepEqual(addons.installedAddons('/no/such/project'), []);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('only an owner/repo shape is even considered', () => {
|
|
60
|
+
// The rest of what `ddev add-on get` accepts is a download-and-run from anywhere.
|
|
61
|
+
for (const bad of ['https://example.com/x.tar.gz', '../../etc', '/tmp/evil', 'no-slash', 'a//b', '']) {
|
|
62
|
+
assert.equal(addons.ADDON_RE.test(bad), false, bad);
|
|
63
|
+
}
|
|
64
|
+
for (const good of ['ddev/ddev-redis', 'Lullabot/ddev-playwright', 'a.b/c_d-e']) {
|
|
65
|
+
assert.ok(addons.ADDON_RE.test(good), good);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('the registry is the allowlist, and an unreadable one installs nothing', async () => {
|
|
70
|
+
// Verified against the live registry too: `evil/not-a-real-addon` is refused by name, and
|
|
71
|
+
// `ddev/ddev-redis` is accepted.
|
|
72
|
+
const fn = actions;
|
|
73
|
+
assert.match(fn, /const known = await addons\.isKnownAddon\(repo\);/);
|
|
74
|
+
assert.match(fn, /if \(!known\.ok\) return send/);
|
|
75
|
+
const mod = fs.readFileSync(path.join(APP, 'ddev-addons.js'), 'utf8');
|
|
76
|
+
assert.match(mod, /if \(!reg\.list \|\| !reg\.list\.length\) \{\s*\n\s*return \{ ok: false/,
|
|
77
|
+
'a registry that could not be read refuses everything');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('the registry comes from the topic, not from the table that truncates', () => {
|
|
81
|
+
// `ddev add-on list` renders a table and truncates 38 of the 294 names with `…`; a truncated name
|
|
82
|
+
// cannot be installed. The registry IS the GitHub topic, so that is what is asked.
|
|
83
|
+
const mod = fs.readFileSync(path.join(APP, 'ddev-addons.js'), 'utf8');
|
|
84
|
+
assert.match(mod, /const REGISTRY_TOPIC = 'ddev-get';/);
|
|
85
|
+
assert.match(mod, /run\('gh', \['search', 'repos', '--topic', REGISTRY_TOPIC/);
|
|
86
|
+
// And when it falls back to the table, a truncated row says so instead of offering a button.
|
|
87
|
+
assert.match(mod, /truncated: m\[1\]\.includes\('…'\)/);
|
|
88
|
+
assert.match(page, /name truncated/);
|
|
89
|
+
assert.match(page, /installButton\(r\.repo, vals, !r\.truncated\)/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('searching covers the description, the way ddev add-on search does', () => {
|
|
93
|
+
const list = [
|
|
94
|
+
{ repo: 'ddev/ddev-solr', description: 'Apache Solr for DDEV' },
|
|
95
|
+
{ repo: 'ddev/ddev-redis', description: 'Redis caching' },
|
|
96
|
+
{ repo: 'someone/ddev-thing', description: 'Uses Solr under the hood' },
|
|
97
|
+
];
|
|
98
|
+
assert.deepEqual(addons.searchAddons(list, 'solr').map((r) => r.repo), ['ddev/ddev-solr', 'someone/ddev-thing']);
|
|
99
|
+
assert.deepEqual(addons.searchAddons(list, 'REDIS').map((r) => r.repo), ['ddev/ddev-redis'], 'case does not matter');
|
|
100
|
+
assert.equal(addons.searchAddons(list, '').length, 3, 'an empty search is not a filter');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('removing and updating name something this project actually has', () => {
|
|
104
|
+
assert.match(actions, /const found = installed\.find\(\(a\) => a\.name === name\);/);
|
|
105
|
+
assert.match(actions, /is not installed in this project/);
|
|
106
|
+
// Removing takes files out of the project, so it is confirmed — in the handler, not only in the markup.
|
|
107
|
+
assert.match(actions, /if \(removing && form\.confirm !== 'yes'\)/);
|
|
108
|
+
assert.match(page, /hx-trigger="confirmed-remove"/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('a recommendation that the registry does not list says THAT, not "truncated"', () => {
|
|
112
|
+
// Found by opening the panel: `ddev/ddev-playwright` does not exist — the maintained one is
|
|
113
|
+
// Lullabot's — and the row said "name truncated", which is a different problem and sent me looking
|
|
114
|
+
// in the wrong place. Two reasons, two messages.
|
|
115
|
+
assert.match(page, /why === 'unknown'/);
|
|
116
|
+
assert.match(page, /not in the registry/);
|
|
117
|
+
assert.match(page, /installButton\(r\.repo, vals, !!known, 'unknown'\)/, 'a recommendation is checked as a name');
|
|
118
|
+
assert.match(page, /installButton\(r\.repo, vals, !r\.truncated\)/, 'a registry row is checked as a truncation');
|
|
119
|
+
// And the name itself is fixed. Every recommendation was checked against the live registry.
|
|
120
|
+
assert.ok(addons.RECOMMENDED.some((r) => r.repo === 'Lullabot/ddev-playwright'));
|
|
121
|
+
assert.ok(!addons.RECOMMENDED.some((r) => r.repo === 'ddev/ddev-playwright'));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('each recommendation says what it is for', () => {
|
|
125
|
+
// A list of eight repository names is a shelf; the reason is what makes it a recommendation.
|
|
126
|
+
assert.ok(addons.RECOMMENDED.length >= 6);
|
|
127
|
+
for (const r of addons.RECOMMENDED) {
|
|
128
|
+
assert.ok(addons.ADDON_RE.test(r.repo), r.repo);
|
|
129
|
+
assert.ok(r.why && r.why.length > 30, `${r.repo} says why`);
|
|
130
|
+
}
|
|
131
|
+
// And one already installed is not offered again.
|
|
132
|
+
assert.match(page, /RECOMMENDED\.filter\(\(r\) => !have\.has\(r\.repo\.toLowerCase\(\)\)\)/);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('the panel is its own module, and the route only resolves the project', () => {
|
|
136
|
+
// One module per area, like the commands and navigation pages: what a panel looks like is one file
|
|
137
|
+
// to open, and server.js stays a router.
|
|
138
|
+
assert.match(server, /const \{ addonsPanelHtml \} = require\('\.\/ddev-addons-page'\);/);
|
|
139
|
+
assert.match(server, /const html = await addonsPanelHtml\(\{/);
|
|
140
|
+
assert.match(server, /\.\.\.require\('\.\/actions\/ddev-addons'\),/);
|
|
141
|
+
assert.match(page, /module\.exports = \{ addonsPanelHtml, PER_PAGE \};/);
|
|
142
|
+
// Installing writes into .ddev and does nothing until a restart — the panel says so.
|
|
143
|
+
assert.match(page, /needs a\s*\n\s*<strong>ddev restart<\/strong>/);
|
|
144
|
+
});
|