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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/PLUGINS.md +249 -0
  2. package/README.md +379 -2
  3. package/WORKSPACES.md +69 -0
  4. package/actions/ai-items.js +293 -0
  5. package/actions/audio.js +48 -0
  6. package/actions/build-order.js +79 -0
  7. package/actions/checks.js +95 -0
  8. package/actions/claude-session.js +104 -0
  9. package/actions/commands-center.js +258 -0
  10. package/actions/composer.js +267 -0
  11. package/actions/ddev-addons.js +90 -0
  12. package/actions/ddev-mail.js +80 -0
  13. package/actions/ddev.js +237 -0
  14. package/actions/folders.js +165 -0
  15. package/actions/images-svg.js +33 -0
  16. package/actions/images.js +145 -0
  17. package/actions/navigation.js +190 -0
  18. package/actions/node.js +222 -0
  19. package/actions/notes.js +55 -0
  20. package/actions/plugins.js +36 -0
  21. package/actions/project-make.js +28 -0
  22. package/actions/rag.js +202 -0
  23. package/actions/row-actions.js +262 -0
  24. package/actions/screens.js +270 -0
  25. package/actions/testing.js +121 -0
  26. package/actions/video-edit.js +54 -0
  27. package/ai-inventory.js +182 -0
  28. package/ai-items.js +202 -0
  29. package/assistant-cast.js +176 -0
  30. package/assistant-suggestions.js +91 -0
  31. package/assistant.js +484 -0
  32. package/audio-voice.js +528 -0
  33. package/auth-google.js +260 -0
  34. package/build-ai.js +52 -0
  35. package/build-order-page.js +191 -0
  36. package/build-order.js +313 -0
  37. package/builder-args.js +117 -0
  38. package/changelog.js +302 -0
  39. package/chat-threads.js +134 -0
  40. package/checks.js +105 -0
  41. package/claude-args.js +282 -0
  42. package/claude-sessions.js +129 -0
  43. package/command-author.js +117 -0
  44. package/command-block.js +20 -0
  45. package/commands/defaults.yml +159 -0
  46. package/commands-page.js +460 -0
  47. package/commands.js +568 -0
  48. package/cucumber.js +43 -0
  49. package/ddev-addons-page.js +124 -0
  50. package/ddev-addons.js +154 -0
  51. package/ddev-custom-commands.js +96 -0
  52. package/ddev-domains.js +84 -0
  53. package/ddev-import.js +93 -0
  54. package/ddev-mail-page.js +167 -0
  55. package/ddev-mail.js +110 -0
  56. package/ddev-options.js +145 -0
  57. package/ddev-snapshots.js +126 -0
  58. package/ddev.js +145 -0
  59. package/docs/DEVELOPER-GUIDE.md +224 -0
  60. package/docs/README.md +252 -0
  61. package/docs/REFERENCE.md +684 -0
  62. package/docs/USER-GUIDE.md +697 -0
  63. package/docs/screenshots/assistant/avatar-board.png +0 -0
  64. package/docs/screenshots/assistant/avatar-coffee.png +0 -0
  65. package/docs/screenshots/assistant/avatar-colours.png +0 -0
  66. package/docs/screenshots/assistant/avatar-icons.png +0 -0
  67. package/docs/screenshots/assistant/avatar-moves.png +0 -0
  68. package/docs/screenshots/assistant/avatar-pick-cookieman.png +0 -0
  69. package/docs/screenshots/assistant/avatar-settings-voices.png +0 -0
  70. package/docs/screenshots/assistant/avatar-status.png +0 -0
  71. package/docs/screenshots/create-card/create-card.png +0 -0
  72. package/docs/screenshots/create-card/start-ai.png +0 -0
  73. package/docs/screenshots/guide-clone-address.png +0 -0
  74. package/docs/screenshots/guide-clone-worktree-off.png +0 -0
  75. package/docs/screenshots/guide-clone-worktree-on.png +0 -0
  76. package/docs/screenshots/guide-ddev-menu.png +0 -0
  77. package/docs/screenshots/guide-domains.png +0 -0
  78. package/docs/screenshots/guide-home.png +0 -0
  79. package/docs/screenshots/guide-ide-menu.png +0 -0
  80. package/docs/screenshots/guide-remove-refusal.png +0 -0
  81. package/docs/screenshots/guide-rows-worktree.png +0 -0
  82. package/docs/screenshots/guide-screens.png +0 -0
  83. package/docs/screenshots/guide-settings.png +0 -0
  84. package/docs/screenshots/guide-workspace-page.png +0 -0
  85. package/docs/screenshots/home/card-settings.png +0 -0
  86. package/docs/screenshots/home/drag-reorder.png +0 -0
  87. package/docs/screenshots/home/edit-card.png +0 -0
  88. package/docs/screenshots/images/claude-code-image.png +0 -0
  89. package/docs/screenshots/images/edit-with-ai.png +0 -0
  90. package/docs/screenshots/images/gallery.png +0 -0
  91. package/docs/screenshots/images/generate-description.png +0 -0
  92. package/docs/screenshots/images/generate-plan.png +0 -0
  93. package/docs/screenshots/images/providers.png +0 -0
  94. package/docs/screenshots/images/svg-form.png +0 -0
  95. package/docs/screenshots/images/svg-result.png +0 -0
  96. package/docs/screenshots/images/upload.png +0 -0
  97. package/docs/screenshots/new-folder/2-form.png +0 -0
  98. package/docs/screenshots/new-folder/8-ddev-options.png +0 -0
  99. package/docs/screenshots/new-folder/files-folder.png +0 -0
  100. package/docs/screenshots/projects/ddev-custom-commands.png +0 -0
  101. package/docs/screenshots/projects/state-chip-stop.png +0 -0
  102. package/docs/screenshots/readme/dashboard-dark.png +0 -0
  103. package/docs/screenshots/readme/dashboard-light.png +0 -0
  104. package/docs/screenshots/readme/dev-dark.png +0 -0
  105. package/docs/screenshots/readme/dev-light.png +0 -0
  106. package/docs/screenshots/readme/docs-dark.png +0 -0
  107. package/docs/screenshots/readme/docs-light.png +0 -0
  108. package/docs/screenshots/readme/icons/ai.svg +4 -0
  109. package/docs/screenshots/readme/icons/code.svg +4 -0
  110. package/docs/screenshots/readme/icons/docs.svg +4 -0
  111. package/docs/screenshots/readme/icons/services.svg +4 -0
  112. package/docs/screenshots/readme/icons/sites.svg +4 -0
  113. package/docs/screenshots/readme/test-report-dark.png +0 -0
  114. package/docs/screenshots/readme/test-report-light.png +0 -0
  115. package/docs/screenshots/readme/test-scenarios.png +0 -0
  116. package/docs/screenshots/readme/test-steps.png +0 -0
  117. package/docs/screenshots/readme/videos-dark.png +0 -0
  118. package/docs/screenshots/readme/videos-light.png +0 -0
  119. package/docs/screenshots/screens-menu.png +0 -0
  120. package/docs/screenshots/screens-new.png +0 -0
  121. package/docs/screenshots/settings/auth-google-lists.png +0 -0
  122. package/docs/screenshots/settings/delete-1-row.png +0 -0
  123. package/docs/screenshots/settings/delete-2-confirm.png +0 -0
  124. package/docs/screenshots/settings/delete-3-type-name.png +0 -0
  125. package/docs/screenshots/settings/delete-4-type-yes.png +0 -0
  126. package/event-log.js +59 -0
  127. package/files-browser.js +247 -0
  128. package/folders.js +121 -0
  129. package/format.js +73 -0
  130. package/git-info.js +81 -0
  131. package/github-auth.js +62 -0
  132. package/html.js +10 -0
  133. package/icons.js +83 -0
  134. package/images-ai.js +447 -0
  135. package/images-create.js +226 -0
  136. package/images-page.js +417 -0
  137. package/images-records.js +39 -0
  138. package/images-render.js +36 -0
  139. package/images-svg-core.js +214 -0
  140. package/images-svg-sanitize.js +188 -0
  141. package/images-svg.js +241 -0
  142. package/images.js +270 -0
  143. package/jobs.js +430 -0
  144. package/linger.js +21 -0
  145. package/lists.js +292 -0
  146. package/media-info.js +136 -0
  147. package/navigation-page.js +264 -0
  148. package/navigation.js +394 -0
  149. package/notes.js +0 -0
  150. package/package.json +47 -4
  151. package/palette-index.js +46 -0
  152. package/playwright.config.js +23 -0
  153. package/plugin-install.js +239 -0
  154. package/plugin-loader.js +466 -0
  155. package/plugin-manifest.js +83 -0
  156. package/plugin-settings.js +44 -0
  157. package/plugins/workspace-notes/index.js +59 -0
  158. package/plugins-page.js +103 -0
  159. package/project-make.js +241 -0
  160. package/project-name.js +210 -0
  161. package/public/assistant-avatar-moves.js +646 -0
  162. package/public/assistant-avatar-ui.js +1223 -0
  163. package/public/assistant-avatar.js +530 -0
  164. package/public/avatar/druplicon.vrm +0 -0
  165. package/public/css/base.css +260 -0
  166. package/public/css/components/ai-assistant.css +263 -0
  167. package/public/css/components/assistant-avatar.css +279 -0
  168. package/public/css/components/assistant-views.css +198 -0
  169. package/public/css/components/build-order.css +74 -0
  170. package/public/css/components/clone-as-a-git-worktree.css +30 -0
  171. package/public/css/components/command-palette.css +61 -0
  172. package/public/css/components/commands-center.css +139 -0
  173. package/public/css/components/confirm-dialog.css +40 -0
  174. package/public/css/components/create-card.css +8 -0
  175. package/public/css/components/ddev-menu.css +23 -0
  176. package/public/css/components/deep-chat.css +17 -0
  177. package/public/css/components/density.css +11 -0
  178. package/public/css/components/floating-assistant-workspace-pages.css +55 -0
  179. package/public/css/components/folders.css +5 -0
  180. package/public/css/components/full-height-left-assistant-sidebar-all-pages.css +39 -0
  181. package/public/css/components/full-height-right-actions-rail-pages-that-have-actions.css +267 -0
  182. package/public/css/components/grid-and-list.css +203 -0
  183. package/public/css/components/help-page.css +36 -0
  184. package/public/css/components/hero.css +16 -0
  185. package/public/css/components/images.css +70 -0
  186. package/public/css/components/layout-rhythm-and-spacing-polish.css +139 -0
  187. package/public/css/components/mic-on-any-field-that-wants-typing.css +416 -0
  188. package/public/css/components/modal-fit.css +113 -0
  189. package/public/css/components/page-width.css +23 -0
  190. package/public/css/components/plugins-page.css +9 -0
  191. package/public/css/components/project-cards.css +256 -0
  192. package/public/css/components/project-rows.css +390 -0
  193. package/public/css/components/reordering-the-workspace-cards.css +85 -0
  194. package/public/css/components/responsive-home-and-the-workspace-pages.css +457 -0
  195. package/public/css/components/running-commands-as-toasts.css +327 -0
  196. package/public/css/components/settings-editor-core-config-yml.css +828 -0
  197. package/public/css/components/short-results-as-a-toast-at-the-top.css +203 -0
  198. package/public/css/components/sound-player.css +84 -0
  199. package/public/css/components/suggest-agents-skills-commands.css +112 -0
  200. package/public/css/components/tab-modal.css +142 -0
  201. package/public/css/components/terminals.css +244 -0
  202. package/public/css/components/test-report-media-one-file-per-row-paged.css +144 -0
  203. package/public/css/components/top-toolbar-logo-breadcrumb.css +107 -0
  204. package/public/css/components/video-edit.css +6 -0
  205. package/public/css/components/web-ide-open-with.css +23 -0
  206. package/public/css/components/working-indicators.css +158 -0
  207. package/public/css/components/workspace-cards.css +79 -0
  208. package/public/favicon.ico +0 -0
  209. package/public/favicon.png +0 -0
  210. package/public/htmx.min.js +1 -0
  211. package/public/logo.png +0 -0
  212. package/public/media-player.js +215 -0
  213. package/public/mic.js +255 -0
  214. package/public/narration.js +110 -0
  215. package/public/palette.js +278 -0
  216. package/public/sortable.js +251 -0
  217. package/public/suggest.js +337 -0
  218. package/public/terminal.js +903 -0
  219. package/public/themes/default/theme.css +267 -0
  220. package/public/themes/ember/theme.css +180 -0
  221. package/public/themes/press/theme.css +290 -0
  222. package/public/themes/slate/theme.css +182 -0
  223. package/public/touch-icon.png +0 -0
  224. package/public/ui.js +2721 -0
  225. package/public/vendor/ace/LICENSE.txt +24 -0
  226. package/public/vendor/ace/ace.js +23 -0
  227. package/public/vendor/ace/ext-language_tools.js +8 -0
  228. package/public/vendor/ace/ext-searchbox.js +8 -0
  229. package/public/vendor/ace/mode-css.js +8 -0
  230. package/public/vendor/ace/mode-html.js +8 -0
  231. package/public/vendor/ace/mode-javascript.js +8 -0
  232. package/public/vendor/ace/mode-markdown.js +8 -0
  233. package/public/vendor/ace/mode-sh.js +8 -0
  234. package/public/vendor/ace/mode-yaml.js +8 -0
  235. package/public/vendor/ace/theme-textmate.js +8 -0
  236. package/public/vendor/ace/theme-tomorrow_night.js +8 -0
  237. package/public/vendor/ace/worker-yaml.js +1 -0
  238. package/public/vendor/deep-chat.LICENSE +21 -0
  239. package/public/vendor/deep-chat.bundle.js +1 -0
  240. package/public/vendor/fonts/dm-serif-text-LICENSE.txt +93 -0
  241. package/public/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
  242. package/public/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
  243. package/public/vendor/fonts/figtree-LICENSE.txt +93 -0
  244. package/public/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
  245. package/public/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
  246. package/public/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
  247. package/public/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
  248. package/public/vendor/fonts/jetbrains-mono-LICENSE.txt +93 -0
  249. package/public/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  250. package/public/vendor/fonts/ubuntu-mono-LICENSE.txt +98 -0
  251. package/public/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
  252. package/public/vendor/tabler-LICENSE.txt +21 -0
  253. package/public/vendor/tabler-sprite.svg +1 -0
  254. package/public/vendor/three/LICENSE +21 -0
  255. package/public/vendor/three/addons/loaders/GLTFLoader.js +4860 -0
  256. package/public/vendor/three/addons/utils/BufferGeometryUtils.js +1501 -0
  257. package/public/vendor/three/addons/utils/SkeletonUtils.js +496 -0
  258. package/public/vendor/three/three.core.min.js +6 -0
  259. package/public/vendor/three/three.module.min.js +6 -0
  260. package/public/vendor/three-vrm/LICENSE +22 -0
  261. package/public/vendor/three-vrm/three-vrm.module.min.js +983 -0
  262. package/public/vendor/uikit-icons.min.js +1 -0
  263. package/public/vendor/uikit.min.css +1 -0
  264. package/public/vendor/uikit.min.js +1 -0
  265. package/public/vendor/webship-icons/ui/bookmark-outlined.svg +1 -0
  266. package/public/vendor/webship-icons/ui/headset.svg +1 -0
  267. package/public/vendor/webship-icons/ui/menu.svg +1 -0
  268. package/public/vendor/webship-icons/ui/pause.svg +1 -0
  269. package/public/vendor/webship-icons/ui/play.svg +1 -0
  270. package/public/vendor/webship-icons/ui/share.svg +1 -0
  271. package/public/vendor/webship-icons/ui/sound-off.svg +1 -0
  272. package/public/vendor/webship-icons/ui/sound-on.svg +1 -0
  273. package/public/vendor/webship-icons/ui/stop.svg +1 -0
  274. package/public/vendor/xterm/addon-fit.js +2 -0
  275. package/public/vendor/xterm/addon-unicode11.js +2 -0
  276. package/public/vendor/xterm/addon-web-links.js +2 -0
  277. package/public/vendor/xterm/xterm.css +218 -0
  278. package/public/vendor/xterm/xterm.js +2 -0
  279. package/rag.js +169 -0
  280. package/reader.js +132 -0
  281. package/repo-blobs.js +66 -0
  282. package/report-pdf.js +337 -0
  283. package/row-layout.js +1439 -0
  284. package/screenrc +37 -0
  285. package/screens.js +1063 -0
  286. package/scripts/code-server-start.sh +190 -0
  287. package/scripts/images-claude-finish.js +107 -0
  288. package/scripts/screen-host.sh +114 -0
  289. package/scripts/screen-runner.js +362 -0
  290. package/scripts/screen-then.js +47 -0
  291. package/scripts/update.sh +350 -0
  292. package/server.js +7923 -0
  293. package/settings-file.js +423 -0
  294. package/speech.js +297 -0
  295. package/tab-modal.js +75 -0
  296. package/test-reports.js +260 -0
  297. package/themes.js +381 -0
  298. package/transcripts.js +168 -0
  299. package/urls.js +13 -0
  300. package/validate.js +18 -0
  301. package/version-check.js +94 -0
  302. package/video-edit.js +588 -0
  303. package/waiting.js +85 -0
  304. package/workspace-card-tools.js +41 -0
  305. package/workspace-delete.js +139 -0
  306. package/workspace-types.js +85 -0
  307. package/workspaces.js +553 -0
@@ -0,0 +1,159 @@
1
+ # The built-in commands of the Webship Workspace dashboard (read by core/workspace-app/commands.js).
2
+ #
3
+ # Shipped with the dashboard, so an update brings new ones. Your own commands, and your versions of these,
4
+ # are kept in core/config/commands.yml by the Commands page: a command there with one of these ids replaces
5
+ # it, and deleting yours brings this one back.
6
+ #
7
+ # on: workspace | project | folder | file what it runs on
8
+ # when: every clause must match — any, ddev, composer, node, git, workspace:<name>, each with an optional !
9
+ # run: a shell line, or script: <workspace>/cmd-….sh with args
10
+ # values: {{workspace}} {{project}} {{folder}} {{file}} {{path}} {{opt.name}} — each single-quoted for you
11
+ #
12
+ # DDEV only: composer and drush always go through ddev.
13
+ commands:
14
+ # ---- a workspace folder
15
+ - id: workspace-ddev-list
16
+ label: DDEV projects
17
+ group: Workspace
18
+ description: Every DDEV project on this machine, with its status and URL
19
+ on: workspace
20
+ run: ddev list
21
+ - id: workspace-disk-use
22
+ label: Disk use of each folder
23
+ group: Workspace
24
+ description: How much space each folder in the workspace takes
25
+ on: workspace
26
+ run: du -sh -- */
27
+ - id: elevenlabs-status
28
+ label: ElevenLabs status
29
+ group: Voiceover
30
+ description: Whether a key is set, and the voice, model and server. Nothing is sent
31
+ on: workspace
32
+ when: [workspace:videos]
33
+ script: videos/cmd-tools-elevenlabs.sh
34
+ args: '-a status'
35
+ - id: elevenlabs-check
36
+ label: ElevenLabs key and credits
37
+ group: Voiceover
38
+ description: Checks the key with ElevenLabs and shows the plan and the characters left. Spends nothing
39
+ on: workspace
40
+ when: [workspace:videos]
41
+ script: videos/cmd-tools-elevenlabs.sh
42
+ args: '-a check'
43
+
44
+ # ---- a project
45
+ - id: ddev-describe
46
+ label: Describe (URLs, services)
47
+ group: DDEV
48
+ on: project
49
+ when: [ddev]
50
+ run: ddev describe
51
+ - id: ddev-restart
52
+ label: Restart
53
+ group: DDEV
54
+ on: project
55
+ when: [ddev]
56
+ run: ddev restart
57
+ - id: drush-status
58
+ label: Drush status
59
+ group: Drupal
60
+ on: project
61
+ when: [ddev, composer]
62
+ run: ddev drush status
63
+ - id: drush-cache-rebuild
64
+ label: Rebuild caches
65
+ group: Drupal
66
+ on: project
67
+ when: [ddev, composer]
68
+ run: ddev drush cr
69
+ - id: drush-update-database
70
+ label: Run database updates
71
+ group: Drupal
72
+ description: drush updatedb, then a cache rebuild
73
+ on: project
74
+ when: [ddev, composer]
75
+ confirm: true
76
+ run: ddev drush updatedb -y && ddev drush cr
77
+ - id: composer-validate
78
+ label: Validate composer.json
79
+ group: Composer
80
+ on: project
81
+ when: [ddev, composer]
82
+ run: ddev composer validate --no-check-publish
83
+ - id: composer-outdated-direct
84
+ label: Outdated direct packages
85
+ group: Composer
86
+ on: project
87
+ when: [ddev, composer]
88
+ run: ddev composer outdated --direct
89
+ - id: git-status
90
+ label: Git status
91
+ group: Git
92
+ on: project
93
+ when: [git]
94
+ run: git status --short --branch
95
+ - id: git-log
96
+ label: Last 20 commits
97
+ group: Git
98
+ on: project
99
+ when: [git]
100
+ run: git log --oneline --decorate -20
101
+ - id: git-switch-branch
102
+ label: Switch branch
103
+ group: Git
104
+ description: Fetch, then switch to a branch that exists on the remote or here
105
+ on: project
106
+ when: [git]
107
+ confirm: true
108
+ options:
109
+ - name: branch
110
+ label: Branch
111
+ value: main
112
+ regex: '^[A-Za-z0-9._/-]+$'
113
+ required: true
114
+ run: git fetch --prune && git switch {{opt.branch}}
115
+ - id: backup-dev-project
116
+ label: Back up the project
117
+ group: Backups
118
+ description: A tar of the files and a database export, into backups/dev
119
+ on: project
120
+ when: [workspace:dev]
121
+ script: dev/cmd-tools-backup-dev.sh
122
+ args: '{{project}}'
123
+
124
+ # ---- a folder
125
+ - id: folder-list
126
+ label: List the folder
127
+ group: Folders
128
+ on: folder
129
+ run: ls -la
130
+ - id: folder-largest
131
+ label: Largest files and folders
132
+ group: Folders
133
+ on: folder
134
+ run: du -ah . 2>/dev/null | sort -rh | head -n 30
135
+ - id: folder-git-status
136
+ label: Git changes in the folder
137
+ group: Folders
138
+ on: folder
139
+ when: [git]
140
+ run: git status --short -- .
141
+
142
+ # ---- a file
143
+ - id: file-head
144
+ label: Show the first 200 lines
145
+ group: Files
146
+ on: file
147
+ run: head -n 200 -- {{file}}
148
+ - id: file-git-log
149
+ label: History of the file
150
+ group: Files
151
+ on: file
152
+ when: [git]
153
+ run: git log --oneline --follow -20 -- {{file}}
154
+ - id: file-php-lint
155
+ label: Check PHP syntax
156
+ group: Files
157
+ on: file
158
+ when: [ddev]
159
+ run: ddev exec --dir /var/www/html php -l {{file}}
@@ -0,0 +1,460 @@
1
+ /**
2
+ * The Commands page, as HTML: the catalog, one command's page, the run form, a new command, and your commands as
3
+ * YAML. The rules are in commands.js and the writes in actions/commands-center.js.
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const yaml = require('js-yaml');
8
+
9
+ const { esc } = require('./html');
10
+ const { loadWorkspaces, workspaceDir } = require('./workspaces');
11
+ const { paginate, listControlsHtml, listPagerHtml } = require('./lists');
12
+ const { tildePath } = require('./format');
13
+ const rl = require('./row-layout');
14
+ const cc = require('./commands');
15
+
16
+ const icon = (name, ratio = 0.7) => `<span uk-icon="icon: ${name}; ratio: ${ratio}"></span>`;
17
+ const vals = (o) => `hx-vals='${esc(JSON.stringify(o))}'`;
18
+ const MODAL = 'hx-target="#editor-modal-body" hx-swap="innerHTML"';
19
+ const OUT = 'hx-target="#webship-workspace-output" hx-swap="innerHTML"';
20
+
21
+ // Code shown on a page: Ace, read-only, and a read-only textarea still readable when Ace does not load.
22
+ const codeView = (mode, text, label) => `<textarea class="uk-textarea cc-yaml" rows="${Math.min(32, String(text).split('\n').length + 1)}" readonly spellcheck="false"
23
+ data-ace="${mode}" data-ace-readonly="1" aria-label="${esc(label)}">${esc(text)}</textarea>`;
24
+
25
+ function tabsHtml(active) {
26
+ const tab = (href, label, key, title) => `<a class="uk-button uk-button-${active === key ? 'primary' : 'default'} uk-button-small settings-tab"
27
+ href="${href}"${active === key ? ' aria-current="page"' : ''} title="${esc(title)}">${esc(label)}</a>`;
28
+ return `<p class="settings-tabs cc-tabs">
29
+ ${tab('/commands', 'Commands', 'list', 'Every command: built in, yours, and the cmd-*.sh scripts')}
30
+ ${tab('/commands/new', 'New', 'new', 'A command, a script, or an action on a button')}
31
+ ${tab('/commands/definition', 'Definition', 'definition', 'Your commands as YAML')}
32
+ </p>`;
33
+ }
34
+
35
+ function sourceBadge(e) {
36
+ if (e.source === 'yours' && e.overrides) return '<span class="uk-label uk-label-warning" title="Your version of a built-in command">changed</span>';
37
+ if (e.source === 'yours') return '<span class="uk-label uk-label-success">yours</span>';
38
+ if (e.source === 'script') return '<span class="uk-label" title="A cmd-*.sh in a workspace folder">script</span>';
39
+ return '<span class="uk-label" title="Shipped with the dashboard">built in</span>';
40
+ }
41
+
42
+ function describeTarget(e) {
43
+ if (e.source === 'script') return `a script in ${e.workspace}/`;
44
+ const when = e.when.length === 1 && e.when[0] === 'any' ? '' : ` · ${rl.describeWhen(e.when)}`;
45
+ return `runs on ${cc.ON[e.on] || e.on}${when}`;
46
+ }
47
+
48
+ const scriptHref = (ws, file) => `/commands/s/${encodeURIComponent(ws)}/${encodeURIComponent(file)}`;
49
+
50
+ /** What a script can do, the same set the old Settings → Commands rows had, plus Run and Use in a command. */
51
+ function scriptButtons(ws, file) {
52
+ const w = encodeURIComponent(ws);
53
+ const f = encodeURIComponent(file);
54
+ const withFile = `${vals({ file })} ${OUT}`;
55
+ return [
56
+ `<button class="uk-button uk-button-primary uk-button-small" hx-get="/fragments/cc/script-run/${w}/${f}" ${MODAL}>${icon('play')} Run</button>`,
57
+ `<button class="uk-button uk-button-default uk-button-small" hx-get="/fragments/commands/edit/${w}/${f}" ${MODAL}>${icon('pencil')} Edit</button>`,
58
+ `<a class="uk-button uk-button-default uk-button-small" href="/commands/new?script=${encodeURIComponent(`${ws}/${file}`)}" title="A command that runs this script, with its arguments and what it runs on">${icon('plus-circle')} Use in a command</a>`,
59
+ `<button class="uk-button uk-button-default uk-button-small" hx-get="/fragments/commands/clone/${w}/${f}" ${OUT}>${icon('copy')} Clone</button>`,
60
+ `<button class="uk-button uk-button-default uk-button-small" hx-post="/actions/command-diff" ${withFile}>${icon('git-fork')} Diff repo</button>`,
61
+ `<button class="uk-button uk-button-default uk-button-small" hx-post="/actions/command-pull" ${withFile} title="Overwrite with the copy in webship/workspace; a differing local copy is kept under backups/">${icon('cloud-download')} Pull</button>`,
62
+ `<button class="uk-button uk-button-secondary uk-button-small" hx-get="/fragments/commands/propose/${w}/${f}" ${OUT}>${icon('git-branch')} Propose PR</button>`,
63
+ ].join('');
64
+ }
65
+
66
+ function commandRowHtml(e, mtime, badge) {
67
+ const isScript = e.source === 'script';
68
+ const href = isScript ? scriptHref(e.workspace, e.file) : `/commands/c/${encodeURIComponent(e.id)}`;
69
+ const buttons = isScript ? scriptButtons(e.workspace, e.file) : [
70
+ `<button class="uk-button uk-button-primary uk-button-small" hx-get="/fragments/cc/run/${encodeURIComponent(e.id)}" ${MODAL}>${icon('play')} Run</button>`,
71
+ `<a class="uk-button uk-button-default uk-button-small" href="${href}#edit">${icon('pencil')} Edit</a>`,
72
+ `<button class="uk-button uk-button-default uk-button-small" hx-post="/actions/cc-copy" ${vals({ id: e.id, mtime })} ${OUT}>${icon('copy')} Copy</button>`,
73
+ e.source === 'yours'
74
+ ? `<button class="uk-button uk-button-danger uk-button-small arm-step" hx-post="/actions/cc-remove" ${vals({ id: e.id, mtime })} ${OUT} hx-trigger="confirmed-remove">${icon('trash')} ${e.overrides ? 'Back to built in' : 'Delete'}</button>`
75
+ : '',
76
+ ].join('');
77
+ return `
78
+ <div class="uk-card uk-card-default uk-card-small uk-card-body uk-margin-small project-row cc-row">
79
+ <div class="cmd-row-stack">
80
+ <span class="cmd-id">
81
+ <span class="cmd-title">${icon(isScript ? 'file-text' : 'play-circle', 0.8)} <a href="${href}">${esc(e.label)}</a> ${sourceBadge(e)} ${isScript && badge ? badge(e) : ''}</span>
82
+ <code class="fname cmd-file">${esc(isScript ? e.file : cc.lineOf(e))}</code>
83
+ <span class="cmd-desc">${e.description ? `${esc(e.description)} · ` : ''}${esc(describeTarget(e))}</span>
84
+ </span>
85
+ <div class="project-actions">${buttons}</div>
86
+ </div>
87
+ </div>`;
88
+ }
89
+
90
+ /** The whole catalog, searchable and filterable, grouped, and paged over the flat list so pages stay even. */
91
+ function catalogListHtml(state = {}, { scripts = [], badge = null } = {}) {
92
+ const cat = cc.loadCatalog();
93
+ const all = [...cat.commands, ...cc.scriptEntries(scripts)];
94
+ const source = ['builtin', 'yours', 'script'].includes(state.source) ? state.source : 'all';
95
+ const on = cc.ON[state.on] ? state.on : 'all';
96
+ const rank = new Map();
97
+ for (const e of all) if (!rank.has(e.group)) rank.set(e.group, rank.size);
98
+ // Groups keep their order; inside a group the Sort choice applies (by name either way, since a command
99
+ // carries no date of its own; Newest and Oldest keep the catalog's order, forwards or back).
100
+ const sortBy = String(state.sort || '');
101
+ const within = sortBy === 'name' ? (a, b) => a.e.label.localeCompare(b.e.label)
102
+ : sortBy === 'nameDesc' ? (a, b) => b.e.label.localeCompare(a.e.label)
103
+ : sortBy === 'oldest' ? (a, b) => b.i - a.i
104
+ : (a, b) => a.i - b.i;
105
+ const matched = cc.filterCommands(all, { q: state.q, source, on })
106
+ .map((e, i) => ({ e, i }))
107
+ .sort((a, b) => rank.get(a.e.group) - rank.get(b.e.group) || within(a, b))
108
+ .map((x) => x.e);
109
+ const inGroup = {};
110
+ for (const e of matched) inGroup[e.group] = (inGroup[e.group] || 0) + 1;
111
+ const pg = paginate(matched, state);
112
+ const shown = [];
113
+ for (const e of pg.slice) {
114
+ if (!shown.length || shown[shown.length - 1].group !== e.group) shown.push({ group: e.group, items: [] });
115
+ shown[shown.length - 1].items.push(e);
116
+ }
117
+ const body = shown.map(({ group, items }) => `
118
+ <h4 class="uk-margin-small-top">${esc(group)} <span class="uk-badge">${items.length === inGroup[group] ? items.length : `${items.length} of ${inGroup[group]}`}</span></h4>
119
+ ${items.map((e) => commandRowHtml(e, cat.mtime, badge)).join('')}`).join('');
120
+
121
+ const bare = '/fragments/cc/list';
122
+ const qs = new URLSearchParams();
123
+ if (state.q) qs.set('q', state.q);
124
+ if (source !== 'all') qs.set('source', source);
125
+ if (on !== 'all') qs.set('on', on);
126
+ const pager = listPagerHtml(`${bare}${qs.toString() ? `?${qs}` : ''}`, '#webship-workspace-projects', pg, state, 'commands');
127
+ const count = (s) => all.filter((e) => e.source === s).length;
128
+ const select = (name, label, current, options) => `<label class="uk-text-meta">${label}
129
+ <select class="uk-select uk-form-small" name="${name}" aria-label="${esc(label)}"
130
+ hx-get="${bare}" hx-target="#webship-workspace-projects" hx-swap="innerHTML"
131
+ hx-trigger="change" hx-include="closest .list-controls" hx-vals='{"page":"1"}'>
132
+ ${options.map(([v, t]) => `<option value="${v}"${v === current ? ' selected' : ''}>${esc(t)}</option>`).join('')}
133
+ </select></label>`;
134
+ // One group each, so the two wrap onto their own lines on a phone instead of pushing the page sideways.
135
+ const filters = [
136
+ select('source', 'Source', source, [['all', `Every source (${all.length})`], ['builtin', `Built in (${count('builtin')})`],
137
+ ['yours', `Yours (${count('yours')})`], ['script', `Scripts (${count('script')})`]]),
138
+ select('on', 'Runs on', on, [['all', 'Anything'], ...Object.entries(cc.ON)]),
139
+ ];
140
+ const problems = [
141
+ ...(cat.error ? [`${tildePath(cc.userFile())} could not be read (${cat.error}), so only the built-in commands are listed. Saving a command replaces the file.`] : []),
142
+ ...(cat.builtinError ? [`The built-in commands could not be read: ${cat.builtinError}.`] : []),
143
+ ...cat.problems.map((p) => `Skipped ${p}.`),
144
+ ];
145
+ const narrowed = state.q || source !== 'all' || on !== 'all';
146
+ return `<h3 class="uk-margin-small-bottom">Commands <span class="uk-badge">${all.length}</span></h3>
147
+ <p class="uk-text-meta uk-margin-small-bottom">Every command the dashboard can run, and what each runs on: a workspace folder, a project,
148
+ a folder or a file. The built-in ones ship with the dashboard, yours are kept in <code>${esc(tildePath(cc.userFile()))}</code>, and every
149
+ <code>cmd-*.sh</code> in the tree is listed as a script.</p>
150
+ ${problems.map((p) => `<div class="msg error">${esc(p)}</div>`).join('')}
151
+ ${listControlsHtml(bare, '#webship-workspace-projects', state, 'commands', filters)}
152
+ ${narrowed ? `<p class="uk-text-meta">${matched.length} of ${all.length} commands.</p>` : ''}
153
+ ${matched.length ? '' : '<p class="uk-text-meta">No commands match.</p>'}
154
+ ${pager}
155
+ ${body}
156
+ ${pager}`;
157
+ }
158
+
159
+ /** Where a run goes: a workspace, then a project, then a path — only as much as the command runs on. */
160
+ function targetFieldsHtml(e, preset = {}) {
161
+ const workspaces = loadWorkspaces();
162
+ const keys = Object.keys(workspaces);
163
+ const ws = keys.includes(preset.ws) ? preset.ws : (keys.includes('dev') ? 'dev' : keys[0]);
164
+ const refresh = 'hx-get="/fragments/cc/target" hx-target="closest .cc-target" hx-swap="outerHTML" hx-trigger="change" hx-include="closest form"';
165
+ const parts = [`<label>Workspace <select class="uk-select uk-form-small" name="target_ws" ${refresh}>
166
+ ${keys.map((k) => `<option value="${esc(k)}"${k === ws ? ' selected' : ''}>${esc(workspaces[k].label)}</option>`).join('')}
167
+ </select></label>`];
168
+ if (e.on !== 'workspace' && ws) {
169
+ const names = cc.listFolders(workspaceDir(ws));
170
+ const project = names.includes(preset.project) ? preset.project : '';
171
+ parts.push(`<label>Project <select class="uk-select uk-form-small" name="target_project" ${refresh}>
172
+ <option value="">${e.on === 'project' ? 'Pick a project…' : `None — inside the ${esc(ws)} folder`}</option>
173
+ ${names.map((n) => `<option value="${esc(n)}"${n === project ? ' selected' : ''}>${esc(n)}</option>`).join('')}
174
+ </select></label>`);
175
+ if (e.on === 'folder' || e.on === 'file') {
176
+ const listId = `cc-paths-${esc(e.id)}`;
177
+ parts.push(`<label class="cc-wide">${e.on === 'file' ? 'File' : 'Folder'} <span class="uk-text-meta">${e.on === 'file'
178
+ ? 'relative to the project, or to the workspace folder when no project is picked'
179
+ : 'relative; empty is the project or workspace folder itself'}</span>
180
+ <input class="uk-input uk-form-small cc-code" name="target_path" value="${esc(preset.path || '')}" list="${listId}" autocomplete="off" spellcheck="false"
181
+ placeholder="${e.on === 'file' ? 'composer.json' : 'web/modules/custom'}"
182
+ hx-get="/fragments/cc/paths" hx-target="#${listId}" hx-swap="outerHTML" hx-trigger="focus, keyup changed delay:300ms" hx-include="closest form"></label>
183
+ <datalist id="${listId}"></datalist>`);
184
+ }
185
+ }
186
+ return `<fieldset class="cc-target"><legend class="uk-text-meta">What it runs on: ${esc(cc.ON[e.on] || e.on)}</legend>${parts.join('')}</fieldset>`;
187
+ }
188
+
189
+ function pathsDatalistHtml(e, form) {
190
+ return `<datalist id="cc-paths-${esc(e.id)}">${cc.pathSuggestions(e, form).map((p) => `<option value="${esc(p)}">`).join('')}</datalist>`;
191
+ }
192
+
193
+ function optionFieldsHtml(e) {
194
+ if (!e.options.length) return '';
195
+ return `<fieldset class="cc-options"><legend class="uk-text-meta">Options</legend>${e.options.map((o) => {
196
+ const name = `opt_${esc(o.name)}`;
197
+ const label = esc(o.label || o.name);
198
+ if (o.values.length && o.enforced) {
199
+ return `<label>${label} <select class="uk-select uk-form-small" name="${name}">${o.required ? '' : '<option value=""></option>'}
200
+ ${o.values.map((v) => `<option value="${esc(v)}"${v === o.value ? ' selected' : ''}>${esc(v)}</option>`).join('')}</select></label>`;
201
+ }
202
+ const listId = `cc-opt-${esc(e.id)}-${esc(o.name)}`;
203
+ return `<label>${label} <input class="uk-input uk-form-small" name="${name}" value="${esc(o.value)}"${o.required ? ' required' : ''}${o.values.length ? ` list="${listId}"` : ''}>
204
+ ${o.values.length ? `<datalist id="${listId}">${o.values.map((v) => `<option value="${esc(v)}">`).join('')}</datalist>` : ''}</label>`;
205
+ }).join('')}</fieldset>`;
206
+ }
207
+
208
+ /** The run form: in the modal from the list, or inline on a command's own page. */
209
+ function runFormHtml(e, preset = {}, { inline = false } = {}) {
210
+ const result = inline ? '#cc-run-result' : '#webship-workspace-output';
211
+ const run = `<button type="button" class="uk-button ${e.confirm ? 'uk-button-danger arm-step' : 'uk-button-primary'}"${e.confirm ? ' hx-trigger="confirmed-remove"' : ''}
212
+ hx-post="/actions/cc-run" hx-include="closest form" hx-target="${result}" hx-swap="innerHTML">${icon('play', 0.8)} Run</button>`;
213
+ const form = `<form class="cc-run-form" onsubmit="return false">
214
+ <input type="hidden" name="id" value="${esc(e.id)}">
215
+ <input type="hidden" name="submitted" value="1">
216
+ <p class="uk-text-meta"><code>${esc(cc.lineOf(e))}</code> — ${esc(describeTarget(e))}${e.confirm ? '. It asks first, in a dialog.' : ''}</p>
217
+ ${targetFieldsHtml(e, preset)}
218
+ ${optionFieldsHtml(e)}
219
+ <div class="uk-margin-small-top">${run}</div>
220
+ </form>
221
+ ${inline ? '<div id="cc-run-result"></div>' : ''}`;
222
+ return inline ? form : `<div class="editor-card"><h3 class="uk-margin-small-bottom">Run ${esc(e.label)}</h3>${form}</div>`;
223
+ }
224
+
225
+ /**
226
+ * `suggest` is the project name a BUILDER would be given — the same one the Build form proposes.
227
+ * A builder run from here takes its name as the first argument, so the box opens with it already
228
+ * typed, and the flags are added after. Empty for anything that is not a builder: a tool takes a
229
+ * project that exists, and proposing a new name for it would be wrong.
230
+ */
231
+ function scriptRunFormHtml(ws, file, flags = [], suggest = '') {
232
+ return `<div class="editor-card">
233
+ <h3 class="uk-margin-small-bottom">Run ${esc(file)}</h3>
234
+ <p class="uk-text-meta">Runs from <code>${esc(ws)}/</code> with the arguments you give, in a screen session.${flags.length
235
+ ? ` Its flags: ${flags.map((f) => `<code>${esc(f.long)}</code>`).join(' ')}` : ''}${suggest
236
+ ? ` The name is filled in past what <code>${esc(ws)}/</code> already holds — type over it freely.` : ''}</p>
237
+ <form hx-post="/actions/command-run" ${OUT}>
238
+ <input type="hidden" name="target" value="${esc(ws)}">
239
+ <input type="hidden" name="script" value="${esc(file)}">
240
+ <input class="uk-input uk-margin-small-bottom cc-code" name="args" value="${esc(suggest)}"
241
+ placeholder="Arguments, e.g. myproject --install" spellcheck="false"
242
+ pattern="[a-zA-Z0-9 ._@^~:\\/=,\\-]*">
243
+ <button type="submit" class="uk-button uk-button-primary">${icon('play', 0.8)} Run</button>
244
+ </form>
245
+ </div>`;
246
+ }
247
+
248
+ function scriptPageBody({ ws, file, desc = '', flags = [], source = '', badge = '' }) {
249
+ return `<div class="cc-command-head"><span class="uk-label">script</span> ${badge} <code>${esc(ws)}/${esc(file)}</code></div>
250
+ ${desc ? `<p class="uk-margin-small">${esc(desc)}</p>` : ''}
251
+ ${flags.length ? `<p class="uk-text-meta">Flags: ${flags.map((f) => `<code>${esc(f.long)}</code>`).join(' ')}</p>` : ''}
252
+ <div class="project-actions cc-manage">${scriptButtons(ws, file)}</div>
253
+ <h4 class="uk-margin-small-bottom">The script</h4>
254
+ ${codeView('sh', source, `The script ${file}`)}`;
255
+ }
256
+
257
+ /** The add and edit form. `v` is the command as saved, or as it was just submitted when saving it was refused. */
258
+ function commandFormHtml(v, { cat, scripts = [], isNew = false }) {
259
+ const kind = v.script ? 'script' : 'run';
260
+ const groups = [...new Set(cat.commands.map((c) => c.group))];
261
+ const refs = scripts.flatMap((g) => g.items.map((it) => `${g.key}/${it.file}`));
262
+ if (v.script && !refs.includes(v.script)) refs.unshift(v.script);
263
+ const idField = isNew
264
+ ? `<label>Id <span class="uk-text-meta">made from the label when empty</span>
265
+ <input class="uk-input uk-form-small cc-code" name="cc_id" value="${esc(v.id || '')}" maxlength="45" spellcheck="false" placeholder="rebuild-caches"></label>`
266
+ : `<input type="hidden" name="id" value="${esc(v.id)}">`;
267
+ return `<form class="cc-form" hx-post="/actions/cc-save" hx-target="#cc-command" hx-swap="outerHTML">
268
+ <input type="hidden" name="mtime" value="${cat.mtime}">
269
+ <div class="cc-form-grid">
270
+ <label>Label <input class="uk-input uk-form-small" name="cc_label" value="${esc(v.label)}" maxlength="60" required></label>
271
+ ${idField}
272
+ <label>Group <input class="uk-input uk-form-small" name="cc_group" value="${esc(v.group)}" maxlength="40" list="cc-groups" placeholder="General"></label>
273
+ <datalist id="cc-groups">${groups.map((g) => `<option value="${esc(g)}">`).join('')}</datalist>
274
+ <label>Runs on <select class="uk-select uk-form-small" name="cc_on">${Object.entries(cc.ON)
275
+ .map(([k, t]) => `<option value="${k}"${k === v.on ? ' selected' : ''}>${esc(t)}</option>`).join('')}</select></label>
276
+ <label>Shows on <input class="uk-input uk-form-small cc-code" name="cc_when" value="${esc(v.when.join(' '))}" placeholder="any"
277
+ title="any, ddev, composer, node, git, workspace:name — each may start with !"></label>
278
+ <label class="cc-wide">Description <input class="uk-input uk-form-small" name="cc_description" value="${esc(v.description)}" maxlength="200"></label>
279
+ <fieldset class="cc-wide cc-kind"><legend class="uk-text-meta">What it runs</legend>
280
+ <label class="cc-inline"><input class="uk-radio" type="radio" name="cc_kind" value="run"${kind === 'run' ? ' checked' : ''}> A command line</label>
281
+ <label class="cc-inline"><input class="uk-radio" type="radio" name="cc_kind" value="script"${kind === 'script' ? ' checked' : ''}> A script of the tree</label>
282
+ <label class="cc-wide">Command line <textarea class="uk-textarea uk-form-small cc-code" name="cc_run" rows="1" maxlength="400" spellcheck="false" placeholder="ddev drush cr" data-ace="sh" data-ace-single="1">${esc(v.run)}</textarea></label>
283
+ <label>Script <select class="uk-select uk-form-small" name="cc_script"><option value="">Pick a script…</option>
284
+ ${refs.map((r) => `<option value="${esc(r)}"${r === v.script ? ' selected' : ''}>${esc(r)}</option>`).join('')}</select></label>
285
+ <label>Arguments <textarea class="uk-textarea uk-form-small cc-code" name="cc_args" rows="1" maxlength="400" spellcheck="false" placeholder="{{project}} --install" data-ace="sh" data-ace-single="1">${esc(v.args)}</textarea></label>
286
+ </fieldset>
287
+ <label>Tags <input class="uk-input uk-form-small" name="cc_tags" value="${esc(v.tags.join(' '))}" placeholder="drupal database"></label>
288
+ <label>Opens <select class="uk-select uk-form-small" name="cc_open">
289
+ <option value="toast"${v.open === 'toast' ? ' selected' : ''}>a toast linking to it</option>
290
+ <option value="screen"${v.open === 'screen' ? ' selected' : ''}>its screen, right away</option></select></label>
291
+ <label class="cc-inline"><input class="uk-checkbox" type="checkbox" name="cc_confirm" value="1"${v.confirm ? ' checked' : ''}> ask before running</label>
292
+ </div>
293
+ <p class="uk-text-meta">A line gets values only as <code>{{workspace}}</code> <code>{{project}}</code> <code>{{folder}}</code> <code>{{file}}</code>
294
+ <code>{{path}}</code> and <code>{{opt.name}}</code>, each single-quoted for you. It cannot contain $ or a backtick.
295
+ ${v.options.length ? 'Its options are kept as they are: change those in Definition.' : 'Options are added in Definition.'}</p>
296
+ <button type="submit" class="uk-button uk-button-primary uk-button-small">${icon('check')} ${isNew ? 'Add the command' : 'Save the command'}</button>
297
+ </form>`;
298
+ }
299
+
300
+ /** Where a command is used on the project rows, and a form to put it on another button. */
301
+ function onButtonsHtml(e) {
302
+ const used = cc.usedBy(e.id);
303
+ const list = used.length
304
+ ? `<ul class="uk-list uk-list-collapse cc-used">${used.map((a) => `<li><strong>${esc(rl.buttonLabel(a.button))}</strong> → ${esc(a.group)} → ${esc(a.label)}${a.steps.length > 1
305
+ ? ` <span class="uk-text-meta">(one of its ${a.steps.length} steps)</span>` : ''}</li>`).join('')}</ul>`
306
+ : '<p class="uk-text-meta">Not on any button yet.</p>';
307
+ if (e.on === 'file') {
308
+ return `<h4 class="uk-margin-small-bottom">On buttons</h4>${list}
309
+ <p class="uk-text-meta">It runs on a file, so it cannot go on a button: a project row gives no file to run it on.</p>`;
310
+ }
311
+ const layout = rl.loadRowLayout();
312
+ const buttons = rl.knownButtonIds(layout).filter((b) => rl.hasMenu(b));
313
+ return `<h4 class="uk-margin-small-bottom">On buttons</h4>
314
+ ${list}
315
+ <form class="cc-add-to-button" hx-post="/actions/cc-add-to-button" hx-target="#cc-command" hx-swap="outerHTML">
316
+ <input type="hidden" name="id" value="${esc(e.id)}">
317
+ <input type="hidden" name="mtime" value="${rl.layoutMtime()}">
318
+ <div class="cc-form-grid">
319
+ <label>Button <select class="uk-select uk-form-small" name="button">${buttons.map((b) => `<option value="${esc(b)}"${b === 'more' ? ' selected' : ''}>${esc(rl.buttonLabel(b))}</option>`).join('')}</select></label>
320
+ <label>Group <input class="uk-input uk-form-small" name="group" value="${esc(e.group)}" maxlength="40"></label>
321
+ <label>Label <input class="uk-input uk-form-small" name="label" value="${esc(e.label)}" maxlength="60"></label>
322
+ </div>
323
+ <p class="uk-text-meta">It shows on the rows its node filter matches (${esc(rl.describeWhen(e.when))}), and runs on that row's project.</p>
324
+ <button type="submit" class="uk-button uk-button-default uk-button-small">${icon('plus')} Add to the button</button>
325
+ </form>`;
326
+ }
327
+
328
+ function commandPageBody(e, { message = '', draft = null, scripts = [] } = {}) {
329
+ const cat = cc.loadCatalog();
330
+ const hasBuiltin = cat.builtin.some((c) => c.id === e.id);
331
+ const row = (k, v) => (v ? `<dt>${esc(k)}</dt><dd>${v}</dd>` : '');
332
+ const options = e.options.map((o) => `<code>${esc(o.name)}</code>${o.required ? ' (required)' : ''}${o.values.length ? `: ${esc(o.values.join(', '))}` : ''}`).join('; ');
333
+ const manage = 'hx-target="#cc-command" hx-swap="outerHTML"';
334
+ const script = e.script
335
+ ? `<a href="${scriptHref(...e.script.split('/'))}"><code>${esc(e.script)}</code></a>${e.args ? ` <code>${esc(e.args)}</code>` : ''}`
336
+ : '';
337
+ return `<div id="cc-command">
338
+ ${message}
339
+ <div class="cc-command-head">${sourceBadge(e)} <span class="uk-text-meta">${esc(describeTarget(e))}</span></div>
340
+ ${e.description ? `<p class="uk-margin-small">${esc(e.description)}</p>` : ''}
341
+ <dl class="cc-dl">
342
+ ${row('Id', `<code>${esc(e.id)}</code>`)}
343
+ ${row('Group', esc(e.group))}
344
+ ${row('Runs on', esc(cc.ON[e.on]))}
345
+ ${row('Shows on', esc(rl.describeWhen(e.when)))}
346
+ ${e.run ? row('Runs', `<code>${esc(e.run)}</code>`) : row('Script', script)}
347
+ ${row('Options', options)}
348
+ ${row('Asks first', e.confirm ? 'yes — it asks in a dialog before it runs' : '')}
349
+ ${row('Opens', e.open === 'screen' ? 'its screen, right away' : 'a toast linking to its session')}
350
+ ${row('Tags', esc(e.tags.join(', ')))}
351
+ </dl>
352
+ <h4 class="uk-margin-small-bottom">Run</h4>
353
+ ${runFormHtml(e, {}, { inline: true })}
354
+ ${onButtonsHtml(e)}
355
+ <h4 id="edit" class="uk-margin-small-bottom">Edit</h4>
356
+ ${e.source === 'builtin' ? '<p class="uk-text-meta">A built-in command. Saving keeps your version in your commands, in the same place in the list; deleting yours brings this one back.</p>' : ''}
357
+ ${commandFormHtml(draft || e, { cat, scripts })}
358
+ <div class="project-actions cc-manage">
359
+ <button type="button" class="uk-button uk-button-default uk-button-small" hx-post="/actions/cc-copy" ${vals({ id: e.id, mtime: cat.mtime, from: 'page' })} ${manage}>${icon('copy')} Copy</button>
360
+ ${e.source === 'yours' ? `<button type="button" class="uk-button uk-button-danger uk-button-small arm-step" hx-post="/actions/cc-remove"
361
+ ${vals({ id: e.id, mtime: cat.mtime, from: 'page' })} ${manage} hx-trigger="confirmed-remove">${icon('trash')} ${hasBuiltin ? 'Delete yours, back to the built-in one' : 'Delete'}</button>` : ''}
362
+ </div>
363
+ <details class="row-definition-help"><summary>As YAML</summary>${codeView('yaml', yaml.dump({ commands: [cc.commandToDoc(e)] }, { lineWidth: 140, noRefs: true }), 'This command as YAML')}</details>
364
+ </div>`;
365
+ }
366
+
367
+ function newPageBody({ scripts = [], draft = null, message = '', preset = {} } = {}) {
368
+ const cat = cc.loadCatalog();
369
+ const v = draft || cc.cleanCommand({ on: 'project', script: preset.script || '' }, 'yours');
370
+ return `<div id="cc-command">
371
+ ${message}
372
+ <h3 class="uk-margin-small-bottom">A command</h3>
373
+ <p class="uk-text-meta">A command line, or a script of the tree with its arguments, that runs on a workspace folder, a project, a folder or a
374
+ file. It joins the list and runs from its own page.</p>
375
+ ${commandFormHtml(v, { cat, scripts, isNew: true })}
376
+ <div class="cc-new-other">
377
+ <h3 class="uk-margin-small-bottom">A script</h3>
378
+ <p class="uk-text-meta">A <code>cmd-*.sh</code> in a workspace folder, scaffolded with the bootstrap chain already correct. It is listed here as a script.</p>
379
+ <div class="project-actions">
380
+ <button class="uk-button uk-button-default uk-button-small" hx-get="/fragments/commands/new" ${MODAL}>${icon('plus-circle')} New script</button>
381
+ <button class="uk-button uk-button-secondary uk-button-small" hx-get="/fragments/commands/new?ai=1" ${MODAL}>${icon('bolt')} Generate a script with AI</button>
382
+ </div>
383
+ <h3 class="uk-margin-small-bottom">An action on a button</h3>
384
+ <p class="uk-text-meta">Steps in a project row's button menu (DDEV, GIT, More, or a button of your own), with options, a node filter and on_error steps.</p>
385
+ <a class="uk-button uk-button-default uk-button-small" href="/settings/buttons">${icon('thumbnails')} Open Buttons</a>
386
+ </div>
387
+ </div>`;
388
+ }
389
+
390
+ const EXAMPLE = `commands:
391
+ - id: update-database
392
+ label: Update the database
393
+ group: Drupal
394
+ description: Switch branch, install, run the updates
395
+ on: project # workspace | project | folder | file
396
+ when: [ddev, composer, "!workspace:products"]
397
+ confirm: true # ask in a dialog before it runs
398
+ open: screen # toast (the default) | screen
399
+ tags: [drupal, database]
400
+ options:
401
+ - name: branch
402
+ label: Branch
403
+ value: main
404
+ regex: '^[A-Za-z0-9._/-]+$'
405
+ required: true
406
+ run: git switch {{opt.branch}} && ddev composer install && ddev drush updb -y
407
+ - id: backup-dev-project
408
+ label: Back up the project
409
+ on: project
410
+ when: [workspace:dev]
411
+ script: dev/cmd-tools-backup-dev.sh # a script of the tree, instead of run
412
+ args: '{{project}}'`;
413
+
414
+ function definitionBody({ message = '', draft = null } = {}) {
415
+ const cat = cc.loadCatalog();
416
+ let defaults = '';
417
+ try { defaults = fs.readFileSync(cc.defaultsFile(), 'utf8'); } catch (_) { /* shown as empty */ }
418
+ return `<div id="cc-definition">
419
+ ${message}
420
+ <form hx-post="/actions/cc-definition-save" hx-target="#cc-definition" hx-swap="outerHTML">
421
+ <input type="hidden" name="mtime" value="${cat.mtime}">
422
+ <p class="uk-text-meta">Your commands as YAML — the file the other tabs edit. Copy it to keep or share; paste one in to load it.
423
+ <strong>Check</strong> says what would change and saves nothing. A command with a built-in id is your version of that command.</p>
424
+ <textarea class="uk-textarea row-definition-text" name="definition" rows="22" spellcheck="false" data-ace="yaml">${esc(draft === null ? cc.definitionText() : draft)}</textarea>
425
+ <div class="row-definition-buttons">
426
+ <button type="button" class="uk-button uk-button-default uk-button-small" hx-post="/actions/cc-definition-check" hx-include="closest form"
427
+ hx-target="#cc-definition-report" hx-swap="innerHTML">Check</button>
428
+ <button type="submit" class="uk-button uk-button-primary uk-button-small">${icon('check')} Save</button>
429
+ </div>
430
+ <div id="cc-definition-report"></div>
431
+ </form>
432
+ <details class="row-definition-help"><summary>A command with every field</summary>${codeView('yaml', EXAMPLE, 'A command with every field')}
433
+ <p class="uk-text-meta"><code>on</code> says what it runs on, and so which values its line can use: <code>{{workspace}}</code> always,
434
+ <code>{{project}}</code> on a project, folder or file, <code>{{folder}}</code> on a folder or file, <code>{{file}}</code> on a file, and
435
+ <code>{{path}}</code> for the full path of whatever it runs on. <code>when</code>: every clause must match. Each value is single-quoted.</p>
436
+ </details>
437
+ <details class="row-definition-help"><summary>The built-in commands (${cat.builtin.length})</summary>${codeView('yaml', defaults, 'The built-in commands')}</details>
438
+ <h4>Start again</h4>
439
+ <p class="uk-text-meta">Deletes <code>${esc(tildePath(cc.userFile()))}</code>: every command of yours goes, and every built-in one is as shipped.</p>
440
+ <button type="button" class="uk-button uk-button-danger uk-button-small arm-step" hx-post="/actions/cc-reset" ${vals({ mtime: cat.mtime })}
441
+ hx-target="#cc-definition" hx-swap="outerHTML" hx-trigger="confirmed-remove">Remove all of yours</button>
442
+ </div>`;
443
+ }
444
+
445
+ module.exports = {
446
+ tabsHtml,
447
+ sourceBadge,
448
+ describeTarget,
449
+ catalogListHtml,
450
+ commandRowHtml,
451
+ targetFieldsHtml,
452
+ pathsDatalistHtml,
453
+ runFormHtml,
454
+ scriptRunFormHtml,
455
+ scriptPageBody,
456
+ commandFormHtml,
457
+ commandPageBody,
458
+ newPageBody,
459
+ definitionBody,
460
+ };