@webship.co/workspace 0.0.0-stage → 1.0.0-beta1

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.
Files changed (307) hide show
  1. package/PLUGINS.md +249 -0
  2. package/README.md +379 -2
  3. package/WORKSPACES.md +69 -0
  4. package/actions/ai-items.js +293 -0
  5. package/actions/audio.js +48 -0
  6. package/actions/build-order.js +79 -0
  7. package/actions/checks.js +95 -0
  8. package/actions/claude-session.js +104 -0
  9. package/actions/commands-center.js +258 -0
  10. package/actions/composer.js +267 -0
  11. package/actions/ddev-addons.js +90 -0
  12. package/actions/ddev-mail.js +80 -0
  13. package/actions/ddev.js +237 -0
  14. package/actions/folders.js +165 -0
  15. package/actions/images-svg.js +33 -0
  16. package/actions/images.js +145 -0
  17. package/actions/navigation.js +190 -0
  18. package/actions/node.js +222 -0
  19. package/actions/notes.js +55 -0
  20. package/actions/plugins.js +36 -0
  21. package/actions/project-make.js +28 -0
  22. package/actions/rag.js +202 -0
  23. package/actions/row-actions.js +262 -0
  24. package/actions/screens.js +270 -0
  25. package/actions/testing.js +121 -0
  26. package/actions/video-edit.js +54 -0
  27. package/ai-inventory.js +182 -0
  28. package/ai-items.js +202 -0
  29. package/assistant-cast.js +176 -0
  30. package/assistant-suggestions.js +91 -0
  31. package/assistant.js +484 -0
  32. package/audio-voice.js +528 -0
  33. package/auth-google.js +260 -0
  34. package/build-ai.js +52 -0
  35. package/build-order-page.js +191 -0
  36. package/build-order.js +313 -0
  37. package/builder-args.js +117 -0
  38. package/changelog.js +302 -0
  39. package/chat-threads.js +134 -0
  40. package/checks.js +105 -0
  41. package/claude-args.js +282 -0
  42. package/claude-sessions.js +129 -0
  43. package/command-author.js +117 -0
  44. package/command-block.js +20 -0
  45. package/commands/defaults.yml +159 -0
  46. package/commands-page.js +460 -0
  47. package/commands.js +568 -0
  48. package/cucumber.js +43 -0
  49. package/ddev-addons-page.js +124 -0
  50. package/ddev-addons.js +154 -0
  51. package/ddev-custom-commands.js +96 -0
  52. package/ddev-domains.js +84 -0
  53. package/ddev-import.js +93 -0
  54. package/ddev-mail-page.js +167 -0
  55. package/ddev-mail.js +110 -0
  56. package/ddev-options.js +145 -0
  57. package/ddev-snapshots.js +126 -0
  58. package/ddev.js +145 -0
  59. package/docs/DEVELOPER-GUIDE.md +224 -0
  60. package/docs/README.md +252 -0
  61. package/docs/REFERENCE.md +684 -0
  62. package/docs/USER-GUIDE.md +697 -0
  63. package/docs/screenshots/assistant/avatar-board.png +0 -0
  64. package/docs/screenshots/assistant/avatar-coffee.png +0 -0
  65. package/docs/screenshots/assistant/avatar-colours.png +0 -0
  66. package/docs/screenshots/assistant/avatar-icons.png +0 -0
  67. package/docs/screenshots/assistant/avatar-moves.png +0 -0
  68. package/docs/screenshots/assistant/avatar-pick-cookieman.png +0 -0
  69. package/docs/screenshots/assistant/avatar-settings-voices.png +0 -0
  70. package/docs/screenshots/assistant/avatar-status.png +0 -0
  71. package/docs/screenshots/create-card/create-card.png +0 -0
  72. package/docs/screenshots/create-card/start-ai.png +0 -0
  73. package/docs/screenshots/guide-clone-address.png +0 -0
  74. package/docs/screenshots/guide-clone-worktree-off.png +0 -0
  75. package/docs/screenshots/guide-clone-worktree-on.png +0 -0
  76. package/docs/screenshots/guide-ddev-menu.png +0 -0
  77. package/docs/screenshots/guide-domains.png +0 -0
  78. package/docs/screenshots/guide-home.png +0 -0
  79. package/docs/screenshots/guide-ide-menu.png +0 -0
  80. package/docs/screenshots/guide-remove-refusal.png +0 -0
  81. package/docs/screenshots/guide-rows-worktree.png +0 -0
  82. package/docs/screenshots/guide-screens.png +0 -0
  83. package/docs/screenshots/guide-settings.png +0 -0
  84. package/docs/screenshots/guide-workspace-page.png +0 -0
  85. package/docs/screenshots/home/card-settings.png +0 -0
  86. package/docs/screenshots/home/drag-reorder.png +0 -0
  87. package/docs/screenshots/home/edit-card.png +0 -0
  88. package/docs/screenshots/images/claude-code-image.png +0 -0
  89. package/docs/screenshots/images/edit-with-ai.png +0 -0
  90. package/docs/screenshots/images/gallery.png +0 -0
  91. package/docs/screenshots/images/generate-description.png +0 -0
  92. package/docs/screenshots/images/generate-plan.png +0 -0
  93. package/docs/screenshots/images/providers.png +0 -0
  94. package/docs/screenshots/images/svg-form.png +0 -0
  95. package/docs/screenshots/images/svg-result.png +0 -0
  96. package/docs/screenshots/images/upload.png +0 -0
  97. package/docs/screenshots/new-folder/2-form.png +0 -0
  98. package/docs/screenshots/new-folder/8-ddev-options.png +0 -0
  99. package/docs/screenshots/new-folder/files-folder.png +0 -0
  100. package/docs/screenshots/projects/ddev-custom-commands.png +0 -0
  101. package/docs/screenshots/projects/state-chip-stop.png +0 -0
  102. package/docs/screenshots/readme/dashboard-dark.png +0 -0
  103. package/docs/screenshots/readme/dashboard-light.png +0 -0
  104. package/docs/screenshots/readme/dev-dark.png +0 -0
  105. package/docs/screenshots/readme/dev-light.png +0 -0
  106. package/docs/screenshots/readme/docs-dark.png +0 -0
  107. package/docs/screenshots/readme/docs-light.png +0 -0
  108. package/docs/screenshots/readme/icons/ai.svg +4 -0
  109. package/docs/screenshots/readme/icons/code.svg +4 -0
  110. package/docs/screenshots/readme/icons/docs.svg +4 -0
  111. package/docs/screenshots/readme/icons/services.svg +4 -0
  112. package/docs/screenshots/readme/icons/sites.svg +4 -0
  113. package/docs/screenshots/readme/test-report-dark.png +0 -0
  114. package/docs/screenshots/readme/test-report-light.png +0 -0
  115. package/docs/screenshots/readme/test-scenarios.png +0 -0
  116. package/docs/screenshots/readme/test-steps.png +0 -0
  117. package/docs/screenshots/readme/videos-dark.png +0 -0
  118. package/docs/screenshots/readme/videos-light.png +0 -0
  119. package/docs/screenshots/screens-menu.png +0 -0
  120. package/docs/screenshots/screens-new.png +0 -0
  121. package/docs/screenshots/settings/auth-google-lists.png +0 -0
  122. package/docs/screenshots/settings/delete-1-row.png +0 -0
  123. package/docs/screenshots/settings/delete-2-confirm.png +0 -0
  124. package/docs/screenshots/settings/delete-3-type-name.png +0 -0
  125. package/docs/screenshots/settings/delete-4-type-yes.png +0 -0
  126. package/event-log.js +59 -0
  127. package/files-browser.js +247 -0
  128. package/folders.js +121 -0
  129. package/format.js +73 -0
  130. package/git-info.js +81 -0
  131. package/github-auth.js +62 -0
  132. package/html.js +10 -0
  133. package/icons.js +83 -0
  134. package/images-ai.js +447 -0
  135. package/images-create.js +226 -0
  136. package/images-page.js +417 -0
  137. package/images-records.js +39 -0
  138. package/images-render.js +36 -0
  139. package/images-svg-core.js +214 -0
  140. package/images-svg-sanitize.js +188 -0
  141. package/images-svg.js +241 -0
  142. package/images.js +270 -0
  143. package/jobs.js +430 -0
  144. package/linger.js +21 -0
  145. package/lists.js +292 -0
  146. package/media-info.js +136 -0
  147. package/navigation-page.js +264 -0
  148. package/navigation.js +394 -0
  149. package/notes.js +0 -0
  150. package/package.json +47 -4
  151. package/palette-index.js +46 -0
  152. package/playwright.config.js +23 -0
  153. package/plugin-install.js +239 -0
  154. package/plugin-loader.js +466 -0
  155. package/plugin-manifest.js +83 -0
  156. package/plugin-settings.js +44 -0
  157. package/plugins/workspace-notes/index.js +59 -0
  158. package/plugins-page.js +103 -0
  159. package/project-make.js +241 -0
  160. package/project-name.js +210 -0
  161. package/public/assistant-avatar-moves.js +646 -0
  162. package/public/assistant-avatar-ui.js +1223 -0
  163. package/public/assistant-avatar.js +530 -0
  164. package/public/avatar/druplicon.vrm +0 -0
  165. package/public/css/base.css +260 -0
  166. package/public/css/components/ai-assistant.css +263 -0
  167. package/public/css/components/assistant-avatar.css +279 -0
  168. package/public/css/components/assistant-views.css +198 -0
  169. package/public/css/components/build-order.css +74 -0
  170. package/public/css/components/clone-as-a-git-worktree.css +30 -0
  171. package/public/css/components/command-palette.css +61 -0
  172. package/public/css/components/commands-center.css +139 -0
  173. package/public/css/components/confirm-dialog.css +40 -0
  174. package/public/css/components/create-card.css +8 -0
  175. package/public/css/components/ddev-menu.css +23 -0
  176. package/public/css/components/deep-chat.css +17 -0
  177. package/public/css/components/density.css +11 -0
  178. package/public/css/components/floating-assistant-workspace-pages.css +55 -0
  179. package/public/css/components/folders.css +5 -0
  180. package/public/css/components/full-height-left-assistant-sidebar-all-pages.css +39 -0
  181. package/public/css/components/full-height-right-actions-rail-pages-that-have-actions.css +267 -0
  182. package/public/css/components/grid-and-list.css +203 -0
  183. package/public/css/components/help-page.css +36 -0
  184. package/public/css/components/hero.css +16 -0
  185. package/public/css/components/images.css +70 -0
  186. package/public/css/components/layout-rhythm-and-spacing-polish.css +139 -0
  187. package/public/css/components/mic-on-any-field-that-wants-typing.css +416 -0
  188. package/public/css/components/modal-fit.css +113 -0
  189. package/public/css/components/page-width.css +23 -0
  190. package/public/css/components/plugins-page.css +9 -0
  191. package/public/css/components/project-cards.css +256 -0
  192. package/public/css/components/project-rows.css +390 -0
  193. package/public/css/components/reordering-the-workspace-cards.css +85 -0
  194. package/public/css/components/responsive-home-and-the-workspace-pages.css +457 -0
  195. package/public/css/components/running-commands-as-toasts.css +327 -0
  196. package/public/css/components/settings-editor-core-config-yml.css +828 -0
  197. package/public/css/components/short-results-as-a-toast-at-the-top.css +203 -0
  198. package/public/css/components/sound-player.css +84 -0
  199. package/public/css/components/suggest-agents-skills-commands.css +112 -0
  200. package/public/css/components/tab-modal.css +142 -0
  201. package/public/css/components/terminals.css +244 -0
  202. package/public/css/components/test-report-media-one-file-per-row-paged.css +144 -0
  203. package/public/css/components/top-toolbar-logo-breadcrumb.css +107 -0
  204. package/public/css/components/video-edit.css +6 -0
  205. package/public/css/components/web-ide-open-with.css +23 -0
  206. package/public/css/components/working-indicators.css +158 -0
  207. package/public/css/components/workspace-cards.css +79 -0
  208. package/public/favicon.ico +0 -0
  209. package/public/favicon.png +0 -0
  210. package/public/htmx.min.js +1 -0
  211. package/public/logo.png +0 -0
  212. package/public/media-player.js +215 -0
  213. package/public/mic.js +255 -0
  214. package/public/narration.js +110 -0
  215. package/public/palette.js +278 -0
  216. package/public/sortable.js +251 -0
  217. package/public/suggest.js +337 -0
  218. package/public/terminal.js +903 -0
  219. package/public/themes/default/theme.css +267 -0
  220. package/public/themes/ember/theme.css +180 -0
  221. package/public/themes/press/theme.css +290 -0
  222. package/public/themes/slate/theme.css +182 -0
  223. package/public/touch-icon.png +0 -0
  224. package/public/ui.js +2721 -0
  225. package/public/vendor/ace/LICENSE.txt +24 -0
  226. package/public/vendor/ace/ace.js +23 -0
  227. package/public/vendor/ace/ext-language_tools.js +8 -0
  228. package/public/vendor/ace/ext-searchbox.js +8 -0
  229. package/public/vendor/ace/mode-css.js +8 -0
  230. package/public/vendor/ace/mode-html.js +8 -0
  231. package/public/vendor/ace/mode-javascript.js +8 -0
  232. package/public/vendor/ace/mode-markdown.js +8 -0
  233. package/public/vendor/ace/mode-sh.js +8 -0
  234. package/public/vendor/ace/mode-yaml.js +8 -0
  235. package/public/vendor/ace/theme-textmate.js +8 -0
  236. package/public/vendor/ace/theme-tomorrow_night.js +8 -0
  237. package/public/vendor/ace/worker-yaml.js +1 -0
  238. package/public/vendor/deep-chat.LICENSE +21 -0
  239. package/public/vendor/deep-chat.bundle.js +1 -0
  240. package/public/vendor/fonts/dm-serif-text-LICENSE.txt +93 -0
  241. package/public/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
  242. package/public/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
  243. package/public/vendor/fonts/figtree-LICENSE.txt +93 -0
  244. package/public/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
  245. package/public/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
  246. package/public/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
  247. package/public/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
  248. package/public/vendor/fonts/jetbrains-mono-LICENSE.txt +93 -0
  249. package/public/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  250. package/public/vendor/fonts/ubuntu-mono-LICENSE.txt +98 -0
  251. package/public/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
  252. package/public/vendor/tabler-LICENSE.txt +21 -0
  253. package/public/vendor/tabler-sprite.svg +1 -0
  254. package/public/vendor/three/LICENSE +21 -0
  255. package/public/vendor/three/addons/loaders/GLTFLoader.js +4860 -0
  256. package/public/vendor/three/addons/utils/BufferGeometryUtils.js +1501 -0
  257. package/public/vendor/three/addons/utils/SkeletonUtils.js +496 -0
  258. package/public/vendor/three/three.core.min.js +6 -0
  259. package/public/vendor/three/three.module.min.js +6 -0
  260. package/public/vendor/three-vrm/LICENSE +22 -0
  261. package/public/vendor/three-vrm/three-vrm.module.min.js +983 -0
  262. package/public/vendor/uikit-icons.min.js +1 -0
  263. package/public/vendor/uikit.min.css +1 -0
  264. package/public/vendor/uikit.min.js +1 -0
  265. package/public/vendor/webship-icons/ui/bookmark-outlined.svg +1 -0
  266. package/public/vendor/webship-icons/ui/headset.svg +1 -0
  267. package/public/vendor/webship-icons/ui/menu.svg +1 -0
  268. package/public/vendor/webship-icons/ui/pause.svg +1 -0
  269. package/public/vendor/webship-icons/ui/play.svg +1 -0
  270. package/public/vendor/webship-icons/ui/share.svg +1 -0
  271. package/public/vendor/webship-icons/ui/sound-off.svg +1 -0
  272. package/public/vendor/webship-icons/ui/sound-on.svg +1 -0
  273. package/public/vendor/webship-icons/ui/stop.svg +1 -0
  274. package/public/vendor/xterm/addon-fit.js +2 -0
  275. package/public/vendor/xterm/addon-unicode11.js +2 -0
  276. package/public/vendor/xterm/addon-web-links.js +2 -0
  277. package/public/vendor/xterm/xterm.css +218 -0
  278. package/public/vendor/xterm/xterm.js +2 -0
  279. package/rag.js +169 -0
  280. package/reader.js +132 -0
  281. package/repo-blobs.js +66 -0
  282. package/report-pdf.js +337 -0
  283. package/row-layout.js +1439 -0
  284. package/screenrc +37 -0
  285. package/screens.js +1063 -0
  286. package/scripts/code-server-start.sh +190 -0
  287. package/scripts/images-claude-finish.js +107 -0
  288. package/scripts/screen-host.sh +114 -0
  289. package/scripts/screen-runner.js +362 -0
  290. package/scripts/screen-then.js +47 -0
  291. package/scripts/update.sh +350 -0
  292. package/server.js +7923 -0
  293. package/settings-file.js +423 -0
  294. package/speech.js +297 -0
  295. package/tab-modal.js +75 -0
  296. package/test-reports.js +260 -0
  297. package/themes.js +381 -0
  298. package/transcripts.js +168 -0
  299. package/urls.js +13 -0
  300. package/validate.js +18 -0
  301. package/version-check.js +94 -0
  302. package/video-edit.js +588 -0
  303. package/waiting.js +85 -0
  304. package/workspace-card-tools.js +41 -0
  305. package/workspace-delete.js +139 -0
  306. package/workspace-types.js +85 -0
  307. package/workspaces.js +553 -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` 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
- # Temporary Holding Version
1
+ # workspace-app
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.