@webship.co/workspace 0.0.0-stage → 1.0.0-alpha1
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/PLUGINS.md +249 -0
- package/README.md +379 -2
- package/WORKSPACES.md +69 -0
- package/actions/ai-items.js +293 -0
- package/actions/audio.js +48 -0
- package/actions/build-order.js +79 -0
- package/actions/checks.js +95 -0
- package/actions/claude-session.js +104 -0
- package/actions/commands-center.js +258 -0
- package/actions/composer.js +267 -0
- package/actions/ddev-addons.js +90 -0
- package/actions/ddev-mail.js +80 -0
- package/actions/ddev.js +237 -0
- package/actions/folders.js +165 -0
- package/actions/graphs.js +107 -0
- package/actions/images-svg.js +33 -0
- package/actions/images.js +145 -0
- package/actions/navigation.js +190 -0
- package/actions/node.js +222 -0
- package/actions/notes.js +55 -0
- package/actions/obsidian.js +59 -0
- package/actions/plugins.js +36 -0
- package/actions/project-make.js +28 -0
- package/actions/rag.js +202 -0
- package/actions/row-actions.js +262 -0
- package/actions/screens.js +270 -0
- package/actions/testing.js +121 -0
- package/actions/video-edit.js +54 -0
- package/ai-inventory.js +182 -0
- package/ai-items.js +202 -0
- package/assistant-cast.js +176 -0
- package/assistant-suggestions.js +91 -0
- package/assistant.js +484 -0
- package/audio-voice.js +528 -0
- package/auth-google.js +260 -0
- package/build-ai.js +52 -0
- package/build-order-page.js +191 -0
- package/build-order.js +313 -0
- package/builder-args.js +117 -0
- package/changelog.js +302 -0
- package/chat-threads.js +134 -0
- package/checks.js +105 -0
- package/claude-args.js +282 -0
- package/claude-sessions.js +129 -0
- package/command-author.js +117 -0
- package/command-block.js +20 -0
- package/commands/defaults.yml +159 -0
- package/commands-page.js +460 -0
- package/commands.js +568 -0
- package/cucumber.js +43 -0
- package/ddev-addons-page.js +124 -0
- package/ddev-addons.js +154 -0
- package/ddev-custom-commands.js +96 -0
- package/ddev-domains.js +84 -0
- package/ddev-import.js +93 -0
- package/ddev-mail-page.js +167 -0
- package/ddev-mail.js +110 -0
- package/ddev-options.js +145 -0
- package/ddev-snapshots.js +126 -0
- package/ddev.js +145 -0
- package/docs/DEVELOPER-GUIDE.md +224 -0
- package/docs/README.md +252 -0
- package/docs/REFERENCE.md +684 -0
- package/docs/USER-GUIDE.md +697 -0
- package/docs/screenshots/assistant/avatar-board.png +0 -0
- package/docs/screenshots/assistant/avatar-coffee.png +0 -0
- package/docs/screenshots/assistant/avatar-colours.png +0 -0
- package/docs/screenshots/assistant/avatar-icons.png +0 -0
- package/docs/screenshots/assistant/avatar-moves.png +0 -0
- package/docs/screenshots/assistant/avatar-pick-cookieman.png +0 -0
- package/docs/screenshots/assistant/avatar-settings-voices.png +0 -0
- package/docs/screenshots/assistant/avatar-status.png +0 -0
- package/docs/screenshots/create-card/create-card.png +0 -0
- package/docs/screenshots/create-card/start-ai.png +0 -0
- package/docs/screenshots/guide-clone-address.png +0 -0
- package/docs/screenshots/guide-clone-worktree-off.png +0 -0
- package/docs/screenshots/guide-clone-worktree-on.png +0 -0
- package/docs/screenshots/guide-ddev-menu.png +0 -0
- package/docs/screenshots/guide-domains.png +0 -0
- package/docs/screenshots/guide-home.png +0 -0
- package/docs/screenshots/guide-ide-menu.png +0 -0
- package/docs/screenshots/guide-remove-refusal.png +0 -0
- package/docs/screenshots/guide-rows-worktree.png +0 -0
- package/docs/screenshots/guide-screens.png +0 -0
- package/docs/screenshots/guide-settings.png +0 -0
- package/docs/screenshots/guide-workspace-page.png +0 -0
- package/docs/screenshots/home/card-settings.png +0 -0
- package/docs/screenshots/home/drag-reorder.png +0 -0
- package/docs/screenshots/home/edit-card.png +0 -0
- package/docs/screenshots/images/claude-code-image.png +0 -0
- package/docs/screenshots/images/edit-with-ai.png +0 -0
- package/docs/screenshots/images/gallery.png +0 -0
- package/docs/screenshots/images/generate-description.png +0 -0
- package/docs/screenshots/images/generate-plan.png +0 -0
- package/docs/screenshots/images/providers.png +0 -0
- package/docs/screenshots/images/svg-form.png +0 -0
- package/docs/screenshots/images/svg-result.png +0 -0
- package/docs/screenshots/images/upload.png +0 -0
- package/docs/screenshots/new-folder/2-form.png +0 -0
- package/docs/screenshots/new-folder/8-ddev-options.png +0 -0
- package/docs/screenshots/new-folder/files-folder.png +0 -0
- package/docs/screenshots/projects/ddev-custom-commands.png +0 -0
- package/docs/screenshots/projects/state-chip-stop.png +0 -0
- package/docs/screenshots/readme/dashboard-dark.png +0 -0
- package/docs/screenshots/readme/dashboard-light.png +0 -0
- package/docs/screenshots/readme/dev-dark.png +0 -0
- package/docs/screenshots/readme/dev-light.png +0 -0
- package/docs/screenshots/readme/docs-dark.png +0 -0
- package/docs/screenshots/readme/docs-light.png +0 -0
- package/docs/screenshots/readme/icons/ai.svg +4 -0
- package/docs/screenshots/readme/icons/code.svg +4 -0
- package/docs/screenshots/readme/icons/docs.svg +4 -0
- package/docs/screenshots/readme/icons/services.svg +4 -0
- package/docs/screenshots/readme/icons/sites.svg +4 -0
- package/docs/screenshots/readme/test-report-dark.png +0 -0
- package/docs/screenshots/readme/test-report-light.png +0 -0
- package/docs/screenshots/readme/test-scenarios.png +0 -0
- package/docs/screenshots/readme/test-steps.png +0 -0
- package/docs/screenshots/readme/videos-dark.png +0 -0
- package/docs/screenshots/readme/videos-light.png +0 -0
- package/docs/screenshots/screens-menu.png +0 -0
- package/docs/screenshots/screens-new.png +0 -0
- package/docs/screenshots/settings/auth-google-lists.png +0 -0
- package/docs/screenshots/settings/delete-1-row.png +0 -0
- package/docs/screenshots/settings/delete-2-confirm.png +0 -0
- package/docs/screenshots/settings/delete-3-type-name.png +0 -0
- package/docs/screenshots/settings/delete-4-type-yes.png +0 -0
- package/event-log.js +59 -0
- package/files-browser.js +247 -0
- package/folders.js +121 -0
- package/format.js +73 -0
- package/git-info.js +81 -0
- package/github-auth.js +62 -0
- package/graphs.js +104 -0
- package/html.js +10 -0
- package/icons.js +83 -0
- package/images-ai.js +447 -0
- package/images-create.js +226 -0
- package/images-page.js +417 -0
- package/images-records.js +39 -0
- package/images-render.js +36 -0
- package/images-svg-core.js +214 -0
- package/images-svg-sanitize.js +188 -0
- package/images-svg.js +241 -0
- package/images.js +270 -0
- package/jobs.js +430 -0
- package/linger.js +21 -0
- package/lists.js +292 -0
- package/media-info.js +136 -0
- package/navigation-page.js +264 -0
- package/navigation.js +394 -0
- package/notes.js +0 -0
- package/obsidian.js +405 -0
- package/package.json +47 -4
- package/palette-index.js +46 -0
- package/playwright.config.js +23 -0
- package/plugin-install.js +239 -0
- package/plugin-loader.js +448 -0
- package/plugin-manifest.js +83 -0
- package/plugin-settings.js +44 -0
- package/plugins/workspace-notes/index.js +59 -0
- package/plugins-page.js +103 -0
- package/project-make.js +241 -0
- package/project-name.js +210 -0
- package/public/assistant-avatar-moves.js +646 -0
- package/public/assistant-avatar-ui.js +1223 -0
- package/public/assistant-avatar.js +530 -0
- package/public/avatar/druplicon.vrm +0 -0
- package/public/css/base.css +260 -0
- package/public/css/components/ai-assistant.css +263 -0
- package/public/css/components/assistant-avatar.css +279 -0
- package/public/css/components/assistant-views.css +198 -0
- package/public/css/components/build-order.css +74 -0
- package/public/css/components/clone-as-a-git-worktree.css +30 -0
- package/public/css/components/command-palette.css +61 -0
- package/public/css/components/commands-center.css +139 -0
- package/public/css/components/confirm-dialog.css +40 -0
- package/public/css/components/create-card.css +8 -0
- package/public/css/components/ddev-menu.css +23 -0
- package/public/css/components/deep-chat.css +17 -0
- package/public/css/components/density.css +11 -0
- package/public/css/components/floating-assistant-workspace-pages.css +55 -0
- package/public/css/components/folders.css +5 -0
- package/public/css/components/full-height-left-assistant-sidebar-all-pages.css +39 -0
- package/public/css/components/full-height-right-actions-rail-pages-that-have-actions.css +267 -0
- package/public/css/components/grid-and-list.css +203 -0
- package/public/css/components/help-page.css +36 -0
- package/public/css/components/hero.css +16 -0
- package/public/css/components/images.css +70 -0
- package/public/css/components/layout-rhythm-and-spacing-polish.css +139 -0
- package/public/css/components/mic-on-any-field-that-wants-typing.css +416 -0
- package/public/css/components/modal-fit.css +113 -0
- package/public/css/components/page-width.css +23 -0
- package/public/css/components/plugins-page.css +9 -0
- package/public/css/components/project-cards.css +256 -0
- package/public/css/components/project-rows.css +390 -0
- package/public/css/components/reordering-the-workspace-cards.css +85 -0
- package/public/css/components/responsive-home-and-the-workspace-pages.css +457 -0
- package/public/css/components/running-commands-as-toasts.css +327 -0
- package/public/css/components/settings-editor-core-config-yml.css +828 -0
- package/public/css/components/short-results-as-a-toast-at-the-top.css +203 -0
- package/public/css/components/sound-player.css +84 -0
- package/public/css/components/suggest-agents-skills-commands.css +112 -0
- package/public/css/components/tab-modal.css +142 -0
- package/public/css/components/terminals.css +244 -0
- package/public/css/components/test-report-media-one-file-per-row-paged.css +144 -0
- package/public/css/components/top-toolbar-logo-breadcrumb.css +107 -0
- package/public/css/components/video-edit.css +6 -0
- package/public/css/components/web-ide-open-with.css +23 -0
- package/public/css/components/working-indicators.css +158 -0
- package/public/css/components/workspace-cards.css +79 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/public/htmx.min.js +1 -0
- package/public/logo.png +0 -0
- package/public/media-player.js +215 -0
- package/public/mic.js +255 -0
- package/public/narration.js +110 -0
- package/public/palette.js +278 -0
- package/public/sortable.js +251 -0
- package/public/suggest.js +337 -0
- package/public/terminal.js +903 -0
- package/public/themes/default/theme.css +267 -0
- package/public/themes/ember/theme.css +180 -0
- package/public/themes/press/theme.css +290 -0
- package/public/themes/slate/theme.css +182 -0
- package/public/touch-icon.png +0 -0
- package/public/ui.js +2721 -0
- package/public/vendor/ace/LICENSE.txt +24 -0
- package/public/vendor/ace/ace.js +23 -0
- package/public/vendor/ace/ext-language_tools.js +8 -0
- package/public/vendor/ace/ext-searchbox.js +8 -0
- package/public/vendor/ace/mode-css.js +8 -0
- package/public/vendor/ace/mode-html.js +8 -0
- package/public/vendor/ace/mode-javascript.js +8 -0
- package/public/vendor/ace/mode-markdown.js +8 -0
- package/public/vendor/ace/mode-sh.js +8 -0
- package/public/vendor/ace/mode-yaml.js +8 -0
- package/public/vendor/ace/theme-textmate.js +8 -0
- package/public/vendor/ace/theme-tomorrow_night.js +8 -0
- package/public/vendor/ace/worker-yaml.js +1 -0
- package/public/vendor/deep-chat.LICENSE +21 -0
- package/public/vendor/deep-chat.bundle.js +1 -0
- package/public/vendor/fonts/dm-serif-text-LICENSE.txt +93 -0
- package/public/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
- package/public/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/figtree-LICENSE.txt +93 -0
- package/public/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
- package/public/vendor/fonts/jetbrains-mono-LICENSE.txt +93 -0
- package/public/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/ubuntu-mono-LICENSE.txt +98 -0
- package/public/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
- package/public/vendor/tabler-LICENSE.txt +21 -0
- package/public/vendor/tabler-sprite.svg +1 -0
- package/public/vendor/three/LICENSE +21 -0
- package/public/vendor/three/addons/loaders/GLTFLoader.js +4860 -0
- package/public/vendor/three/addons/utils/BufferGeometryUtils.js +1501 -0
- package/public/vendor/three/addons/utils/SkeletonUtils.js +496 -0
- package/public/vendor/three/three.core.min.js +6 -0
- package/public/vendor/three/three.module.min.js +6 -0
- package/public/vendor/three-vrm/LICENSE +22 -0
- package/public/vendor/three-vrm/three-vrm.module.min.js +983 -0
- package/public/vendor/uikit-icons.min.js +1 -0
- package/public/vendor/uikit.min.css +1 -0
- package/public/vendor/uikit.min.js +1 -0
- package/public/vendor/webship-icons/ui/bookmark-outlined.svg +1 -0
- package/public/vendor/webship-icons/ui/headset.svg +1 -0
- package/public/vendor/webship-icons/ui/menu.svg +1 -0
- package/public/vendor/webship-icons/ui/pause.svg +1 -0
- package/public/vendor/webship-icons/ui/play.svg +1 -0
- package/public/vendor/webship-icons/ui/share.svg +1 -0
- package/public/vendor/webship-icons/ui/sound-off.svg +1 -0
- package/public/vendor/webship-icons/ui/sound-on.svg +1 -0
- package/public/vendor/webship-icons/ui/stop.svg +1 -0
- package/public/vendor/xterm/addon-fit.js +2 -0
- package/public/vendor/xterm/addon-unicode11.js +2 -0
- package/public/vendor/xterm/addon-web-links.js +2 -0
- package/public/vendor/xterm/xterm.css +218 -0
- package/public/vendor/xterm/xterm.js +2 -0
- package/rag.js +169 -0
- package/reader.js +132 -0
- package/repo-blobs.js +66 -0
- package/report-pdf.js +337 -0
- package/row-layout.js +1440 -0
- package/screenrc +37 -0
- package/screens.js +1063 -0
- package/scripts/code-server-start.sh +190 -0
- package/scripts/images-claude-finish.js +107 -0
- package/scripts/screen-host.sh +114 -0
- package/scripts/screen-runner.js +362 -0
- package/scripts/screen-then.js +47 -0
- package/scripts/update.sh +350 -0
- package/server.js +8140 -0
- package/settings-file.js +423 -0
- package/speech.js +297 -0
- package/tab-modal.js +75 -0
- package/test-reports.js +260 -0
- package/themes.js +381 -0
- package/transcripts.js +168 -0
- package/urls.js +13 -0
- package/validate.js +18 -0
- package/version-check.js +94 -0
- package/video-edit.js +588 -0
- package/waiting.js +85 -0
- package/workspace-card-tools.js +41 -0
- package/workspace-delete.js +139 -0
- package/workspace-types.js +85 -0
- package/workspaces.js +557 -0
package/PLUGINS.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Dashboard plugins
|
|
2
|
+
|
|
3
|
+
Optional features, each a package of its own, turned on in four steps. The lean core is the
|
|
4
|
+
workspaces, the builders, the settings and the assistant shell; everything else — Upsun, Jira, the
|
|
5
|
+
Web IDE, git, tests, graphs, RAG, media, avatars, designs — is a plugin in its own repository
|
|
6
|
+
(`github.com/webship/workspace-<name>`), required with npm when a machine needs it.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
bash core/scripts/plugins.sh --require github:webship/workspace-upsun # 1. require: npm install into the dashboard
|
|
10
|
+
bash core/scripts/plugins.sh --activate upsun # 2. activate: plugins.upsun in settings.yml
|
|
11
|
+
bash core/scripts/plugins.sh --install upsun # 3. install: place what the manifest names
|
|
12
|
+
bash core/scripts/plugins.sh --configure upsun # 4. configure: its own settings file
|
|
13
|
+
bash core/scripts/plugins.sh --list # where each plugin is
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The same four steps are buttons on **Settings → Plugins**. `--install` is idempotent, records what
|
|
17
|
+
it placed in `core/config/plugins.installed.yml`, keeps a managed block in `.gitignore` so a dev
|
|
18
|
+
clone never commits what an install placed, and says when the dashboard container needs
|
|
19
|
+
`ddev restart` (a compose file or host mount the plugin brought). `--uninstall` removes exactly
|
|
20
|
+
what `--install` placed.
|
|
21
|
+
|
|
22
|
+
A plugin is a node package with a `workspacePlugin` manifest in its `package.json` and an
|
|
23
|
+
`index.js` exporting `register(api)` — or a folder under `core/workspace-app/plugins/<name>/` with
|
|
24
|
+
the same shape, which is how one is developed before it gets a repository.
|
|
25
|
+
|
|
26
|
+
## The manifest
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
"workspacePlugin": {
|
|
30
|
+
"name": "upsun", // its settings key and route namespace
|
|
31
|
+
"label": "Upsun",
|
|
32
|
+
"settingsFile": "plugin.upsun.settings.yml", // core/config/<file>, created from…
|
|
33
|
+
"settingsTemplate": "config/plugin.upsun.settings.yml",
|
|
34
|
+
"legacySettingsKey": "upsun", // the settings.yml block it replaces: its values carry over
|
|
35
|
+
"labels": "config/settings.labels.yml", // labels for its settings keys, merged into the Settings page
|
|
36
|
+
"typeActions": { "upsun": ["sites"] }, // actions workspace types may allow
|
|
37
|
+
"commands": { "types": ["sites"], "workspaces": ["dev", "test"], "files": ["commands/cmd-tools-upsun.sh"] },
|
|
38
|
+
"scripts": { "functions": ["scripts/functions/fun-upsun.sh"], "args": ["scripts/args/arg-upsun.sh"],
|
|
39
|
+
"machine": ["scripts/upsun-ai.sh"], "hostInstall": "scripts/host-install.sh" },
|
|
40
|
+
"ddev": { "compose": ["ddev/docker-compose.upsun-cli.yaml"], "hostMounts": ["~/.upsun-cli"] },
|
|
41
|
+
"themes": ["config/theme.upsun.yml"],
|
|
42
|
+
"css": ["public/upsun.css"]
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Where each piece lands: functions, args and machine scripts in `core/scripts/plugins/<name>/`
|
|
47
|
+
(bootstrap sources every `functions/*.sh` there, so a `cmd-*.sh` can call them; an args file is
|
|
48
|
+
sourced as `${WORKSPACE_SCRIPTS}/plugins/<name>/args/<file>`); commands in every workspace the
|
|
49
|
+
manifest names (or every one of its types), with `__WORKSPACE__` replaced by the folder's name;
|
|
50
|
+
compose files as `core/workspace-app/.ddev/docker-compose.plugin-<name>-<file>`; host mounts in a
|
|
51
|
+
`host-mounts.txt` that `host-mounts.sh` reads; themes in `core/config/`. `hostInstall` runs with
|
|
52
|
+
`--check` after an install and in full from `install.sh`.
|
|
53
|
+
|
|
54
|
+
Everything stays HTMX + UIKit: a plugin serves server-rendered fragments and wires them with
|
|
55
|
+
`hx-get`/`hx-post`; no client frameworks.
|
|
56
|
+
|
|
57
|
+
## A plugin is a node module
|
|
58
|
+
|
|
59
|
+
A plugin can be a folder under `plugins/<name>/` or any npm package. A package is the way to share one: name
|
|
60
|
+
it with `module:` and the loader requires it by name, **before** looking for a folder (the folder is the fallback
|
|
61
|
+
when the package is not installed yet). To ship one inside this repo, keep it in `plugins/<name>/` with its own
|
|
62
|
+
`package.json` and link it from the dashboard's:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
"dependencies": { "@webship.co/workspace-google-drive": "file:plugins/google-drive-sync" }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
plugins:
|
|
70
|
+
google-drive-sync:
|
|
71
|
+
enabled: true
|
|
72
|
+
module: "@webship.co/workspace-google-drive"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`npm install` links it into `node_modules`. `plugins/google-drive-sync` is the worked example: `index.js`
|
|
76
|
+
(`register(api)`), `lib/`, its own `tests/` (picked up by `npm run test:unit`), and a rule its tests enforce:
|
|
77
|
+
**require nothing outside the package**. Anything the command line or an MCP server of yours also runs must work
|
|
78
|
+
without the dashboard, so reach the dashboard only through `api`.
|
|
79
|
+
|
|
80
|
+
## A plugin's own settings file
|
|
81
|
+
|
|
82
|
+
Each plugin keeps its settings in `core/config/plugin.<name>.settings.yml`, not in `settings.yml`: edited,
|
|
83
|
+
backed up and protected separately (the file is created mode 600, and `client_secret`, `token` and similar keys
|
|
84
|
+
are masked on the Settings page like any secret), and gone when the plugin is. `sync-tooling` never copies it.
|
|
85
|
+
A plugin reads and writes it through `api.settings` and keeps a shipped template of it in `core/config/`.
|
|
86
|
+
|
|
87
|
+
## Turn one on
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
# core/config/settings.yml
|
|
91
|
+
plugins:
|
|
92
|
+
workspace-notes:
|
|
93
|
+
enabled: true
|
|
94
|
+
workspaces: "dev,test" # empty or absent = every workspace
|
|
95
|
+
my-npm-plugin:
|
|
96
|
+
enabled: true
|
|
97
|
+
module: "@webship/my-npm-plugin"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Restart the app (`docker exec ddev-workspace-web supervisorctl restart webextradaemons:workspace-app`) to load
|
|
101
|
+
changes — plugins are read once at startup. A plugin that fails to load is reported on the
|
|
102
|
+
console and skipped; it cannot take the dashboard down.
|
|
103
|
+
|
|
104
|
+
## Write one
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
// core/workspace-app/plugins/hello/index.js
|
|
108
|
+
module.exports.register = (api) => {
|
|
109
|
+
const { esc, workspaceDir, isValidWorkspace } = api.lib;
|
|
110
|
+
|
|
111
|
+
// A rail button on every workspace page this plugin applies to.
|
|
112
|
+
api.addPageActions(({ view, key, meta }) => {
|
|
113
|
+
if (view !== 'workspace' || !key || !api.appliesTo(key)) return null;
|
|
114
|
+
return { label: 'Hello', items: [
|
|
115
|
+
`<button class="uk-button uk-button-default action-btn"
|
|
116
|
+
hx-get="/fragments/p/${api.name}/greet?workspace=${esc(key)}"
|
|
117
|
+
hx-target="#webship-workspace-output" hx-swap="innerHTML">
|
|
118
|
+
<span uk-icon="icon: happy; ratio: .8"></span><span class="action-label">Hello</span></button>`,
|
|
119
|
+
] };
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// GET /fragments/p/hello/<rest>
|
|
123
|
+
api.addFragment(({ send, rest, searchParams }) => {
|
|
124
|
+
send(`<div class="msg assistant">Hello from ${esc(searchParams.get('workspace') || '?')}.</div>`);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// POST /actions/p/hello/<action> — context is the same one built-in actions get.
|
|
128
|
+
api.addAction('save', ({ form, send, workspace }) => {
|
|
129
|
+
send('<div class="msg assistant">Saved.</div>');
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## The api
|
|
135
|
+
|
|
136
|
+
| Member | What it is |
|
|
137
|
+
| --- | --- |
|
|
138
|
+
| `api.name` | The plugin's settings key. Also its route namespace. |
|
|
139
|
+
| `api.config` | Its `plugins.<name>` block from settings.yml, verbatim. |
|
|
140
|
+
| `api.appliesTo(key)` | Whether `config.workspaces` includes that workspace (empty list = all). |
|
|
141
|
+
| `api.addAction(name, handler)` | POST handler at `/actions/p/<plugin>/<name>`. Context: `{ form, res, send, workspace, pathname }` — identical to built-in actions. |
|
|
142
|
+
| `api.addFragment(handler)` | GET handler for `/fragments/p/<plugin>/<rest>`. Context: `{ req, res, send, rest, searchParams }`. |
|
|
143
|
+
| `api.addPageActions(fn)` | `fn({ view, key, meta })` returns a rail group `{ label, items: [html] }` or null. Runs on every page render. |
|
|
144
|
+
| `api.addProjectActions(fn)` | `fn({ key, project, dir, meta })` returns HTML appended to that project row's actions (or null). Runs per row on `projects`-kind pages. |
|
|
145
|
+
| `api.addRowMenu(slot, { label, row }, fn)` | A named menu on every project row, in a slot the Buttons settings arrange like a built-in one. `fn({ key, project, dir, meta, allowed, vals })` returns the menu's HTML, or null for a row it has nothing for. |
|
|
146
|
+
| `api.addPage(re, handler)` | A whole page: GET requests whose path matches `re`. `handler({ req, res, match, send })` writes the response itself. |
|
|
147
|
+
| `api.addProjectChip(fn)` | A chip in the project card's meta line. `fn({ key, project, dir, meta })` returns HTML or null. |
|
|
148
|
+
| `api.addFolderSetup({ html, steps, prompt, problem })` | A section of the New-folder form (`html(ws, { allowed })`), the shell steps it adds (`steps(ws, name, form, ctx)` → array), the lines it adds to the Claude Code prompt (`prompt(…)` → array) and its validation (`problem(ws, form, ctx)` → '' or why). |
|
|
149
|
+
| `api.addTypeAction(action, types)` | What the manifest's `typeActions` does, from code. |
|
|
150
|
+
| `api.addCss(file)` | A stylesheet appended after the components (what the manifest's `css` does). |
|
|
151
|
+
| `api.addLabels(map)` | Labels for its settings keys (what the manifest's `labels` does). |
|
|
152
|
+
| `api.addProjectBadge(fn)` | A badge in the project card's corner, sent with every screens poll so it clears itself. `fn({ key, project, dir, sessions })` returns HTML or null; `sessions` is the project's live screens. |
|
|
153
|
+
| `api.addSettingsOptions(file, dotted, fn)` | Live options for a field of a settings file (what a CLI can see rather than a hand-typed key). `fn()` is async and returns `[value, label][]`, or null for a plain input. |
|
|
154
|
+
| `api.addScreenStartHook(fn)` | Called after a screen session started for a work item (the New screen form's `work` key): `fn({ session, workKey, workspace, project, dir, form })` notes the run, posts if asked, returns HTML or ''. |
|
|
155
|
+
| `api.addWorkKeyLink(fn)` | Where a work-item key found in a project's name links to: `fn(key)` returns a URL or null (the chip is plain text without one). |
|
|
156
|
+
| `api.addWorkPrompt(fn)` | The prompt a screen started for a work item begins with: `fn({ key, workspace, project })` returns it, or null for the dashboard's own. |
|
|
157
|
+
| `api.addSessionLog(fn)` | Where a session's log lives when the dashboard does not know: `fn(session, dirs)` returns a file in one of `dirs`, or ''. |
|
|
158
|
+
| `api.addAssistantContext(fn)` | A paragraph of the assistant's system context: what this plugin lets it reach, and the rules. |
|
|
159
|
+
|
|
160
|
+
`api.lib` also lends `tabModalHtml`, `hubDomain()`, `listScreens()`, `agentSessions()`, `screenLogFor()`, `claudeSessionsFor()` and
|
|
161
|
+
`screensCached()` (the last `screen -ls`, for a renderer that cannot wait). The types of all of it are
|
|
162
|
+
`@webship.co/workspace-plugin-api`; a plugin written in TypeScript is checked against them.
|
|
163
|
+
|
|
164
|
+
## TypeScript
|
|
165
|
+
|
|
166
|
+
A plugin package is TypeScript: `src/` compiled to `dist/` by `tsc` on install (`prepare`), so the dashboard
|
|
167
|
+
requires `dist/src/index.js` and a clone of the repository builds itself. Its tests are TypeScript too
|
|
168
|
+
and run compiled (`npm test`); `@webship.co/workspace-upsun` is the shape to copy.
|
|
169
|
+
| `api.settings` | This plugin's own settings file: `file`, `read()`, `set(dotted, value)` and `setList(dotted, items)`, which change one key or list in place, comments kept, creating the file when it is missing. |
|
|
170
|
+
| `api.lib` | Shared helpers: `esc`, `humanSize`, `humanTime`, `ROOT`, `loadWorkspaces`, `workspaceDir`, `isValidWorkspace`, `startJob`, `jobFragment`, `listProjects`, `NAME_RE`, `wsUrl` — `screenAction` (start a command as a Screen session, the way every long action does), `auth` (`sessionOf(req)`, `googleButton`, `BUTTON_CSS` from the dashboard's Google sign-in), plus the type layer: `TYPES`, `workspaceType(key)`, `typeAllows(key, action)`. |
|
|
171
|
+
|
|
172
|
+
Namespacing is the safety model: a plugin's routes all live under its own name, so it can never
|
|
173
|
+
shadow a built-in, and disabling it removes its whole surface at once.
|
|
174
|
+
|
|
175
|
+
## Useful targets
|
|
176
|
+
|
|
177
|
+
- `#webship-workspace-output` — the page's output area (results, job toasts land near it).
|
|
178
|
+
- `#editor-modal-body` — the editor modal; anything swapped in opens it automatically.
|
|
179
|
+
- `api.lib.startJob(title, cmd, args, cwd, opts)` + `jobFragment(id).html` — run a command as a
|
|
180
|
+
live job toast, the way every built-in long action does.
|
|
181
|
+
|
|
182
|
+
## Types and plugins
|
|
183
|
+
|
|
184
|
+
Workspace folders have a **type** (`sites`, `code`, `files`, `media`, `ai-items` — see
|
|
185
|
+
`WORKSPACES.md`) that decides which built-in actions their project rows carry. A plugin can read
|
|
186
|
+
the same layer to offer itself only where it makes sense:
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
api.addProjectActions(({ key, project, meta }) => {
|
|
190
|
+
if (!api.appliesTo(key) || api.lib.workspaceType(key) !== 'sites') return null;
|
|
191
|
+
return `<button class="uk-button uk-button-default uk-button-small"
|
|
192
|
+
hx-post="/actions/p/${api.name}/ping" hx-vals='{"workspace":"${api.lib.esc(key)}","projectName":"${api.lib.esc(project)}"}'
|
|
193
|
+
hx-target="#webship-workspace-output" hx-swap="innerHTML">
|
|
194
|
+
<span uk-icon="icon: bolt; ratio: .7"></span> Ping</button>`;
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## A workspace as a plugin
|
|
199
|
+
|
|
200
|
+
A workspace folder (modules, themes, recipes, dev, …) can itself be a plugin: the package carries
|
|
201
|
+
the folder's `workspace.<key>.settings.yml` template, its `cmd-*.sh` commands and its docs, and the
|
|
202
|
+
manifest names the folder:
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
"workspacePlugin": {
|
|
206
|
+
"name": "drupal-themes",
|
|
207
|
+
"workspace": { "key": "themes", "settingsTemplate": "config/workspace.themes.settings.yml",
|
|
208
|
+
"files": ["commands/cmd-build-theme.sh"], "docs": ["docs/AGENTS.md"] }
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`--install` makes the folder, places the settings file once (never overwritten: it holds this
|
|
213
|
+
machine's lists) and the commands (`__WORKSPACE__` filled with the key), and writes the docs once.
|
|
214
|
+
`--activate` puts the key on settings.yml's `workspaces:` list, at the end (the dashboard's order;
|
|
215
|
+
move it in Settings → Workspaces); `--deactivate` takes it off, and the folder, its content and its
|
|
216
|
+
settings stay. `--uninstall` removes the commands and keeps the settings file and the docs. The bash
|
|
217
|
+
functions the commands call still live in `core/scripts/functions` (and in `scripts.functions` of
|
|
218
|
+
the plugin, placed under `core/scripts/plugins/<name>/functions/`).
|
|
219
|
+
|
|
220
|
+
## Hooks a feature plugin reaches for
|
|
221
|
+
|
|
222
|
+
Besides the row menus, pages, fragments and actions, the loader offers: `addForge(host, …)` (the
|
|
223
|
+
credential the dashboard uses for a git host: `github-auth.js` asks it first), `addOpenWith(fn)` (the
|
|
224
|
+
Open-with menu for a folder: project rows, the files pages, the images gallery) and `addFolderLink(fn)`
|
|
225
|
+
(a link that opens a folder elsewhere, inside the COMPOSER menu). `api.lib` also hands a plugin what
|
|
226
|
+
git says about a folder (`hasOwnGit`, `gitWorktreeInfo`, `projectHasGit`, `gitBranchOf`), the
|
|
227
|
+
lingering-session tail (`lingerTail`, `LINGER_MARK`), `tildePath` and `hostRunnerRequest`.
|
|
228
|
+
|
|
229
|
+
## Families
|
|
230
|
+
|
|
231
|
+
Plugins that do the same job for different services share a base package, so a provider is thin and
|
|
232
|
+
the surface is one. A base is an ordinary private npm package the provider depends on; it is not a
|
|
233
|
+
plugin itself (no manifest) and the dashboard never sees it.
|
|
234
|
+
|
|
235
|
+
| Family | Base | What the base draws | A provider supplies |
|
|
236
|
+
| --- | --- | --- | --- |
|
|
237
|
+
| Hosting (`upsun`, `acquia`, `digitalocean`, `cpanel`, `aws`, `azure`, `godaddy`) | `@webship.co/workspace-hosting` — `registerHosting(api, provider)` | the per-provider row menu (linked / unlinked), the live-site chip, the environments modal and list, the get-database-and-files flow, the branch-and-data block, the tool verbs as screen sessions, the `/<id>-site/<ws>/<project>` jump, the New-folder section, the family stylesheet | a `HostingProvider`: how a project is detected as linked, the CLI and its token, the environments and their URLs, the verbs its `cmd-tools-<id>.sh` carries (status, activities, logs, deploy…), the pull flags, the console URL |
|
|
238
|
+
|
|
239
|
+
| Forges (`github`, `gitlab`, `bitbucket`) | `@webship.co/workspace-forge` — `registerForge(api, forge)` | the credential the dashboard uses for the host (`api.addForge`, which `github-auth.js` asks before the legacy `git.github` block), the row menu on every project whose git remote is at the host (repository, open issues and pull or merge requests, open one from the branch in a terminal, clone URL), the repository chip, the New-folder clone option | a `ForgeProvider`: the host, the CLI, how a remote URL reads, the pages, the issue and request lists, the clone URL |
|
|
240
|
+
| Container runtimes (`docker`, `lando`, `docksal`, `podman`, `kubernetes`) | `@webship.co/workspace-container` — `registerContainer(api, runtime)` | the row menu on every project the runtime recognises (its verbs as terminal sessions in the project, grouped, with the site link), the up/down caption answered after the row is drawn, the New-folder init option | a `ContainerRuntime`: how a folder is detected as a project, the CLI, how to tell whether it is up, the verbs and their commands |
|
|
241
|
+
|
|
242
|
+
`@webship.co/workspace-upsun` 2.0.0 is the reference provider: `register(api)` is one line, the facts are
|
|
243
|
+
in `src/lib/provider.ts`, and the bash tool, compose file, host install, settings template and theme
|
|
244
|
+
are what the installer places.
|
|
245
|
+
|
|
246
|
+
## The reference plugin
|
|
247
|
+
|
|
248
|
+
`plugins/workspace-notes/` is the working example: a Notes rail button per enabled workspace,
|
|
249
|
+
editing `NOTES.md` in the workspace folder through the modal, saved by a namespaced action.
|
package/README.md
CHANGED
|
@@ -1,3 +1,380 @@
|
|
|
1
|
-
#
|
|
1
|
+
# workspace-app
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The **workspace** web dashboard — a visual UI + AI assistant for this tooling,
|
|
4
|
+
served at **https://workspace.ddev.site** through DDEV.
|
|
5
|
+
|
|
6
|
+
Basic Node.js (`node:http`, no framework) + [HTMX](https://htmx.org) (vendored
|
|
7
|
+
in `public/htmx.min.js`). All pages are server-rendered; interactions swap in
|
|
8
|
+
HTML fragments. Destructive actions ask in one shared confirm dialog — Wait!
|
|
9
|
+
Are you sure you want to proceed with this action? — and fire on its Yes alone.
|
|
10
|
+
|
|
11
|
+
## What version is this machine on?
|
|
12
|
+
|
|
13
|
+
The toolbar says, beside the product name, and `core/workspace-app/scripts/update.sh --check`
|
|
14
|
+
says the same thing. Both read **`core/workspace-app/.app-version`**, a file holding a tag and
|
|
15
|
+
nothing else.
|
|
16
|
+
|
|
17
|
+
It is written by all three things that install this tree, which is the point:
|
|
18
|
+
|
|
19
|
+
| | writes the stamp |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `core/scripts/install.sh` | the latest release tag, read from the repository |
|
|
22
|
+
| `core/scripts/sync-tooling.sh --pull` | the tag the clone is sitting on |
|
|
23
|
+
| `core/workspace-app/scripts/update.sh` | the tag it just installed |
|
|
24
|
+
|
|
25
|
+
Before that only the **updater** wrote it, so a machine installed any other way answered
|
|
26
|
+
`Installed: (untracked)` and was told an update was available to the version it already had —
|
|
27
|
+
for ever. With one person that is a curiosity; across a team it is the support question nobody
|
|
28
|
+
can answer, and an update prompt everyone learns to ignore.
|
|
29
|
+
|
|
30
|
+
**When it cannot be told, it says so** — `version unknown` in the toolbar, with the three
|
|
31
|
+
commands that would set it in the tooltip. A wrong version is worse than an absent one, because
|
|
32
|
+
it stops the question being asked.
|
|
33
|
+
|
|
34
|
+
`package.json`'s `"version"` is **not** it. That is npm metadata for an app that is never
|
|
35
|
+
published, it has said `1.0.0` since the first commit, and a unit test fails if anything starts
|
|
36
|
+
displaying it.
|
|
37
|
+
|
|
38
|
+
## One `ddev start` at a time
|
|
39
|
+
|
|
40
|
+
Every router-mutating verb — `start`, `stop`, `restart`, `poweroff`, `delete` — tears down the
|
|
41
|
+
**one shared** `ddev-router` and rebuilds it with the routes of whatever is running. Two at once
|
|
42
|
+
race on a single container, and the loser can leave it removed: every `*.ddev.site` address dies,
|
|
43
|
+
the hub included, with healthy web containers behind it, and DDEV then refuses to start anything
|
|
44
|
+
until `ddev poweroff`. Five clicks in thirty-eight seconds did exactly that.
|
|
45
|
+
|
|
46
|
+
`core/scripts/ddev-shim/ddev` is a wrapper that holds a lock for those five verbs and `exec`s the
|
|
47
|
+
real binary for everything else. It is **first on PATH inside the dashboard's container**
|
|
48
|
+
(`/usr/local/sbin` precedes `/usr/local/bin`), so it covers the assistant's Bash calls, the
|
|
49
|
+
`cmd-*.sh` scripts, the Web IDE's terminals and anyone in `ddev ssh` — not just the buttons.
|
|
50
|
+
`install.sh` offers the same file on the host, where it needs root.
|
|
51
|
+
|
|
52
|
+
Four rules it lives by, because a wrong wrapper breaks *every* `ddev` call including the
|
|
53
|
+
dashboard's own twenty-second status reads:
|
|
54
|
+
|
|
55
|
+
1. the real binary is found by **absolute path** and checked not to be the wrapper — resolving
|
|
56
|
+
through PATH would find the wrapper again and fork for ever;
|
|
57
|
+
2. anything that is not a router verb is `exec`'d with **no lock and no subshell** —
|
|
58
|
+
`ddev list --json-output` costs what it always cost, and read-only verbs still run in parallel;
|
|
59
|
+
3. **nothing is fatal** — a missing `flock`, an uncreatable lock file, and it says so once and runs
|
|
60
|
+
the command anyway, because refusing to run `ddev` is worse than the fault being prevented;
|
|
61
|
+
4. it **never re-locks a lock already held**. The dashboard wraps its own calls, and taking the same
|
|
62
|
+
lock from inside deadlocks for the full 1800s timeout — proven by experiment before
|
|
63
|
+
`DDEV_ROUTER_LOCK_HELD` existed. Whoever holds the lock sets it; the wrapper exports it onward.
|
|
64
|
+
|
|
65
|
+
The shim exists twice, because Docker cannot `COPY` from outside its build context:
|
|
66
|
+
`core/scripts/ddev-shim/ddev` is the source, `.ddev/web-build/ddev-shim-ddev` is the copy the image
|
|
67
|
+
takes, and a unit test fails if they differ.
|
|
68
|
+
|
|
69
|
+
## The Web IDE (code-server)
|
|
70
|
+
|
|
71
|
+
Every project row carries a **Codebase** menu, right after SCREENS, and every document
|
|
72
|
+
workspace (`docs`, `worklogs`, `specs`, the `ai-*` folders) carries a **Files** one. Both offer
|
|
73
|
+
the same three destinations, and the split is the point — it is GitLab's, and it is what stops
|
|
74
|
+
a link that silently does nothing:
|
|
75
|
+
|
|
76
|
+
| | opens | needs |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| **Web IDE** | VS Code in the browser, at `https://ide.<hub_domain>/?folder=<path>` | nothing — works from any device |
|
|
79
|
+
| **Visual Studio Code** | `vscode://file<path>` — the VS Code on the machine the *browser* is running on | the `vscode://` handler, which VS Code registers on install |
|
|
80
|
+
| **IntelliJ IDEA** | `jetbrains://idea/open?file=<path>` — likewise, IDEA on that machine | the JetBrains Toolbox URL handler |
|
|
81
|
+
| **Copy the path** | the path, for `code <path>` or `idea <path>` in a terminal | nothing |
|
|
82
|
+
|
|
83
|
+
**It is code-server, pinned, on its own hostname.** `4.135.0` is baked into
|
|
84
|
+
`.ddev/web-build/Dockerfile.workspace-app` from the standalone tarball — pinned because an
|
|
85
|
+
editor that changes under people between image rebuilds is a support question nobody can
|
|
86
|
+
answer. It runs as a `web_extra_daemon` bound to **127.0.0.1:8443**, and
|
|
87
|
+
`.ddev/nginx_full/nginx-site.conf` proxies `ide.workspace.ddev.site` to it. Loopback plus that
|
|
88
|
+
one vhost is the whole access control.
|
|
89
|
+
|
|
90
|
+
A **hostname, not a subpath**: code-server is a single-page app that builds absolute URLs for
|
|
91
|
+
its workers, its websocket and its static assets, and upstream's guidance is a dedicated
|
|
92
|
+
(sub)domain. It costs nothing here — `*.workspace` is already in `additional_hostnames`, so
|
|
93
|
+
DDEV's router routes it and the project certificate already covers it. The vhost uses an
|
|
94
|
+
**exact** `server_name` so it wins over the regex block that sends everything else to the Node
|
|
95
|
+
app, and carries the websocket `Upgrade`/`Connection` headers plus long read timeouts —
|
|
96
|
+
without those the editor loads and then hangs "reconnecting…", because the terminal, the
|
|
97
|
+
extension host and the file watcher are all websockets.
|
|
98
|
+
|
|
99
|
+
**Its state lives in the workspace tree**, at `.code-server/` (gitignored): settings,
|
|
100
|
+
keybindings and installed extensions survive an image rebuild, which they would not if they
|
|
101
|
+
sat in the container.
|
|
102
|
+
|
|
103
|
+
**When it is down the menu says so** rather than offering a dead link — a cached loopback
|
|
104
|
+
probe, warmed at boot so the first render after a restart already knows. The note names the
|
|
105
|
+
fix: `ddev restart`, or `docker exec ddev-workspace-web supervisorctl status`.
|
|
106
|
+
|
|
107
|
+
### What makes it work — the actual options
|
|
108
|
+
|
|
109
|
+
**Serving it** (`scripts/code-server-start.sh`, run as a `web_extra_daemon`):
|
|
110
|
+
|
|
111
|
+
| flag | why |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `--bind-addr 127.0.0.1:8443` | loopback only; the `ide.*` vhost is the whole access control |
|
|
114
|
+
| `--auth none` | see the security note below — not a casual choice |
|
|
115
|
+
| `--user-data-dir`, `--extensions-dir` under `.code-server/` | settings and extensions survive an image rebuild |
|
|
116
|
+
| `--disable-telemetry`, `--disable-update-check` | nothing phones home; the version is pinned in the image |
|
|
117
|
+
| `--app-name 'Webship Workspace IDE'` | the title bar and welcome page say where you are |
|
|
118
|
+
|
|
119
|
+
**Nginx**, in `.ddev/nginx_full/nginx-site.conf`: an **exact** `server_name ide.workspace.ddev.site`
|
|
120
|
+
(beats the regex block that routes everything else to the Node app), `proxy_set_header Upgrade` +
|
|
121
|
+
`Connection $connection_upgrade` with a `map` for it, `proxy_read_timeout 86400s`,
|
|
122
|
+
`proxy_buffering off`, `client_max_body_size 0`. The websocket headers are not optional: the
|
|
123
|
+
terminal, the extension host and the file watcher are all websockets, and without them the editor
|
|
124
|
+
loads and then hangs *reconnecting…*.
|
|
125
|
+
|
|
126
|
+
**Editor settings**, seeded once into `.code-server/data/User/settings.json` (never overwritten, so
|
|
127
|
+
your own changes stick):
|
|
128
|
+
|
|
129
|
+
| setting | why |
|
|
130
|
+
| --- | --- |
|
|
131
|
+
| `security.workspace.trust.enabled: false` | VS Code opens every folder in **Restricted Mode** and refuses to start a terminal until you answer *"Do you trust the authors…"*. That prompt is for code you downloaded; here every folder is a project on this machine, opened from a dashboard that already offers a shell |
|
|
132
|
+
| `terminal.integrated.profiles.linux` → **Claude Code**, **Claude Code (accept edits)** | the terminal dropdown offers them by name, so agents and skills are one click away instead of requiring you to know to type `claude` |
|
|
133
|
+
| `files.associations` → `.module`, `.install`, `.theme`, `.profile`, `.engine`, `.inc` as PHP | without it the editor treats the files a Drupal developer opens most as plain text: no highlighting, no outline |
|
|
134
|
+
| `files.exclude` / `search.exclude` → `node_modules`, `vendor`, `web/core`, `web/modules/contrib` | a Drupal site is ~21,000 PHP files of which almost none are yours; search is useless otherwise |
|
|
135
|
+
| `files.watcherExclude` → the same trees plus `.git/objects`, `graphify-out`, `.ddev` | **one** built Drupal project is ~21,000 directories, 11,800 of them in those three trees, against a kernel limit of 65,536 watches **shared across the machine**. Without this, two open projects and the editor reports *Unable to watch for file changes* and silently stops noticing edits made outside it — a `composer install`, a `git checkout`, an agent |
|
|
136
|
+
| `extensions.autoUpdate: "off"` | the same reason code-server is pinned |
|
|
137
|
+
| `workbench.startupEditor: "none"`, `update.mode: "none"`, `telemetry.telemetryLevel: "off"` | no welcome page, no update nag, no telemetry |
|
|
138
|
+
|
|
139
|
+
### Claude Code is in the editor, when it is configured
|
|
140
|
+
|
|
141
|
+
The official extension (**`Anthropic.claude-code`**, on Open VSX — which is the registry
|
|
142
|
+
code-server queries, so no marketplace switch and no `.vsix` juggling) is installed by
|
|
143
|
+
`scripts/code-server-start.sh` on first start, and appears in the activity bar with its own
|
|
144
|
+
chat (`Ctrl+Alt+I`).
|
|
145
|
+
|
|
146
|
+
**Gated on Claude Code being usable here**, which means two things: the `claude` CLI on PATH, and
|
|
147
|
+
a credentials file in `~/.claude` (the container bind-mounts it from the host). On a machine where
|
|
148
|
+
Claude Code has never been logged in, installing it would put a panel in the editor whose only
|
|
149
|
+
behaviour is to ask for a login that belongs on the host — so there the IDE keeps the terminal
|
|
150
|
+
profiles and nothing else, and the script says so. Log in on the host with `claude`, then
|
|
151
|
+
`ddev restart`.
|
|
152
|
+
|
|
153
|
+
Installed **once**, when missing, with a timeout, and never fatally: an editor that will not start
|
|
154
|
+
because a registry was slow is worse than an editor without one extension. `extensions.autoUpdate`
|
|
155
|
+
is `"off"` for the same reason the code-server version is pinned.
|
|
156
|
+
|
|
157
|
+
### Agents and skills are already loaded
|
|
158
|
+
|
|
159
|
+
Nothing to install per project. The IDE runs **inside the dashboard's container**, which bind-mounts
|
|
160
|
+
`~/.claude` — so a terminal there has the `claude` CLI with the **same agents, skills and commands as
|
|
161
|
+
a session on the host** (86 / 42 / 6 on this machine), and Claude Code reads `CLAUDE.md` and
|
|
162
|
+
`AGENTS.md` from the working directory upwards, so the folder's own rules apply automatically.
|
|
163
|
+
|
|
164
|
+
In the IDE: **Terminal → new terminal dropdown → Claude Code**. Or type `claude` in any terminal.
|
|
165
|
+
Inside it, `@` and `/` list the same items the dashboard's assistant offers, because they are the
|
|
166
|
+
same files.
|
|
167
|
+
|
|
168
|
+
**For local VS Code / IntelliJ** (the other two entries in the menu) nothing is configured — they
|
|
169
|
+
open the folder on your own machine, with your own settings and your own extensions. Agents and
|
|
170
|
+
skills come from your own `~/.claude` there, which is the point of that split.
|
|
171
|
+
|
|
172
|
+
### Security posture, stated plainly
|
|
173
|
+
|
|
174
|
+
**This dashboard has no authentication, and that is only safe because it is loopback-only.**
|
|
175
|
+
It runs shell commands, serves an interactive terminal at `/terminal/<session>`, and a Web IDE
|
|
176
|
+
with write access to the whole tree and a terminal of its own — in a container that mounts the
|
|
177
|
+
Docker socket and the DDEV registry. Anyone who can reach the port has all of it.
|
|
178
|
+
|
|
179
|
+
What keeps that acceptable is that nothing is reachable from the network: DDEV's router binds
|
|
180
|
+
`127.0.0.1` (`router_bind_all_interfaces: false`, its default) and the machine answers nothing
|
|
181
|
+
on its LAN address.
|
|
182
|
+
|
|
183
|
+
**Two settings end that**, and both get changed for a good-looking reason:
|
|
184
|
+
|
|
185
|
+
| setting | why someone changes it |
|
|
186
|
+
| --- | --- |
|
|
187
|
+
| `router_bind_all_interfaces: true` | DDEV documents it for Let's Encrypt, and for reaching a project from a phone |
|
|
188
|
+
| `hub_domain: <a public domain>` | this README says the same system runs as a remote hub |
|
|
189
|
+
|
|
190
|
+
So both are watched for, in the two places a person will be:
|
|
191
|
+
|
|
192
|
+
- **`install.sh --check`** prints `✓ exposure loopback only` or a block naming the setting that
|
|
193
|
+
changed and what is reachable because of it;
|
|
194
|
+
- **the dashboard itself** shows a banner on every page, not dismissible, because the person who
|
|
195
|
+
flips that setting a month later is not running the installer.
|
|
196
|
+
|
|
197
|
+
If the hub is ever put on a public domain, the dashboard **and** the IDE need real
|
|
198
|
+
authentication in front of them. One decision, not two.
|
|
199
|
+
|
|
200
|
+
### Other things the security pass fixed
|
|
201
|
+
|
|
202
|
+
- `core/config/settings.yml` (real API keys) and `.screens/.runner-token` (the screen runner's
|
|
203
|
+
entire access control) were **world-readable at `775`** while the CLI-managed secrets beside
|
|
204
|
+
them were correctly `600`. Both are `600` now.
|
|
205
|
+
- One **high-severity** dependency advisory (`js-yaml`, exponential parsing → DoS) — fixed.
|
|
206
|
+
- Security headers were set at **three** `writeHead` sites out of fifty-six, so every page but
|
|
207
|
+
the graph viewer went without. They are set once in the request handler now: `nosniff`,
|
|
208
|
+
`no-referrer`, `SAMEORIGIN`.
|
|
209
|
+
|
|
210
|
+
### The IDE’s own posture
|
|
211
|
+
|
|
212
|
+
`--auth none`. The dashboard has no login of its own until Sign in with Google (`auth.google`) is
|
|
213
|
+
turned on, and it already serves a real interactive terminal at `/terminal/<session>`, so a second
|
|
214
|
+
password in front of one door would be theatre.
|
|
215
|
+
What this means concretely: **anyone who can reach the dashboard's origin can edit any file in
|
|
216
|
+
`~/workspace` and run commands in that container**, which has the Docker socket and the DDEV
|
|
217
|
+
registry mounted.
|
|
218
|
+
|
|
219
|
+
That is acceptable for a local tool on `*.ddev.site` and **not** acceptable on a public
|
|
220
|
+
`hub_domain`. If the hub is ever put on a public domain, the IDE *and* the dashboard need real
|
|
221
|
+
authentication in front of them — the same decision, not two.
|
|
222
|
+
|
|
223
|
+
## What it does
|
|
224
|
+
|
|
225
|
+
- **Home** (`/`): the workspace cards grid — one card per folder in
|
|
226
|
+
`core/config/settings.yml`'s `workspaces:` list (read live, so adding a
|
|
227
|
+
workspace there shows up without code changes), with live project counts.
|
|
228
|
+
- **Per-workspace pages** (`/dev`, `/test`, `/demos`, …): each workspace's own
|
|
229
|
+
project list with per-project **Backup / Filemode / Remove** actions, plus a
|
|
230
|
+
**Build** form listing that folder's `cmd-*-project.sh` builder scripts.
|
|
231
|
+
- **AI Assistant** on every page (inline on home, floating 🤖 widget on
|
|
232
|
+
workspace pages): quick actions (New Drupal 11, New Webship, Status) and a
|
|
233
|
+
free-text chat backed by the local `claude` CLI in restricted, tool-free
|
|
234
|
+
print mode.
|
|
235
|
+
- All actions run the real `cmd-*.sh` scripts (and `ddev`) against
|
|
236
|
+
`~/workspace` — this is a control panel, not a mock.
|
|
237
|
+
|
|
238
|
+
## Setup
|
|
239
|
+
|
|
240
|
+
You need the workspace files and DDEV. The quickest way to the files is the ZIP — no git, no
|
|
241
|
+
clone: on [github.com/webship/workspace](https://github.com/webship/workspace) switch to the `1.0.x`
|
|
242
|
+
branch, press **Code ▾ → Download ZIP**, unzip it and rename the folder to `~/workspace` (the full
|
|
243
|
+
walkthrough, and two other ways, are in the [root README](docs/REFERENCE.md#get-started)).
|
|
244
|
+
|
|
245
|
+
Then, once:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
cd ~/workspace/core/workspace-app
|
|
249
|
+
ddev start
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Then open https://workspace.ddev.site. Everything else — building, starting, stopping, backing up,
|
|
253
|
+
restoring and removing projects — can be done from there without another command.
|
|
254
|
+
|
|
255
|
+
### How it runs
|
|
256
|
+
|
|
257
|
+
The Node app runs **inside** the DDEV web container as a `web_extra_daemon`,
|
|
258
|
+
with Docker-outside-of-Docker so it can manage sibling DDEV projects:
|
|
259
|
+
|
|
260
|
+
- `.ddev/docker-compose.dood.yaml` bind-mounts the host's
|
|
261
|
+
`/var/run/docker.sock` and your `~/workspace` tree (at the same absolute
|
|
262
|
+
path, via `${HOME}` interpolation).
|
|
263
|
+
- `.ddev/web-build/Dockerfile.workspace-app` installs the `docker` CLI and the
|
|
264
|
+
`ddev` binary in the web image, and aligns the container's `docker` group
|
|
265
|
+
GID with the host's.
|
|
266
|
+
- `.ddev/nginx_full/nginx-site.conf` proxies the site to the app on
|
|
267
|
+
`127.0.0.1:3000` inside the container.
|
|
268
|
+
|
|
269
|
+
### Per-machine adjustments
|
|
270
|
+
|
|
271
|
+
- **docker group GID — handled for you.** The container has to share this
|
|
272
|
+
host's docker GID or every `ddev` command inside it fails with a permission
|
|
273
|
+
error, and the symptom is a dashboard where every project reads as Stopped
|
|
274
|
+
forever while running perfectly well. Two files carry the number: the
|
|
275
|
+
`groupmod`/`usermod` line in `.ddev/web-build/Dockerfile.workspace-app`
|
|
276
|
+
(which is what `sg docker` in the supervisord command resolves) and
|
|
277
|
+
`group_add` in `.ddev/docker-compose.dood.yaml` (which is what actually puts
|
|
278
|
+
the running process in the group — DDEV pins `user: 1000:1000`, so a
|
|
279
|
+
build-time `usermod` never reaches it).
|
|
280
|
+
|
|
281
|
+
`core/scripts/docker-gid.sh` writes both from the host's socket.
|
|
282
|
+
`core/scripts/install.sh` runs it at setup and reports it in its survey, and
|
|
283
|
+
`core/scripts/sync-tooling.sh` runs it **after** mirroring — this directory is
|
|
284
|
+
mirrored wholesale, so a hand edit would be overwritten by the next `--pull`.
|
|
285
|
+
Check it any time:
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
bash core/scripts/docker-gid.sh --check
|
|
289
|
+
```
|
|
290
|
+
- **The DDEV project registry** (`~/.ddev`) is bind-mounted in, so `ddev list`
|
|
291
|
+
in here sees the same projects as the host. Without it the container has its
|
|
292
|
+
own empty registry and reports one project — itself — which produces the same
|
|
293
|
+
"everything is Stopped" symptom by a different route.
|
|
294
|
+
- **Bind-mounted host paths are a trap Docker sets.** A bind mount whose source
|
|
295
|
+
does not exist is *created* — as a directory, owned by root. Every optional tool
|
|
296
|
+
the dashboard shares from the host is such a source, so on a machine without
|
|
297
|
+
`twg` the first start used to create `~/.local/bin/twg` **and `~/.local/bin`**
|
|
298
|
+
as root: the JIRA button then never appeared however complete `jira:` was, and
|
|
299
|
+
nothing could install a user binary there any more, including the
|
|
300
|
+
`drupal-ci-local` wrapper the GitLab CI green-gate writes.
|
|
301
|
+
|
|
302
|
+
`core/scripts/host-mounts.sh` creates each of those paths as you before Docker
|
|
303
|
+
can, and comments the `twg` volume out of `.ddev/docker-compose.twg.yaml` while
|
|
304
|
+
`twg` is not installed — restoring it the moment it is. It runs from the
|
|
305
|
+
project's own `pre-start` hook and from `install.sh`. `--check` reports without
|
|
306
|
+
changing anything, and it names the paths only root can hand back.
|
|
307
|
+
- **Jira** needs the Teamwork Graph CLI on the host:
|
|
308
|
+
[teamworkgraph.com/cli](https://teamworkgraph.com/cli) ·
|
|
309
|
+
[developer.atlassian.com/cloud/twg-cli](https://developer.atlassian.com/cloud/twg-cli/)
|
|
310
|
+
```
|
|
311
|
+
curl -fsSL --retry 2 https://teamwork-graph.atlassian.com/cli/install | bash
|
|
312
|
+
~/.local/bin/twg doctor
|
|
313
|
+
```
|
|
314
|
+
It is mounted in rather than installed in the image: one binary, one OAuth
|
|
315
|
+
session, and `twg login` on the host reaches the container instantly. When it is
|
|
316
|
+
absent the JIRA button renders **disabled**, with the reason in its tooltip,
|
|
317
|
+
rather than vanishing.
|
|
318
|
+
- **Upsun** needs its CLI on the host, mounted in the same way and for the same
|
|
319
|
+
reason as `twg` — one binary, one session:
|
|
320
|
+
[developer.upsun.com/cli](https://developer.upsun.com/cli)
|
|
321
|
+
|
|
322
|
+
Upsun's own installer sets up an apt repository, so it needs root and an
|
|
323
|
+
interactive password; in a pipe it fails with *"sudo: A terminal is required to
|
|
324
|
+
authenticate"*. `install.sh` therefore prefers the **release tarball** — one
|
|
325
|
+
static binary into `~/.local/bin`, no root at all — and falls back to the
|
|
326
|
+
official installer only if GitHub is unreachable:
|
|
327
|
+
|
|
328
|
+
```
|
|
329
|
+
ver=$(wget -qO- https://api.github.com/repos/upsun/cli/releases/latest \
|
|
330
|
+
| sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p' | head -1)
|
|
331
|
+
wget -qO- https://github.com/upsun/cli/releases/download/v$ver/upsun_${ver}_linux_amd64.tar.gz \
|
|
332
|
+
| tar -xz -C /tmp upsun && install -m 0755 /tmp/upsun ~/.local/bin/upsun
|
|
333
|
+
upsun login # browser OAuth; writes SSH certificates
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**The dashboard needs its own credential, and this is not optional.** `upsun login`
|
|
337
|
+
writes a session into the host keyring, and a container cannot reach a keyring — so
|
|
338
|
+
every UPSUN action in the dashboard (the environment list, the live URLs, *Get
|
|
339
|
+
database & files*) authenticates with an API **token** instead:
|
|
340
|
+
|
|
341
|
+
```yaml
|
|
342
|
+
# core/config/settings.yml
|
|
343
|
+
upsun:
|
|
344
|
+
cli_token: … # Upsun console → My profile → API tokens
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
`upsunJobEnv()` injects it as `UPSUN_CLI_TOKEN` for each CLI call. Without it the
|
|
348
|
+
actions answer *"No Upsun API token configured"*; with the wrong **kind** of token
|
|
349
|
+
they answer *"The request could not be authorized. Unable to authenticate the
|
|
350
|
+
provided API token"* — which reads like an expiry. An **Atlassian** token in that
|
|
351
|
+
field, recognisable because they start `ATATT`, produces exactly that message.
|
|
352
|
+
`bash core/scripts/install.sh --check` reports the field by shape, never its value.
|
|
353
|
+
|
|
354
|
+
Two related details worth keeping:
|
|
355
|
+
|
|
356
|
+
- **The CLI prints the reason FIRST and a usage synopsis LAST.** These errors are
|
|
357
|
+
rendered through `cliError()` in `format.js`, which drops the synopsis and keeps
|
|
358
|
+
the head — a tail slice showed a list of flags and threw the reason away.
|
|
359
|
+
- **It hard-wraps mid-word.** The real output is `provided AP` / newline / `I token.`,
|
|
360
|
+
so matching its text has to survive the wrap; the check in `actions/upsun.js` does.
|
|
361
|
+
- **Chat**: the assistant chat shells out to the `claude` CLI **inside the
|
|
362
|
+
container** (installed in the web image via `npm install -g
|
|
363
|
+
@anthropic-ai/claude-code`); it authenticates through your host's
|
|
364
|
+
`~/.claude` + `~/.claude.json`, which are bind-mounted in. If you've never
|
|
365
|
+
logged in to Claude Code on the host, run `claude` there once first.
|
|
366
|
+
- **Voice input**: the mic button uses the browser's Web Speech API
|
|
367
|
+
(Chrome/Edge; hidden automatically on browsers without support).
|
|
368
|
+
|
|
369
|
+
## Security note
|
|
370
|
+
|
|
371
|
+
This app executes shell scripts with real side effects (build, backup,
|
|
372
|
+
**delete**) and has host-Docker access. The AI assistant runs in **agent
|
|
373
|
+
mode**: its `claude -p` calls are allowed Bash/Read/Glob/Grep inside the
|
|
374
|
+
container (Write/Edit stay disallowed, and its instructions forbid deleting
|
|
375
|
+
anything unasked), so a chat message can really build, start, stop, back up
|
|
376
|
+
and open projects — and steer the page afterwards via NAVIGATE/OPEN/REFRESH
|
|
377
|
+
directives. Every action and chat message is appended to `actions.log`.
|
|
378
|
+
It binds only to the DDEV-internal port and is meant for local development
|
|
379
|
+
machines — do not expose `workspace.ddev.site` beyond localhost without
|
|
380
|
+
adding authentication.
|