@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.
- package/PLUGINS.md +249 -0
- package/README.md +379 -2
- package/WORKSPACES.md +69 -0
- package/actions/ai-items.js +293 -0
- package/actions/audio.js +48 -0
- package/actions/build-order.js +79 -0
- package/actions/checks.js +95 -0
- package/actions/claude-session.js +104 -0
- package/actions/commands-center.js +258 -0
- package/actions/composer.js +267 -0
- package/actions/ddev-addons.js +90 -0
- package/actions/ddev-mail.js +80 -0
- package/actions/ddev.js +237 -0
- package/actions/folders.js +165 -0
- package/actions/images-svg.js +33 -0
- package/actions/images.js +145 -0
- package/actions/navigation.js +190 -0
- package/actions/node.js +222 -0
- package/actions/notes.js +55 -0
- package/actions/plugins.js +36 -0
- package/actions/project-make.js +28 -0
- package/actions/rag.js +202 -0
- package/actions/row-actions.js +262 -0
- package/actions/screens.js +270 -0
- package/actions/testing.js +121 -0
- package/actions/video-edit.js +54 -0
- package/ai-inventory.js +182 -0
- package/ai-items.js +202 -0
- package/assistant-cast.js +176 -0
- package/assistant-suggestions.js +91 -0
- package/assistant.js +484 -0
- package/audio-voice.js +528 -0
- package/auth-google.js +260 -0
- package/build-ai.js +52 -0
- package/build-order-page.js +191 -0
- package/build-order.js +313 -0
- package/builder-args.js +117 -0
- package/changelog.js +302 -0
- package/chat-threads.js +134 -0
- package/checks.js +105 -0
- package/claude-args.js +282 -0
- package/claude-sessions.js +129 -0
- package/command-author.js +117 -0
- package/command-block.js +20 -0
- package/commands/defaults.yml +159 -0
- package/commands-page.js +460 -0
- package/commands.js +568 -0
- package/cucumber.js +43 -0
- package/ddev-addons-page.js +124 -0
- package/ddev-addons.js +154 -0
- package/ddev-custom-commands.js +96 -0
- package/ddev-domains.js +84 -0
- package/ddev-import.js +93 -0
- package/ddev-mail-page.js +167 -0
- package/ddev-mail.js +110 -0
- package/ddev-options.js +145 -0
- package/ddev-snapshots.js +126 -0
- package/ddev.js +145 -0
- package/docs/DEVELOPER-GUIDE.md +224 -0
- package/docs/README.md +252 -0
- package/docs/REFERENCE.md +684 -0
- package/docs/USER-GUIDE.md +697 -0
- package/docs/screenshots/assistant/avatar-board.png +0 -0
- package/docs/screenshots/assistant/avatar-coffee.png +0 -0
- package/docs/screenshots/assistant/avatar-colours.png +0 -0
- package/docs/screenshots/assistant/avatar-icons.png +0 -0
- package/docs/screenshots/assistant/avatar-moves.png +0 -0
- package/docs/screenshots/assistant/avatar-pick-cookieman.png +0 -0
- package/docs/screenshots/assistant/avatar-settings-voices.png +0 -0
- package/docs/screenshots/assistant/avatar-status.png +0 -0
- package/docs/screenshots/create-card/create-card.png +0 -0
- package/docs/screenshots/create-card/start-ai.png +0 -0
- package/docs/screenshots/guide-clone-address.png +0 -0
- package/docs/screenshots/guide-clone-worktree-off.png +0 -0
- package/docs/screenshots/guide-clone-worktree-on.png +0 -0
- package/docs/screenshots/guide-ddev-menu.png +0 -0
- package/docs/screenshots/guide-domains.png +0 -0
- package/docs/screenshots/guide-home.png +0 -0
- package/docs/screenshots/guide-ide-menu.png +0 -0
- package/docs/screenshots/guide-remove-refusal.png +0 -0
- package/docs/screenshots/guide-rows-worktree.png +0 -0
- package/docs/screenshots/guide-screens.png +0 -0
- package/docs/screenshots/guide-settings.png +0 -0
- package/docs/screenshots/guide-workspace-page.png +0 -0
- package/docs/screenshots/home/card-settings.png +0 -0
- package/docs/screenshots/home/drag-reorder.png +0 -0
- package/docs/screenshots/home/edit-card.png +0 -0
- package/docs/screenshots/images/claude-code-image.png +0 -0
- package/docs/screenshots/images/edit-with-ai.png +0 -0
- package/docs/screenshots/images/gallery.png +0 -0
- package/docs/screenshots/images/generate-description.png +0 -0
- package/docs/screenshots/images/generate-plan.png +0 -0
- package/docs/screenshots/images/providers.png +0 -0
- package/docs/screenshots/images/svg-form.png +0 -0
- package/docs/screenshots/images/svg-result.png +0 -0
- package/docs/screenshots/images/upload.png +0 -0
- package/docs/screenshots/new-folder/2-form.png +0 -0
- package/docs/screenshots/new-folder/8-ddev-options.png +0 -0
- package/docs/screenshots/new-folder/files-folder.png +0 -0
- package/docs/screenshots/projects/ddev-custom-commands.png +0 -0
- package/docs/screenshots/projects/state-chip-stop.png +0 -0
- package/docs/screenshots/readme/dashboard-dark.png +0 -0
- package/docs/screenshots/readme/dashboard-light.png +0 -0
- package/docs/screenshots/readme/dev-dark.png +0 -0
- package/docs/screenshots/readme/dev-light.png +0 -0
- package/docs/screenshots/readme/docs-dark.png +0 -0
- package/docs/screenshots/readme/docs-light.png +0 -0
- package/docs/screenshots/readme/icons/ai.svg +4 -0
- package/docs/screenshots/readme/icons/code.svg +4 -0
- package/docs/screenshots/readme/icons/docs.svg +4 -0
- package/docs/screenshots/readme/icons/services.svg +4 -0
- package/docs/screenshots/readme/icons/sites.svg +4 -0
- package/docs/screenshots/readme/test-report-dark.png +0 -0
- package/docs/screenshots/readme/test-report-light.png +0 -0
- package/docs/screenshots/readme/test-scenarios.png +0 -0
- package/docs/screenshots/readme/test-steps.png +0 -0
- package/docs/screenshots/readme/videos-dark.png +0 -0
- package/docs/screenshots/readme/videos-light.png +0 -0
- package/docs/screenshots/screens-menu.png +0 -0
- package/docs/screenshots/screens-new.png +0 -0
- package/docs/screenshots/settings/auth-google-lists.png +0 -0
- package/docs/screenshots/settings/delete-1-row.png +0 -0
- package/docs/screenshots/settings/delete-2-confirm.png +0 -0
- package/docs/screenshots/settings/delete-3-type-name.png +0 -0
- package/docs/screenshots/settings/delete-4-type-yes.png +0 -0
- package/event-log.js +59 -0
- package/files-browser.js +247 -0
- package/folders.js +121 -0
- package/format.js +73 -0
- package/git-info.js +81 -0
- package/github-auth.js +62 -0
- package/html.js +10 -0
- package/icons.js +83 -0
- package/images-ai.js +447 -0
- package/images-create.js +226 -0
- package/images-page.js +417 -0
- package/images-records.js +39 -0
- package/images-render.js +36 -0
- package/images-svg-core.js +214 -0
- package/images-svg-sanitize.js +188 -0
- package/images-svg.js +241 -0
- package/images.js +270 -0
- package/jobs.js +430 -0
- package/linger.js +21 -0
- package/lists.js +292 -0
- package/media-info.js +136 -0
- package/navigation-page.js +264 -0
- package/navigation.js +394 -0
- package/notes.js +0 -0
- package/package.json +47 -4
- package/palette-index.js +46 -0
- package/playwright.config.js +23 -0
- package/plugin-install.js +239 -0
- package/plugin-loader.js +466 -0
- package/plugin-manifest.js +83 -0
- package/plugin-settings.js +44 -0
- package/plugins/workspace-notes/index.js +59 -0
- package/plugins-page.js +103 -0
- package/project-make.js +241 -0
- package/project-name.js +210 -0
- package/public/assistant-avatar-moves.js +646 -0
- package/public/assistant-avatar-ui.js +1223 -0
- package/public/assistant-avatar.js +530 -0
- package/public/avatar/druplicon.vrm +0 -0
- package/public/css/base.css +260 -0
- package/public/css/components/ai-assistant.css +263 -0
- package/public/css/components/assistant-avatar.css +279 -0
- package/public/css/components/assistant-views.css +198 -0
- package/public/css/components/build-order.css +74 -0
- package/public/css/components/clone-as-a-git-worktree.css +30 -0
- package/public/css/components/command-palette.css +61 -0
- package/public/css/components/commands-center.css +139 -0
- package/public/css/components/confirm-dialog.css +40 -0
- package/public/css/components/create-card.css +8 -0
- package/public/css/components/ddev-menu.css +23 -0
- package/public/css/components/deep-chat.css +17 -0
- package/public/css/components/density.css +11 -0
- package/public/css/components/floating-assistant-workspace-pages.css +55 -0
- package/public/css/components/folders.css +5 -0
- package/public/css/components/full-height-left-assistant-sidebar-all-pages.css +39 -0
- package/public/css/components/full-height-right-actions-rail-pages-that-have-actions.css +267 -0
- package/public/css/components/grid-and-list.css +203 -0
- package/public/css/components/help-page.css +36 -0
- package/public/css/components/hero.css +16 -0
- package/public/css/components/images.css +70 -0
- package/public/css/components/layout-rhythm-and-spacing-polish.css +139 -0
- package/public/css/components/mic-on-any-field-that-wants-typing.css +416 -0
- package/public/css/components/modal-fit.css +113 -0
- package/public/css/components/page-width.css +23 -0
- package/public/css/components/plugins-page.css +9 -0
- package/public/css/components/project-cards.css +256 -0
- package/public/css/components/project-rows.css +390 -0
- package/public/css/components/reordering-the-workspace-cards.css +85 -0
- package/public/css/components/responsive-home-and-the-workspace-pages.css +457 -0
- package/public/css/components/running-commands-as-toasts.css +327 -0
- package/public/css/components/settings-editor-core-config-yml.css +828 -0
- package/public/css/components/short-results-as-a-toast-at-the-top.css +203 -0
- package/public/css/components/sound-player.css +84 -0
- package/public/css/components/suggest-agents-skills-commands.css +112 -0
- package/public/css/components/tab-modal.css +142 -0
- package/public/css/components/terminals.css +244 -0
- package/public/css/components/test-report-media-one-file-per-row-paged.css +144 -0
- package/public/css/components/top-toolbar-logo-breadcrumb.css +107 -0
- package/public/css/components/video-edit.css +6 -0
- package/public/css/components/web-ide-open-with.css +23 -0
- package/public/css/components/working-indicators.css +158 -0
- package/public/css/components/workspace-cards.css +79 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/public/htmx.min.js +1 -0
- package/public/logo.png +0 -0
- package/public/media-player.js +215 -0
- package/public/mic.js +255 -0
- package/public/narration.js +110 -0
- package/public/palette.js +278 -0
- package/public/sortable.js +251 -0
- package/public/suggest.js +337 -0
- package/public/terminal.js +903 -0
- package/public/themes/default/theme.css +267 -0
- package/public/themes/ember/theme.css +180 -0
- package/public/themes/press/theme.css +290 -0
- package/public/themes/slate/theme.css +182 -0
- package/public/touch-icon.png +0 -0
- package/public/ui.js +2721 -0
- package/public/vendor/ace/LICENSE.txt +24 -0
- package/public/vendor/ace/ace.js +23 -0
- package/public/vendor/ace/ext-language_tools.js +8 -0
- package/public/vendor/ace/ext-searchbox.js +8 -0
- package/public/vendor/ace/mode-css.js +8 -0
- package/public/vendor/ace/mode-html.js +8 -0
- package/public/vendor/ace/mode-javascript.js +8 -0
- package/public/vendor/ace/mode-markdown.js +8 -0
- package/public/vendor/ace/mode-sh.js +8 -0
- package/public/vendor/ace/mode-yaml.js +8 -0
- package/public/vendor/ace/theme-textmate.js +8 -0
- package/public/vendor/ace/theme-tomorrow_night.js +8 -0
- package/public/vendor/ace/worker-yaml.js +1 -0
- package/public/vendor/deep-chat.LICENSE +21 -0
- package/public/vendor/deep-chat.bundle.js +1 -0
- package/public/vendor/fonts/dm-serif-text-LICENSE.txt +93 -0
- package/public/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
- package/public/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/figtree-LICENSE.txt +93 -0
- package/public/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
- package/public/vendor/fonts/jetbrains-mono-LICENSE.txt +93 -0
- package/public/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/ubuntu-mono-LICENSE.txt +98 -0
- package/public/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
- package/public/vendor/tabler-LICENSE.txt +21 -0
- package/public/vendor/tabler-sprite.svg +1 -0
- package/public/vendor/three/LICENSE +21 -0
- package/public/vendor/three/addons/loaders/GLTFLoader.js +4860 -0
- package/public/vendor/three/addons/utils/BufferGeometryUtils.js +1501 -0
- package/public/vendor/three/addons/utils/SkeletonUtils.js +496 -0
- package/public/vendor/three/three.core.min.js +6 -0
- package/public/vendor/three/three.module.min.js +6 -0
- package/public/vendor/three-vrm/LICENSE +22 -0
- package/public/vendor/three-vrm/three-vrm.module.min.js +983 -0
- package/public/vendor/uikit-icons.min.js +1 -0
- package/public/vendor/uikit.min.css +1 -0
- package/public/vendor/uikit.min.js +1 -0
- package/public/vendor/webship-icons/ui/bookmark-outlined.svg +1 -0
- package/public/vendor/webship-icons/ui/headset.svg +1 -0
- package/public/vendor/webship-icons/ui/menu.svg +1 -0
- package/public/vendor/webship-icons/ui/pause.svg +1 -0
- package/public/vendor/webship-icons/ui/play.svg +1 -0
- package/public/vendor/webship-icons/ui/share.svg +1 -0
- package/public/vendor/webship-icons/ui/sound-off.svg +1 -0
- package/public/vendor/webship-icons/ui/sound-on.svg +1 -0
- package/public/vendor/webship-icons/ui/stop.svg +1 -0
- package/public/vendor/xterm/addon-fit.js +2 -0
- package/public/vendor/xterm/addon-unicode11.js +2 -0
- package/public/vendor/xterm/addon-web-links.js +2 -0
- package/public/vendor/xterm/xterm.css +218 -0
- package/public/vendor/xterm/xterm.js +2 -0
- package/rag.js +169 -0
- package/reader.js +132 -0
- package/repo-blobs.js +66 -0
- package/report-pdf.js +337 -0
- package/row-layout.js +1439 -0
- package/screenrc +37 -0
- package/screens.js +1063 -0
- package/scripts/code-server-start.sh +190 -0
- package/scripts/images-claude-finish.js +107 -0
- package/scripts/screen-host.sh +114 -0
- package/scripts/screen-runner.js +362 -0
- package/scripts/screen-then.js +47 -0
- package/scripts/update.sh +350 -0
- package/server.js +7923 -0
- package/settings-file.js +423 -0
- package/speech.js +297 -0
- package/tab-modal.js +75 -0
- package/test-reports.js +260 -0
- package/themes.js +381 -0
- package/transcripts.js +168 -0
- package/urls.js +13 -0
- package/validate.js +18 -0
- package/version-check.js +94 -0
- package/video-edit.js +588 -0
- package/waiting.js +85 -0
- package/workspace-card-tools.js +41 -0
- package/workspace-delete.js +139 -0
- package/workspace-types.js +85 -0
- package/workspaces.js +553 -0
package/ddev.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
/**
|
|
4
|
+
* The DDEV verbs a row's menu offers, as a table rather than as six near-identical branches.
|
|
5
|
+
*
|
|
6
|
+
* `needsRunning` is refused rather than attempted: `ddev drush` on a stopped project produces a
|
|
7
|
+
* wall of connection errors instead of a sentence. `toBackups` writes into the workspace's backups
|
|
8
|
+
* directory, so an export lands where the restore button looks for it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ONE `ddev start` at a time, machine-wide.
|
|
13
|
+
*
|
|
14
|
+
* Every router-mutating verb (`start`, `stop`, `restart`, `poweroff`, `delete`) tears the
|
|
15
|
+
* SHARED `ddev-router` down and brings it back up with the routes of whatever is running.
|
|
16
|
+
* Run two at once and they race on one container: one removes it while the other has just
|
|
17
|
+
* created it, and the loser can leave it removed for good. What that looks like is the whole
|
|
18
|
+
* workspace going dark — every *.ddev.site address, the dashboard's own hub included, with
|
|
19
|
+
* healthy web containers behind it and DDEV then refusing to start anything at all:
|
|
20
|
+
*
|
|
21
|
+
* Failed to start workspace: Error response from daemon: No such container: ddev-router
|
|
22
|
+
*
|
|
23
|
+
* Recovering needs `ddev poweroff`. It is easy to trigger: five clicks on Start in
|
|
24
|
+
* thirty-eight seconds did it here, because each one is its own detached screen session.
|
|
25
|
+
*
|
|
26
|
+
* `flock` and not an in-process queue: the dashboard is not the only door. A person at a
|
|
27
|
+
* terminal, an agent, and a cmd-*.sh script all run `ddev` too, and a lock in this process
|
|
28
|
+
* would not be seen by any of them. The lock file lives at the root of the workspace tree,
|
|
29
|
+
* which is bind-mounted at the SAME absolute path inside the container — so the host and the
|
|
30
|
+
* dashboard take the same lock.
|
|
31
|
+
*
|
|
32
|
+
* The command runs EXACTLY ONCE. The obvious shape — `flock -n … cmd || flock -w … cmd` —
|
|
33
|
+
* is wrong: `||` fires on any non-zero exit, so a start that failed on its own merits would
|
|
34
|
+
* be run a second time. So the lock is probed with `true` purely to decide whether to say
|
|
35
|
+
* "waiting", and the real call is a single bounded acquire whose status is the session's.
|
|
36
|
+
*
|
|
37
|
+
* NO SHELL VARIABLES IN HERE. The line travels through two shells — the runner builds
|
|
38
|
+
* `bash -lc "… screen -dmS <s> bash -lc \"<this>\" …"` — so the OUTER one expands anything
|
|
39
|
+
* starting with `$` before the inner one ever sees it. A first version captured `rc=$?` and
|
|
40
|
+
* tested it, and the session logged `[: : integer expected` because `$rc` had already been
|
|
41
|
+
* expanded away to nothing by the outer shell. Keeping the string free of `$` makes it
|
|
42
|
+
* immune to however many quoting layers it passes through.
|
|
43
|
+
*/
|
|
44
|
+
const ROUTER_VERBS = new Set(['start', 'stop', 'restart', 'poweroff', 'delete']);
|
|
45
|
+
|
|
46
|
+
function routerLockPath() {
|
|
47
|
+
const { ROOT } = require('./workspaces');
|
|
48
|
+
return path.join(ROOT, '.ddev-router.lock');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Wrap a command line so it holds the router lock while it runs. */
|
|
52
|
+
function serialiseRouter(cmdLine) {
|
|
53
|
+
const lock = JSON.stringify(routerLockPath());
|
|
54
|
+
const busy = 'Waiting: another ddev start/stop/restart holds the router lock. '
|
|
55
|
+
+ 'They cannot run at once — each one recreates the shared ddev-router.';
|
|
56
|
+
// The probe cannot fail the session: `|| echo` only decides whether to explain the wait.
|
|
57
|
+
// The second flock is the last command, so ITS status is the session's status.
|
|
58
|
+
// DDEV_ROUTER_LOCK_HELD tells the `ddev` shim (core/scripts/ddev-shim/ddev, on PATH in
|
|
59
|
+
// this container) that the lock is ALREADY held by this very command, so it runs instead
|
|
60
|
+
// of taking the same lock again. Without it the two protections deadlock each other:
|
|
61
|
+
// proven — an outer flock plus an unaware shim hangs for the full 1800s timeout.
|
|
62
|
+
return `{ flock -n ${lock} true || echo ${JSON.stringify(busy)} ; } ; `
|
|
63
|
+
+ `flock -w 1800 ${lock} env DDEV_ROUTER_LOCK_HELD=1 ${cmdLine}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Does this argv touch the router? */
|
|
67
|
+
function touchesRouter(args) { return ROUTER_VERBS.has(String((args || [])[0] || '')); }
|
|
68
|
+
|
|
69
|
+
const DDEV_ACTIONS = {
|
|
70
|
+
'/actions/ddev-restart': { icon: '🔄', label: 'ddev restart', args: ['restart'], ms: 5 * 60 * 1000 },
|
|
71
|
+
'/actions/ddev-describe': { icon: 'ℹ️', label: 'ddev describe', args: ['describe'], ms: 60 * 1000 },
|
|
72
|
+
'/actions/ddev-logs': { icon: '📜', label: 'ddev logs', args: ['logs', '-s', 'web', '--tail', '100'], ms: 60 * 1000 },
|
|
73
|
+
'/actions/ddev-drush-cr': { icon: '⚡', label: 'ddev drush cr', args: ['drush', 'cr'], ms: 5 * 60 * 1000, needsRunning: true },
|
|
74
|
+
'/actions/ddev-drush-uli': { icon: '🔑', label: 'ddev drush uli', args: ['drush', 'uli'], ms: 60 * 1000, needsRunning: true },
|
|
75
|
+
'/actions/ddev-export-db': { icon: '💾', label: 'ddev export-db', args: ['export-db'], ms: 10 * 60 * 1000, needsRunning: true, toBackups: true },
|
|
76
|
+
// Long ones run detached in a screen session: a composer update outlives any
|
|
77
|
+
// page, and it must be watchable and reattachable rather than tied to a toast.
|
|
78
|
+
'/actions/ddev-composer-install': { icon: '📦', label: 'ddev composer install', args: ['composer', 'install'], ms: 60 * 60 * 1000, inScreen: true },
|
|
79
|
+
'/actions/ddev-composer-update': { icon: '⬆️', label: 'ddev composer update', args: ['composer', 'update'], ms: 60 * 60 * 1000, inScreen: true },
|
|
80
|
+
'/actions/ddev-drush-updb': { icon: '🗃️', label: 'ddev drush updb', args: ['drush', 'updb', '-y'], ms: 30 * 60 * 1000, needsRunning: true, inScreen: true },
|
|
81
|
+
'/actions/ddev-drush-cim': { icon: '📥', label: 'ddev drush cim', args: ['drush', 'cim', '-y'], ms: 30 * 60 * 1000, needsRunning: true, inScreen: true },
|
|
82
|
+
'/actions/ddev-drush-cex': { icon: '📤', label: 'ddev drush cex', args: ['drush', 'cex', '-y'], ms: 30 * 60 * 1000, needsRunning: true, inScreen: true },
|
|
83
|
+
'/actions/ddev-drush-status': { icon: '🩺', label: 'ddev drush status', args: ['drush', 'status'], ms: 2 * 60 * 1000, needsRunning: true },
|
|
84
|
+
// A snapshot can be named, and one that is not gets DDEV's timestamp. `needsRunning` is honest
|
|
85
|
+
// rather than strict: `ddev snapshot` will START a stopped project to take one and stop it again,
|
|
86
|
+
// which is a surprise worth not springing on somebody who clicked a menu item.
|
|
87
|
+
'/actions/ddev-snapshot': { icon: '📸', label: 'ddev snapshot', args: ['snapshot'], ms: 15 * 60 * 1000, needsRunning: true, named: true },
|
|
88
|
+
// Restoring REPLACES the database — there is no merge and no undo, which is why it is confirmed
|
|
89
|
+
// and why it runs in a screen: a large database takes minutes and must be watchable.
|
|
90
|
+
'/actions/ddev-snapshot-restore': { icon: '⏮️', label: 'ddev snapshot restore', args: ['snapshot', 'restore'], ms: 30 * 60 * 1000, destructive: true, inScreen: true, named: true },
|
|
91
|
+
// Removing one is `--cleanup --name <n> -y`; it takes the same confirm, because a snapshot is
|
|
92
|
+
// somebody's only copy of a database often enough.
|
|
93
|
+
'/actions/ddev-snapshot-delete': { icon: '🗑️', label: 'ddev snapshot cleanup', args: ['snapshot', '--cleanup', '-y'], ms: 5 * 60 * 1000, destructive: true, named: true },
|
|
94
|
+
'/actions/ddev-mutagen-sync': { icon: '🔃', label: 'ddev mutagen sync', args: ['mutagen', 'sync'], ms: 10 * 60 * 1000 },
|
|
95
|
+
// Step debugging, on and off. It is a toggle in the web container rather than a config change, so
|
|
96
|
+
// it costs nothing and needs no restart — which is exactly why it belongs on a menu instead of in
|
|
97
|
+
// somebody's shell history. Xdebug makes PHP roughly three times slower while it is on, so turning
|
|
98
|
+
// it OFF is the half people forget, and the menu says which state it is in before either is clicked.
|
|
99
|
+
'/actions/ddev-xdebug-on': { icon: '🐞', label: 'ddev xdebug on', args: ['xdebug', 'on'], ms: 2 * 60 * 1000, needsRunning: true },
|
|
100
|
+
'/actions/ddev-xdebug-off': { icon: '🐞', label: 'ddev xdebug off', args: ['xdebug', 'off'], ms: 2 * 60 * 1000, needsRunning: true },
|
|
101
|
+
// Which it is now. `ddev describe` does not report it — checked — so a row cannot show the state
|
|
102
|
+
// without a subprocess of its own, and asking is a click rather than a cost on every render.
|
|
103
|
+
'/actions/ddev-xdebug-status': { icon: '🐞', label: 'ddev xdebug status', args: ['xdebug', 'status'], ms: 60 * 1000, needsRunning: true },
|
|
104
|
+
// Your SSH keys, into the container that lends them to every project. It is what makes a composer
|
|
105
|
+
// install against a private repository work, and it is INTERACTIVE — ssh-add asks for the
|
|
106
|
+
// passphrase — so it runs in a screen session you can open and type into. Nothing else here needs
|
|
107
|
+
// a human at the keyboard, which is exactly why it would be baffling as a fire-and-forget button.
|
|
108
|
+
'/actions/ddev-auth-ssh': { icon: '🔑', label: 'ddev auth ssh', args: ['auth', 'ssh'], ms: 10 * 60 * 1000, inScreen: true },
|
|
109
|
+
// A public tunnel to this project, for showing somebody the work. It RUNS UNTIL IT IS STOPPED and
|
|
110
|
+
// it puts a local site on the open internet, so: a screen session (the URL it prints is in there,
|
|
111
|
+
// and closing a browser tab must not take the tunnel down with it), and the confirm dialog — this
|
|
112
|
+
// is the only verb here whose effect is visible to people who are not on this machine.
|
|
113
|
+
'/actions/ddev-share': { icon: '🌍', label: 'ddev share', args: ['share'], ms: 8 * 60 * 60 * 1000, needsRunning: true, destructive: true, inScreen: true,
|
|
114
|
+
confirmWhy: 'puts this project on the open internet' },
|
|
115
|
+
// When a project is broken, these are the four questions worth asking before anyone starts
|
|
116
|
+
// guessing — Docker and the network, the certificate, the ports, and whether the database volume
|
|
117
|
+
// is the version the config claims. Each reads and reports; none of them changes anything, which
|
|
118
|
+
// is why they are one group and why none is confirmed.
|
|
119
|
+
'/actions/ddev-diagnose': { icon: '🩺', label: 'ddev utility diagnose', args: ['utility', 'diagnose'], ms: 5 * 60 * 1000 },
|
|
120
|
+
'/actions/ddev-tls-diagnose': { icon: '🔏', label: 'ddev utility tls-diagnose', args: ['utility', 'tls-diagnose'], ms: 5 * 60 * 1000 },
|
|
121
|
+
'/actions/ddev-port-diagnose': { icon: '🔌', label: 'ddev utility port-diagnose', args: ['utility', 'port-diagnose'], ms: 5 * 60 * 1000 },
|
|
122
|
+
// The one that answers "it started, and then the database was empty / the wrong version": the
|
|
123
|
+
// volume on disk against what config.yaml says it should be.
|
|
124
|
+
'/actions/ddev-db-match': { icon: '🗄️', label: 'ddev utility check-db-match', args: ['utility', 'check-db-match'], ms: 2 * 60 * 1000 },
|
|
125
|
+
// Mutagen beyond `sync`. Status answers the question a slow or stale project raises — is the sync
|
|
126
|
+
// behind, or is it broken — and it says plainly when Mutagen is not enabled at all, which is worth
|
|
127
|
+
// knowing before anybody blames it.
|
|
128
|
+
'/actions/ddev-mutagen-status': { icon: '🔃', label: 'ddev mutagen status', args: ['mutagen', 'status'], ms: 2 * 60 * 1000 },
|
|
129
|
+
// Reset STOPS THE PROJECT and removes the sync volume, which is why it is confirmed: it is the
|
|
130
|
+
// answer to a sync that cannot be repaired, and it is not a thing to click while somebody is
|
|
131
|
+
// working. The files on disk are the source; the volume is rebuilt on the next start.
|
|
132
|
+
'/actions/ddev-mutagen-reset': { icon: '♻️', label: 'ddev mutagen reset', args: ['mutagen', 'reset'], ms: 10 * 60 * 1000, destructive: true,
|
|
133
|
+
confirmWhy: 'stops the project and removes its sync volume' },
|
|
134
|
+
// Destructive: every table goes. It takes a dump into the workspace's backups
|
|
135
|
+
// folder first, because there is nothing else keeping that database.
|
|
136
|
+
'/actions/ddev-drush-sql-drop': { icon: '🧨', label: 'ddev drush sql:drop', args: ['drush', 'sql:drop', '-y'], ms: 10 * 60 * 1000, needsRunning: true, destructive: true, backupFirst: true },
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// The project's OWN ddev commands, from every .ddev/commands/<service>/ folder. See ddev-custom-commands.js.
|
|
140
|
+
function projectDdevCommands(projectDir) {
|
|
141
|
+
return require('./ddev-custom-commands').customCommands(projectDir).map((c) => ({ ...c, help: c.description }));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = { DDEV_ACTIONS, projectDdevCommands,
|
|
145
|
+
serialiseRouter, touchesRouter, routerLockPath, ROUTER_VERBS };
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Developer guide
|
|
2
|
+
|
|
3
|
+
For changing the tooling itself: adding a command, a builder, a workspace, or a dashboard feature.
|
|
4
|
+
If you only want to *use* the workspace, [USER-GUIDE.md](USER-GUIDE.md) is the one you want.
|
|
5
|
+
|
|
6
|
+
Everything here is enforced somewhere — by `cmd-smoke-test.sh`, by a unit test, or by a failure that
|
|
7
|
+
has actually happened on a real machine. Where a rule exists because something broke, it says so.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## The shape of it
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
~/workspace/
|
|
15
|
+
├── core/
|
|
16
|
+
│ ├── scripts/ the whole build chain, shared by every command
|
|
17
|
+
│ │ ├── bootstrap.sh locates the workspace from its own path
|
|
18
|
+
│ │ ├── bootstrap-libraries.sh argparse, YAML, progress bar
|
|
19
|
+
│ │ ├── bootstrap-functions.sh an explicit source list — NOT a glob
|
|
20
|
+
│ │ ├── args/arg-*.sh one argument surface per command family
|
|
21
|
+
│ │ ├── functions/fun-*.sh where the work actually happens
|
|
22
|
+
│ │ └── tests/cmd-smoke-test.sh the test suite for the commands
|
|
23
|
+
│ ├── config/
|
|
24
|
+
│ │ ├── settings.yml the registry: workspaces, secrets, style
|
|
25
|
+
│ │ └── workspace.<name>.settings.yml one per workspace
|
|
26
|
+
│ └── workspace-app/ the dashboard (node:http + HTMX, no framework)
|
|
27
|
+
└── <workspace>/ dev, test, products, … each with its own cmd-*.sh
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**A fresh clone works with no setup.** Every `cmd-*.sh` resolves `WORKSPACE_SCRIPTS` from its own
|
|
31
|
+
`${BASH_SOURCE[0]}` before sourcing `bootstrap.sh`, which derives `WORKSPACE_PATH`, `_ROOT` and
|
|
32
|
+
`_CONFIG` from itself. There is no installer step for this and no `.bashrc` edit; exported
|
|
33
|
+
`WORKSPACE_*` values still win, which is how the dashboard container points at its bind-mounted tree.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Hard rules
|
|
38
|
+
|
|
39
|
+
These are not style preferences. Each one has cost somebody a rebuild or an outage.
|
|
40
|
+
|
|
41
|
+
| | |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| **DDEV only** | `ddev composer`, `ddev drush`, `ddev delete -y -O`, `ddev export-db`. Never host composer, drush or mysql |
|
|
44
|
+
| **Never write a `$databases` block into `settings.php`** | DDEV's `settings.ddev.php` owns it. `set_default_settings` only ensures the config sync directory |
|
|
45
|
+
| **`ddev start` takes `-y`; `ddev stop` does not** | |
|
|
46
|
+
| **Every `cmd-*-project.sh` carries `# workspace-name: <Human Name>`** | the smoke test enforces it and the Build dropdown shows it |
|
|
47
|
+
| **Spawned commands must close stdin** | `stdio: ['ignore','pipe','pipe']` — `ddev`'s `docker exec -i` hangs for ever on an open stdin pipe |
|
|
48
|
+
| **`--help` is only safe on argparse scripts** | never "test" a non-argparse `cmd-*.sh` by running it |
|
|
49
|
+
| **Never hardcode a person** | `contributor.name`/`.email` in `settings.yml`. A shared tool that names one author makes everyone else's commits claim someone else's work |
|
|
50
|
+
|
|
51
|
+
**A DDEV project does not come back by itself.** Its containers carry `restart: no`, so after a reboot
|
|
52
|
+
every workspace URL answers 404 from a router with nothing behind it. `core/scripts/autostart.sh
|
|
53
|
+
--enable` installs the systemd user service that fixes it. Do **not** reach for a Docker
|
|
54
|
+
`restart: unless-stopped` policy instead: DDEV re-pushes router config on `ddev start`, so a container
|
|
55
|
+
resurrected behind its back comes up with no route and answers nothing — the same 404, harder to find.
|
|
56
|
+
|
|
57
|
+
**Every router-mutating verb serialises.** `start`, `stop`, `restart`, `poweroff` and `delete` each
|
|
58
|
+
tear down and recreate the one shared `ddev-router`; two at once can leave it destroyed, which takes
|
|
59
|
+
every `*.ddev.site` address down — the dashboard included — while the web containers sit there
|
|
60
|
+
healthy. `core/scripts/ddev-shim/ddev` holds an `flock` for every caller, and `DDEV_ROUTER_LOCK_HELD`
|
|
61
|
+
stops a lock being taken twice by nested callers.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Adding a command
|
|
66
|
+
|
|
67
|
+
A `cmd-*.sh` is a thin declaration; the work lives in `core/`.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
#!/usr/bin/env bash
|
|
71
|
+
# workspace-name: My Thing
|
|
72
|
+
|
|
73
|
+
WORKSPACE_SCRIPTS="${WORKSPACE_SCRIPTS:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../core/scripts" && pwd)}";
|
|
74
|
+
source ${WORKSPACE_SCRIPTS}/bootstrap.sh || exit 1 ;
|
|
75
|
+
|
|
76
|
+
eval $(parse_yaml ${WORKSPACE_CONFIG}/workspace.dev.settings.yml);
|
|
77
|
+
|
|
78
|
+
ARGPARSE_DESCRIPTION="What it does, in one line"
|
|
79
|
+
source ${WORKSPACE_SCRIPTS}/args/arg-my-thing.sh || exit 1 ;
|
|
80
|
+
|
|
81
|
+
shift $#;
|
|
82
|
+
cd ${WORKSPACE_ROOT}/${doc_name};
|
|
83
|
+
my_thing_command ;
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then `args/arg-my-thing.sh` (an `argparse` heredoc) and `functions/fun-my-thing.sh`.
|
|
87
|
+
|
|
88
|
+
**Add the function file to `bootstrap-functions.sh`.** It is an explicit `source` list, not a glob, so
|
|
89
|
+
a new `fun-*.sh` stays invisible until it is listed there — and the failure is a "command not found"
|
|
90
|
+
a long way from the cause.
|
|
91
|
+
|
|
92
|
+
Three argparse traps, each found by hitting it:
|
|
93
|
+
|
|
94
|
+
- **`store_true` yields the string `yes`**, so test `[ "${FLAG}" == 'yes' ]`.
|
|
95
|
+
- **The dest becomes a shell variable**, so avoid names bash already owns. `--hostname` would clobber
|
|
96
|
+
`$HOSTNAME`; the clone command uses `--extra-hostname` for exactly that reason.
|
|
97
|
+
- **Several `store_const` aliases sharing one dest overwrite each other.** Use one option with a value
|
|
98
|
+
(`-a start|stop|status`) instead.
|
|
99
|
+
|
|
100
|
+
Read a flag that a caller may not set as `${FLAG:-}`: `remove_project` is called from the dashboard
|
|
101
|
+
and from other scripts that know nothing about `--force`.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Adding a builder
|
|
106
|
+
|
|
107
|
+
A builder declares five distribution facts **inline** and lets `build_distribution` do the rest:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
site_version="~11.0.0"
|
|
111
|
+
distribution_name="webship"
|
|
112
|
+
distribution_title="Webship 11"
|
|
113
|
+
distribution_webroot="docroot"
|
|
114
|
+
distribution_profile_repo="..."
|
|
115
|
+
distribution_project_template="drupal/webship_project"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
There is no `core/config/distributions/` — a builder is readable on its own, and changing what one
|
|
119
|
+
builds touches only that file.
|
|
120
|
+
|
|
121
|
+
**Version strings follow the file name.** `cmd-<distro><major>-<minor>-0-project.sh` pins
|
|
122
|
+
`site_version="~<major>.<minor>.0"` — the three-segment form, so it means "that minor, latest patch".
|
|
123
|
+
A `-x-` builder tracks the branch (`11.0.x-dev`); latest-per-major builders use a caret (`^11`, `^2`).
|
|
124
|
+
|
|
125
|
+
A whole new distribution is one `args/arg-<name>.sh`, one `functions/fun-distribution-<name>.sh`
|
|
126
|
+
(with `install_<name>_with_drush`, `enable_<name>_extra_components`, `set_<name>_users`) and the
|
|
127
|
+
`cmd-*-project.sh` files that pick versions.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Adding a workspace
|
|
132
|
+
|
|
133
|
+
Two entries and a directory:
|
|
134
|
+
|
|
135
|
+
1. the name in the `workspaces:` list in `core/config/settings.yml` — that list is also the home
|
|
136
|
+
page's card order;
|
|
137
|
+
2. `core/config/workspace.<name>.settings.yml`, holding `doc_name` and optionally
|
|
138
|
+
`presentation: icon:`/`subtitle:`.
|
|
139
|
+
|
|
140
|
+
The dashboard re-reads config on a 2-second memo, so there is no restart and no code change. Settings
|
|
141
|
+
→ the `workspaces:` list does all of it from the browser, including the folder and the DDEV hostname
|
|
142
|
+
(which needs a `ddev restart` to resolve — the response says so).
|
|
143
|
+
|
|
144
|
+
**Deleting one** goes through the same list, and only for a workspace nothing uses (`workspace-delete.js`):
|
|
145
|
+
no item in its folder but its own tooling, no backups, no screen session running there, and not the last
|
|
146
|
+
workspace. It asks three times (the confirm dialog, the name typed, `YES` typed), checks again on the
|
|
147
|
+
server, and removes the list entry, the settings file, the tooling and the folder, and the DDEV hostname.
|
|
148
|
+
Test it on a throwaway `zz-*` workspace, never on one you use.
|
|
149
|
+
|
|
150
|
+
**Icons are Tabler**, referenced as `tabler:<name>` and parsed out of the vendored sprite, so a name
|
|
151
|
+
that does not exist is refused rather than rendering an empty box.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## The dashboard
|
|
156
|
+
|
|
157
|
+
`server.js` (single file, no framework) + `workspaces.js` (the config bridge) + `public/`.
|
|
158
|
+
Server-rendered HTML fragments, swapped by HTMX.
|
|
159
|
+
|
|
160
|
+
- **`workspaces.js` reads the config live.** The only hardcoded table is `PRESENTATION`; script
|
|
161
|
+
discovery is by filename regex, not a hand-maintained map.
|
|
162
|
+
- **Jobs are an in-memory `Map`**, so **do not restart the app while jobs are running** — running
|
|
163
|
+
builds get orphaned. Build jobs get a 60-minute timeout: a cold-cache `composer create-project` can
|
|
164
|
+
exceed fifteen.
|
|
165
|
+
- **Every action shells out to the real `cmd-*.sh`.** The button and the command line cannot drift,
|
|
166
|
+
and the collision checks, refusals and guards live in one place — the script.
|
|
167
|
+
- **The stylesheet is assembled** from `public/css/base.css` + `public/css/components/*.css` + a theme.
|
|
168
|
+
`public/style.css` is not the source; editing it changes nothing. `cssVersion()` stamps the URL from
|
|
169
|
+
the parts' mtimes, so a cached browser picks changes up.
|
|
170
|
+
|
|
171
|
+
Four things that have taken the app down or hidden a feature:
|
|
172
|
+
|
|
173
|
+
| | |
|
|
174
|
+
| --- | --- |
|
|
175
|
+
| **a backtick inside a template literal** | ends the literal and takes the whole app down with a `SyntaxError` hundreds of lines away. There is a test asserting no HTML comment in these files contains one |
|
|
176
|
+
| **a variable defined in the wrong IIFE** | renders as *nothing at all* — page rendering swallows errors, so a whole action group silently disappeared |
|
|
177
|
+
| **HTMX only swaps 2xx by default** | every 4xx this app returns used to be discarded silently, so a refused save looked like a broken button. `htmx:beforeSwap` in `ui.js` sets `shouldSwap` for 4xx/5xx |
|
|
178
|
+
| **UIkit closes a dropdown on an outside click only** | opening a modal is not one, so the menu stayed open on top of the dialog it had just opened |
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Testing
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
bash core/scripts/tests/cmd-smoke-test.sh # every cmd-*.sh: syntax, bootstrap chain, header
|
|
186
|
+
cd core/workspace-app && npm run test:unit # node --test
|
|
187
|
+
cd core/workspace-app && npm test # unit + the cucumber browser suite
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The smoke test's exit code is the number of failures, and it needs no environment setup.
|
|
191
|
+
|
|
192
|
+
**A test that needs machine state skips with the reason**, at run time, rather than failing or being
|
|
193
|
+
excluded — see `tests/step-definitions/preconditions.steps.js`. A suite with permanent red in it stops
|
|
194
|
+
being read, which is how this one drifted for weeks without anyone noticing it could not even launch a
|
|
195
|
+
browser. (In cucumber that is `return 'skipped'`; `this.skip()` is Mocha's and does nothing here.)
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Shipping a change
|
|
200
|
+
|
|
201
|
+
1. **An issue first**, then a PR referencing it. Both end with the Checkpoints checklist; never tick
|
|
202
|
+
the human-review box.
|
|
203
|
+
2. Work in the git clone at `~/workspace/products/workspace` (branch `1.0.x`), not in the live tree.
|
|
204
|
+
3. After a merge, bring it back with `bash ~/workspace/core/scripts/sync-tooling.sh --pull` — it
|
|
205
|
+
mirrors the scripts, the dashboard and each workspace's commands, prunes what the clone dropped,
|
|
206
|
+
re-applies this machine's docker GID, and runs the smoke test.
|
|
207
|
+
4. A release is an annotated tag on `1.0.x` plus a GitHub release titled with the tag. There is no
|
|
208
|
+
version-bump commit; the installed version is a per-machine stamp, not a committed file.
|
|
209
|
+
|
|
210
|
+
**Never commit** real credentials, API keys, machine paths or project names. The published files are
|
|
211
|
+
team-installable templates: `CHANGE_ME` and `sk-change-this` are what ship.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Where the rest is
|
|
216
|
+
|
|
217
|
+
| | |
|
|
218
|
+
| --- | --- |
|
|
219
|
+
| the dashboard screen by screen, with pictures | [README.md](README.md) |
|
|
220
|
+
| using it | [USER-GUIDE.md](USER-GUIDE.md) |
|
|
221
|
+
| every command and flag, and how to install | `README.md` at the top of the repository |
|
|
222
|
+
| the distilled rules and traps | `docs/workspace-tips-and-rules.md` |
|
|
223
|
+
| the dashboard's internals and DDEV wiring | `core/workspace-app/README.md` |
|
|
224
|
+
| what changed and what was learned, dated | `worklogs/` |
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Webship Workspace documentation
|
|
2
|
+
|
|
3
|
+
Three documents, and this is the index.
|
|
4
|
+
|
|
5
|
+
| | |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| **[USER-GUIDE.md](USER-GUIDE.md)** | using the workspace: day one, the things you will actually do, and what to try when something is wrong. This is also the **Help** page in the dashboard |
|
|
8
|
+
| **[DEVELOPER-GUIDE.md](DEVELOPER-GUIDE.md)** | changing the tooling: adding a command, a builder, a workspace or a dashboard feature, and the rules that exist because something broke |
|
|
9
|
+
| **this file** | the dashboard screen by screen, with pictures — the long way round |
|
|
10
|
+
|
|
11
|
+
Beyond these three: `README.md` at the top of the repository is the full reference (every command,
|
|
12
|
+
every flag, how to install), `docs/workspace-tips-and-rules.md` is the distilled rules,
|
|
13
|
+
`core/workspace-app/README.md` is the dashboard's internals, and `worklogs/` is what changed, dated.
|
|
14
|
+
|
|
15
|
+
Every screenshot here is of a real machine. If the interface you have does not match one, the
|
|
16
|
+
screenshot is the thing that is out of date — say so.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## The home page
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
Every folder in `~/workspace` is a **workspace** with a card. The count under each is real — projects
|
|
25
|
+
on disk, videos, agents — and **BUILDABLE** means that folder carries builders, so a project can be
|
|
26
|
+
created there.
|
|
27
|
+
|
|
28
|
+
Three parts of the frame are on every page:
|
|
29
|
+
|
|
30
|
+
| | |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| **the assistant**, left | the **AI Assistant** on every page, scoped to the one you are on: on `/dev` it knows dev's projects, commands and rules. It can actually run things. Rename it, per workspace or for the whole dashboard, with `assistant: name:` |
|
|
33
|
+
| **the actions rail**, right | collapsed to icons by default, because a project table wants the width. It never disappears, so "what can I do here" is always in the same place |
|
|
34
|
+
| **the version**, in the toolbar | beside the product name. `version unknown` means the tree was installed a way that recorded none — the tooltip lists the commands that set it |
|
|
35
|
+
|
|
36
|
+
Each card carries **Edit** (its icon and description, in a window over the page) and **Settings** (its
|
|
37
|
+
own settings file, with the other files as tabs) in its corner:
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
**Drag a card** to reorder: it lifts and follows the pointer, the others slide aside, the page scrolls at
|
|
42
|
+
its edges, and the order is saved when you let go (Escape puts it back; Alt with an arrow key moves the
|
|
43
|
+
focused card). The order is the `workspaces:` list in `core/config/settings.yml`, which drags the same
|
|
44
|
+
way in Settings. The icons come from each workspace's own settings file.
|
|
45
|
+
|
|
46
|
+

|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## A workspace page
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
**What would you like to create?** at the top (a builder and a name, or **New folder**), then the
|
|
55
|
+
projects. **Build** runs in the background in a screen session — close the tab if you like, it keeps
|
|
56
|
+
going. **Start AI** under it starts Claude Code in the new project once the build succeeds.
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
The state on each card (Running, Stopped, Paused) is also the switch: click it to start or stop the
|
|
61
|
+
project.
|
|
62
|
+
|
|
63
|
+
Each row carries the project's state and its addresses. A project has two that always work:
|
|
64
|
+
|
|
65
|
+
- `https://<project>.ddev.site` — the canonical one DDEV gives it
|
|
66
|
+
- `https://<project>.<workspace>.workspace.ddev.site` — the hierarchical one the dashboard hands out
|
|
67
|
+
|
|
68
|
+
and can be given more (see [Domains](#domains-what-a-project-answers-at)).
|
|
69
|
+
|
|
70
|
+
The captions under the buttons are status, not decoration: `22 commands`, `not linked`, `no test
|
|
71
|
+
report yet`, `offline`, `none`. They are what tells you whether pressing the button will do anything.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## The DDEV menu
|
|
76
|
+
|
|
77
|
+

|
|
78
|
+
|
|
79
|
+
Everything DDEV can do to one project, grouped: start and stop, the build (composer), drush, the
|
|
80
|
+
database, and the project itself. Each entry runs the real command in a screen session and streams the
|
|
81
|
+
output — there is nothing here you could not type, which is the point: the button and the command line
|
|
82
|
+
cannot drift.
|
|
83
|
+
|
|
84
|
+
What the project defines itself is there too, each in its own folding group: its **custom commands**
|
|
85
|
+
(`.ddev/commands/`), its **composer.json scripts** and its **package.json scripts**.
|
|
86
|
+
|
|
87
|
+

|
|
88
|
+
|
|
89
|
+
**Destructive entries ask before they fire.** Drop the database opens the confirm dialog. Remove names
|
|
90
|
+
what goes, offers a database backup first, and keeps its button off until the project's name is typed.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Cloning: a copy, or a branch
|
|
95
|
+
|
|
96
|
+

|
|
97
|
+
|
|
98
|
+
**Clone** makes a second working project from an existing one, and the form asks which of the two
|
|
99
|
+
things you mean.
|
|
100
|
+
|
|
101
|
+
A **copy** (the default) copies the files and sends the database through a dump. The two share
|
|
102
|
+
nothing and can diverge for ever. Right when they are two different sites.
|
|
103
|
+
|
|
104
|
+
A **git worktree** is offered only when the project is a git checkout, labelled with the branch that
|
|
105
|
+
checkout is on:
|
|
106
|
+
|
|
107
|
+

|
|
108
|
+
|
|
109
|
+
One shared `.git` and one history, a branch per folder — and still its own DDEV project with its own
|
|
110
|
+
containers and its own database. Right when they are two *branches of one piece of work*: somebody's
|
|
111
|
+
merge request running beside your own build, without a second copy of a repository whose history is
|
|
112
|
+
often larger than the checkout.
|
|
113
|
+
|
|
114
|
+
The branch field appears only once the option is ticked. It defaults to the clone name, and the branch
|
|
115
|
+
is created from the current HEAD if it does not exist yet.
|
|
116
|
+
|
|
117
|
+
Three things it does for you, each of which is a mistake people make by hand:
|
|
118
|
+
|
|
119
|
+
1. **The folder names the project.** The `name:` key is removed from the worktree's
|
|
120
|
+
`.ddev/config.yaml`, so the directory decides — leave it and the worktree collides with its source.
|
|
121
|
+
2. **The database is never shared.** Two DDEV projects on one database corrupts on the first
|
|
122
|
+
`drush updatedb`.
|
|
123
|
+
3. **The build is copied.** `vendor/`, contrib, the files directory and `settings.local.php` are
|
|
124
|
+
git-ignored, so a bare worktree is a source tree and not a running site.
|
|
125
|
+
|
|
126
|
+
Rows say which is which, so a listing shows what belongs to what:
|
|
127
|
+
|
|
128
|
+

|
|
129
|
+
|
|
130
|
+
The **IDE** caption says whether there is a repository at all — `code`, `code · git`,
|
|
131
|
+
`code · worktree` — and the branch is inside the menu, with anything unpushed:
|
|
132
|
+
|
|
133
|
+

|
|
134
|
+
|
|
135
|
+
### Removing one is guarded
|
|
136
|
+
|
|
137
|
+

|
|
138
|
+
|
|
139
|
+
Two removals are refused before anything is deleted, because neither is recoverable and `rm -rf`
|
|
140
|
+
warns about neither:
|
|
141
|
+
|
|
142
|
+
- **the main checkout, while worktrees still live off it** — they would be left files with no
|
|
143
|
+
repository at all;
|
|
144
|
+
- **anything holding commits that are on no remote**, or uncommitted changes.
|
|
145
|
+
|
|
146
|
+
Tick the extra box (or pass `--force`) to go ahead anyway. Removing a worktree normally also prunes
|
|
147
|
+
its registration in the main checkout, so a later clone to the same folder is not refused by git.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Domains: what a project answers at
|
|
152
|
+
|
|
153
|
+

|
|
154
|
+
|
|
155
|
+
**DDEV → Domains this answers at…** lists the canonical address, the hierarchical one, and any extra
|
|
156
|
+
ones, and offers `<project>-<workspace>` — the workspace in one flat hostname.
|
|
157
|
+
|
|
158
|
+
It **adds**; it never renames. The canonical URL keeps working, nothing is rebuilt, and the DDEV
|
|
159
|
+
project name goes on matching the folder. A new address reaches the router only when the project next
|
|
160
|
+
starts, so the dialog offers to do that and says why.
|
|
161
|
+
|
|
162
|
+
Two projects cannot share a hostname — the router gives it to whichever started last and the other
|
|
163
|
+
quietly stops answering — so that is refused, naming the project that already has it.
|
|
164
|
+
|
|
165
|
+
The clone form offers the same for the copy it is about to make, and the label follows the name as you
|
|
166
|
+
type it:
|
|
167
|
+
|
|
168
|
+

|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Screens: anything that takes time
|
|
173
|
+
|
|
174
|
+

|
|
175
|
+
|
|
176
|
+
Builds, installs, test suites and AI agents run in **screen sessions**, so they survive you closing
|
|
177
|
+
the tab, logging out, or the dashboard restarting. This page lists them all and opens a real terminal
|
|
178
|
+
on any of them — you can type into it.
|
|
179
|
+
|
|
180
|
+
The useful part is the state. A session that has **stopped to ask you something** is flagged
|
|
181
|
+
`✋ waiting on a choice` instead of looking busy for ever, which is the difference between a job that
|
|
182
|
+
is running and a job that has been waiting for you for an hour.
|
|
183
|
+
|
|
184
|
+
Every card has a **SCREENS** menu too: the sessions running in that project, **New screen here…**,
|
|
185
|
+
**Claude Code here…**, and **Exit Claude Code** or **Resume Claude Code**.
|
|
186
|
+
|
|
187
|
+

|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Settings
|
|
192
|
+
|
|
193
|
+

|
|
194
|
+
|
|
195
|
+
`core/config/settings.yml` and every `workspace.<name>.settings.yml`, edited from the browser. Two
|
|
196
|
+
things about it are deliberate and worth knowing:
|
|
197
|
+
|
|
198
|
+
- **A secret's value never leaves the server.** Anything whose key looks like a password, token or API
|
|
199
|
+
key renders as an empty field marked *set — leave blank to keep it*. The value is not in the page,
|
|
200
|
+
not in the response after saving, and not in `actions.log`.
|
|
201
|
+
- **The file is rewritten line by line**, not parsed and re-dumped, so your comments and formatting
|
|
202
|
+
survive and a one-key change is a one-line diff.
|
|
203
|
+
|
|
204
|
+
Every YAML list is edited as rows with an Add field, nested ones included
|
|
205
|
+
(`auth.google.allowed_domains`, `auth.google.allowed_emails`):
|
|
206
|
+
|
|
207
|
+

|
|
208
|
+
|
|
209
|
+
The `workspaces:` list drags like the home page cards — that order is the card order. Adding one here
|
|
210
|
+
creates the settings file, the folder and the DDEV hostname together. A workspace nothing uses can be
|
|
211
|
+
deleted from its row, after three asks: the confirm dialog, its name typed, and `YES` typed.
|
|
212
|
+
|
|
213
|
+

|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Keeping it up to date
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
bash core/workspace-app/scripts/update.sh --check # what you have vs the latest release
|
|
221
|
+
bash core/workspace-app/scripts/update.sh # update the dashboard
|
|
222
|
+
bash core/scripts/sync-tooling.sh --pull # the cmd-*.sh commands and the shared core
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The version you are on is in the toolbar. The updater touches **only** `core/workspace-app`, and it
|
|
226
|
+
refuses the tooling job rather than half-doing it — so a release that adds or changes a `cmd-*.sh`
|
|
227
|
+
needs `sync-tooling.sh --pull` as well, and the release notes say when.
|
|
228
|
+
|
|
229
|
+
Some releases change the container image; the notes say so, and then
|
|
230
|
+
`cd core/workspace-app && ddev restart` is needed too.
|
|
231
|
+
|
|
232
|
+
> **If you are on 1.0.9 exactly**, run `bash core/scripts/docker-gid.sh` once. That release's updater
|
|
233
|
+
> mirrored the release's `.ddev/` over yours without putting this machine's docker GID back, and the
|
|
234
|
+
> symptom is delayed: nothing is wrong until the next `ddev restart`, when the dashboard shows **every
|
|
235
|
+
> project as Stopped** while they all run perfectly well. The next release re-applies it for you.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Where the rest is
|
|
240
|
+
|
|
241
|
+
| | |
|
|
242
|
+
| --- | --- |
|
|
243
|
+
| changing the tooling | [DEVELOPER-GUIDE.md](DEVELOPER-GUIDE.md) |
|
|
244
|
+
| installing, updating, the command chain | `README.md` at the top of this repository |
|
|
245
|
+
| using it | [USER-GUIDE.md](USER-GUIDE.md) |
|
|
246
|
+
| the dashboard's internals, DDEV wiring, the security note | `core/workspace-app/README.md` |
|
|
247
|
+
| the hard rules and the operational traps | `docs/workspace-tips-and-rules.md` |
|
|
248
|
+
| what changed, dated | `worklogs/` |
|
|
249
|
+
|
|
250
|
+
Every picture in `screenshots/` is used by one of these guides or the top `README.md`, and all of them
|
|
251
|
+
were taken on 22 October 2026 at 1920×1080, in light mode (the README's dark copies aside). A pull
|
|
252
|
+
request carries its own pictures on its branch.
|