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