@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.
Files changed (311) 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/graphs.js +107 -0
  16. package/actions/images-svg.js +33 -0
  17. package/actions/images.js +145 -0
  18. package/actions/navigation.js +190 -0
  19. package/actions/node.js +222 -0
  20. package/actions/notes.js +55 -0
  21. package/actions/obsidian.js +59 -0
  22. package/actions/plugins.js +36 -0
  23. package/actions/project-make.js +28 -0
  24. package/actions/rag.js +202 -0
  25. package/actions/row-actions.js +262 -0
  26. package/actions/screens.js +270 -0
  27. package/actions/testing.js +121 -0
  28. package/actions/video-edit.js +54 -0
  29. package/ai-inventory.js +182 -0
  30. package/ai-items.js +202 -0
  31. package/assistant-cast.js +176 -0
  32. package/assistant-suggestions.js +91 -0
  33. package/assistant.js +484 -0
  34. package/audio-voice.js +528 -0
  35. package/auth-google.js +260 -0
  36. package/build-ai.js +52 -0
  37. package/build-order-page.js +191 -0
  38. package/build-order.js +313 -0
  39. package/builder-args.js +117 -0
  40. package/changelog.js +302 -0
  41. package/chat-threads.js +134 -0
  42. package/checks.js +105 -0
  43. package/claude-args.js +282 -0
  44. package/claude-sessions.js +129 -0
  45. package/command-author.js +117 -0
  46. package/command-block.js +20 -0
  47. package/commands/defaults.yml +159 -0
  48. package/commands-page.js +460 -0
  49. package/commands.js +568 -0
  50. package/cucumber.js +43 -0
  51. package/ddev-addons-page.js +124 -0
  52. package/ddev-addons.js +154 -0
  53. package/ddev-custom-commands.js +96 -0
  54. package/ddev-domains.js +84 -0
  55. package/ddev-import.js +93 -0
  56. package/ddev-mail-page.js +167 -0
  57. package/ddev-mail.js +110 -0
  58. package/ddev-options.js +145 -0
  59. package/ddev-snapshots.js +126 -0
  60. package/ddev.js +145 -0
  61. package/docs/DEVELOPER-GUIDE.md +224 -0
  62. package/docs/README.md +252 -0
  63. package/docs/REFERENCE.md +684 -0
  64. package/docs/USER-GUIDE.md +697 -0
  65. package/docs/screenshots/assistant/avatar-board.png +0 -0
  66. package/docs/screenshots/assistant/avatar-coffee.png +0 -0
  67. package/docs/screenshots/assistant/avatar-colours.png +0 -0
  68. package/docs/screenshots/assistant/avatar-icons.png +0 -0
  69. package/docs/screenshots/assistant/avatar-moves.png +0 -0
  70. package/docs/screenshots/assistant/avatar-pick-cookieman.png +0 -0
  71. package/docs/screenshots/assistant/avatar-settings-voices.png +0 -0
  72. package/docs/screenshots/assistant/avatar-status.png +0 -0
  73. package/docs/screenshots/create-card/create-card.png +0 -0
  74. package/docs/screenshots/create-card/start-ai.png +0 -0
  75. package/docs/screenshots/guide-clone-address.png +0 -0
  76. package/docs/screenshots/guide-clone-worktree-off.png +0 -0
  77. package/docs/screenshots/guide-clone-worktree-on.png +0 -0
  78. package/docs/screenshots/guide-ddev-menu.png +0 -0
  79. package/docs/screenshots/guide-domains.png +0 -0
  80. package/docs/screenshots/guide-home.png +0 -0
  81. package/docs/screenshots/guide-ide-menu.png +0 -0
  82. package/docs/screenshots/guide-remove-refusal.png +0 -0
  83. package/docs/screenshots/guide-rows-worktree.png +0 -0
  84. package/docs/screenshots/guide-screens.png +0 -0
  85. package/docs/screenshots/guide-settings.png +0 -0
  86. package/docs/screenshots/guide-workspace-page.png +0 -0
  87. package/docs/screenshots/home/card-settings.png +0 -0
  88. package/docs/screenshots/home/drag-reorder.png +0 -0
  89. package/docs/screenshots/home/edit-card.png +0 -0
  90. package/docs/screenshots/images/claude-code-image.png +0 -0
  91. package/docs/screenshots/images/edit-with-ai.png +0 -0
  92. package/docs/screenshots/images/gallery.png +0 -0
  93. package/docs/screenshots/images/generate-description.png +0 -0
  94. package/docs/screenshots/images/generate-plan.png +0 -0
  95. package/docs/screenshots/images/providers.png +0 -0
  96. package/docs/screenshots/images/svg-form.png +0 -0
  97. package/docs/screenshots/images/svg-result.png +0 -0
  98. package/docs/screenshots/images/upload.png +0 -0
  99. package/docs/screenshots/new-folder/2-form.png +0 -0
  100. package/docs/screenshots/new-folder/8-ddev-options.png +0 -0
  101. package/docs/screenshots/new-folder/files-folder.png +0 -0
  102. package/docs/screenshots/projects/ddev-custom-commands.png +0 -0
  103. package/docs/screenshots/projects/state-chip-stop.png +0 -0
  104. package/docs/screenshots/readme/dashboard-dark.png +0 -0
  105. package/docs/screenshots/readme/dashboard-light.png +0 -0
  106. package/docs/screenshots/readme/dev-dark.png +0 -0
  107. package/docs/screenshots/readme/dev-light.png +0 -0
  108. package/docs/screenshots/readme/docs-dark.png +0 -0
  109. package/docs/screenshots/readme/docs-light.png +0 -0
  110. package/docs/screenshots/readme/icons/ai.svg +4 -0
  111. package/docs/screenshots/readme/icons/code.svg +4 -0
  112. package/docs/screenshots/readme/icons/docs.svg +4 -0
  113. package/docs/screenshots/readme/icons/services.svg +4 -0
  114. package/docs/screenshots/readme/icons/sites.svg +4 -0
  115. package/docs/screenshots/readme/test-report-dark.png +0 -0
  116. package/docs/screenshots/readme/test-report-light.png +0 -0
  117. package/docs/screenshots/readme/test-scenarios.png +0 -0
  118. package/docs/screenshots/readme/test-steps.png +0 -0
  119. package/docs/screenshots/readme/videos-dark.png +0 -0
  120. package/docs/screenshots/readme/videos-light.png +0 -0
  121. package/docs/screenshots/screens-menu.png +0 -0
  122. package/docs/screenshots/screens-new.png +0 -0
  123. package/docs/screenshots/settings/auth-google-lists.png +0 -0
  124. package/docs/screenshots/settings/delete-1-row.png +0 -0
  125. package/docs/screenshots/settings/delete-2-confirm.png +0 -0
  126. package/docs/screenshots/settings/delete-3-type-name.png +0 -0
  127. package/docs/screenshots/settings/delete-4-type-yes.png +0 -0
  128. package/event-log.js +59 -0
  129. package/files-browser.js +247 -0
  130. package/folders.js +121 -0
  131. package/format.js +73 -0
  132. package/git-info.js +81 -0
  133. package/github-auth.js +62 -0
  134. package/graphs.js +104 -0
  135. package/html.js +10 -0
  136. package/icons.js +83 -0
  137. package/images-ai.js +447 -0
  138. package/images-create.js +226 -0
  139. package/images-page.js +417 -0
  140. package/images-records.js +39 -0
  141. package/images-render.js +36 -0
  142. package/images-svg-core.js +214 -0
  143. package/images-svg-sanitize.js +188 -0
  144. package/images-svg.js +241 -0
  145. package/images.js +270 -0
  146. package/jobs.js +430 -0
  147. package/linger.js +21 -0
  148. package/lists.js +292 -0
  149. package/media-info.js +136 -0
  150. package/navigation-page.js +264 -0
  151. package/navigation.js +394 -0
  152. package/notes.js +0 -0
  153. package/obsidian.js +405 -0
  154. package/package.json +47 -4
  155. package/palette-index.js +46 -0
  156. package/playwright.config.js +23 -0
  157. package/plugin-install.js +239 -0
  158. package/plugin-loader.js +448 -0
  159. package/plugin-manifest.js +83 -0
  160. package/plugin-settings.js +44 -0
  161. package/plugins/workspace-notes/index.js +59 -0
  162. package/plugins-page.js +103 -0
  163. package/project-make.js +241 -0
  164. package/project-name.js +210 -0
  165. package/public/assistant-avatar-moves.js +646 -0
  166. package/public/assistant-avatar-ui.js +1223 -0
  167. package/public/assistant-avatar.js +530 -0
  168. package/public/avatar/druplicon.vrm +0 -0
  169. package/public/css/base.css +260 -0
  170. package/public/css/components/ai-assistant.css +263 -0
  171. package/public/css/components/assistant-avatar.css +279 -0
  172. package/public/css/components/assistant-views.css +198 -0
  173. package/public/css/components/build-order.css +74 -0
  174. package/public/css/components/clone-as-a-git-worktree.css +30 -0
  175. package/public/css/components/command-palette.css +61 -0
  176. package/public/css/components/commands-center.css +139 -0
  177. package/public/css/components/confirm-dialog.css +40 -0
  178. package/public/css/components/create-card.css +8 -0
  179. package/public/css/components/ddev-menu.css +23 -0
  180. package/public/css/components/deep-chat.css +17 -0
  181. package/public/css/components/density.css +11 -0
  182. package/public/css/components/floating-assistant-workspace-pages.css +55 -0
  183. package/public/css/components/folders.css +5 -0
  184. package/public/css/components/full-height-left-assistant-sidebar-all-pages.css +39 -0
  185. package/public/css/components/full-height-right-actions-rail-pages-that-have-actions.css +267 -0
  186. package/public/css/components/grid-and-list.css +203 -0
  187. package/public/css/components/help-page.css +36 -0
  188. package/public/css/components/hero.css +16 -0
  189. package/public/css/components/images.css +70 -0
  190. package/public/css/components/layout-rhythm-and-spacing-polish.css +139 -0
  191. package/public/css/components/mic-on-any-field-that-wants-typing.css +416 -0
  192. package/public/css/components/modal-fit.css +113 -0
  193. package/public/css/components/page-width.css +23 -0
  194. package/public/css/components/plugins-page.css +9 -0
  195. package/public/css/components/project-cards.css +256 -0
  196. package/public/css/components/project-rows.css +390 -0
  197. package/public/css/components/reordering-the-workspace-cards.css +85 -0
  198. package/public/css/components/responsive-home-and-the-workspace-pages.css +457 -0
  199. package/public/css/components/running-commands-as-toasts.css +327 -0
  200. package/public/css/components/settings-editor-core-config-yml.css +828 -0
  201. package/public/css/components/short-results-as-a-toast-at-the-top.css +203 -0
  202. package/public/css/components/sound-player.css +84 -0
  203. package/public/css/components/suggest-agents-skills-commands.css +112 -0
  204. package/public/css/components/tab-modal.css +142 -0
  205. package/public/css/components/terminals.css +244 -0
  206. package/public/css/components/test-report-media-one-file-per-row-paged.css +144 -0
  207. package/public/css/components/top-toolbar-logo-breadcrumb.css +107 -0
  208. package/public/css/components/video-edit.css +6 -0
  209. package/public/css/components/web-ide-open-with.css +23 -0
  210. package/public/css/components/working-indicators.css +158 -0
  211. package/public/css/components/workspace-cards.css +79 -0
  212. package/public/favicon.ico +0 -0
  213. package/public/favicon.png +0 -0
  214. package/public/htmx.min.js +1 -0
  215. package/public/logo.png +0 -0
  216. package/public/media-player.js +215 -0
  217. package/public/mic.js +255 -0
  218. package/public/narration.js +110 -0
  219. package/public/palette.js +278 -0
  220. package/public/sortable.js +251 -0
  221. package/public/suggest.js +337 -0
  222. package/public/terminal.js +903 -0
  223. package/public/themes/default/theme.css +267 -0
  224. package/public/themes/ember/theme.css +180 -0
  225. package/public/themes/press/theme.css +290 -0
  226. package/public/themes/slate/theme.css +182 -0
  227. package/public/touch-icon.png +0 -0
  228. package/public/ui.js +2721 -0
  229. package/public/vendor/ace/LICENSE.txt +24 -0
  230. package/public/vendor/ace/ace.js +23 -0
  231. package/public/vendor/ace/ext-language_tools.js +8 -0
  232. package/public/vendor/ace/ext-searchbox.js +8 -0
  233. package/public/vendor/ace/mode-css.js +8 -0
  234. package/public/vendor/ace/mode-html.js +8 -0
  235. package/public/vendor/ace/mode-javascript.js +8 -0
  236. package/public/vendor/ace/mode-markdown.js +8 -0
  237. package/public/vendor/ace/mode-sh.js +8 -0
  238. package/public/vendor/ace/mode-yaml.js +8 -0
  239. package/public/vendor/ace/theme-textmate.js +8 -0
  240. package/public/vendor/ace/theme-tomorrow_night.js +8 -0
  241. package/public/vendor/ace/worker-yaml.js +1 -0
  242. package/public/vendor/deep-chat.LICENSE +21 -0
  243. package/public/vendor/deep-chat.bundle.js +1 -0
  244. package/public/vendor/fonts/dm-serif-text-LICENSE.txt +93 -0
  245. package/public/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
  246. package/public/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
  247. package/public/vendor/fonts/figtree-LICENSE.txt +93 -0
  248. package/public/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
  249. package/public/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
  250. package/public/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
  251. package/public/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
  252. package/public/vendor/fonts/jetbrains-mono-LICENSE.txt +93 -0
  253. package/public/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  254. package/public/vendor/fonts/ubuntu-mono-LICENSE.txt +98 -0
  255. package/public/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
  256. package/public/vendor/tabler-LICENSE.txt +21 -0
  257. package/public/vendor/tabler-sprite.svg +1 -0
  258. package/public/vendor/three/LICENSE +21 -0
  259. package/public/vendor/three/addons/loaders/GLTFLoader.js +4860 -0
  260. package/public/vendor/three/addons/utils/BufferGeometryUtils.js +1501 -0
  261. package/public/vendor/three/addons/utils/SkeletonUtils.js +496 -0
  262. package/public/vendor/three/three.core.min.js +6 -0
  263. package/public/vendor/three/three.module.min.js +6 -0
  264. package/public/vendor/three-vrm/LICENSE +22 -0
  265. package/public/vendor/three-vrm/three-vrm.module.min.js +983 -0
  266. package/public/vendor/uikit-icons.min.js +1 -0
  267. package/public/vendor/uikit.min.css +1 -0
  268. package/public/vendor/uikit.min.js +1 -0
  269. package/public/vendor/webship-icons/ui/bookmark-outlined.svg +1 -0
  270. package/public/vendor/webship-icons/ui/headset.svg +1 -0
  271. package/public/vendor/webship-icons/ui/menu.svg +1 -0
  272. package/public/vendor/webship-icons/ui/pause.svg +1 -0
  273. package/public/vendor/webship-icons/ui/play.svg +1 -0
  274. package/public/vendor/webship-icons/ui/share.svg +1 -0
  275. package/public/vendor/webship-icons/ui/sound-off.svg +1 -0
  276. package/public/vendor/webship-icons/ui/sound-on.svg +1 -0
  277. package/public/vendor/webship-icons/ui/stop.svg +1 -0
  278. package/public/vendor/xterm/addon-fit.js +2 -0
  279. package/public/vendor/xterm/addon-unicode11.js +2 -0
  280. package/public/vendor/xterm/addon-web-links.js +2 -0
  281. package/public/vendor/xterm/xterm.css +218 -0
  282. package/public/vendor/xterm/xterm.js +2 -0
  283. package/rag.js +169 -0
  284. package/reader.js +132 -0
  285. package/repo-blobs.js +66 -0
  286. package/report-pdf.js +337 -0
  287. package/row-layout.js +1440 -0
  288. package/screenrc +37 -0
  289. package/screens.js +1063 -0
  290. package/scripts/code-server-start.sh +190 -0
  291. package/scripts/images-claude-finish.js +107 -0
  292. package/scripts/screen-host.sh +114 -0
  293. package/scripts/screen-runner.js +362 -0
  294. package/scripts/screen-then.js +47 -0
  295. package/scripts/update.sh +350 -0
  296. package/server.js +8140 -0
  297. package/settings-file.js +423 -0
  298. package/speech.js +297 -0
  299. package/tab-modal.js +75 -0
  300. package/test-reports.js +260 -0
  301. package/themes.js +381 -0
  302. package/transcripts.js +168 -0
  303. package/urls.js +13 -0
  304. package/validate.js +18 -0
  305. package/version-check.js +94 -0
  306. package/video-edit.js +588 -0
  307. package/waiting.js +85 -0
  308. package/workspace-card-tools.js +41 -0
  309. package/workspace-delete.js +139 -0
  310. package/workspace-types.js +85 -0
  311. package/workspaces.js +557 -0
package/workspaces.js ADDED
@@ -0,0 +1,557 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const yaml = require('js-yaml');
4
+ const { resolveType, typeActions } = require('./workspace-types');
5
+ // What the assistant is called on this workspace's page, and what its composer says when empty.
6
+ // The rule about which level wins lives in speech.js with the rest of the assistant's settings.
7
+ const { assistantIdentity } = require('./speech');
8
+
9
+ // Locate the workspace from this file — core/workspace-app/workspaces.js — so a
10
+ // fresh clone needs no environment setup. The WORKSPACE_* variables still
11
+ // win, which is how the dashboard container points at the bind-mounted tree.
12
+ const APP_ROOT = path.resolve(__dirname, '..', '..');
13
+ const ROOT = process.env.WORKSPACE_ROOT || APP_ROOT;
14
+ const CONFIG_DIR = process.env.WORKSPACE_CONFIG || path.join(ROOT, 'core', 'config');
15
+
16
+ // Presentational-only (icon/subtitle) — everything functional (paths, script names,
17
+ // which workspaces exist) is read live from settings.yml / workspace.<name>.settings.yml
18
+ // and the filesystem, so a new workspace added there shows up with no code change.
19
+ // `icon` is a UIKit icon name (https://getuikit.com/docs/icon), rendered
20
+ // with the vendored uikit-icons.min.js.
21
+ // `noun` is what one item in the folder is called (only real projects are
22
+ // "projects"); `plural` overrides irregular plurals.
23
+ const PRESENTATION = {
24
+ products: { icon: 'tabler:shopping-cart', subtitle: 'Internal & External Products', noun: 'product' },
25
+ dev: { icon: 'tabler:code', subtitle: 'Development Projects', noun: 'project' },
26
+ test: { icon: 'tabler:checkbox', subtitle: 'Testing Environment', noun: 'project' },
27
+ demos: { icon: 'tabler:device-desktop', subtitle: 'Demo Sites', noun: 'demo' },
28
+ sandboxes: { icon: 'tabler:flask', subtitle: 'Experimental Projects', noun: 'sandbox', plural: 'sandboxes' },
29
+ projects: { icon: 'tabler:folder', subtitle: 'Client Projects', noun: 'project' },
30
+ profiles: { icon: 'tabler:world', subtitle: 'Distros', noun: 'profile' },
31
+ 'site-templates': { icon: 'tabler:photo', subtitle: 'Drupal CMS Site Templates', label: 'Site Templates', noun: 'site template' },
32
+ themes: { icon: 'tabler:paint', subtitle: 'Contrib & Private Themes', noun: 'theme' },
33
+ modules: { icon: 'tabler:apps', subtitle: 'Contrib & Private Modules', noun: 'module' },
34
+ libraries: { icon: 'tabler:library', subtitle: 'Third-Party Libraries', noun: 'library', plural: 'libraries' },
35
+ designs: { icon: 'tabler:palette', subtitle: 'Figma Sources & Exports', noun: 'design', kind: 'files' },
36
+ images: { icon: 'tabler:photo', subtitle: 'Images, screenshots and generated pictures', noun: 'image', kind: 'files' },
37
+ videos: { icon: 'tabler:player-play', subtitle: 'Recorded Walkthroughs & Demos', noun: 'video', kind: 'files' },
38
+ audio: { icon: 'tabler:headphones', subtitle: 'Voices & Sounds', noun: 'sound', kind: 'files' },
39
+ avatars: { icon: 'tabler:mood-smile', subtitle: 'Talking AI Characters, Their Agents & Voices', noun: 'avatar', kind: 'projects' },
40
+ docs: { icon: 'tabler:file-text', subtitle: 'Documentation Projects', noun: 'doc', kind: 'files' },
41
+ worklogs: { icon: 'tabler:history', subtitle: 'Session Worklogs', noun: 'worklog', kind: 'files' },
42
+ 'ai-agents': { icon: 'tabler:mood-smile', subtitle: 'AI Automation', label: 'AI Agents', noun: 'agent', kind: 'files' },
43
+ 'ai-skills': { icon: 'tabler:star', subtitle: 'AI Skill Definitions', label: 'AI Skills', noun: 'skill', kind: 'files' },
44
+ 'ai-commands': { icon: 'tabler:command', subtitle: 'Claude Code Slash Commands', label: 'AI Commands', noun: 'command', kind: 'files' },
45
+ 'ai-prompts': { icon: 'tabler:pencil', subtitle: 'Reusable AI Prompts', label: 'AI Prompts', noun: 'prompt', kind: 'files' },
46
+ recipes: { icon: 'tabler:list-check', subtitle: 'Development Recipes', noun: 'recipe' },
47
+ components: { icon: 'tabler:layout-grid', subtitle: 'SDC, React, Canvas Code, HTMX & Web Components', noun: 'component' },
48
+ };
49
+
50
+ function loadYaml(file) {
51
+ try {
52
+ return yaml.load(fs.readFileSync(file, 'utf8')) || {};
53
+ } catch (_) {
54
+ return {};
55
+ }
56
+ }
57
+
58
+ function loadSettings() {
59
+ return loadYaml(path.join(CONFIG_DIR, 'settings.yml'));
60
+ }
61
+
62
+ // The environment, then settings.yml's `sources:`, then the shipped default —
63
+ // the same precedence bootstrap.sh gives the shell side. Read per call so a
64
+ // settings change needs no restart.
65
+ function sourceOf(kind, field, fallback) {
66
+ try {
67
+ const v = (((loadSettings().sources || {})[kind]) || {})[field];
68
+ if (v) return String(v);
69
+ } catch (_) { /* fall through */ }
70
+ return fallback;
71
+ }
72
+
73
+ // The hub's base domain. Locally this is workspace.ddev.site; set
74
+ // `hub_domain:` in settings.yml (plus matching additional_fqdns + DNS +
75
+ // nginx server_names) to run the whole system as a remote development
76
+ // workspace hub on a public domain, e.g. workspace.example.com →
77
+ // dev.workspace.example.com → myproject.dev.workspace.example.com.
78
+ function hubDomain() {
79
+ return loadSettings().hub_domain || `${process.env.DDEV_PROJECT || 'workspace'}.ddev.site`;
80
+ }
81
+
82
+ // Dashboard styling from settings.yml's `style:` block, with the shipped defaults
83
+ // filled in. Read live like everything else, so /settings takes effect on the next
84
+ // page load rather than needing a restart.
85
+ //
86
+ // `mode` is the DEFAULT only: a visitor who has used the theme toggle has their choice
87
+ // in localStorage, and that keeps winning — a default is not an override.
88
+ /**
89
+ * Everything about how the dashboard LOOKS, read from `core/config/style.yml`.
90
+ *
91
+ * Styling is its own concern and now has its own file, beside `theme.<name>.yml`: which palette,
92
+ * which mode, the logos and favicons, the page width, how the assistant is shown. It sat in
93
+ * `settings.yml` under a `style:` key, in a file that is otherwise about where projects are built,
94
+ * who commits, and which API keys this machine holds — so the one file nobody should open casually
95
+ * was also the file you had to open to change a logo.
96
+ *
97
+ * `settings.yml`'s `style:` block still works and fills in anything `style.yml` does not set, so a
98
+ * machine that has not moved its block keeps the appearance it had. The dedicated file wins where
99
+ * the two overlap, because that is the point of having it.
100
+ */
101
+ function loadStyleFile() {
102
+ try {
103
+ const doc = loadYaml(path.join(CONFIG_DIR, 'style.yml'));
104
+ return doc && typeof doc === 'object' ? doc : {};
105
+ } catch (_) {
106
+ return {};
107
+ }
108
+ }
109
+
110
+ function styleSettings() {
111
+ const s = { ...(loadSettings().style || {}), ...loadStyleFile() };
112
+ const one_of = (v, allowed, fallback) => (allowed.includes(String(v)) ? String(v) : fallback);
113
+ return {
114
+ workspaceName: typeof s.workspace_name === 'string' && s.workspace_name.trim() ? s.workspace_name.trim() : 'Workspace',
115
+ mode: one_of(s.mode, ['light', 'dark', 'system'], 'light'),
116
+ editorTheme: one_of(s.editor_theme, ['auto', 'light', 'dark'], 'auto'),
117
+ logoOnLight: s.logo_on_light || 'logo.png',
118
+ logoOnDark: s.logo_on_dark || 'logo.png',
119
+ faviconSvg: s.favicon_svg || '',
120
+ faviconPng: s.favicon_png || 'favicon.png',
121
+ // A separate, larger file: a home-screen icon is 180px, and the 32px tab icon blown up to that
122
+ // is a blurry tile. Both are rendered from the same SVG, so they cannot drift apart.
123
+ touchIcon: s.touch_icon || 'touch-icon.png',
124
+ // Rows before a list pages. A default, not an override: a size picked from a pager
125
+ // wins (it is in a cookie), as with `mode`.
126
+ pageSize: one_of(s.page_size, ['20', '50', '100', '200', 'all'], '50'),
127
+ pageSort: one_of(s.page_sort, ['newest', 'oldest', 'name', 'nameDesc'], 'newest'),
128
+ // Which palette the dashboard wears. Resolved in themes.js — a name that is not a theme
129
+ // falls back to the default, so a typo is an ordinary-looking dashboard rather than an
130
+ // unstyled one.
131
+ theme: typeof s.theme === 'string' && s.theme.trim() ? s.theme.trim() : 'default',
132
+ // How the assistant is shown, and whether it starts open. Defaults, never overrides: a view
133
+ // chosen from the panel's own menu is in localStorage and wins, exactly like `mode`.
134
+ // sidebar — docked left, the width it has always had
135
+ // wide — the same, twice as wide, for a long answer
136
+ // float — a launcher, with the panel over the page
137
+ // full — every page's assistant link goes to /assistant, the whole width
138
+ // How much of the window a page's content may use. comfortable is UIKit's own small container
139
+ // (900px), which is a reading width and the right default; wide and full are offered for the
140
+ // pages that want the room — a command catalog, a settings file, a terminal.
141
+ pageWidth: one_of(s.page_width, ['comfortable', 'wide', 'full'], 'comfortable'),
142
+ assistantView: one_of(s.assistant_view, ['sidebar', 'wide', 'float', 'full'], 'sidebar'),
143
+ // Quoted in the shipped file, because unquoted `yes` is a BOOLEAN in YAML 1.1 and would arrive
144
+ // here as true — which is not in the list and would silently fall back. Both are accepted.
145
+ assistantOpen: one_of(s.assistant_open === true ? 'yes' : s.assistant_open === false ? 'no' : s.assistant_open,
146
+ ['yes', 'no'], 'yes'),
147
+ };
148
+ }
149
+
150
+ // Rebuilt with a 2s TTL memo: still effectively live (a settings.yml edit
151
+ // shows up on the next page load) without re-reading ~17 YAML files for the
152
+ // many helper calls within a single request.
153
+ let _wsCache = null;
154
+ let _wsCacheAt = 0;
155
+ function loadWorkspaces() {
156
+ const now = Date.now();
157
+ if (_wsCache && now - _wsCacheAt < 2000) return _wsCache;
158
+ const map = _loadWorkspacesFresh();
159
+ _wsCache = map;
160
+ _wsCacheAt = now;
161
+ return map;
162
+ }
163
+
164
+ function _loadWorkspacesFresh() {
165
+ const settings = loadSettings();
166
+ const names = Array.isArray(settings.workspaces) ? settings.workspaces : [];
167
+ const backupsRoot = settings.backups || path.join(ROOT, 'backups');
168
+
169
+ const map = {};
170
+ for (const name of names) {
171
+ const wsSettings = loadYaml(path.join(CONFIG_DIR, `workspace.${name}.settings.yml`));
172
+ const doc = wsSettings.doc || {};
173
+ const dir = doc.path || path.join(ROOT, name);
174
+ // The hardcoded table is the default; a workspace's own file overrides it, so a
175
+ // workspace added from the dashboard can carry its icon and description without
176
+ // a code change — which is the only way a new one gets anything but the folder
177
+ // icon.
178
+ const own = wsSettings.presentation || {};
179
+ const pres = { ...(PRESENTATION[name] || { icon: 'tabler:folder', subtitle: name }), ...own };
180
+
181
+ const kind = pres.kind || 'projects';
182
+ // What the folder's projects are FOR — the action policy its rows follow. Chosen in
183
+ // this workspace's own settings file (`type:`), defaulting per shipped folder so an
184
+ // unset file behaves as it always did. See workspace-types.js.
185
+ const type = resolveType(name, wsSettings.type, kind);
186
+ map[name] = {
187
+ key: name,
188
+ dir,
189
+ backupsDir: path.join(backupsRoot, doc.name || name),
190
+ icon: pres.icon,
191
+ subtitle: pres.subtitle,
192
+ label: pres.label || name.charAt(0).toUpperCase() + name.slice(1),
193
+ noun: pres.noun || 'item',
194
+ nounPlural: pres.plural || `${pres.noun || 'item'}s`,
195
+ kind,
196
+ type,
197
+ typeActions: typeActions(type),
198
+ // A workspace that both BUILDS things and HOLDS files — designs runs Penpot instances
199
+ // and keeps the Figma exports beside them, so its page needs the project table and the
200
+ // file list. Off everywhere else: on modules or dev a stray file is noise, not an item.
201
+ alsoFiles: pres.also_files === true,
202
+ // What the COMPOSER menu may do here (`composer:` in this workspace's own file). Only `full` runs
203
+ // install, update and scripts — the site workspaces set it. Anything else, unset included, is
204
+ // `manifest`: composer.json is edited and nothing is installed. See actions/composer.js.
205
+ composerMode: wsSettings.composer === 'full' ? 'full' : 'manifest',
206
+ // The assistant's name and composer placeholder ON THIS WORKSPACE'S PAGE. `assistant:` in
207
+ // settings.yml is the default and `assistant:` here overrides it, so a folder can call its
208
+ // assistant what the work in it is called. Resolved once, here, rather than at each of the
209
+ // five places that name the panel — a heading and a button disagreeing about what the thing
210
+ // is called is worse than either name.
211
+ assistant: assistantIdentity(settings, wsSettings),
212
+ // `assistant: suggestions:` from this workspace's file, unvalidated (assistant-suggestions.js cleans it).
213
+ assistantSuggestions: (wsSettings.assistant || {}).suggestions,
214
+ };
215
+ }
216
+ return map;
217
+ }
218
+
219
+ // After a write to settings.yml the 2s memo would serve the OLD order for up to two seconds —
220
+ // long enough that a dragged card grid re-renders exactly as it was and the drag looks lost.
221
+ function invalidateWorkspaces() {
222
+ _wsCache = null;
223
+ _wsCacheAt = 0;
224
+ }
225
+
226
+ function isValidWorkspace(name) {
227
+ return Object.prototype.hasOwnProperty.call(loadWorkspaces(), name);
228
+ }
229
+
230
+ function workspaceDir(name) {
231
+ const ws = loadWorkspaces()[name];
232
+ return ws ? ws.dir : null;
233
+ }
234
+
235
+ // Discover which of a folder's cmd-*.sh scripts serve a given purpose, by filename
236
+ // convention, instead of hand-mapping every workspace to a fixed script name.
237
+ function findScript(dir, patterns) {
238
+ let files = [];
239
+ try {
240
+ files = fs.readdirSync(dir);
241
+ } catch (_) {
242
+ return null;
243
+ }
244
+ for (const re of patterns) {
245
+ const hit = files.find((f) => re.test(f));
246
+ if (hit) return hit;
247
+ }
248
+ return null;
249
+ }
250
+
251
+ function findBackupScript(dir) {
252
+ return findScript(dir, [/^cmd-tools?-backup-.*\.sh$/]);
253
+ }
254
+
255
+ function findRemoveScript(dir) {
256
+ return findScript(dir, [/^cmd-tools-remove\.sh$/]);
257
+ }
258
+
259
+ function findGraphifyScript(dir) {
260
+ return findScript(dir, [/^cmd-tools?-graphify\.sh$/]);
261
+ }
262
+
263
+ function findRagifyScript(dir) {
264
+ return findScript(dir, [/^cmd-tools?-ragify\.sh$/]);
265
+ }
266
+
267
+
268
+ // Where a builder sits in the list is a SETTING now — `build-order.js`, one order for the machine and
269
+ // one per workspace, over the order the dashboard ships. Required lazily and inside the call, because
270
+ // build-order.js reads CONFIG_DIR from this module: at require time the cycle would hand it an empty
271
+ // object, and the order would silently fall back to alphabetical.
272
+ function sortBuilders(files, dir) {
273
+ // The scope is the workspace the folder IS. Derived rather than passed, so the ~40 callers that only
274
+ // ever had a directory keep working unchanged.
275
+ const scope = path.basename(dir || '') || 'global';
276
+ try {
277
+ return require('./build-order').sortBuilders(files, scope);
278
+ } catch (_) {
279
+ // A broken or missing order file must not cost you the Build form.
280
+ return [...files].sort();
281
+ }
282
+ }
283
+
284
+ function findBuilderScripts(dir) {
285
+ let files = [];
286
+ try {
287
+ files = fs.readdirSync(dir);
288
+ } catch (_) {
289
+ return [];
290
+ }
291
+ files = files
292
+ // cmd-tool-/cmd-tools- scripts are per-project tools (backup, graphify, …), not builders, even
293
+ // when they end in -project.sh.
294
+ //
295
+ // `cmd-automated-testing-*-project.sh` USED to be excluded here as well, and that was wrong:
296
+ // they are builders. Each one builds a whole site and scaffolds its testing stack on top —
297
+ // which is the entire reason the `test` workspace exists — and each carries the same
298
+ // `# workspace-name:` header, takes the same project name and the same --install as any other
299
+ // builder. Hiding them meant the test workspace's Build a new project offered nine of its
300
+ // fourteen builders, and the five you would actually go there for were the missing ones.
301
+ .filter((f) => /^cmd-.*-project\.sh$/.test(f) && !/^cmd-tools?-/.test(f))
302
+ // Not alphabetical: alphabetical put eleven `cmd-automated-testing-…` builds at the top of
303
+ // the test workspace and `cmd-drupalcms2-x-website_starter…` above Drupal and Webship everywhere
304
+ // else, so the first thing in "Build a new project" was never the thing most people came to
305
+ // build. The order is Settings → Build order, over the one the dashboard ships.
306
+ .sort();
307
+ return sortBuilders(files, dir);
308
+ }
309
+
310
+ // Human-readable label for a builder script: the `# workspace-name: ...`
311
+ // header near the top of the cmd-*.sh file, falling back to the filename.
312
+ function builderLabel(dir, script) {
313
+ try {
314
+ const head = fs.readFileSync(path.join(dir, script), 'utf8').slice(0, 500);
315
+ const m = head.match(/^# workspace-name:\s*(.+)$/m);
316
+ if (m) return m[1].trim();
317
+ } catch (_) { /* fall through */ }
318
+ return script;
319
+ }
320
+
321
+ /**
322
+ * A short name for a command, for the line above its description.
323
+ *
324
+ * Short is the point. A builder's `# workspace-name:` is already short — "Webship 11.0.x" — and is
325
+ * used as-is. Everything else gets a name derived from its own file name, which is short by
326
+ * construction and is what the person is looking for in the list anyway:
327
+ *
328
+ * cmd-tools-reclaim.sh -> Reclaim
329
+ * cmd-tools-graphify.sh -> Graphify
330
+ * cmd-all-front-end-build-themes -> All front end build themes
331
+ *
332
+ * The `tool`/`tools` segment is dropped: every one of them has it, so it distinguishes nothing and
333
+ * costs a word in every title. `-project` goes for the same reason — the folder already says these
334
+ * build projects.
335
+ *
336
+ * A long `# workspace-name:` is NOT used. "Sync the workspace tooling from the clone" sitting above
337
+ * "Sync the workspace tooling from the git clone into the live workspace" is the same sentence twice,
338
+ * and the row reads as though it holds two facts when it holds one.
339
+ */
340
+ function commandTitle(dir, script) {
341
+ const declared = builderLabel(dir, script);
342
+ if (declared !== script) {
343
+ // A BUILDER's declared name is a product name — "Website Starter 1.0.x on Drupal CMS 2 (site
344
+ // template)" — and is the title however long it runs. `cmd-*-project.sh` is what the smoke test
345
+ // enforces that header on, so it is also what identifies one.
346
+ if (/^cmd-.*-project\.sh$/.test(script)) return declared;
347
+ // On anything else the header is being used as a free-text label, and it tends to be a sentence
348
+ // that repeats the description: "Sync the workspace tooling from the clone" above "Sync the
349
+ // workspace tooling from the git clone into the live workspace". Kept only while it stays short.
350
+ if (declared.split(/\s+/).length <= 4) return declared;
351
+ }
352
+
353
+ const stem = script.replace(/^cmd-/, '').replace(/\.sh$/, '')
354
+ .replace(/^tools?-/, '')
355
+ .replace(/-project$/, '')
356
+ .replace(/[-_]+/g, ' ')
357
+ .trim();
358
+ if (!stem) return script;
359
+ return stem.charAt(0).toUpperCase() + stem.slice(1);
360
+ }
361
+
362
+ /**
363
+ * What a command says about itself.
364
+ *
365
+ * Read from the script, not from a list kept beside it. Every `cmd-*.sh` already declares this —
366
+ * 218 of the 239 carry an `ARGPARSE_DESCRIPTION`, which is the same sentence `--help` prints — so a
367
+ * separate file of descriptions would be a second copy to forget to update, and it would be wrong
368
+ * the first time somebody edited a command without knowing the file existed.
369
+ *
370
+ * Three sources, in the order they are worth reading:
371
+ *
372
+ * 1. `ARGPARSE_DESCRIPTION` — written to be read by someone running `--help`, so it is already a
373
+ * sentence aimed at a person.
374
+ * 2. `# workspace-name:` — the human name of a builder, when there is no description.
375
+ * 3. The leading comment block — the first real sentence of it. Longer and more internal, but a
376
+ * command with neither of the above still has one.
377
+ *
378
+ * `ARGPARSE_DESCRIPTION` often interpolates shell variables the script sets just above it
379
+ * (`Build a ${distribution_title} ${site_version} project`). Those are resolved from the same file
380
+ * rather than shown raw, because `Build a ${distribution_title} project` in a list reads as a bug.
381
+ * Anything still unresolved is dropped along with its surrounding spaces.
382
+ */
383
+ // The flags a builder actually accepts, read from the args/arg-*.sh file it
384
+ // sources — so the AI context can state the real contract (--install, --launch,
385
+ // --dev, ...) instead of guessing. Empty for scripts without an arg file.
386
+ function commandFlags(dir, script) {
387
+ let text;
388
+ try {
389
+ text = fs.readFileSync(path.join(dir, script), 'utf8');
390
+ } catch (_) { return []; }
391
+ const m = text.match(/args\/(arg-[a-zA-Z0-9_$"{}-]+\.sh)/);
392
+ if (!m) return [];
393
+ // Builders source args/arg-${distribution_name}.sh — resolve the variable from
394
+ // the script's own plain assignments, the way commandDescription does.
395
+ const argFile = m[1].replace(/\$\{?([a-zA-Z_][a-zA-Z0-9_]*)\}?/g, (whole, name) => {
396
+ const assigned = text.match(new RegExp(`^${name}=["']?([^"'\n;]+)["']?`, 'm'));
397
+ return assigned ? assigned[1].trim() : '';
398
+ });
399
+ if (!/^arg-[a-z0-9_-]+\.sh$/.test(argFile)) return [];
400
+ let argText;
401
+ try {
402
+ argText = fs.readFileSync(path.join(ROOT, 'core', 'scripts', 'args', argFile), 'utf8');
403
+ } catch (_) { return []; }
404
+ const flags = [];
405
+ const re = /add_argument\('(-[a-zA-Z])', '(--[a-z-]+)'[\s\S]*?help='([^']*)'/g;
406
+ let match;
407
+ while ((match = re.exec(argText)) !== null) {
408
+ flags.push({ short: match[1], long: match[2], help: match[3] });
409
+ }
410
+ return flags;
411
+ }
412
+
413
+ function commandDescription(dir, script) {
414
+ let text;
415
+ try {
416
+ text = fs.readFileSync(path.join(dir, script), 'utf8');
417
+ } catch (_) {
418
+ return '';
419
+ }
420
+ const head = text.slice(0, 4000);
421
+
422
+ const resolve = (value) => value.replace(/\$\{?([a-zA-Z_][a-zA-Z0-9_]*)\}?/g, (whole, name) => {
423
+ // The script assigns these as plain `name=value` or `name="value"` before using them.
424
+ const assigned = head.match(new RegExp(`^${name}=["']?([^"'\n]+)["']?`, 'm'));
425
+ return assigned ? assigned[1].trim() : '\u0000';
426
+ });
427
+
428
+ const argparse = head.match(/^ARGPARSE_DESCRIPTION=["'](.+?)["']\s*;?\s*$/m);
429
+ if (argparse) {
430
+ const out = resolve(argparse[1]).replace(/\s*\u0000\s*/g, ' ').replace(/\s+/g, ' ').trim();
431
+ if (out) return out;
432
+ }
433
+
434
+ const name = head.match(/^# workspace-name:\s*(.+)$/m);
435
+ if (name) return `Build a ${name[1].trim()} project`;
436
+
437
+ // The leading comment block, minus the shebang and any `# workspace-name:` line. Its first
438
+ // sentence is the one written to say what the command is for.
439
+ const lines = head.split('\n').slice(1);
440
+ const comment = [];
441
+ for (const line of lines) {
442
+ if (/^#\s?/.test(line)) {
443
+ const body = line.replace(/^#\s?/, '').trim();
444
+ if (!body) { if (comment.length) break; continue; }
445
+ if (/^workspace-name:/.test(body)) continue;
446
+ comment.push(body);
447
+ } else if (comment.length) break;
448
+ else if (line.trim()) break;
449
+ }
450
+ const joined = comment.join(' ').trim();
451
+ if (!joined) return '';
452
+ const stop = joined.search(/\.\s|\.$/);
453
+ return (stop > 0 ? joined.slice(0, stop + 1) : joined).trim();
454
+ }
455
+
456
+ // Backup archives for a workspace: ${backups}/<workspace>/*.tar.gz written by the
457
+ // cmd-tool*-backup-*.sh scripts, newest first. It lives here rather than in the page that
458
+ // renders them because the assistant names them too, and neither should own the other.
459
+ function listBackups(key) {
460
+ const meta = loadWorkspaces()[key];
461
+ try {
462
+ return fs.readdirSync(meta.backupsDir)
463
+ .filter((f) => f.endsWith('.tar.gz'))
464
+ .map((f) => {
465
+ const st = fs.statSync(path.join(meta.backupsDir, f));
466
+ return { file: f, size: st.size, mtime: st.mtime };
467
+ })
468
+ .sort((a, b) => b.mtime - a.mtime);
469
+ } catch (_) {
470
+ return [];
471
+ }
472
+ }
473
+
474
+ /**
475
+ * The release this tree is running, and whether that is knowable.
476
+ *
477
+ * Read from core/workspace-app/.app-version, which install.sh, sync-tooling.sh --pull and
478
+ * update.sh all write. Before they did, only the updater wrote it — so a freshly installed
479
+ * machine answered "(untracked)" for ever and was told to update to the version it already
480
+ * had. Across a team that is the support question nobody can answer.
481
+ *
482
+ * package.json is deliberately NOT the source: it says 1.0.0 and has since the first commit,
483
+ * which is worse than no answer because it looks like one.
484
+ *
485
+ * Returns '' when unknown, and callers must render that as unknown rather than as anything
486
+ * else — a wrong version stops the question being asked.
487
+ */
488
+ let _appVersion = { at: 0, value: null };
489
+ function appVersion() {
490
+ if (_appVersion.value !== null && Date.now() - _appVersion.at < 10000) return _appVersion.value;
491
+ let v = '';
492
+ try {
493
+ v = fs.readFileSync(path.join(__dirname, '.app-version'), 'utf8').trim().split('\n')[0];
494
+ } catch (_) { v = ''; }
495
+ _appVersion = { at: Date.now(), value: v };
496
+ return v;
497
+ }
498
+
499
+ /**
500
+ * Is this workspace reachable from the network, and therefore dangerous?
501
+ *
502
+ * There is no authentication anywhere in this dashboard. It runs shell commands, serves a
503
+ * browser terminal at /terminal/<session>, and a Web IDE with write access to the whole tree
504
+ * and a terminal of its own, in a container holding the Docker socket. That is defensible
505
+ * ONLY because everything binds to loopback.
506
+ *
507
+ * Two settings end that, and both get changed for a good-looking reason: DDEV's
508
+ * router_bind_all_interfaces (documented for Let's Encrypt, and for reaching a project from a
509
+ * phone) and a public hub_domain (the README says the same system runs as a remote hub).
510
+ *
511
+ * The installer warns, but the person who flips the setting a month later is not running the
512
+ * installer — so the dashboard says it too, where they are.
513
+ */
514
+ let _exposure = { at: 0, value: null };
515
+ function exposureRisk() {
516
+ if (_exposure.value && Date.now() - _exposure.at < 30000) return _exposure.value;
517
+ const reasons = [];
518
+ try {
519
+ const gc = fs.readFileSync(path.join(process.env.HOME || '', '.ddev', 'global_config.yaml'), 'utf8');
520
+ const m = gc.match(/^router_bind_all_interfaces:\s*(\S+)/m);
521
+ if (m && /^(true|yes)$/i.test(m[1])) {
522
+ reasons.push('DDEV\u2019s router is bound to every interface (router_bind_all_interfaces)');
523
+ }
524
+ } catch (_) { /* no global config readable: say nothing rather than guess */ }
525
+ try {
526
+ const hub = hubDomain();
527
+ if (hub && !/\.ddev\.site$|^localhost$|^127\.0\.0\.1$/.test(hub)) {
528
+ reasons.push(`hub_domain is ${hub}, which is not a local address`);
529
+ }
530
+ } catch (_) { /* ignore */ }
531
+ _exposure = { at: Date.now(), value: reasons };
532
+ return reasons;
533
+ }
534
+
535
+ module.exports = {
536
+ appVersion, exposureRisk,
537
+ sourceOf,
538
+ commandDescription,
539
+ commandFlags,
540
+ commandTitle,
541
+ ROOT,
542
+ loadSettings,
543
+ styleSettings,
544
+ hubDomain,
545
+ CONFIG_DIR,
546
+ loadWorkspaces,
547
+ invalidateWorkspaces,
548
+ isValidWorkspace,
549
+ workspaceDir,
550
+ findBackupScript,
551
+ findRemoveScript,
552
+ findGraphifyScript,
553
+ findRagifyScript,
554
+ findBuilderScripts,
555
+ builderLabel,
556
+ listBackups,
557
+ };