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