@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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
|
2
|
+
<rect width="64" height="64" rx="14" fill="#deedff"/>
|
|
3
|
+
<g transform="translate(16 16)" fill="none" stroke="#0074e0" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /> <path d="M9 10l.01 0" /> <path d="M15 10l.01 0" /> <path d="M9.5 15a3.5 3.5 0 0 0 5 0" /></g>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
|
2
|
+
<rect width="64" height="64" rx="14" fill="#deedff"/>
|
|
3
|
+
<g transform="translate(16 16)" fill="none" stroke="#0074e0" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" /> <path d="M4 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" /> <path d="M14 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" /> <path d="M14 7l6 0" /> <path d="M17 4l0 6" /></g>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
|
2
|
+
<rect width="64" height="64" rx="14" fill="#deedff"/>
|
|
3
|
+
<g transform="translate(16 16)" fill="none" stroke="#0074e0" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3v4a1 1 0 0 0 1 1h4" /> <path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2" /> <path d="M9 9l1 0" /> <path d="M9 13l6 0" /> <path d="M9 17l6 0" /></g>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
|
2
|
+
<rect width="64" height="64" rx="14" fill="#deedff"/>
|
|
3
|
+
<g transform="translate(16 16)" fill="none" stroke="#0074e0" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6a8 3 0 1 0 16 0a8 3 0 1 0 -16 0" /> <path d="M4 6v6a8 3 0 0 0 16 0v-6" /> <path d="M4 12v6a8 3 0 0 0 16 0v-6" /></g>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
|
2
|
+
<rect width="64" height="64" rx="14" fill="#deedff"/>
|
|
3
|
+
<g transform="translate(16 16)" fill="none" stroke="#0074e0" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M7 8l-4 4l4 4" /> <path d="M17 8l4 4l-4 4" /> <path d="M14 4l-4 16" /></g>
|
|
4
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/event-log.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A log for tracking what went wrong, and when.
|
|
3
|
+
*
|
|
4
|
+
* actions.log says what was done; nothing said what was refused or what broke, and the request that failed
|
|
5
|
+
* left no trace at all: a rejected form answers 400 to one browser and is gone. This is that trace, one JSON
|
|
6
|
+
* object per line in logs/dashboard.log, kept on this machine (gitignored, never synced).
|
|
7
|
+
*
|
|
8
|
+
* Text that reaches it goes through redact() and clip() first: a prompt can hold a token, and a log is
|
|
9
|
+
* the one file nobody reviews.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const MAX_BYTES = 5 * 1024 * 1024;
|
|
16
|
+
const KEEP = 3;
|
|
17
|
+
|
|
18
|
+
function logFile() {
|
|
19
|
+
return process.env.WORKSPACE_APP_LOG || path.join(__dirname, 'logs', 'dashboard.log');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Known secret shapes and `key: value` pairs that name one, replaced before anything is written. */
|
|
23
|
+
function redact(text) {
|
|
24
|
+
return String(text == null ? '' : text)
|
|
25
|
+
.replace(/\b(sk|pk|ghp|gho|ghs|github_pat|xox[abprs]|glpat|AIza)[-_A-Za-z0-9]{12,}/g, '[redacted]')
|
|
26
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, 'Bearer [redacted]')
|
|
27
|
+
.replace(/\b([A-Za-z_]*(?:password|passwd|token|secret|api[_-]?key)[A-Za-z_]*)(\s*[:=]\s*)("[^"]*"|'[^']*'|\S+)/gi, '$1$2[redacted]');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function clip(text, max = 300) {
|
|
31
|
+
const s = redact(text).replace(/\s+/g, ' ').trim();
|
|
32
|
+
return s.length > max ? `${s.slice(0, max)}…` : s;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The text of an HTML message, for a log line: tags gone, entities left as they are. */
|
|
36
|
+
function plain(html, max = 240) {
|
|
37
|
+
return clip(String(html || '').replace(/<[^>]*>/g, ' '), max);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function rotate(file) {
|
|
41
|
+
for (let i = KEEP - 1; i >= 1; i--) {
|
|
42
|
+
try { fs.renameSync(`${file}.${i}`, `${file}.${i + 1}`); } catch (_) { /* not there yet */ }
|
|
43
|
+
}
|
|
44
|
+
try { fs.renameSync(file, `${file}.1`); } catch (_) { /* nothing to rotate */ }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function logEvent(level, event, fields = {}) {
|
|
48
|
+
const file = logFile();
|
|
49
|
+
const line = JSON.stringify({ time: new Date().toISOString(), level, event, ...fields });
|
|
50
|
+
try {
|
|
51
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
52
|
+
let size = 0;
|
|
53
|
+
try { size = fs.statSync(file).size; } catch (_) { /* first write */ }
|
|
54
|
+
if (size > MAX_BYTES) rotate(file);
|
|
55
|
+
fs.appendFileSync(file, `${line}\n`, { mode: 0o600 });
|
|
56
|
+
} catch (_) { /* a log must never take an action down with it */ }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { logEvent, redact, clip, plain, logFile, MAX_BYTES, KEEP };
|
package/files-browser.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Walking a documents folder, not just the top of it.
|
|
3
|
+
*
|
|
4
|
+
* `docs/` holds 29 folders and eight thousand files; the listing showed 21 rows. Everything in a
|
|
5
|
+
* subfolder was invisible, and so was every file whose extension was not on a curated list — a
|
|
6
|
+
* screenshot, a recording, a zip of a client's exports. The folder on disk and the page describing it
|
|
7
|
+
* disagreed, which is the one thing a file listing must not do.
|
|
8
|
+
*
|
|
9
|
+
* So: folders are rows you open, at any depth, and anything in the folder is listed. Three rules keep
|
|
10
|
+
* that honest and bounded:
|
|
11
|
+
*
|
|
12
|
+
* - **One level at a time.** A folder row says how many entries are inside and opens into them. The
|
|
13
|
+
* alternative — every file in the tree on one page — is 8,105 rows here, which is not a listing.
|
|
14
|
+
* - **A path is checked, never trusted.** Every subfolder arrives from the browser, so it is split
|
|
15
|
+
* into segments, refused if any is `..`, resolved with realpath and refused again if the result
|
|
16
|
+
* left the workspace. A symlink out of the tree is the case the segment check alone misses.
|
|
17
|
+
* - **Noise is named, not guessed at.** The workspace's own plumbing (`cmd-*.sh`, the
|
|
18
|
+
* `*.code-workspace`, `README`/`AGENTS`/`CLAUDE`) and the directories nobody browses
|
|
19
|
+
* (`node_modules`, the `dev-ai-agents` checkout, dotfiles) are excluded by name. Everything else is
|
|
20
|
+
* shown, whatever its extension — the point of the change is that nothing in the folder hides.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
|
|
26
|
+
const { workspaceDir } = require('./workspaces');
|
|
27
|
+
|
|
28
|
+
// Which workspaces browse their folders: the ones that hold files a person put in a folder.
|
|
29
|
+
//
|
|
30
|
+
// Not the `ai-*` ones — they list items by the name declared in their frontmatter, and a subdirectory
|
|
31
|
+
// there is a skill they already handle, so folders would be a second, competing idea of what a row is.
|
|
32
|
+
//
|
|
33
|
+
// `videos` is here too, and the rule that kept it out is kept rather than dropped: a poster frame is
|
|
34
|
+
// an input to the player, not something to browse, and listing it doubled the rows and made half of
|
|
35
|
+
// them stills. It is now attached to its recording as the THUMBNAIL (see `attachPosters`), which is
|
|
36
|
+
// what a poster was always for.
|
|
37
|
+
// `specs` is browsable for the same reason docs is, and it is the one that most needs it: a spec is
|
|
38
|
+
// a FOLDER (products/<project>/NNN-topic/ holding four files, or products/<project>/ holding the
|
|
39
|
+
// product's constitution), so a flat read of the top level found nothing at all and the page said
|
|
40
|
+
// "No specs here yet" on a workspace with twelve of them.
|
|
41
|
+
const BROWSABLE = new Set(['docs', 'designs', 'images', 'videos', 'audio', 'specs']);
|
|
42
|
+
|
|
43
|
+
/** Is this the still that belongs to a recording, rather than a picture in its own right? */
|
|
44
|
+
const IMAGE_RE = /\.(png|jpe?g|gif|webp|avif)$/i;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Give each recording its poster, and take the poster off the list.
|
|
48
|
+
*
|
|
49
|
+
* Two shapes, both of which the recorder writes: `demo.mp4` beside `demo-poster.jpg`, and `demo.mp4`
|
|
50
|
+
* beside `demo.jpg` — the second already groups into the same row, so it only has to be told that the
|
|
51
|
+
* picture is the poster rather than a second format to offer a button for.
|
|
52
|
+
*
|
|
53
|
+
* Mutates and returns the groups map, because it is the same pass that built it.
|
|
54
|
+
*/
|
|
55
|
+
function attachPosters(groups) {
|
|
56
|
+
for (const [stem, g] of groups) {
|
|
57
|
+
if (!g.formats.some((f) => f.video)) continue;
|
|
58
|
+
// The picture sharing the recording's name: its poster, not a format of it.
|
|
59
|
+
const inline = g.formats.find((f) => IMAGE_RE.test(f.file));
|
|
60
|
+
if (inline) {
|
|
61
|
+
g.poster = inline.path || inline.file;
|
|
62
|
+
g.formats = g.formats.filter((f) => f !== inline);
|
|
63
|
+
}
|
|
64
|
+
// And the `-poster` one, which readdir gave a row of its own.
|
|
65
|
+
const sidecar = groups.get(`${stem}-poster`);
|
|
66
|
+
if (sidecar && sidecar.formats.every((f) => IMAGE_RE.test(f.file))) {
|
|
67
|
+
g.poster = g.poster || (sidecar.formats[0].path || sidecar.formats[0].file);
|
|
68
|
+
groups.delete(`${stem}-poster`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return groups;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const TEXT_RE = /\.(txt|md)$/i;
|
|
75
|
+
const SOUND_SIDECARS = ['-cover', '-poster', '-transcript', '-script', '-voiceover-script'];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The audio workspace lists SOUNDS and what belongs to them, the way videos lists recordings.
|
|
79
|
+
*
|
|
80
|
+
* A sound's belongings are its cover picture, the text it was made from (or spoken), and the record of
|
|
81
|
+
* how it was made. They sit beside it as `<name>.jpg`, `<name>.txt`, `<name>.json` (or `<name>-cover.jpg`,
|
|
82
|
+
* `<name>-transcript.md`), and they are part of its row, not rows of their own. A file that is neither a
|
|
83
|
+
* sound nor the belonging of one is not listed: this folder is for audio.
|
|
84
|
+
*/
|
|
85
|
+
function attachSoundBelongings(groups) {
|
|
86
|
+
const sounds = [...groups].filter(([, g]) => g.formats.some((f) => f.audio));
|
|
87
|
+
for (const [stem, g] of sounds) {
|
|
88
|
+
const own = g.formats.filter((f) => !f.audio);
|
|
89
|
+
const take = (f) => {
|
|
90
|
+
const at = f.path || f.file;
|
|
91
|
+
if (IMAGE_RE.test(f.file)) g.poster = g.poster || at;
|
|
92
|
+
else if (TEXT_RE.test(f.file)) g.transcript = g.transcript || at;
|
|
93
|
+
else if (/\.json$/i.test(f.file)) g.record = g.record || at;
|
|
94
|
+
else return false;
|
|
95
|
+
return true;
|
|
96
|
+
};
|
|
97
|
+
for (const f of own) take(f);
|
|
98
|
+
for (const suffix of SOUND_SIDECARS) {
|
|
99
|
+
const side = groups.get(`${stem}${suffix}`);
|
|
100
|
+
if (side && side.formats.every((f) => !f.audio) && side.formats.some((f) => take(f))) groups.delete(`${stem}${suffix}`);
|
|
101
|
+
}
|
|
102
|
+
g.formats = g.formats.filter((f) => f.audio);
|
|
103
|
+
g.size = g.formats.reduce((n, f) => n + f.size, 0);
|
|
104
|
+
}
|
|
105
|
+
for (const [stem, g] of [...groups]) if (!g.formats.some((f) => f.audio)) groups.delete(stem);
|
|
106
|
+
return groups;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Directories nobody browses: an installed dependency tree, the gitignored mirror of the shared
|
|
110
|
+
// agents repository, and anything hidden.
|
|
111
|
+
const NOISE_DIRS = new Set(['node_modules', 'dev-ai-agents', 'workspace-repo', '.git']);
|
|
112
|
+
|
|
113
|
+
// The workspace's own plumbing, which is tooling rather than content: the builders, the editor's
|
|
114
|
+
// project file, and the three documents about the folder (which the page already renders on its own,
|
|
115
|
+
// under the list).
|
|
116
|
+
const NOISE_FILES = /^(cmd-.*\.sh|.*\.code-workspace|README\.md|AGENTS\.md|CLAUDE\.md)$/i;
|
|
117
|
+
|
|
118
|
+
const isNoise = (name, isDir) => name.startsWith('.')
|
|
119
|
+
|| (isDir ? NOISE_DIRS.has(name) : NOISE_FILES.test(name));
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The folder a request names, or an error saying why not.
|
|
123
|
+
*
|
|
124
|
+
* `rel` is what the page puts in its links and `abs` is what it reads. Both are derived here, so no
|
|
125
|
+
* caller builds a path out of a browser value itself.
|
|
126
|
+
*/
|
|
127
|
+
function safeSub(key, sub) {
|
|
128
|
+
return safeSubIn(workspaceDir(key), sub);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The same check against a root given directly, which is what makes it testable: a fixture with a
|
|
133
|
+
* symlink pointing out of the tree can be built in a temp directory, and the rule that matters — a
|
|
134
|
+
* link out is refused even though every segment was innocent — is then actually exercised.
|
|
135
|
+
*/
|
|
136
|
+
function safeSubIn(root, sub) {
|
|
137
|
+
if (!root) return { ok: false, error: 'Unknown workspace.' };
|
|
138
|
+
const raw = String(sub || '').replace(/^\/+|\/+$/g, '');
|
|
139
|
+
if (!raw) return { ok: true, rel: '', abs: root, root };
|
|
140
|
+
if (raw.includes('\0')) return { ok: false, error: 'Bad path.' };
|
|
141
|
+
const segs = raw.split('/');
|
|
142
|
+
// `.` and `..` never reach the filesystem, and neither does an empty segment (`a//b`).
|
|
143
|
+
if (segs.some((s) => !s || s === '.' || s === '..')) return { ok: false, error: 'Bad path.' };
|
|
144
|
+
const abs = path.join(root, ...segs);
|
|
145
|
+
let real;
|
|
146
|
+
let realRoot;
|
|
147
|
+
try {
|
|
148
|
+
real = fs.realpathSync(abs);
|
|
149
|
+
realRoot = fs.realpathSync(root);
|
|
150
|
+
} catch (_) {
|
|
151
|
+
return { ok: false, error: 'No such folder.' };
|
|
152
|
+
}
|
|
153
|
+
// The check the segments cannot make: a symlink inside the workspace pointing out of it.
|
|
154
|
+
if (real !== realRoot && !real.startsWith(realRoot + path.sep)) return { ok: false, error: 'Bad path.' };
|
|
155
|
+
try {
|
|
156
|
+
if (!fs.statSync(real).isDirectory()) return { ok: false, error: 'Not a folder.' };
|
|
157
|
+
} catch (_) {
|
|
158
|
+
return { ok: false, error: 'No such folder.' };
|
|
159
|
+
}
|
|
160
|
+
return { ok: true, rel: segs.join('/'), abs: real, root };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The real path of a file inside a workspace, or '' when it is not one. Containment is judged against
|
|
165
|
+
* the workspace's OWN real path, so a workspace that is itself a symlink onto a second disk serves its
|
|
166
|
+
* files, while a link from inside it to anywhere else is still refused.
|
|
167
|
+
*/
|
|
168
|
+
function realFileIn(root, rel) {
|
|
169
|
+
try {
|
|
170
|
+
const real = fs.realpathSync(path.join(root, rel));
|
|
171
|
+
const base = fs.realpathSync(root);
|
|
172
|
+
if (real !== base && !real.startsWith(base + path.sep)) return '';
|
|
173
|
+
return fs.statSync(real).isFile() ? real : '';
|
|
174
|
+
} catch (_) {
|
|
175
|
+
return '';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The subfolders of one directory, each with what is inside it.
|
|
181
|
+
*
|
|
182
|
+
* The count is of DIRECT entries, not of the whole subtree: one `readdir` per folder is cheap and
|
|
183
|
+
* true, while a recursive count of `screenshots/` would walk thousands of files to render one row —
|
|
184
|
+
* and "12 items" is what tells you whether opening it is worth it anyway.
|
|
185
|
+
*/
|
|
186
|
+
function subfolders(abs, rel) {
|
|
187
|
+
let entries = [];
|
|
188
|
+
try {
|
|
189
|
+
entries = fs.readdirSync(abs, { withFileTypes: true });
|
|
190
|
+
} catch (_) {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
return entries
|
|
194
|
+
.filter((e) => e.isDirectory() && !isNoise(e.name, true))
|
|
195
|
+
.map((e) => {
|
|
196
|
+
const full = path.join(abs, e.name);
|
|
197
|
+
let count = 0;
|
|
198
|
+
let folders = 0;
|
|
199
|
+
try {
|
|
200
|
+
for (const c of fs.readdirSync(full, { withFileTypes: true })) {
|
|
201
|
+
if (isNoise(c.name, c.isDirectory())) continue;
|
|
202
|
+
count += 1;
|
|
203
|
+
if (c.isDirectory()) folders += 1;
|
|
204
|
+
}
|
|
205
|
+
} catch (_) { /* unreadable: it is still a folder, it just says nothing */ }
|
|
206
|
+
let mtime = new Date(0);
|
|
207
|
+
try { mtime = fs.statSync(full).mtime; } catch (_) { /* keep the epoch */ }
|
|
208
|
+
return {
|
|
209
|
+
folder: true,
|
|
210
|
+
name: e.name,
|
|
211
|
+
rel: rel ? `${rel}/${e.name}` : e.name,
|
|
212
|
+
count,
|
|
213
|
+
folders,
|
|
214
|
+
mtime,
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** The trail back out: `[{ name, rel }]`, the workspace itself first, the current folder last. */
|
|
220
|
+
function crumbs(key, rel, rootLabel) {
|
|
221
|
+
const trail = [{ name: rootLabel || key, rel: '' }];
|
|
222
|
+
if (!rel) return trail;
|
|
223
|
+
const segs = rel.split('/');
|
|
224
|
+
segs.forEach((s, i) => trail.push({ name: s, rel: segs.slice(0, i + 1).join('/') }));
|
|
225
|
+
return trail;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// What a file looks like in a list, by extension. A kind, not a format: a row should say "this is a
|
|
229
|
+
// picture" at a glance, and the format is already written on its buttons.
|
|
230
|
+
const KIND_ICONS = [
|
|
231
|
+
[/\.(pdf)$/i, 'tabler:file-type-pdf'],
|
|
232
|
+
[/\.(docx?|odt|rtf)$/i, 'tabler:file-type-doc'],
|
|
233
|
+
[/\.(xlsx?|ods|csv)$/i, 'tabler:table'],
|
|
234
|
+
[/\.(pptx?|odp)$/i, 'tabler:presentation'],
|
|
235
|
+
[/\.(html?|xml)$/i, 'tabler:world'],
|
|
236
|
+
[/\.(png|jpe?g|gif|webp|avif|svg|bmp|ico|tiff?)$/i, 'tabler:photo'],
|
|
237
|
+
[/\.(mp4|m4v|webm|ogv|mov|avi|mkv)$/i, 'tabler:player-play'],
|
|
238
|
+
[/\.(mp3|wav|ogg|oga|opus|m4a|aac|flac|weba)$/i, 'tabler:music'],
|
|
239
|
+
[/\.(zip|tar|gz|tgz|bz2|xz|7z|rar)$/i, 'tabler:file-zip'],
|
|
240
|
+
[/\.(md|markdown|txt|log)$/i, 'tabler:file-text'],
|
|
241
|
+
[/\.(json|ya?ml|toml|ini|conf)$/i, 'tabler:code'],
|
|
242
|
+
[/\.(js|ts|jsx|tsx|css|scss|php|py|rb|sh|bash)$/i, 'tabler:code'],
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
const kindIcon = (name) => (KIND_ICONS.find(([re]) => re.test(name)) || [null, 'tabler:file'])[1];
|
|
246
|
+
|
|
247
|
+
module.exports = { BROWSABLE, isNoise, safeSub, safeSubIn, realFileIn, subfolders, crumbs, kindIcon, attachPosters, attachSoundBelongings, IMAGE_RE, NOISE_FILES, NOISE_DIRS };
|
package/folders.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A new empty folder in a workspace: a place to run commands, screens or Claude Code that no builder made.
|
|
3
|
+
*
|
|
4
|
+
* It shows up as a row like any project, so everything a folder without DDEV already gets (Screens, IDE,
|
|
5
|
+
* GIT, the folder and project commands, the note) works on it from the moment it exists.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const { esc } = require('./html');
|
|
12
|
+
|
|
13
|
+
// Lowercase so it can later become a DDEV project or a hostname without a rename.
|
|
14
|
+
const NAME_RE = /^[a-z0-9][a-z0-9_-]{0,62}$/;
|
|
15
|
+
// A files workspace keeps names people read (Webship-Translation-Demo-Guide), so case, spaces and dots are fine there.
|
|
16
|
+
const FILES_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9 _.-]{0,62}$/;
|
|
17
|
+
const RESERVED = new Set(['node_modules', 'backups', 'logs']);
|
|
18
|
+
|
|
19
|
+
const THEN = {
|
|
20
|
+
none: 'Just make the folder',
|
|
21
|
+
shell: 'Open a terminal in it',
|
|
22
|
+
claude: 'Start Claude Code in it',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Why a name cannot be used, or ''. */
|
|
26
|
+
function nameProblem(name, { files = false } = {}) {
|
|
27
|
+
const n = String(name || '');
|
|
28
|
+
if (!n) return 'Give the folder a name.';
|
|
29
|
+
if (files) {
|
|
30
|
+
if (!FILES_NAME_RE.test(n) || /[ .]$/.test(n)) return 'A folder name is letters, digits, spaces, dots, - and _, starting with a letter or digit (63 max).';
|
|
31
|
+
} else if (!NAME_RE.test(n)) return 'A folder name is lowercase letters, digits, - and _, starting with a letter or digit (63 max).';
|
|
32
|
+
if (RESERVED.has(n)) return `<code>${esc(n)}</code> is reserved in a workspace folder.`;
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Make <root>/<name>. Returns { ok, dir } or { ok: false, status, error }. */
|
|
37
|
+
function createFolder(root, name, { files = false } = {}) {
|
|
38
|
+
const problem = nameProblem(name, { files });
|
|
39
|
+
if (problem) return { ok: false, status: 400, error: problem };
|
|
40
|
+
let base;
|
|
41
|
+
try { base = fs.realpathSync(root); } catch (_) { return { ok: false, status: 404, error: 'The workspace folder does not exist.' }; }
|
|
42
|
+
const dir = path.join(base, name);
|
|
43
|
+
if (path.dirname(dir) !== base) return { ok: false, status: 400, error: 'That name would leave the workspace.' };
|
|
44
|
+
if (fs.existsSync(dir)) return { ok: false, status: 409, error: `<code>${esc(name)}</code> already exists here.` };
|
|
45
|
+
try {
|
|
46
|
+
fs.mkdirSync(dir, { mode: 0o755 });
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return { ok: false, status: 500, error: `The folder could not be made: ${esc(e.message)}` };
|
|
49
|
+
}
|
|
50
|
+
return { ok: true, dir };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The form in the editor modal. */
|
|
54
|
+
/** The form for a files workspace: a folder inside the one being viewed, with nothing to set up. */
|
|
55
|
+
function filesFolderFormHtml(ws, { label = ws, sub = '', suggestion = '' } = {}) {
|
|
56
|
+
return `
|
|
57
|
+
<div class="editor-card">
|
|
58
|
+
<h3 class="uk-margin-small-bottom">New folder in ${esc(label)}${sub ? ` / ${esc(sub)}` : ''}</h3>
|
|
59
|
+
<form class="folder-new-form" hx-post="/actions/folder-new" hx-target="#editor-modal-body" hx-swap="innerHTML">
|
|
60
|
+
<input type="hidden" name="workspace" value="${esc(ws)}">
|
|
61
|
+
<input type="hidden" name="sub" value="${esc(sub)}">
|
|
62
|
+
<label class="uk-form-label" for="folder-name">Folder name</label>
|
|
63
|
+
<input class="uk-input uk-margin-small-bottom" id="folder-name" name="name" required autofocus
|
|
64
|
+
maxlength="63" value="${esc(suggestion)}">
|
|
65
|
+
<label class="uk-form-label" for="folder-note">What is it for? <span class="uk-text-meta">(optional, becomes its note)</span></label>
|
|
66
|
+
<input class="uk-input uk-margin-small-bottom" id="folder-note" name="note" maxlength="200">
|
|
67
|
+
<p class="uk-margin-small-top"><button class="uk-button uk-button-primary" type="submit">Make the folder</button></p>
|
|
68
|
+
</form>
|
|
69
|
+
</div>`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function folderFormHtml(ws, { label = ws, suggestion = '', projectTypes = [], canTest = false, pluginSetups = '' } = {}) {
|
|
73
|
+
const radios = Object.entries(THEN).map(([value, text], i) => `
|
|
74
|
+
<label class="uk-margin-small-right"><input class="uk-radio" type="radio" name="then" value="${value}"${i === 0 ? ' checked' : ''}> ${esc(text)}</label>`).join('');
|
|
75
|
+
return `
|
|
76
|
+
<div class="editor-card">
|
|
77
|
+
<h3 class="uk-margin-small-bottom">New folder in ${esc(label)}</h3>
|
|
78
|
+
<p class="uk-text-meta">An empty folder, listed with the projects. Run commands, screens or Claude Code in it,
|
|
79
|
+
and set up DDEV later if it becomes a site.</p>
|
|
80
|
+
<form class="folder-new-form" hx-post="/actions/folder-new" hx-target="#editor-modal-body" hx-swap="innerHTML">
|
|
81
|
+
<input type="hidden" name="workspace" value="${esc(ws)}">
|
|
82
|
+
<label class="uk-form-label" for="folder-name">Folder name</label>
|
|
83
|
+
<input class="uk-input uk-margin-small-bottom" id="folder-name" name="name" required autofocus
|
|
84
|
+
pattern="[a-z0-9][a-z0-9_\\-]{0,62}" maxlength="63" value="${esc(suggestion)}" placeholder="my-folder"
|
|
85
|
+
title="Lowercase letters, digits, - and _">
|
|
86
|
+
<label class="uk-form-label" for="folder-note">What is it for? <span class="uk-text-meta">(optional, becomes its note)</span></label>
|
|
87
|
+
<input class="uk-input uk-margin-small-bottom" id="folder-note" name="note" maxlength="200"
|
|
88
|
+
placeholder="Trying the new search module">
|
|
89
|
+
<div class="uk-form-label">Set it up <span class="uk-text-meta">(optional, runs as a screen session)</span></div>
|
|
90
|
+
<div class="uk-margin-small-bottom folder-setup">
|
|
91
|
+
<label class="uk-display-block"><input class="uk-checkbox" type="checkbox" name="ddev" value="1"> Configure DDEV, so it can start</label>
|
|
92
|
+
${canTest ? `<label class="uk-display-block"><input class="uk-checkbox" type="checkbox" name="e2e" value="1"> Set up <code>webship-js</code> testing (Playwright and Cucumber), which configures DDEV too</label>` : ''}
|
|
93
|
+
<div class="folder-ddev-type uk-margin-small-top">${require('./ddev-options').fieldsHtml({ prefix: 'folder' })}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
${pluginSetups ? `<div class="uk-form-label">Connect it <span class="uk-text-meta">(optional)</span></div>
|
|
97
|
+
<div class="uk-margin-small-bottom folder-services">
|
|
98
|
+
${pluginSetups}
|
|
99
|
+
</div>` : ''}
|
|
100
|
+
<div class="uk-form-label">Then</div>
|
|
101
|
+
<div class="uk-margin-small-bottom folder-then">${radios}
|
|
102
|
+
</div>
|
|
103
|
+
<div class="folder-prompt">
|
|
104
|
+
<label class="uk-form-label" for="folder-prompt">First message to Claude Code <span class="uk-text-meta">(optional)</span></label>
|
|
105
|
+
<textarea class="uk-textarea" id="folder-prompt" name="prompt" rows="3" maxlength="4000"
|
|
106
|
+
placeholder="Set up a small Node.js app here"></textarea>
|
|
107
|
+
</div>
|
|
108
|
+
<p class="uk-margin-small-top"><button class="uk-button uk-button-primary" type="submit">Make the folder</button></p>
|
|
109
|
+
</form>
|
|
110
|
+
</div>`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The first free <prefix><n> in a workspace folder, so the form opens with a name that works. */
|
|
114
|
+
function suggestName(root, prefix = 'folder') {
|
|
115
|
+
for (let n = 1; n < 1000; n += 1) {
|
|
116
|
+
if (!fs.existsSync(path.join(root, `${prefix}${n}`))) return `${prefix}${n}`;
|
|
117
|
+
}
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { NAME_RE, FILES_NAME_RE, THEN, nameProblem, createFolder, folderFormHtml, filesFolderFormHtml, suggestName };
|