@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/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
|
+
};
|
package/builder-args.js
ADDED
|
@@ -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 };
|