@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,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin system end to end, against a temporary tree: a fake plugin that uses every hook, the
|
|
3
|
+
* manifest reader, the installer's place / record / uninstall, and plugins.sh's verbs.
|
|
4
|
+
*
|
|
5
|
+
* The dashboard's own modules read ROOT and the plugins folder once at load, so each check runs in a
|
|
6
|
+
* child node process with WORKSPACE_ROOT / WORKSPACE_CONFIG / WORKSPACE_PLUGINS_DIR pointing at the
|
|
7
|
+
* temporary tree — the same way a test of a shipped default does.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const test = require('node:test');
|
|
11
|
+
const assert = require('node:assert');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { spawnSync } = require('child_process');
|
|
16
|
+
|
|
17
|
+
const APP = path.join(__dirname, '..', '..');
|
|
18
|
+
const SCRIPTS = path.join(APP, '..', 'scripts');
|
|
19
|
+
|
|
20
|
+
/** A workspace tree with one `sites` workspace, and a plugins folder with one plugin that uses every hook. */
|
|
21
|
+
function tree() {
|
|
22
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-plugins-'));
|
|
23
|
+
const config = path.join(root, 'core', 'config');
|
|
24
|
+
fs.mkdirSync(config, { recursive: true });
|
|
25
|
+
fs.mkdirSync(path.join(root, 'core', 'scripts'), { recursive: true });
|
|
26
|
+
fs.mkdirSync(path.join(root, 'core', 'workspace-app', '.ddev'), { recursive: true });
|
|
27
|
+
fs.mkdirSync(path.join(root, 'dev', 'site1'), { recursive: true });
|
|
28
|
+
fs.mkdirSync(path.join(root, 'modules'), { recursive: true });
|
|
29
|
+
fs.writeFileSync(path.join(config, 'settings.yml'), [
|
|
30
|
+
'account:', ' name: webmaster', ' pass: CHANGE_ME', ' mail: a@example.com',
|
|
31
|
+
'workspaces:', ' - dev', ' - modules',
|
|
32
|
+
// The shipped Google Drive plugin is on by default; off here, so only the fake one is loaded.
|
|
33
|
+
'plugins:', ' x-cloud:', ' enabled: true', ' google-drive-sync:', ' enabled: false',
|
|
34
|
+
'x_cloud:', ' token: carried-over', 'git:', ' x-forge:', ' username: forge-me', ' token: CHANGE_ME', '',
|
|
35
|
+
].join('\n'));
|
|
36
|
+
fs.writeFileSync(path.join(config, 'workspace.dev.settings.yml'), 'doc:\n name: dev\ntype: sites\n');
|
|
37
|
+
fs.writeFileSync(path.join(config, 'workspace.modules.settings.yml'), 'doc:\n name: modules\ntype: code\n');
|
|
38
|
+
fs.writeFileSync(path.join(root, '.gitignore'), 'node_modules/\n');
|
|
39
|
+
const plugins = path.join(root, 'plugins');
|
|
40
|
+
const dir = path.join(plugins, 'x-cloud');
|
|
41
|
+
for (const d of ['scripts/functions', 'scripts/args', 'commands', 'ddev', 'config', 'public', 'features']) fs.mkdirSync(path.join(dir, d), { recursive: true });
|
|
42
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
|
|
43
|
+
name: '@webship.co/workspace-x-cloud', version: '0.1.0', main: 'index.js',
|
|
44
|
+
workspacePlugin: {
|
|
45
|
+
name: 'x-cloud', label: 'X Cloud', settingsFile: 'plugin.x-cloud.settings.yml', settingsTemplate: 'config/plugin.x-cloud.settings.yml',
|
|
46
|
+
legacySettingsKey: 'x_cloud', labels: 'config/labels.yml', typeActions: { 'x-cloud': ['sites'] },
|
|
47
|
+
commands: { types: ['sites'], files: ['commands/cmd-tools-x-cloud.sh'] },
|
|
48
|
+
scripts: { functions: ['scripts/functions/fun-x-cloud.sh'], args: ['scripts/args/arg-x-cloud.sh'] },
|
|
49
|
+
ddev: { compose: ['ddev/docker-compose.x-cloud.yaml'], hostMounts: ['~/.x-cloud'] },
|
|
50
|
+
css: ['public/x-cloud.css'], features: ['features/x-cloud.feature'],
|
|
51
|
+
},
|
|
52
|
+
}));
|
|
53
|
+
fs.writeFileSync(path.join(dir, 'config', 'plugin.x-cloud.settings.yml'), '# X Cloud\ntoken: CHANGE_ME\nregion: eu\n');
|
|
54
|
+
fs.writeFileSync(path.join(dir, 'config', 'labels.yml'), 'token:\n label: X Cloud token\n');
|
|
55
|
+
fs.writeFileSync(path.join(dir, 'commands', 'cmd-tools-x-cloud.sh'), '#!/usr/bin/env bash\n# in __WORKSPACE__\n');
|
|
56
|
+
fs.writeFileSync(path.join(dir, 'scripts', 'functions', 'fun-x-cloud.sh'), 'function x_cloud_tool() { echo x; }\n');
|
|
57
|
+
fs.writeFileSync(path.join(dir, 'scripts', 'args', 'arg-x-cloud.sh'), '#!/usr/bin/env bash\n');
|
|
58
|
+
fs.writeFileSync(path.join(dir, 'ddev', 'docker-compose.x-cloud.yaml'), 'services:\n web:\n volumes:\n - "${HOME}/.x-cloud:/x:rw"\n');
|
|
59
|
+
fs.writeFileSync(path.join(dir, 'public', 'x-cloud.css'), '.x-cloud { color: red; }\n');
|
|
60
|
+
fs.writeFileSync(path.join(dir, 'features', 'x-cloud.feature'), 'Feature: x\n');
|
|
61
|
+
fs.writeFileSync(path.join(dir, 'index.js'), `
|
|
62
|
+
module.exports.register = (api) => {
|
|
63
|
+
api.addRowMenu('x-cloud', { label: 'X CLOUD', row: 0 }, ({ key, project, allowed }) => allowed.has('x-cloud') ? '<div class="act-menu-x-cloud">' + key + '/' + project + '</div>' : null);
|
|
64
|
+
api.addPage(/^\\/x-cloud\\/([a-z]+)$/, ({ send, match }) => send('page:' + match[1]));
|
|
65
|
+
api.addProjectChip(({ project }) => '<span class="x-chip">' + project + '</span>');
|
|
66
|
+
api.addFolderSetup({ html: () => '<div class="x-setup"></div>', steps: (ws, name, form) => form.x === '1' ? ['echo x ' + name] : [], prompt: () => ['x prompt'], problem: (ws, form) => form.x === 'bad' ? 'bad x' : '' });
|
|
67
|
+
api.addAction('ping', ({ send }) => send('pong'));
|
|
68
|
+
api.addFragment(({ send, rest }) => send('frag:' + rest));
|
|
69
|
+
api.addLabels({ region: { label: 'Region' } });
|
|
70
|
+
api.addForge('forge.test', { label: 'X Forge', env: () => ({ X_TOKEN: 'secret' }), auth: () => ({ username: 'me', how: 'token' }) });
|
|
71
|
+
api.addWorkspaceView('x-store', { listHtml: (state) => '<div class="x-list">' + (state.q || '') + '</div>', count: () => 7, noNewForm: true });
|
|
72
|
+
};`);
|
|
73
|
+
return { root, config, plugins, dir };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function node(code, env) {
|
|
77
|
+
const r = spawnSync(process.execPath, ['-e', code], { cwd: APP, env: { ...process.env, ...env }, encoding: 'utf8', timeout: 60000 });
|
|
78
|
+
if (r.status !== 0) throw new Error(`${r.stderr || r.stdout}`);
|
|
79
|
+
return r.stdout;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function env(t) {
|
|
83
|
+
// NO_RESTART: the verbs must not touch the dashboard that serves this machine.
|
|
84
|
+
return { WORKSPACE_ROOT: t.root, WORKSPACE_CONFIG: t.config, WORKSPACE_PLUGINS_DIR: t.plugins, WORKSPACE_PLUGINS_NO_RESTART: '1' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
test('a plugin reaches every hook, and what it registers is what the dashboard draws', async () => {
|
|
88
|
+
const t = tree();
|
|
89
|
+
const out = node(`
|
|
90
|
+
const l = require('./plugin-loader');
|
|
91
|
+
const { TYPES } = require('./workspace-types');
|
|
92
|
+
const rl = require('./row-layout');
|
|
93
|
+
const vals = () => '';
|
|
94
|
+
const menus = l.pluginRowMenus({ key: 'dev', project: 'site1', dir: '${t.root}/dev', meta: {}, allowed: new Set(['x-cloud']), vals });
|
|
95
|
+
const none = l.pluginRowMenus({ key: 'modules', project: 'm', dir: '${t.root}/modules', meta: {}, allowed: new Set(), vals });
|
|
96
|
+
const chips = l.pluginProjectChipsHtml({ key: 'dev', project: 'site1', dir: '${t.root}/dev', meta: {} });
|
|
97
|
+
const setups = l.pluginFolderSetups();
|
|
98
|
+
let page = ''; const res = { writeHead() {}, end(s) { page = s; } };
|
|
99
|
+
l.pluginPage('/x-cloud/abc', {}, res).then((hit) => {
|
|
100
|
+
let frag = ''; const res2 = { writeHead() {}, end(s) { frag = s; } };
|
|
101
|
+
const hitF = l.pluginFragment('/fragments/p/x-cloud/one/two', { url: '/fragments/p/x-cloud/one/two' }, res2);
|
|
102
|
+
console.log(JSON.stringify({ menus, none, chips, setups: setups.length, steps: setups[0].steps('dev', 'n', { x: '1' }, {}), problem: setups[0].problem('dev', { x: 'bad' }, {}),
|
|
103
|
+
hit, page, hitF, frag, css: l.pluginCssFiles(), labels: l.pluginLabels(), sitesHas: TYPES.sites.actions.includes('x-cloud'), codeHas: TYPES.code.actions.includes('x-cloud'),
|
|
104
|
+
button: rl.DEFAULT_ROWS[0].includes('x-cloud'), action: Object.keys(l.pluginActions), loaded: l.loadedPlugins,
|
|
105
|
+
forge: l.pluginForge('FORGE.test') && l.pluginForge('FORGE.test').env(), noForge: l.pluginForge('other.test'),
|
|
106
|
+
view: l.pluginWorkspaceView('x-store') && { list: l.pluginWorkspaceView('x-store').listHtml({ q: 'abc' }), count: l.pluginWorkspaceView('x-store').count(), noNewForm: l.pluginWorkspaceView('x-store').noNewForm }, noView: l.pluginWorkspaceView('dev') }));
|
|
107
|
+
});`, env(t));
|
|
108
|
+
const r = JSON.parse(out);
|
|
109
|
+
assert.strictEqual(r.menus['x-cloud'], '<div class="act-menu-x-cloud">dev/site1</div>');
|
|
110
|
+
assert.strictEqual(r.none['x-cloud'], '', 'a row whose type does not allow it gets an empty slot');
|
|
111
|
+
assert.strictEqual(r.chips, '<span class="x-chip">site1</span>');
|
|
112
|
+
assert.strictEqual(r.setups, 1);
|
|
113
|
+
assert.deepStrictEqual(r.steps, ['echo x n']);
|
|
114
|
+
assert.strictEqual(r.problem, 'bad x');
|
|
115
|
+
assert.strictEqual(r.hit, true); assert.strictEqual(r.page, 'page:abc');
|
|
116
|
+
assert.strictEqual(r.hitF, true); assert.strictEqual(r.frag, 'frag:one/two');
|
|
117
|
+
assert.ok(r.css[0].endsWith('/x-cloud/public/x-cloud.css'), 'the manifest css is served');
|
|
118
|
+
assert.strictEqual(r.labels.token.label, 'X Cloud token', 'the manifest labels are merged');
|
|
119
|
+
assert.strictEqual(r.labels['plugin.x-cloud.token'].label, 'X Cloud token');
|
|
120
|
+
assert.strictEqual(r.labels.region.label, 'Region', 'labels from register() too');
|
|
121
|
+
assert.ok(r.sitesHas && !r.codeHas, 'the type action lands on the types named');
|
|
122
|
+
assert.ok(r.button, 'the row-menu slot joins the default layout');
|
|
123
|
+
assert.deepStrictEqual(r.action, ['/actions/p/x-cloud/ping']);
|
|
124
|
+
assert.deepStrictEqual(r.loaded, [{ name: 'x-cloud', source: 'plugins/x-cloud' }]);
|
|
125
|
+
assert.deepStrictEqual(r.forge, { X_TOKEN: 'secret' }, 'a forge answers for its host, whatever the case');
|
|
126
|
+
assert.strictEqual(r.noForge, null);
|
|
127
|
+
assert.deepStrictEqual(r.view, { list: '<div class="x-list">abc</div>', count: 7, noNewForm: true }, 'a plugin owns a workspace folder\'s page');
|
|
128
|
+
assert.strictEqual(r.noView, null);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('a dotted legacy key names a nested settings.yml block, for the reader and the installer', () => {
|
|
132
|
+
const t = tree();
|
|
133
|
+
const dir = path.join(t.plugins, 'x-forge');
|
|
134
|
+
fs.mkdirSync(path.join(dir, 'config'), { recursive: true });
|
|
135
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: '@webship.co/workspace-x-forge', version: '0.1.0', main: 'index.js',
|
|
136
|
+
workspacePlugin: { name: 'x-forge', settingsFile: 'plugin.x-forge.settings.yml', settingsTemplate: 'config/plugin.x-forge.settings.yml', legacySettingsKey: 'git.x-forge' } }));
|
|
137
|
+
fs.writeFileSync(path.join(dir, 'config', 'plugin.x-forge.settings.yml'), 'username: CHANGE_ME\ntoken: CHANGE_ME\n');
|
|
138
|
+
fs.writeFileSync(path.join(dir, 'index.js'), 'module.exports.register = () => {};');
|
|
139
|
+
const read = node(`console.log(JSON.stringify(require('./plugin-settings').readPluginSettings('x-forge')))`, env(t));
|
|
140
|
+
assert.deepStrictEqual(JSON.parse(read), { username: 'forge-me', token: 'CHANGE_ME' }, 'before the file exists, the nested block answers');
|
|
141
|
+
const r = spawnSync(process.execPath, ['plugin-install.js', 'install', 'x-forge'], { cwd: APP, env: { ...process.env, ...env(t) }, encoding: 'utf8' });
|
|
142
|
+
assert.strictEqual(r.status, 0, r.stderr);
|
|
143
|
+
const settings = fs.readFileSync(path.join(t.config, 'plugin.x-forge.settings.yml'), 'utf8');
|
|
144
|
+
assert.match(settings, /^username: forge-me$/m, 'the nested block value is carried into the file');
|
|
145
|
+
assert.match(settings, /^token: CHANGE_ME$/m, 'a placeholder is not carried');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('the manifest reader sees a plugin folder and answers the command line', () => {
|
|
149
|
+
const t = tree();
|
|
150
|
+
const out = node(`const m = require('./plugin-manifest'); console.log(JSON.stringify(m.installedPlugins().map((p) => [p.name, p.module, p.spec])));`, env(t));
|
|
151
|
+
assert.deepStrictEqual(JSON.parse(out).filter((x) => x[0] === 'x-cloud'), [['x-cloud', '@webship.co/workspace-x-cloud', 'plugins/x-cloud']]);
|
|
152
|
+
const r = spawnSync(process.execPath, ['plugin-manifest.js', 'nope'], { cwd: APP, env: { ...process.env, ...env(t) }, encoding: 'utf8' });
|
|
153
|
+
assert.strictEqual(r.status, 1, 'an unknown name is a non-zero exit, for the shell');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('install places what the manifest names, records it, carries the legacy block, and uninstall removes exactly that', () => {
|
|
157
|
+
const t = tree();
|
|
158
|
+
const run = (verb) => spawnSync(process.execPath, ['plugin-install.js', verb, 'x-cloud'], { cwd: APP, env: { ...process.env, ...env(t) }, encoding: 'utf8' });
|
|
159
|
+
const r = run('install');
|
|
160
|
+
assert.strictEqual(r.status, 0, r.stderr);
|
|
161
|
+
const placed = r.stdout.split('\n').filter((l) => l.startsWith(' placed ')).map((l) => l.replace(' placed ', ''));
|
|
162
|
+
const expect = [
|
|
163
|
+
'core/scripts/plugins/x-cloud/functions/fun-x-cloud.sh', 'core/scripts/plugins/x-cloud/args/arg-x-cloud.sh', 'core/scripts/plugins/x-cloud/host-mounts.txt',
|
|
164
|
+
'core/workspace-app/.ddev/docker-compose.plugin-x-cloud-x-cloud.yaml', 'dev/cmd-tools-x-cloud.sh',
|
|
165
|
+
'core/workspace-app/tests/features/plugins/x-cloud/x-cloud.feature', 'core/config/plugin.x-cloud.settings.yml',
|
|
166
|
+
];
|
|
167
|
+
for (const p of expect) {
|
|
168
|
+
assert.ok(placed.includes(p), `${p} is reported placed`);
|
|
169
|
+
assert.ok(fs.existsSync(path.join(t.root, p)), `${p} exists`);
|
|
170
|
+
}
|
|
171
|
+
assert.ok(!fs.existsSync(path.join(t.root, 'modules', 'cmd-tools-x-cloud.sh')), 'a code workspace gets no sites command');
|
|
172
|
+
assert.match(fs.readFileSync(path.join(t.root, 'dev', 'cmd-tools-x-cloud.sh'), 'utf8'), /# in dev\n/, '__WORKSPACE__ is filled in');
|
|
173
|
+
const settings = fs.readFileSync(path.join(t.config, 'plugin.x-cloud.settings.yml'), 'utf8');
|
|
174
|
+
assert.match(settings, /^token: carried-over$/m, 'the legacy block value replaces the template placeholder');
|
|
175
|
+
assert.match(settings, /^region: eu$/m, 'the rest of the template stays');
|
|
176
|
+
assert.match(r.stdout, /RESTART_NEEDED/, 'a compose file means the container has to restart');
|
|
177
|
+
assert.match(fs.readFileSync(path.join(t.root, 'core', 'config', 'plugins.installed.yml'), 'utf8'), /x-cloud:/);
|
|
178
|
+
const gi = fs.readFileSync(path.join(t.root, '.gitignore'), 'utf8');
|
|
179
|
+
assert.match(gi, /managed by core\/scripts\/plugins\.sh/);
|
|
180
|
+
assert.match(gi, /^\/dev\/cmd-tools-x-cloud\.sh$/m);
|
|
181
|
+
assert.match(gi, /^node_modules\/$/m, 'what the file had stays');
|
|
182
|
+
// Again: idempotent, and nothing doubled.
|
|
183
|
+
assert.strictEqual(run('install').status, 0);
|
|
184
|
+
assert.strictEqual((fs.readFileSync(path.join(t.root, '.gitignore'), 'utf8').match(/managed by/g) || []).length, 1);
|
|
185
|
+
const u = run('uninstall');
|
|
186
|
+
assert.strictEqual(u.status, 0, u.stderr);
|
|
187
|
+
for (const p of expect) assert.ok(!fs.existsSync(path.join(t.root, p)), `${p} is gone`);
|
|
188
|
+
assert.ok(!fs.existsSync(path.join(t.root, 'core', 'scripts', 'plugins', 'x-cloud')), 'the scripts folder is gone');
|
|
189
|
+
assert.doesNotMatch(fs.readFileSync(path.join(t.root, '.gitignore'), 'utf8'), /cmd-tools-x-cloud/);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('plugins.sh lists the step a plugin is at, activates and deactivates it in settings.yml', () => {
|
|
193
|
+
const t = tree();
|
|
194
|
+
const sh = (...args) => spawnSync('bash', [path.join(SCRIPTS, 'plugins.sh'), ...args], { cwd: t.root, env: { ...process.env, ...env(t), WORKSPACE_SCRIPTS: SCRIPTS }, encoding: 'utf8', timeout: 60000 });
|
|
195
|
+
let r = sh('--list');
|
|
196
|
+
assert.strictEqual(r.status, 0, r.stderr);
|
|
197
|
+
assert.match(r.stdout, /x-cloud\s+@webship\.co\/workspace-x-cloud\s+0\.1\.0\s+activated · next: install/);
|
|
198
|
+
r = sh('--deactivate', 'x-cloud');
|
|
199
|
+
assert.strictEqual(r.status, 0, r.stderr);
|
|
200
|
+
assert.match(fs.readFileSync(path.join(t.config, 'settings.yml'), 'utf8'), /x-cloud:\n(?: .*\n)*? enabled: false/);
|
|
201
|
+
assert.match(sh('--list').stdout, /x-cloud.*required · next: activate/);
|
|
202
|
+
r = sh('--activate', 'x-cloud');
|
|
203
|
+
assert.strictEqual(r.status, 0, r.stderr);
|
|
204
|
+
const s = fs.readFileSync(path.join(t.config, 'settings.yml'), 'utf8');
|
|
205
|
+
assert.match(s, /x-cloud:\n(?: .*\n)*? enabled: true/, 'enabled, wherever in the block the writer put it');
|
|
206
|
+
assert.match(s, /module: "@webship\.co\/workspace-x-cloud"/, 'a module name starting with @ is quoted');
|
|
207
|
+
assert.match(sh('--list').stdout, /x-cloud.*activated · next: install/);
|
|
208
|
+
assert.strictEqual(sh('--activate', 'nope').status, 1, 'an unknown plugin is refused');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('a plugin that is a workspace: install makes the folder, its settings file and commands; activate lists it; deactivate delists it; uninstall keeps the settings', () => {
|
|
212
|
+
const t = tree();
|
|
213
|
+
const dir = path.join(t.plugins, 'x-themes');
|
|
214
|
+
fs.mkdirSync(path.join(dir, 'config'), { recursive: true });
|
|
215
|
+
fs.mkdirSync(path.join(dir, 'commands'), { recursive: true });
|
|
216
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: '@webship.co/workspace-x-themes', version: '0.1.0', main: 'index.js',
|
|
217
|
+
workspacePlugin: { name: 'x-themes', workspace: { key: 'x-themes', settingsTemplate: 'config/workspace.x-themes.settings.yml', files: ['commands/cmd-build-theme.sh'], docs: ['docs/AGENTS.md'] } } }));
|
|
218
|
+
fs.mkdirSync(path.join(dir, 'docs'));
|
|
219
|
+
fs.writeFileSync(path.join(dir, 'docs', 'AGENTS.md'), '# __WORKSPACE__\n');
|
|
220
|
+
fs.writeFileSync(path.join(dir, 'config', 'workspace.x-themes.settings.yml'), 'doc:\n name: x-themes\ntype: code\nthemes:\n - olivero\n');
|
|
221
|
+
fs.writeFileSync(path.join(dir, 'commands', 'cmd-build-theme.sh'), '#!/usr/bin/env bash\n# in __WORKSPACE__\n');
|
|
222
|
+
fs.writeFileSync(path.join(dir, 'index.js'), 'module.exports.register = () => {};');
|
|
223
|
+
const sh = (verb) => spawnSync('bash', [path.join(APP, '..', 'scripts', 'plugins.sh'), verb, 'x-themes'], { env: { ...process.env, ...env(t) }, encoding: 'utf8' });
|
|
224
|
+
const r = spawnSync(process.execPath, ['plugin-install.js', 'install', 'x-themes'], { cwd: APP, env: { ...process.env, ...env(t) }, encoding: 'utf8' });
|
|
225
|
+
assert.strictEqual(r.status, 0, r.stderr);
|
|
226
|
+
assert.ok(fs.existsSync(path.join(t.root, 'x-themes')), 'the folder is made');
|
|
227
|
+
assert.match(fs.readFileSync(path.join(t.config, 'workspace.x-themes.settings.yml'), 'utf8'), /- olivero/);
|
|
228
|
+
assert.match(fs.readFileSync(path.join(t.root, 'x-themes', 'cmd-build-theme.sh'), 'utf8'), /# in x-themes/);
|
|
229
|
+
assert.strictEqual(fs.readFileSync(path.join(t.root, 'x-themes', 'AGENTS.md'), 'utf8'), '# x-themes\n');
|
|
230
|
+
// A person's edits to the settings file and the docs survive a reinstall.
|
|
231
|
+
fs.writeFileSync(path.join(t.config, 'workspace.x-themes.settings.yml'), 'doc:\n name: x-themes\ntype: code\nthemes:\n - mine\n');
|
|
232
|
+
fs.writeFileSync(path.join(t.root, 'x-themes', 'AGENTS.md'), 'edited\n');
|
|
233
|
+
assert.strictEqual(spawnSync(process.execPath, ['plugin-install.js', 'install', 'x-themes'], { cwd: APP, env: { ...process.env, ...env(t) }, encoding: 'utf8' }).status, 0);
|
|
234
|
+
assert.match(fs.readFileSync(path.join(t.config, 'workspace.x-themes.settings.yml'), 'utf8'), /- mine/);
|
|
235
|
+
assert.strictEqual(fs.readFileSync(path.join(t.root, 'x-themes', 'AGENTS.md'), 'utf8'), 'edited\n');
|
|
236
|
+
const a = sh('--activate');
|
|
237
|
+
assert.strictEqual(a.status, 0, a.stderr);
|
|
238
|
+
assert.match(a.stdout, /Workspace x-themes added to the workspaces list/);
|
|
239
|
+
const listed = () => (require('js-yaml').load(fs.readFileSync(path.join(t.config, 'settings.yml'), 'utf8')).workspaces || []);
|
|
240
|
+
assert.deepStrictEqual(listed(), ['dev', 'modules', 'x-themes'], 'appended, in the dashboard order');
|
|
241
|
+
assert.strictEqual(sh('--activate').status, 0);
|
|
242
|
+
assert.deepStrictEqual(listed(), ['dev', 'modules', 'x-themes'], 'activating again does not double it');
|
|
243
|
+
const d = sh('--deactivate');
|
|
244
|
+
assert.strictEqual(d.status, 0, d.stderr);
|
|
245
|
+
assert.deepStrictEqual(listed(), ['dev', 'modules']);
|
|
246
|
+
const u = spawnSync(process.execPath, ['plugin-install.js', 'uninstall', 'x-themes'], { cwd: APP, env: { ...process.env, ...env(t) }, encoding: 'utf8' });
|
|
247
|
+
assert.strictEqual(u.status, 0, u.stderr);
|
|
248
|
+
assert.ok(!fs.existsSync(path.join(t.root, 'x-themes', 'cmd-build-theme.sh')), 'the command is gone');
|
|
249
|
+
assert.ok(fs.existsSync(path.join(t.config, 'workspace.x-themes.settings.yml')), 'the settings file stays: it holds what a person put there');
|
|
250
|
+
assert.ok(fs.existsSync(path.join(t.root, 'x-themes', 'AGENTS.md')), 'the docs stay');
|
|
251
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const { stepOf, needsInstall, STEPS } = require('../../plugins-page');
|
|
6
|
+
const { ROOT } = require('../../workspaces');
|
|
7
|
+
|
|
8
|
+
test('the four steps, in order, and a plugin with nothing to place is installed by being required', () => {
|
|
9
|
+
assert.deepStrictEqual(STEPS, ['required', 'activated', 'installed', 'configured']);
|
|
10
|
+
const bare = { name: 'notes', manifest: { name: 'notes' } };
|
|
11
|
+
assert.strictEqual(stepOf(bare, undefined, undefined), 0, 'not activated');
|
|
12
|
+
assert.strictEqual(stepOf(bare, { enabled: true }, undefined), 3, 'nothing to install, nothing to configure: ready');
|
|
13
|
+
assert.strictEqual(needsInstall({}), false);
|
|
14
|
+
assert.strictEqual(needsInstall({ scripts: { functions: ['f.sh'] } }), true);
|
|
15
|
+
assert.strictEqual(needsInstall({ ddev: { compose: ['c.yaml'] } }), true);
|
|
16
|
+
assert.strictEqual(needsInstall({ settingsTemplate: 'config/x.yml' }), true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('a plugin that ships files waits for the install, then for its settings file to lose CHANGE_ME', () => {
|
|
20
|
+
const p = { name: 'x-cloud', manifest: { name: 'x-cloud', settingsFile: 'plugin.x-cloud.nowhere.settings.yml', settingsTemplate: 'config/t.yml' } };
|
|
21
|
+
assert.strictEqual(stepOf(p, { enabled: 'true' }, { installed: false }), 1, 'activated, not installed');
|
|
22
|
+
assert.strictEqual(stepOf(p, { enabled: true }, { installed: true }), 2, 'installed, no settings file yet');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('the installer records against the workspace root the dashboard knows, not against its own folder', () => {
|
|
26
|
+
const { RECORD } = require('../../plugin-install');
|
|
27
|
+
assert.strictEqual(RECORD, path.join(ROOT, 'core', 'config', 'plugins.installed.yml'));
|
|
28
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin loader: settings decide what loads, namespacing decides what it may touch.
|
|
3
|
+
*
|
|
4
|
+
* A plugin that fails these contracts fails at startup for whoever wrote it — but the loader
|
|
5
|
+
* itself failing would take every plugin's surface off the dashboard at once, so its promises
|
|
6
|
+
* (enabled-only, name rules, namespaced routes, per-workspace scoping) get pinned here.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { test } = require('node:test');
|
|
10
|
+
const assert = require('node:assert');
|
|
11
|
+
|
|
12
|
+
const { pluginActions, pluginPageActionGroups, loadedPlugins } = require('../../plugin-loader');
|
|
13
|
+
const { loadSettings, loadWorkspaces } = require('../../workspaces');
|
|
14
|
+
|
|
15
|
+
const enabled = Object.entries(loadSettings().plugins || {})
|
|
16
|
+
.filter(([, c]) => c && (c.enabled === true || String(c.enabled) === 'true'))
|
|
17
|
+
.map(([name]) => name);
|
|
18
|
+
|
|
19
|
+
test('every enabled plugin with a local folder loads', () => {
|
|
20
|
+
for (const name of enabled) {
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const local = path.join(__dirname, '..', '..', 'plugins', name, 'index.js');
|
|
24
|
+
if (!fs.existsSync(local)) continue; // module: plugins may not be installed here
|
|
25
|
+
assert.ok(loadedPlugins.some((p) => p.name === name), `${name} is enabled but did not load`);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('plugin actions are namespaced under their own name', () => {
|
|
30
|
+
for (const route of Object.keys(pluginActions)) {
|
|
31
|
+
const m = route.match(/^\/actions\/p\/([a-z0-9-]+)\/[a-z0-9-]+$/);
|
|
32
|
+
assert.ok(m, `${route} is not /actions/p/<plugin>/<action>`);
|
|
33
|
+
assert.ok(loadedPlugins.some((p) => p.name === m[1]), `${route} belongs to an unloaded plugin`);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('page action groups render without throwing for every workspace page', () => {
|
|
38
|
+
const workspaces = loadWorkspaces();
|
|
39
|
+
for (const [key, meta] of Object.entries(workspaces)) {
|
|
40
|
+
const groups = pluginPageActionGroups('workspace', key, meta);
|
|
41
|
+
for (const group of groups) {
|
|
42
|
+
assert.ok(group.label, 'a plugin rail group has a label');
|
|
43
|
+
assert.ok(Array.isArray(group.items) && group.items.length, `${group.label} has items`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('workspace-notes respects its workspaces scoping when loaded', () => {
|
|
49
|
+
if (!loadedPlugins.some((p) => p.name === 'workspace-notes')) return;
|
|
50
|
+
const config = (loadSettings().plugins || {})['workspace-notes'] || {};
|
|
51
|
+
const list = String(config.workspaces || '').split(',').map((w) => w.trim()).filter(Boolean);
|
|
52
|
+
const workspaces = loadWorkspaces();
|
|
53
|
+
for (const [key, meta] of Object.entries(workspaces)) {
|
|
54
|
+
const has = pluginPageActionGroups('workspace', key, meta).some((g) => g.label === 'Notes');
|
|
55
|
+
const expected = list.length === 0 || list.includes(key);
|
|
56
|
+
assert.equal(has, expected, `Notes on ${key}: expected ${expected}`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects with something running in them: filter and sort.
|
|
3
|
+
*
|
|
4
|
+
* A workspace page lists dozens of projects and the question asked most is "where is something
|
|
5
|
+
* happening" — which screen has an agent in it, which project somebody is working in. The row's badges
|
|
6
|
+
* answer it one row at a time, and finding the four that matter among thirty was scrolling. The
|
|
7
|
+
* filter and the sort ask the same sources the badges do, once for the whole table.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { test } = require('node:test');
|
|
11
|
+
const assert = require('node:assert');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const { PROJECT_FILTERS, SORTS, defaultSort, readPageState, listControlsHtml } = require('../../lists');
|
|
16
|
+
const { activityFromSessions } = require('../../screens');
|
|
17
|
+
|
|
18
|
+
const server = fs.readFileSync(path.join(__dirname, '../../server.js'), 'utf8');
|
|
19
|
+
const req = (qs) => ({ url: `/fragments/dev/projects?${qs}`, headers: {} });
|
|
20
|
+
|
|
21
|
+
test('the filter offers screens running and agent running, and the query accepts them', () => {
|
|
22
|
+
assert.ok(PROJECT_FILTERS.screens && PROJECT_FILTERS.agents);
|
|
23
|
+
assert.strictEqual(readPageState(req('status=screens')).status, 'screens');
|
|
24
|
+
assert.strictEqual(readPageState(req('status=agents')).status, 'agents');
|
|
25
|
+
// An unknown value falls back to everything rather than showing nothing.
|
|
26
|
+
assert.strictEqual(readPageState(req('status=bogus')).status, 'all');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('the active sort puts agents first, then screens, then the newest', () => {
|
|
30
|
+
const rows = [
|
|
31
|
+
{ name: 'idle-old', mtime: 1, screens: 0, agents: 0 },
|
|
32
|
+
{ name: 'idle-new', mtime: 9, screens: 0, agents: 0 },
|
|
33
|
+
{ name: 'shell', mtime: 2, screens: 1, agents: 0 },
|
|
34
|
+
{ name: 'two-screens', mtime: 3, screens: 2, agents: 0 },
|
|
35
|
+
{ name: 'agent', mtime: 1, screens: 1, agents: 1 },
|
|
36
|
+
];
|
|
37
|
+
const order = rows.slice().sort(SORTS.active.cmp).map((r) => r.name);
|
|
38
|
+
assert.deepStrictEqual(order, ['agent', 'two-screens', 'shell', 'idle-new', 'idle-old']);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('a row with no counts sorts as none, so the sort is harmless on a list that has none', () => {
|
|
42
|
+
const files = [{ name: 'b.md', mtime: 1 }, { name: 'a.md', mtime: 2 }];
|
|
43
|
+
assert.doesNotThrow(() => files.slice().sort(SORTS.active.cmp));
|
|
44
|
+
assert.strictEqual(readPageState(req('sort=active')).sort, 'active');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('the active sort is offered on project lists only', () => {
|
|
48
|
+
const state = { q: '', sort: 'newest', auto: true };
|
|
49
|
+
const plain = listControlsHtml('/fragments/x/items', '#t', state, 'files');
|
|
50
|
+
const projects = listControlsHtml('/fragments/dev/projects', '#t', state, 'projects', null, null, { projectSorts: true });
|
|
51
|
+
assert.ok(!plain.includes('value="active"'), 'a file list must not offer a sort it has no data for');
|
|
52
|
+
assert.ok(projects.includes('value="active"'));
|
|
53
|
+
assert.match(projects, />Active first</);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('activity is counted per project: screens, the agents among them, and those waiting', () => {
|
|
57
|
+
const sessions = [
|
|
58
|
+
{ session: 'dev--a--work', state: '(Detached)' },
|
|
59
|
+
{ session: 'dev--a--agent', state: '(Detached)' },
|
|
60
|
+
{ session: 'dev--a--asks', state: '(Attached)', waiting: true, waitingWhy: 'a question' },
|
|
61
|
+
{ session: 'dev--b--shell', state: '(Detached)' },
|
|
62
|
+
{ session: 'dev--c--gone', state: '(Dead ???)' },
|
|
63
|
+
{ session: 'elsewhere', state: '(Detached)' },
|
|
64
|
+
];
|
|
65
|
+
const owner = { 'dev--a--work': '/w/a', 'dev--a--agent': '/w/a', 'dev--a--asks': '/w/a', 'dev--b--shell': '/w/b', 'dev--c--gone': '/w/c' };
|
|
66
|
+
const got = activityFromSessions(sessions, {
|
|
67
|
+
agents: new Set(['dev--a--agent', 'dev--a--asks']),
|
|
68
|
+
dirOf: (s) => owner[s] || '',
|
|
69
|
+
});
|
|
70
|
+
assert.deepStrictEqual(got.get('/w/a'), { screens: 3, agents: 2, waiting: 1 });
|
|
71
|
+
assert.deepStrictEqual(got.get('/w/b'), { screens: 1, agents: 0, waiting: 0 });
|
|
72
|
+
assert.ok(!got.has('/w/c'), 'a dead session is not running anything');
|
|
73
|
+
assert.strictEqual(got.size, 2, 'a session that belongs to no project here is not counted');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('nothing running gives an empty answer, not a crash', () => {
|
|
77
|
+
assert.strictEqual(activityFromSessions([], { agents: new Set(), dirOf: () => '' }).size, 0);
|
|
78
|
+
assert.strictEqual(activityFromSessions(undefined, { agents: new Set(), dirOf: () => '' }).size, 0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('the project list filters on what is running, and says how many in the menu', () => {
|
|
82
|
+
// includes(), not match(): a failed match would print the whole of server.js as its message.
|
|
83
|
+
assert.ok(server.includes("if (wanted === 'screens') return it.screens > 0;"));
|
|
84
|
+
assert.ok(server.includes("if (wanted === 'agents') return it.agents > 0;"));
|
|
85
|
+
assert.ok(server.includes('{ projectSorts: true }'));
|
|
86
|
+
assert.ok(server.includes("${esc(label)}${n === null ? '' : ` (${n})`}"), 'the counts are in the option labels');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/* ------------------------------------------------------------------ *
|
|
90
|
+
* Sticky: the last search, sort and filter of a workspace's list
|
|
91
|
+
* ------------------------------------------------------------------ */
|
|
92
|
+
|
|
93
|
+
const { setPageCookie } = require('../../lists');
|
|
94
|
+
const cookieOf = (views) => `ws-view=${encodeURIComponent(JSON.stringify(views))}`;
|
|
95
|
+
const reqWith = (qs, cookie) => ({ url: `/fragments/dev/projects?${qs}`, headers: { cookie: cookie || '' } });
|
|
96
|
+
const SAVED = { 'dev/projects': { q: 'webship', sort: 'name', status: 'agents' } };
|
|
97
|
+
|
|
98
|
+
test('a list opens the way it was left: search, sort and filter come from the cookie', () => {
|
|
99
|
+
const st = readPageState(reqWith('', cookieOf(SAVED)), 'dev/projects');
|
|
100
|
+
assert.strictEqual(st.q, 'webship');
|
|
101
|
+
assert.strictEqual(st.sort, 'name');
|
|
102
|
+
assert.strictEqual(st.status, 'agents');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('what the request says wins over what was saved, field by field', () => {
|
|
106
|
+
const st = readPageState(reqWith('status=screens', cookieOf(SAVED)), 'dev/projects');
|
|
107
|
+
assert.strictEqual(st.status, 'screens');
|
|
108
|
+
assert.strictEqual(st.q, 'webship', 'a field the request did not name keeps what was saved');
|
|
109
|
+
const cleared = readPageState(reqWith('q=', cookieOf(SAVED)), 'dev/projects');
|
|
110
|
+
assert.strictEqual(cleared.q, '', 'an empty search typed on purpose is a choice, not an absence');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('it sticks per workspace and per list, and nothing sticks without a list', () => {
|
|
114
|
+
assert.strictEqual(readPageState(reqWith('', cookieOf(SAVED)), 'test/projects').q, '');
|
|
115
|
+
assert.strictEqual(readPageState(reqWith('', cookieOf(SAVED)), 'dev/items').q, '');
|
|
116
|
+
assert.strictEqual(readPageState(reqWith('', cookieOf(SAVED))).q, '', 'every other list is unchanged');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('reset forgets it, and so does the Reset link', () => {
|
|
120
|
+
const st = readPageState(reqWith('reset=1&q=', cookieOf(SAVED)), 'dev/projects');
|
|
121
|
+
assert.deepStrictEqual([st.q, st.sort, st.status], ['', defaultSort(), 'all']);
|
|
122
|
+
assert.deepStrictEqual(st.view.map, {}, 'the entry is removed from the cookie');
|
|
123
|
+
const html = listControlsHtml('/fragments/dev/projects', '#t', { q: '', sort: 'newest', auto: true }, 'projects');
|
|
124
|
+
assert.match(html, /reset=1/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('only a request that chose something is written back; a poll leaves the preference alone', () => {
|
|
128
|
+
assert.strictEqual(readPageState(reqWith('', cookieOf(SAVED)), 'dev/projects').view, null);
|
|
129
|
+
assert.strictEqual(readPageState(reqWith('page=2&per=20', cookieOf(SAVED)), 'dev/projects').view, null);
|
|
130
|
+
const st = readPageState(reqWith('status=screens&sort=name&q=x', ''), 'dev/projects');
|
|
131
|
+
assert.deepStrictEqual(st.view.map['dev/projects'], { q: 'x', sort: 'name', status: 'screens' });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('only what differs from the default is stored, and an all-default list stores nothing', () => {
|
|
135
|
+
const st = readPageState(reqWith(`status=all&q=&sort=${defaultSort()}`, ''), 'dev/projects');
|
|
136
|
+
assert.deepStrictEqual(st.view.map, {}, 'choosing the defaults stores nothing');
|
|
137
|
+
const other = { 'ai-agents/items': { q: 'a' } };
|
|
138
|
+
const next = readPageState(reqWith('status=all&q=', cookieOf(other)), 'dev/projects');
|
|
139
|
+
assert.deepStrictEqual(next.view.map, other, 'another list is left exactly as it was');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('a bad cookie, or a bad value in a good one, falls back to the defaults', () => {
|
|
143
|
+
assert.strictEqual(readPageState(reqWith('', 'ws-view=%7Bnot-json'), 'dev/projects').q, '');
|
|
144
|
+
const hostile = { 'dev/projects': { sort: 'x"><script>', status: 'nope', kind: '../../etc', q: 'ok' } };
|
|
145
|
+
const st = readPageState(reqWith('', cookieOf(hostile)), 'dev/projects');
|
|
146
|
+
assert.strictEqual(st.status, 'all');
|
|
147
|
+
assert.ok(SORTS[st.sort]);
|
|
148
|
+
assert.match(st.kind, /^[a-z-]*$/, 'a kind is a slug whichever way it arrived');
|
|
149
|
+
assert.strictEqual(st.q, 'ok');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('the cookie stays bounded however many lists and however long the text', () => {
|
|
153
|
+
let views = {};
|
|
154
|
+
for (let i = 0; i < 80; i += 1) {
|
|
155
|
+
const st = readPageState(reqWith(`q=${'x'.repeat(190)}`, cookieOf(views)), `w${i}/projects`);
|
|
156
|
+
views = st.view.map;
|
|
157
|
+
}
|
|
158
|
+
assert.ok(Object.keys(views).length <= 40);
|
|
159
|
+
assert.ok(encodeURIComponent(JSON.stringify(views)).length <= 3000);
|
|
160
|
+
assert.ok('w79/projects' in views, 'the newest is the one kept');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('the choice is written to one cookie on the hub domain, and cleared when empty', () => {
|
|
164
|
+
const sent = [];
|
|
165
|
+
const res = { setHeader: (k, v) => sent.push([k, v]) };
|
|
166
|
+
setPageCookie(res, readPageState(reqWith('q=abc', ''), 'dev/projects'));
|
|
167
|
+
const set = sent[0][1].find((c) => c.startsWith('ws-view='));
|
|
168
|
+
assert.match(set, /Domain=\./);
|
|
169
|
+
assert.match(set, /Max-Age=31536000/);
|
|
170
|
+
assert.ok(decodeURIComponent(set.split(';')[0].slice('ws-view='.length)).includes('"q":"abc"'));
|
|
171
|
+
sent.length = 0;
|
|
172
|
+
setPageCookie(res, readPageState(reqWith('reset=1', cookieOf(SAVED)), 'dev/projects'));
|
|
173
|
+
assert.match(sent[0][1].find((c) => c.startsWith('ws-view=')), /Max-Age=0/);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('the routes pass the list, so each workspace remembers its own', () => {
|
|
177
|
+
assert.ok(server.includes('readPageState(req, `${fragMatch[1]}/projects`)'));
|
|
178
|
+
assert.ok(server.includes('readPageState(req, `${itemsMatch[1]}/items`)'));
|
|
179
|
+
assert.ok(server.includes('readPageState(req, sticky)'), 'the page itself opens the way the list was left');
|
|
180
|
+
});
|