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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/PLUGINS.md +249 -0
  2. package/README.md +379 -2
  3. package/WORKSPACES.md +69 -0
  4. package/actions/ai-items.js +293 -0
  5. package/actions/audio.js +48 -0
  6. package/actions/build-order.js +79 -0
  7. package/actions/checks.js +95 -0
  8. package/actions/claude-session.js +104 -0
  9. package/actions/commands-center.js +258 -0
  10. package/actions/composer.js +267 -0
  11. package/actions/ddev-addons.js +90 -0
  12. package/actions/ddev-mail.js +80 -0
  13. package/actions/ddev.js +237 -0
  14. package/actions/folders.js +165 -0
  15. package/actions/images-svg.js +33 -0
  16. package/actions/images.js +145 -0
  17. package/actions/navigation.js +190 -0
  18. package/actions/node.js +222 -0
  19. package/actions/notes.js +55 -0
  20. package/actions/plugins.js +36 -0
  21. package/actions/project-make.js +28 -0
  22. package/actions/rag.js +202 -0
  23. package/actions/row-actions.js +262 -0
  24. package/actions/screens.js +270 -0
  25. package/actions/testing.js +121 -0
  26. package/actions/video-edit.js +54 -0
  27. package/ai-inventory.js +182 -0
  28. package/ai-items.js +202 -0
  29. package/assistant-cast.js +176 -0
  30. package/assistant-suggestions.js +91 -0
  31. package/assistant.js +484 -0
  32. package/audio-voice.js +528 -0
  33. package/auth-google.js +260 -0
  34. package/build-ai.js +52 -0
  35. package/build-order-page.js +191 -0
  36. package/build-order.js +313 -0
  37. package/builder-args.js +117 -0
  38. package/changelog.js +302 -0
  39. package/chat-threads.js +134 -0
  40. package/checks.js +105 -0
  41. package/claude-args.js +282 -0
  42. package/claude-sessions.js +129 -0
  43. package/command-author.js +117 -0
  44. package/command-block.js +20 -0
  45. package/commands/defaults.yml +159 -0
  46. package/commands-page.js +460 -0
  47. package/commands.js +568 -0
  48. package/cucumber.js +43 -0
  49. package/ddev-addons-page.js +124 -0
  50. package/ddev-addons.js +154 -0
  51. package/ddev-custom-commands.js +96 -0
  52. package/ddev-domains.js +84 -0
  53. package/ddev-import.js +93 -0
  54. package/ddev-mail-page.js +167 -0
  55. package/ddev-mail.js +110 -0
  56. package/ddev-options.js +145 -0
  57. package/ddev-snapshots.js +126 -0
  58. package/ddev.js +145 -0
  59. package/docs/DEVELOPER-GUIDE.md +224 -0
  60. package/docs/README.md +252 -0
  61. package/docs/REFERENCE.md +684 -0
  62. package/docs/USER-GUIDE.md +697 -0
  63. package/docs/screenshots/assistant/avatar-board.png +0 -0
  64. package/docs/screenshots/assistant/avatar-coffee.png +0 -0
  65. package/docs/screenshots/assistant/avatar-colours.png +0 -0
  66. package/docs/screenshots/assistant/avatar-icons.png +0 -0
  67. package/docs/screenshots/assistant/avatar-moves.png +0 -0
  68. package/docs/screenshots/assistant/avatar-pick-cookieman.png +0 -0
  69. package/docs/screenshots/assistant/avatar-settings-voices.png +0 -0
  70. package/docs/screenshots/assistant/avatar-status.png +0 -0
  71. package/docs/screenshots/create-card/create-card.png +0 -0
  72. package/docs/screenshots/create-card/start-ai.png +0 -0
  73. package/docs/screenshots/guide-clone-address.png +0 -0
  74. package/docs/screenshots/guide-clone-worktree-off.png +0 -0
  75. package/docs/screenshots/guide-clone-worktree-on.png +0 -0
  76. package/docs/screenshots/guide-ddev-menu.png +0 -0
  77. package/docs/screenshots/guide-domains.png +0 -0
  78. package/docs/screenshots/guide-home.png +0 -0
  79. package/docs/screenshots/guide-ide-menu.png +0 -0
  80. package/docs/screenshots/guide-remove-refusal.png +0 -0
  81. package/docs/screenshots/guide-rows-worktree.png +0 -0
  82. package/docs/screenshots/guide-screens.png +0 -0
  83. package/docs/screenshots/guide-settings.png +0 -0
  84. package/docs/screenshots/guide-workspace-page.png +0 -0
  85. package/docs/screenshots/home/card-settings.png +0 -0
  86. package/docs/screenshots/home/drag-reorder.png +0 -0
  87. package/docs/screenshots/home/edit-card.png +0 -0
  88. package/docs/screenshots/images/claude-code-image.png +0 -0
  89. package/docs/screenshots/images/edit-with-ai.png +0 -0
  90. package/docs/screenshots/images/gallery.png +0 -0
  91. package/docs/screenshots/images/generate-description.png +0 -0
  92. package/docs/screenshots/images/generate-plan.png +0 -0
  93. package/docs/screenshots/images/providers.png +0 -0
  94. package/docs/screenshots/images/svg-form.png +0 -0
  95. package/docs/screenshots/images/svg-result.png +0 -0
  96. package/docs/screenshots/images/upload.png +0 -0
  97. package/docs/screenshots/new-folder/2-form.png +0 -0
  98. package/docs/screenshots/new-folder/8-ddev-options.png +0 -0
  99. package/docs/screenshots/new-folder/files-folder.png +0 -0
  100. package/docs/screenshots/projects/ddev-custom-commands.png +0 -0
  101. package/docs/screenshots/projects/state-chip-stop.png +0 -0
  102. package/docs/screenshots/readme/dashboard-dark.png +0 -0
  103. package/docs/screenshots/readme/dashboard-light.png +0 -0
  104. package/docs/screenshots/readme/dev-dark.png +0 -0
  105. package/docs/screenshots/readme/dev-light.png +0 -0
  106. package/docs/screenshots/readme/docs-dark.png +0 -0
  107. package/docs/screenshots/readme/docs-light.png +0 -0
  108. package/docs/screenshots/readme/icons/ai.svg +4 -0
  109. package/docs/screenshots/readme/icons/code.svg +4 -0
  110. package/docs/screenshots/readme/icons/docs.svg +4 -0
  111. package/docs/screenshots/readme/icons/services.svg +4 -0
  112. package/docs/screenshots/readme/icons/sites.svg +4 -0
  113. package/docs/screenshots/readme/test-report-dark.png +0 -0
  114. package/docs/screenshots/readme/test-report-light.png +0 -0
  115. package/docs/screenshots/readme/test-scenarios.png +0 -0
  116. package/docs/screenshots/readme/test-steps.png +0 -0
  117. package/docs/screenshots/readme/videos-dark.png +0 -0
  118. package/docs/screenshots/readme/videos-light.png +0 -0
  119. package/docs/screenshots/screens-menu.png +0 -0
  120. package/docs/screenshots/screens-new.png +0 -0
  121. package/docs/screenshots/settings/auth-google-lists.png +0 -0
  122. package/docs/screenshots/settings/delete-1-row.png +0 -0
  123. package/docs/screenshots/settings/delete-2-confirm.png +0 -0
  124. package/docs/screenshots/settings/delete-3-type-name.png +0 -0
  125. package/docs/screenshots/settings/delete-4-type-yes.png +0 -0
  126. package/event-log.js +59 -0
  127. package/files-browser.js +247 -0
  128. package/folders.js +121 -0
  129. package/format.js +73 -0
  130. package/git-info.js +81 -0
  131. package/github-auth.js +62 -0
  132. package/html.js +10 -0
  133. package/icons.js +83 -0
  134. package/images-ai.js +447 -0
  135. package/images-create.js +226 -0
  136. package/images-page.js +417 -0
  137. package/images-records.js +39 -0
  138. package/images-render.js +36 -0
  139. package/images-svg-core.js +214 -0
  140. package/images-svg-sanitize.js +188 -0
  141. package/images-svg.js +241 -0
  142. package/images.js +270 -0
  143. package/jobs.js +430 -0
  144. package/linger.js +21 -0
  145. package/lists.js +292 -0
  146. package/media-info.js +136 -0
  147. package/navigation-page.js +264 -0
  148. package/navigation.js +394 -0
  149. package/notes.js +0 -0
  150. package/package.json +47 -4
  151. package/palette-index.js +46 -0
  152. package/playwright.config.js +23 -0
  153. package/plugin-install.js +239 -0
  154. package/plugin-loader.js +466 -0
  155. package/plugin-manifest.js +83 -0
  156. package/plugin-settings.js +44 -0
  157. package/plugins/workspace-notes/index.js +59 -0
  158. package/plugins-page.js +103 -0
  159. package/project-make.js +241 -0
  160. package/project-name.js +210 -0
  161. package/public/assistant-avatar-moves.js +646 -0
  162. package/public/assistant-avatar-ui.js +1223 -0
  163. package/public/assistant-avatar.js +530 -0
  164. package/public/avatar/druplicon.vrm +0 -0
  165. package/public/css/base.css +260 -0
  166. package/public/css/components/ai-assistant.css +263 -0
  167. package/public/css/components/assistant-avatar.css +279 -0
  168. package/public/css/components/assistant-views.css +198 -0
  169. package/public/css/components/build-order.css +74 -0
  170. package/public/css/components/clone-as-a-git-worktree.css +30 -0
  171. package/public/css/components/command-palette.css +61 -0
  172. package/public/css/components/commands-center.css +139 -0
  173. package/public/css/components/confirm-dialog.css +40 -0
  174. package/public/css/components/create-card.css +8 -0
  175. package/public/css/components/ddev-menu.css +23 -0
  176. package/public/css/components/deep-chat.css +17 -0
  177. package/public/css/components/density.css +11 -0
  178. package/public/css/components/floating-assistant-workspace-pages.css +55 -0
  179. package/public/css/components/folders.css +5 -0
  180. package/public/css/components/full-height-left-assistant-sidebar-all-pages.css +39 -0
  181. package/public/css/components/full-height-right-actions-rail-pages-that-have-actions.css +267 -0
  182. package/public/css/components/grid-and-list.css +203 -0
  183. package/public/css/components/help-page.css +36 -0
  184. package/public/css/components/hero.css +16 -0
  185. package/public/css/components/images.css +70 -0
  186. package/public/css/components/layout-rhythm-and-spacing-polish.css +139 -0
  187. package/public/css/components/mic-on-any-field-that-wants-typing.css +416 -0
  188. package/public/css/components/modal-fit.css +113 -0
  189. package/public/css/components/page-width.css +23 -0
  190. package/public/css/components/plugins-page.css +9 -0
  191. package/public/css/components/project-cards.css +256 -0
  192. package/public/css/components/project-rows.css +390 -0
  193. package/public/css/components/reordering-the-workspace-cards.css +85 -0
  194. package/public/css/components/responsive-home-and-the-workspace-pages.css +457 -0
  195. package/public/css/components/running-commands-as-toasts.css +327 -0
  196. package/public/css/components/settings-editor-core-config-yml.css +828 -0
  197. package/public/css/components/short-results-as-a-toast-at-the-top.css +203 -0
  198. package/public/css/components/sound-player.css +84 -0
  199. package/public/css/components/suggest-agents-skills-commands.css +112 -0
  200. package/public/css/components/tab-modal.css +142 -0
  201. package/public/css/components/terminals.css +244 -0
  202. package/public/css/components/test-report-media-one-file-per-row-paged.css +144 -0
  203. package/public/css/components/top-toolbar-logo-breadcrumb.css +107 -0
  204. package/public/css/components/video-edit.css +6 -0
  205. package/public/css/components/web-ide-open-with.css +23 -0
  206. package/public/css/components/working-indicators.css +158 -0
  207. package/public/css/components/workspace-cards.css +79 -0
  208. package/public/favicon.ico +0 -0
  209. package/public/favicon.png +0 -0
  210. package/public/htmx.min.js +1 -0
  211. package/public/logo.png +0 -0
  212. package/public/media-player.js +215 -0
  213. package/public/mic.js +255 -0
  214. package/public/narration.js +110 -0
  215. package/public/palette.js +278 -0
  216. package/public/sortable.js +251 -0
  217. package/public/suggest.js +337 -0
  218. package/public/terminal.js +903 -0
  219. package/public/themes/default/theme.css +267 -0
  220. package/public/themes/ember/theme.css +180 -0
  221. package/public/themes/press/theme.css +290 -0
  222. package/public/themes/slate/theme.css +182 -0
  223. package/public/touch-icon.png +0 -0
  224. package/public/ui.js +2721 -0
  225. package/public/vendor/ace/LICENSE.txt +24 -0
  226. package/public/vendor/ace/ace.js +23 -0
  227. package/public/vendor/ace/ext-language_tools.js +8 -0
  228. package/public/vendor/ace/ext-searchbox.js +8 -0
  229. package/public/vendor/ace/mode-css.js +8 -0
  230. package/public/vendor/ace/mode-html.js +8 -0
  231. package/public/vendor/ace/mode-javascript.js +8 -0
  232. package/public/vendor/ace/mode-markdown.js +8 -0
  233. package/public/vendor/ace/mode-sh.js +8 -0
  234. package/public/vendor/ace/mode-yaml.js +8 -0
  235. package/public/vendor/ace/theme-textmate.js +8 -0
  236. package/public/vendor/ace/theme-tomorrow_night.js +8 -0
  237. package/public/vendor/ace/worker-yaml.js +1 -0
  238. package/public/vendor/deep-chat.LICENSE +21 -0
  239. package/public/vendor/deep-chat.bundle.js +1 -0
  240. package/public/vendor/fonts/dm-serif-text-LICENSE.txt +93 -0
  241. package/public/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
  242. package/public/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
  243. package/public/vendor/fonts/figtree-LICENSE.txt +93 -0
  244. package/public/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
  245. package/public/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
  246. package/public/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
  247. package/public/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
  248. package/public/vendor/fonts/jetbrains-mono-LICENSE.txt +93 -0
  249. package/public/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  250. package/public/vendor/fonts/ubuntu-mono-LICENSE.txt +98 -0
  251. package/public/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
  252. package/public/vendor/tabler-LICENSE.txt +21 -0
  253. package/public/vendor/tabler-sprite.svg +1 -0
  254. package/public/vendor/three/LICENSE +21 -0
  255. package/public/vendor/three/addons/loaders/GLTFLoader.js +4860 -0
  256. package/public/vendor/three/addons/utils/BufferGeometryUtils.js +1501 -0
  257. package/public/vendor/three/addons/utils/SkeletonUtils.js +496 -0
  258. package/public/vendor/three/three.core.min.js +6 -0
  259. package/public/vendor/three/three.module.min.js +6 -0
  260. package/public/vendor/three-vrm/LICENSE +22 -0
  261. package/public/vendor/three-vrm/three-vrm.module.min.js +983 -0
  262. package/public/vendor/uikit-icons.min.js +1 -0
  263. package/public/vendor/uikit.min.css +1 -0
  264. package/public/vendor/uikit.min.js +1 -0
  265. package/public/vendor/webship-icons/ui/bookmark-outlined.svg +1 -0
  266. package/public/vendor/webship-icons/ui/headset.svg +1 -0
  267. package/public/vendor/webship-icons/ui/menu.svg +1 -0
  268. package/public/vendor/webship-icons/ui/pause.svg +1 -0
  269. package/public/vendor/webship-icons/ui/play.svg +1 -0
  270. package/public/vendor/webship-icons/ui/share.svg +1 -0
  271. package/public/vendor/webship-icons/ui/sound-off.svg +1 -0
  272. package/public/vendor/webship-icons/ui/sound-on.svg +1 -0
  273. package/public/vendor/webship-icons/ui/stop.svg +1 -0
  274. package/public/vendor/xterm/addon-fit.js +2 -0
  275. package/public/vendor/xterm/addon-unicode11.js +2 -0
  276. package/public/vendor/xterm/addon-web-links.js +2 -0
  277. package/public/vendor/xterm/xterm.css +218 -0
  278. package/public/vendor/xterm/xterm.js +2 -0
  279. package/rag.js +169 -0
  280. package/reader.js +132 -0
  281. package/repo-blobs.js +66 -0
  282. package/report-pdf.js +337 -0
  283. package/row-layout.js +1439 -0
  284. package/screenrc +37 -0
  285. package/screens.js +1063 -0
  286. package/scripts/code-server-start.sh +190 -0
  287. package/scripts/images-claude-finish.js +107 -0
  288. package/scripts/screen-host.sh +114 -0
  289. package/scripts/screen-runner.js +362 -0
  290. package/scripts/screen-then.js +47 -0
  291. package/scripts/update.sh +350 -0
  292. package/server.js +7923 -0
  293. package/settings-file.js +423 -0
  294. package/speech.js +297 -0
  295. package/tab-modal.js +75 -0
  296. package/test-reports.js +260 -0
  297. package/themes.js +381 -0
  298. package/transcripts.js +168 -0
  299. package/urls.js +13 -0
  300. package/validate.js +18 -0
  301. package/version-check.js +94 -0
  302. package/video-edit.js +588 -0
  303. package/waiting.js +85 -0
  304. package/workspace-card-tools.js +41 -0
  305. package/workspace-delete.js +139 -0
  306. package/workspace-types.js +85 -0
  307. package/workspaces.js +553 -0
package/build-order.js ADDED
@@ -0,0 +1,313 @@
1
+ /**
2
+ * The order of the builders in **Build a new project**.
3
+ *
4
+ * The list used to be alphabetical, which is an order about filenames rather than about work: eleven
5
+ * `cmd-automated-testing-…` builds sat at the top of the test workspace, and `cmd-drupalcms2-x-…`
6
+ * above Drupal and Webship everywhere else, so the first entry was never the thing most people came to
7
+ * build. A shipped order fixed that for the builders we happen to ship. It cannot answer for a machine
8
+ * that builds something else all day, which is what this is: the order is a setting.
9
+ *
10
+ * Two scopes, the same shape the navigation uses — a global order for every workspace, and a workspace's
11
+ * own applied BEFORE it, because a workspace that says anything about its order is saying something more
12
+ * specific than the machine-wide default. `test` can put its automated-testing builds first without
13
+ * changing `dev`.
14
+ *
15
+ * Three ranks, in this order, and a builder is placed by the first one that knows it:
16
+ *
17
+ * 1. the workspace's own order, then the global order (the two concatenated, deduplicated);
18
+ * 2. DEFAULT_ORDER, what the dashboard ships — still a default, not a competitor, so it keeps
19
+ * arranging everything nobody has said anything about;
20
+ * 3. alphabetical, which is where a builder nobody has ever mentioned goes.
21
+ *
22
+ * So a global order of three entries does not scatter the other thirty-six: it moves three and leaves
23
+ * the rest exactly as they were.
24
+ *
25
+ * Nothing here HIDES a builder. A missing builder and a builder you have to scroll to are different
26
+ * problems, and only the second one was asked about; `hidden:` can be added to the same file later
27
+ * without changing a line of what it writes today.
28
+ */
29
+
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+ const yaml = require('js-yaml');
33
+
34
+ const { CONFIG_DIR } = require('./workspaces');
35
+
36
+ // Overridable so a test can arrange an order without touching the machine's own, the way navigation.js
37
+ // and row-layout.js are. Read through the function, never captured at require time.
38
+ const orderFile = () => process.env.BUILD_ORDER_FILE || path.join(CONFIG_DIR, 'build-order.yml');
39
+
40
+ // A builder is named by its own file, because that is the one name that cannot drift: a
41
+ // `# workspace-name:` header is prose somebody will improve, and an order keyed on prose forgets itself
42
+ // the moment the prose changes. The pattern is findBuilderScripts' own, so nothing can be written here
43
+ // that is not a builder.
44
+ const BUILDER_RE = /^cmd-(?!tools?-)[a-z0-9][a-z0-9._-]*-project\.sh$/i;
45
+ const COMMAND_RE = /^cmd-[a-z0-9][a-z0-9._-]*\.sh$/i;
46
+ const isBuilder = (v) => BUILDER_RE.test(String(v || ''));
47
+ /**
48
+ * A tool is every other command in the folder — `cmd-tools-backup-dev.sh`, `cmd-tools-clone.sh`,
49
+ * `cmd-all-front-end-build-themes.sh`. Defined as the REMAINDER rather than by a pattern of its own, so
50
+ * a command shaped like nothing anyone anticipated is still arrangeable instead of silently missing.
51
+ */
52
+ const isTool = (v) => COMMAND_RE.test(String(v || '')) && !isBuilder(v);
53
+ const isCommand = (v) => COMMAND_RE.test(String(v || ''));
54
+
55
+ // The two lists a scope can hold. They are separate, and not one list of everything, because the two
56
+ // answer different questions: a builder starts a project and a tool acts on one, and the places that
57
+ // list them — Build a new project, Run a tool — mostly show one kind or the other.
58
+ const KINDS = ['builders', 'tools'];
59
+ const KIND_OF = (v) => (isBuilder(v) ? 'builders' : 'tools');
60
+
61
+ /**
62
+ * The order the dashboard ships: the three bases first — the current Drupal core, the one after it,
63
+ * Drupal CMS — then the site templates, grouped by the base they are built on.
64
+ *
65
+ * Grouped by BASE and not by template, because the base is the thing you decide first: "a Drupal CMS
66
+ * site" is a different day's work from "a Webship site", and the four templates repeat identically
67
+ * under each. Alphabetically the two sets interleave, so the same template appeared twice in the list with twenty
68
+ * rows between them and nothing saying which base either was.
69
+ *
70
+ * Patterns rather than names, so a builder keeps its place across a version bump: `cmd-webship11-0-x`
71
+ * and `cmd-webship11-0-0` are one entry here, and neither has to be re-listed when 11.0.1 arrives.
72
+ *
73
+ * `cmd-drupalcms2-x-project.sh` is anchored exactly — it is Drupal CMS itself, and the pattern that
74
+ * groups its templates (`^cmd-drupalcms2-x-`) would otherwise swallow it and put the base last.
75
+ */
76
+ const DEFAULT_ORDER = [
77
+ // The bases.
78
+ /^cmd-drupal11-recommended-project\.sh$/,
79
+ /^cmd-drupal12-recommended-project\.sh$/,
80
+ /^cmd-drupalcms2-x-project\.sh$/,
81
+ /^cmd-webship11-/,
82
+ /^cmd-webships/,
83
+ /^cmd-cucumber11-/,
84
+ // The Website stack and the site templates built for it, Website Starter first: it is the
85
+ // reference template, the one to copy and the one to check a change against.
86
+ /^cmd-website_starter-project\.sh$/,
87
+ /^cmd-website_starter-1-/,
88
+ /^cmd-website-1-/,
89
+ /^cmd-webship_starter-1-/,
90
+ /^cmd-webship_portal-1-/,
91
+ /^cmd-webapi_starter-1-/,
92
+ /^cmd-webships_starter-1-/,
93
+ // Anything else on Drupal CMS keeps with its own kind rather than falling to the alphabet.
94
+ /^cmd-drupalcms2-x-/,
95
+ ];
96
+
97
+ // The examples that are not Drupal go after everything else, WordPress before Odoo.
98
+ const LAST_ORDER = [/^cmd-wordpress-/, /^cmd-wpbedrock/, /^cmd-odoo/];
99
+
100
+ /** Where a builder sits in the shipped order, or past the end of it. */
101
+ function defaultRank(file) {
102
+ const i = DEFAULT_ORDER.findIndex((re) => re.test(file));
103
+ if (i !== -1) return i;
104
+ const last = LAST_ORDER.findIndex((re) => re.test(file));
105
+ return last === -1 ? DEFAULT_ORDER.length : DEFAULT_ORDER.length + 1 + last;
106
+ }
107
+
108
+ /* ------------------------------------------------------------------ *
109
+ * Reading
110
+ * ------------------------------------------------------------------ */
111
+
112
+ function readYaml() {
113
+ try {
114
+ return yaml.load(fs.readFileSync(orderFile(), 'utf8')) || {};
115
+ } catch (_) {
116
+ return {};
117
+ }
118
+ }
119
+
120
+ let memo = { at: 0, mtime: 0, raw: null };
121
+
122
+ function loadOrder() {
123
+ const now = Date.now();
124
+ let mtime = 0;
125
+ try { mtime = fs.statSync(orderFile()).mtimeMs; } catch (_) { mtime = 0; }
126
+ // Re-read when the file moved, and at most every 2s otherwise: this is on the path of every workspace
127
+ // page, and workspaces.js memoises its own config exactly this way.
128
+ if (!memo.raw || mtime !== memo.mtime || now - memo.at > 2000) {
129
+ memo = { at: now, mtime, raw: readYaml() };
130
+ }
131
+ return { raw: memo.raw, mtime: memo.mtime };
132
+ }
133
+
134
+ /** One scope's settings, with every key present so callers never test for undefined. */
135
+ function scopeOrder(raw, scope) {
136
+ const src = (scope === 'global' ? raw.global : ((raw.workspaces || {})[scope])) || {};
137
+ const read = (key, ok) => {
138
+ const list = Array.isArray(src[key]) ? src[key] : [];
139
+ // Deduplicated as well as filtered: the same name twice would give one of the two a rank it can
140
+ // never reach, which reads as a drag that did not save.
141
+ return [...new Set(list.map(String).filter(ok))];
142
+ };
143
+ return { builders: read('builders', isBuilder), tools: read('tools', isTool) };
144
+ }
145
+
146
+ /**
147
+ * The names that decide the order on a page of `scope`: the workspace's own first, then the global ones
148
+ * it did not mention.
149
+ *
150
+ * The workspace comes FIRST, which is the opposite of how the navigation layers its two scopes, and
151
+ * deliberately: the navigation applies a workspace's settings after the global ones so a workspace can
152
+ * override individual entries, while here the whole point of a workspace saying anything is that its own
153
+ * answer is the more specific one.
154
+ */
155
+ function orderFor(scope, kind = 'builders', { without = false } = {}) {
156
+ const { raw } = loadOrder();
157
+ const global = without && scope === 'global' ? [] : scopeOrder(raw, 'global')[kind];
158
+ if (scope === 'global') return global;
159
+ const own = without ? [] : scopeOrder(raw, scope)[kind];
160
+ return [...own, ...global.filter((f) => !own.includes(f))];
161
+ }
162
+
163
+ /**
164
+ * Sort a list of builder filenames for one scope.
165
+ *
166
+ * `localeCompare` for the tail, which is what findBuilderScripts has always used — the same order you
167
+ * would get from `sort` in the shell, near enough, and stable across the list being rebuilt.
168
+ *
169
+ * `{ without: true }` sorts as if THIS scope had said nothing, which is how the writer works out which
170
+ * of the names it was posted are decisions and which are just where things already were.
171
+ */
172
+ function sortBuilders(files, scope, opts) {
173
+ return sortWith(files, orderFor(scope, 'builders', opts));
174
+ }
175
+
176
+ /**
177
+ * The same for a workspace's TOOLS. No shipped default under it: what a folder's tools should be called
178
+ * in what order is not something this dashboard can have an opinion about — `cmd-tools-backup-dev.sh`
179
+ * before `cmd-tools-clone.sh` is alphabetical and arbitrary, and arbitrary is exactly what somebody
180
+ * arranging this page is fixing.
181
+ */
182
+ function sortTools(files, scope, opts) {
183
+ const named = orderFor(scope, 'tools', opts);
184
+ const rank = (f) => {
185
+ const i = named.indexOf(f);
186
+ return i === -1 ? named.length : i;
187
+ };
188
+ return [...files].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
189
+ }
190
+
191
+ /**
192
+ * A mixed list — everything a workspace folder holds — as builders first, then tools.
193
+ *
194
+ * Builders first because a list of a workspace's commands is read to find something to DO, and the
195
+ * thing that makes a project is the one people are most often after. The two never interleave: the
196
+ * alphabet used to put `cmd-tools-backup-dev.sh` between two builders, which reads as a mistake.
197
+ */
198
+ function sortCommands(files, scope, opts) {
199
+ const builders = sortBuilders(files.filter(isBuilder), scope, opts);
200
+ const tools = sortTools(files.filter((f) => !isBuilder(f)), scope, opts);
201
+ return [...builders, ...tools];
202
+ }
203
+
204
+ /** The same sort against an explicit list of names — what a caller weighing a candidate order needs. */
205
+ function sortWith(files, named) {
206
+ const rank = (f) => {
207
+ const i = named.indexOf(f);
208
+ return i === -1 ? named.length + defaultRank(f) : i;
209
+ };
210
+ return [...files].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
211
+ }
212
+
213
+ /**
214
+ * The names that would decide the order for `scope` if its own list were `own`.
215
+ *
216
+ * Same rule as orderFor, against a list that has not been written yet — which is how the writer can ask
217
+ * "would saving this much be enough?" without touching the file.
218
+ */
219
+ function namedWith(scope, kind, own) {
220
+ if (scope === 'global') return [...own];
221
+ const global = scopeOrder(loadOrder().raw, 'global')[kind];
222
+ return [...own, ...global.filter((f) => !own.includes(f))];
223
+ }
224
+
225
+ /**
226
+ * The SHORTEST list that reproduces `posted`.
227
+ *
228
+ * Not the posted list: forty names written down would freeze the order of thirty-nine builders nobody
229
+ * has an opinion about, so a builder added next month would land at the bottom instead of in its
230
+ * shipped place — and the file would assert decisions nobody made.
231
+ *
232
+ * It cannot be found by comparing positions, which is what this did first and got wrong: moving one
233
+ * builder from the bottom to the top shifts every other row down by one, so NOTHING sits where the
234
+ * defaults would have put it and all forty names look like decisions. One move wrote twenty-five lines.
235
+ *
236
+ * So ask the question that is actually being asked. Try the shortest prefix first: if naming just the
237
+ * first builder already produces the arrangement in front of you — because the defaults do the rest —
238
+ * that one name is the whole decision. Forty sorts of forty items in the worst case, on a keystroke
239
+ * nobody makes twice a minute.
240
+ */
241
+ function minimalOrder(posted, scope, kind = 'builders') {
242
+ const same = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
243
+ // Tools have no shipped order under them, so their sort is by the named list then the alphabet.
244
+ const sorted = (named) => (kind === 'builders'
245
+ ? sortWith(posted, named)
246
+ : [...posted].sort((a, b) => {
247
+ const r = (f) => (named.indexOf(f) === -1 ? named.length : named.indexOf(f));
248
+ return r(a) - r(b) || a.localeCompare(b);
249
+ }));
250
+ for (let k = 0; k < posted.length; k += 1) {
251
+ const candidate = posted.slice(0, k);
252
+ if (same(sorted(namedWith(scope, kind, candidate)), posted)) return candidate;
253
+ }
254
+ return [...posted];
255
+ }
256
+
257
+ /* ------------------------------------------------------------------ *
258
+ * Writing
259
+ * ------------------------------------------------------------------ */
260
+
261
+ function saveOrder(raw) {
262
+ const body = '# The order of a workspace\'s commands, per scope: its builders and its tools.\n'
263
+ + '# Written by Settings → Command order. Only what you changed is here; deleting this file is a full reset.\n'
264
+ + '# A command nobody names keeps its shipped place, and then its alphabetical one.\n'
265
+ + '# Never synced: sync-tooling.sh copies only workspace.*.settings.yml into core/config.\n'
266
+ + yaml.dump(raw, { lineWidth: 100 });
267
+ fs.mkdirSync(path.dirname(orderFile()), { recursive: true });
268
+ fs.writeFileSync(orderFile(), body);
269
+ memo = { at: 0, mtime: 0, raw: null };
270
+ return fs.statSync(orderFile()).mtimeMs;
271
+ }
272
+
273
+ /** The whole file, for a writer that is about to change one scope in it. */
274
+ function rawOrder() {
275
+ const { raw, mtime } = loadOrder();
276
+ return { raw: JSON.parse(JSON.stringify(raw || {})), mtime };
277
+ }
278
+
279
+ /** Put one scope's order back into the whole file, dropping what is empty so the file stays readable. */
280
+ function withScope(raw, scope, settings) {
281
+ const clean = {};
282
+ for (const kind of KINDS) {
283
+ // Only what this save was given: saving the builders must not erase the tools.
284
+ const list = settings[kind];
285
+ if (list && list.length) clean[kind] = list;
286
+ else if (list === undefined) {
287
+ const had = scopeOrder(raw, scope)[kind];
288
+ if (had.length) clean[kind] = had;
289
+ }
290
+ }
291
+ const out = { ...raw };
292
+ if (scope === 'global') {
293
+ if (Object.keys(clean).length) out.global = clean; else delete out.global;
294
+ } else {
295
+ const ws = { ...(out.workspaces || {}) };
296
+ if (Object.keys(clean).length) ws[scope] = clean; else delete ws[scope];
297
+ if (Object.keys(ws).length) out.workspaces = ws; else delete out.workspaces;
298
+ }
299
+ return out;
300
+ }
301
+
302
+ /** Has this scope said anything of its own? Used to dot its tab. */
303
+ function scopeHasOrder(scope) {
304
+ const { raw } = loadOrder();
305
+ const own = scopeOrder(raw, scope);
306
+ return KINDS.some((k) => own[k].length > 0);
307
+ }
308
+
309
+ module.exports = {
310
+ orderFile, isBuilder, isTool, isCommand, KINDS, KIND_OF, BUILDER_RE, COMMAND_RE, DEFAULT_ORDER, defaultRank,
311
+ loadOrder, scopeOrder, orderFor, sortBuilders, sortTools, sortCommands, sortWith, namedWith, minimalOrder,
312
+ rawOrder, withScope, saveOrder, scopeHasOrder,
313
+ };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * A builder's arguments, read from the file that actually parses them.
3
+ *
4
+ * The dashboard's Arguments panel and the /actions/build endpoint both take their
5
+ * schema from here, so what the form offers is exactly what the script accepts.
6
+ *
7
+ * It used to guess. A script without an inline argparse heredoc was assumed to
8
+ * load `args/arg-<distribution_name>.sh`, which is true only for the builders
9
+ * that source that file through the variable. Measured across the tree it was
10
+ * wrong for 96 builders:
11
+ *
12
+ * - the Drupal CMS site-template builders declare no distribution_name, so they
13
+ * got NO panel at all — no way to pass --install or --site-template-version;
14
+ * - the Webship site-template builders declare distribution_name="webship" but
15
+ * source arg-webship-site-template.sh, so the panel offered --enable and the
16
+ * --skip-* flags, which that parser REFUSES ("unrecognized arguments", the
17
+ * build exits before it starts), and omitted --site-template-version and
18
+ * --php-version;
19
+ * - the free Drupal CMS site-template builders were shown arg-drupal_cms.sh.
20
+ *
21
+ * So the order is the order the script itself runs: an inline heredoc if it has
22
+ * one, otherwise the first `source ${WORKSPACE_SCRIPTS}/args/…` line — a literal
23
+ * file name, or `arg-${distribution_name}.sh` resolved from the script.
24
+ */
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+
29
+ /** Every `name="value"` assignment a script makes at the top level, last one wins. */
30
+ function scriptAssignments(src) {
31
+ const out = {};
32
+ for (const m of src.matchAll(/^[ \t]*([A-Za-z_][A-Za-z0-9_]*)="([^"$`]*)";?[ \t]*(?:#.*)?$/gm)) {
33
+ out[m[1]] = m[2];
34
+ }
35
+ return out;
36
+ }
37
+
38
+ /**
39
+ * Expand `${var}` and `${var:-fallback}` the way the builder's unquoted heredoc
40
+ * would, from the builder's own assignments. Without this the panel showed the
41
+ * literal text `${site_template_version_default:-1.0.x-dev}` as a default.
42
+ */
43
+ function expandDefaults(text, vars) {
44
+ return text.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g,
45
+ (_, name, fallback) => (vars[name] !== undefined && vars[name] !== '' ? vars[name] : (fallback ?? '')));
46
+ }
47
+
48
+ function parseArgparseText(argSrc) {
49
+ const out = [];
50
+ for (const block of argSrc.split('parser.add_argument(').slice(1)) {
51
+ const flags = [...block.matchAll(/'(--?[a-zA-Z0-9_\-]+)'/g)].map((m) => m[1]);
52
+ const long = flags.find((f) => f.startsWith('--'));
53
+ if (!long) continue; // positional (PROJECT_NAME etc.)
54
+ const bool = /action='store_true'/.test(block.split('parser.add_argument')[0]);
55
+ const dm2 = block.match(/default=(?:"([^"]*)"|'([^']*)'|(False|True))/);
56
+ let def = dm2 ? (dm2[1] ?? dm2[2] ?? dm2[3]) : '';
57
+ if (def === 'False') def = false; else if (def === 'True') def = true;
58
+ if (def === '_none_') def = '';
59
+ const hm = block.match(/help='((?:[^'\\]|\\.)*)'/);
60
+ // What the parser will ACCEPT, when it says so. argparse refuses anything else by name, so an
61
+ // argument with choices is a list to pick from rather than a box to type a typo into — and the
62
+ // Build form can draw the control the argument's own shape asks for.
63
+ const cm = block.match(/choices=\[([^\]]*)\]/);
64
+ const choices = cm
65
+ ? [...cm[1].matchAll(/'([^']*)'|"([^"]*)"/g)].map((m) => m[1] ?? m[2]).filter((v) => v !== undefined)
66
+ : [];
67
+ out.push({ flag: long, bool, def, choices, help: hm ? hm[1].replace(/\\'/g, "'") : '' });
68
+ }
69
+ return out;
70
+ }
71
+
72
+ /**
73
+ * Where a builder's arguments come from:
74
+ * { kind: 'inline' } | { kind: 'file', file } | null
75
+ */
76
+ function argSourceOf(src, root) {
77
+ if (/argparse "\$@"/.test(src) && src.includes('parser.add_argument(')) return { kind: 'inline' };
78
+ const m = src.match(/^[ \t]*source[ \t]+\$\{?WORKSPACE_SCRIPTS\}?\/args\/(arg-[A-Za-z0-9_${}.-]+\.sh)/m);
79
+ if (!m) return null;
80
+ let name = m[1];
81
+ if (name.includes('$')) {
82
+ const vars = scriptAssignments(src);
83
+ name = expandDefaults(name, vars);
84
+ if (name.includes('$') || /arg-\.sh$/.test(name)) return null;
85
+ }
86
+ return { kind: 'file', file: path.join(root, 'core', 'scripts', 'args', name) };
87
+ }
88
+
89
+ /**
90
+ * The schema for one builder: [{ flag, bool, def, help }].
91
+ *
92
+ * `def` is the EFFECTIVE default: the arg file's own, expanded from the builder's
93
+ * assignments, and — when that is empty — the value the builder falls back to for
94
+ * that flag (`--site-template-version` → site_template_version), because the Drupal
95
+ * CMS builders pin their line that way: `SITE_TEMPLATE_VERSION="${SITE_TEMPLATE_VERSION:-${site_template_version}}"`.
96
+ * Showing an empty default there would tell the user the builder takes the latest
97
+ * release when it actually builds 1.0.x-dev.
98
+ */
99
+ function parseBuilderArgs(dir, script, root) {
100
+ try {
101
+ const src = fs.readFileSync(path.join(dir, script), 'utf8');
102
+ const where = argSourceOf(src, root);
103
+ if (!where) return [];
104
+ const vars = scriptAssignments(src);
105
+ const text = where.kind === 'inline' ? src : fs.readFileSync(where.file, 'utf8');
106
+ return parseArgparseText(expandDefaults(text, vars)).map((a) => {
107
+ if (a.bool || a.def) return a;
108
+ const dest = a.flag.replace(/^--/, '').replace(/-/g, '_');
109
+ const own = vars[dest] ?? vars[`${dest}_default`];
110
+ return own ? { ...a, def: own } : a;
111
+ });
112
+ } catch (_) {
113
+ return [];
114
+ }
115
+ }
116
+
117
+ module.exports = { parseBuilderArgs, parseArgparseText, argSourceOf, expandDefaults, scriptAssignments };