@webship.co/workspace 0.0.0-stage → 1.0.0-beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLUGINS.md +249 -0
- package/README.md +379 -2
- package/WORKSPACES.md +69 -0
- package/actions/ai-items.js +293 -0
- package/actions/audio.js +48 -0
- package/actions/build-order.js +79 -0
- package/actions/checks.js +95 -0
- package/actions/claude-session.js +104 -0
- package/actions/commands-center.js +258 -0
- package/actions/composer.js +267 -0
- package/actions/ddev-addons.js +90 -0
- package/actions/ddev-mail.js +80 -0
- package/actions/ddev.js +237 -0
- package/actions/folders.js +165 -0
- package/actions/images-svg.js +33 -0
- package/actions/images.js +145 -0
- package/actions/navigation.js +190 -0
- package/actions/node.js +222 -0
- package/actions/notes.js +55 -0
- package/actions/plugins.js +36 -0
- package/actions/project-make.js +28 -0
- package/actions/rag.js +202 -0
- package/actions/row-actions.js +262 -0
- package/actions/screens.js +270 -0
- package/actions/testing.js +121 -0
- package/actions/video-edit.js +54 -0
- package/ai-inventory.js +182 -0
- package/ai-items.js +202 -0
- package/assistant-cast.js +176 -0
- package/assistant-suggestions.js +91 -0
- package/assistant.js +484 -0
- package/audio-voice.js +528 -0
- package/auth-google.js +260 -0
- package/build-ai.js +52 -0
- package/build-order-page.js +191 -0
- package/build-order.js +313 -0
- package/builder-args.js +117 -0
- package/changelog.js +302 -0
- package/chat-threads.js +134 -0
- package/checks.js +105 -0
- package/claude-args.js +282 -0
- package/claude-sessions.js +129 -0
- package/command-author.js +117 -0
- package/command-block.js +20 -0
- package/commands/defaults.yml +159 -0
- package/commands-page.js +460 -0
- package/commands.js +568 -0
- package/cucumber.js +43 -0
- package/ddev-addons-page.js +124 -0
- package/ddev-addons.js +154 -0
- package/ddev-custom-commands.js +96 -0
- package/ddev-domains.js +84 -0
- package/ddev-import.js +93 -0
- package/ddev-mail-page.js +167 -0
- package/ddev-mail.js +110 -0
- package/ddev-options.js +145 -0
- package/ddev-snapshots.js +126 -0
- package/ddev.js +145 -0
- package/docs/DEVELOPER-GUIDE.md +224 -0
- package/docs/README.md +252 -0
- package/docs/REFERENCE.md +684 -0
- package/docs/USER-GUIDE.md +697 -0
- package/docs/screenshots/assistant/avatar-board.png +0 -0
- package/docs/screenshots/assistant/avatar-coffee.png +0 -0
- package/docs/screenshots/assistant/avatar-colours.png +0 -0
- package/docs/screenshots/assistant/avatar-icons.png +0 -0
- package/docs/screenshots/assistant/avatar-moves.png +0 -0
- package/docs/screenshots/assistant/avatar-pick-cookieman.png +0 -0
- package/docs/screenshots/assistant/avatar-settings-voices.png +0 -0
- package/docs/screenshots/assistant/avatar-status.png +0 -0
- package/docs/screenshots/create-card/create-card.png +0 -0
- package/docs/screenshots/create-card/start-ai.png +0 -0
- package/docs/screenshots/guide-clone-address.png +0 -0
- package/docs/screenshots/guide-clone-worktree-off.png +0 -0
- package/docs/screenshots/guide-clone-worktree-on.png +0 -0
- package/docs/screenshots/guide-ddev-menu.png +0 -0
- package/docs/screenshots/guide-domains.png +0 -0
- package/docs/screenshots/guide-home.png +0 -0
- package/docs/screenshots/guide-ide-menu.png +0 -0
- package/docs/screenshots/guide-remove-refusal.png +0 -0
- package/docs/screenshots/guide-rows-worktree.png +0 -0
- package/docs/screenshots/guide-screens.png +0 -0
- package/docs/screenshots/guide-settings.png +0 -0
- package/docs/screenshots/guide-workspace-page.png +0 -0
- package/docs/screenshots/home/card-settings.png +0 -0
- package/docs/screenshots/home/drag-reorder.png +0 -0
- package/docs/screenshots/home/edit-card.png +0 -0
- package/docs/screenshots/images/claude-code-image.png +0 -0
- package/docs/screenshots/images/edit-with-ai.png +0 -0
- package/docs/screenshots/images/gallery.png +0 -0
- package/docs/screenshots/images/generate-description.png +0 -0
- package/docs/screenshots/images/generate-plan.png +0 -0
- package/docs/screenshots/images/providers.png +0 -0
- package/docs/screenshots/images/svg-form.png +0 -0
- package/docs/screenshots/images/svg-result.png +0 -0
- package/docs/screenshots/images/upload.png +0 -0
- package/docs/screenshots/new-folder/2-form.png +0 -0
- package/docs/screenshots/new-folder/8-ddev-options.png +0 -0
- package/docs/screenshots/new-folder/files-folder.png +0 -0
- package/docs/screenshots/projects/ddev-custom-commands.png +0 -0
- package/docs/screenshots/projects/state-chip-stop.png +0 -0
- package/docs/screenshots/readme/dashboard-dark.png +0 -0
- package/docs/screenshots/readme/dashboard-light.png +0 -0
- package/docs/screenshots/readme/dev-dark.png +0 -0
- package/docs/screenshots/readme/dev-light.png +0 -0
- package/docs/screenshots/readme/docs-dark.png +0 -0
- package/docs/screenshots/readme/docs-light.png +0 -0
- package/docs/screenshots/readme/icons/ai.svg +4 -0
- package/docs/screenshots/readme/icons/code.svg +4 -0
- package/docs/screenshots/readme/icons/docs.svg +4 -0
- package/docs/screenshots/readme/icons/services.svg +4 -0
- package/docs/screenshots/readme/icons/sites.svg +4 -0
- package/docs/screenshots/readme/test-report-dark.png +0 -0
- package/docs/screenshots/readme/test-report-light.png +0 -0
- package/docs/screenshots/readme/test-scenarios.png +0 -0
- package/docs/screenshots/readme/test-steps.png +0 -0
- package/docs/screenshots/readme/videos-dark.png +0 -0
- package/docs/screenshots/readme/videos-light.png +0 -0
- package/docs/screenshots/screens-menu.png +0 -0
- package/docs/screenshots/screens-new.png +0 -0
- package/docs/screenshots/settings/auth-google-lists.png +0 -0
- package/docs/screenshots/settings/delete-1-row.png +0 -0
- package/docs/screenshots/settings/delete-2-confirm.png +0 -0
- package/docs/screenshots/settings/delete-3-type-name.png +0 -0
- package/docs/screenshots/settings/delete-4-type-yes.png +0 -0
- package/event-log.js +59 -0
- package/files-browser.js +247 -0
- package/folders.js +121 -0
- package/format.js +73 -0
- package/git-info.js +81 -0
- package/github-auth.js +62 -0
- package/html.js +10 -0
- package/icons.js +83 -0
- package/images-ai.js +447 -0
- package/images-create.js +226 -0
- package/images-page.js +417 -0
- package/images-records.js +39 -0
- package/images-render.js +36 -0
- package/images-svg-core.js +214 -0
- package/images-svg-sanitize.js +188 -0
- package/images-svg.js +241 -0
- package/images.js +270 -0
- package/jobs.js +430 -0
- package/linger.js +21 -0
- package/lists.js +292 -0
- package/media-info.js +136 -0
- package/navigation-page.js +264 -0
- package/navigation.js +394 -0
- package/notes.js +0 -0
- package/package.json +47 -4
- package/palette-index.js +46 -0
- package/playwright.config.js +23 -0
- package/plugin-install.js +239 -0
- package/plugin-loader.js +466 -0
- package/plugin-manifest.js +83 -0
- package/plugin-settings.js +44 -0
- package/plugins/workspace-notes/index.js +59 -0
- package/plugins-page.js +103 -0
- package/project-make.js +241 -0
- package/project-name.js +210 -0
- package/public/assistant-avatar-moves.js +646 -0
- package/public/assistant-avatar-ui.js +1223 -0
- package/public/assistant-avatar.js +530 -0
- package/public/avatar/druplicon.vrm +0 -0
- package/public/css/base.css +260 -0
- package/public/css/components/ai-assistant.css +263 -0
- package/public/css/components/assistant-avatar.css +279 -0
- package/public/css/components/assistant-views.css +198 -0
- package/public/css/components/build-order.css +74 -0
- package/public/css/components/clone-as-a-git-worktree.css +30 -0
- package/public/css/components/command-palette.css +61 -0
- package/public/css/components/commands-center.css +139 -0
- package/public/css/components/confirm-dialog.css +40 -0
- package/public/css/components/create-card.css +8 -0
- package/public/css/components/ddev-menu.css +23 -0
- package/public/css/components/deep-chat.css +17 -0
- package/public/css/components/density.css +11 -0
- package/public/css/components/floating-assistant-workspace-pages.css +55 -0
- package/public/css/components/folders.css +5 -0
- package/public/css/components/full-height-left-assistant-sidebar-all-pages.css +39 -0
- package/public/css/components/full-height-right-actions-rail-pages-that-have-actions.css +267 -0
- package/public/css/components/grid-and-list.css +203 -0
- package/public/css/components/help-page.css +36 -0
- package/public/css/components/hero.css +16 -0
- package/public/css/components/images.css +70 -0
- package/public/css/components/layout-rhythm-and-spacing-polish.css +139 -0
- package/public/css/components/mic-on-any-field-that-wants-typing.css +416 -0
- package/public/css/components/modal-fit.css +113 -0
- package/public/css/components/page-width.css +23 -0
- package/public/css/components/plugins-page.css +9 -0
- package/public/css/components/project-cards.css +256 -0
- package/public/css/components/project-rows.css +390 -0
- package/public/css/components/reordering-the-workspace-cards.css +85 -0
- package/public/css/components/responsive-home-and-the-workspace-pages.css +457 -0
- package/public/css/components/running-commands-as-toasts.css +327 -0
- package/public/css/components/settings-editor-core-config-yml.css +828 -0
- package/public/css/components/short-results-as-a-toast-at-the-top.css +203 -0
- package/public/css/components/sound-player.css +84 -0
- package/public/css/components/suggest-agents-skills-commands.css +112 -0
- package/public/css/components/tab-modal.css +142 -0
- package/public/css/components/terminals.css +244 -0
- package/public/css/components/test-report-media-one-file-per-row-paged.css +144 -0
- package/public/css/components/top-toolbar-logo-breadcrumb.css +107 -0
- package/public/css/components/video-edit.css +6 -0
- package/public/css/components/web-ide-open-with.css +23 -0
- package/public/css/components/working-indicators.css +158 -0
- package/public/css/components/workspace-cards.css +79 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/public/htmx.min.js +1 -0
- package/public/logo.png +0 -0
- package/public/media-player.js +215 -0
- package/public/mic.js +255 -0
- package/public/narration.js +110 -0
- package/public/palette.js +278 -0
- package/public/sortable.js +251 -0
- package/public/suggest.js +337 -0
- package/public/terminal.js +903 -0
- package/public/themes/default/theme.css +267 -0
- package/public/themes/ember/theme.css +180 -0
- package/public/themes/press/theme.css +290 -0
- package/public/themes/slate/theme.css +182 -0
- package/public/touch-icon.png +0 -0
- package/public/ui.js +2721 -0
- package/public/vendor/ace/LICENSE.txt +24 -0
- package/public/vendor/ace/ace.js +23 -0
- package/public/vendor/ace/ext-language_tools.js +8 -0
- package/public/vendor/ace/ext-searchbox.js +8 -0
- package/public/vendor/ace/mode-css.js +8 -0
- package/public/vendor/ace/mode-html.js +8 -0
- package/public/vendor/ace/mode-javascript.js +8 -0
- package/public/vendor/ace/mode-markdown.js +8 -0
- package/public/vendor/ace/mode-sh.js +8 -0
- package/public/vendor/ace/mode-yaml.js +8 -0
- package/public/vendor/ace/theme-textmate.js +8 -0
- package/public/vendor/ace/theme-tomorrow_night.js +8 -0
- package/public/vendor/ace/worker-yaml.js +1 -0
- package/public/vendor/deep-chat.LICENSE +21 -0
- package/public/vendor/deep-chat.bundle.js +1 -0
- package/public/vendor/fonts/dm-serif-text-LICENSE.txt +93 -0
- package/public/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
- package/public/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/figtree-LICENSE.txt +93 -0
- package/public/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
- package/public/vendor/fonts/jetbrains-mono-LICENSE.txt +93 -0
- package/public/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/ubuntu-mono-LICENSE.txt +98 -0
- package/public/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
- package/public/vendor/tabler-LICENSE.txt +21 -0
- package/public/vendor/tabler-sprite.svg +1 -0
- package/public/vendor/three/LICENSE +21 -0
- package/public/vendor/three/addons/loaders/GLTFLoader.js +4860 -0
- package/public/vendor/three/addons/utils/BufferGeometryUtils.js +1501 -0
- package/public/vendor/three/addons/utils/SkeletonUtils.js +496 -0
- package/public/vendor/three/three.core.min.js +6 -0
- package/public/vendor/three/three.module.min.js +6 -0
- package/public/vendor/three-vrm/LICENSE +22 -0
- package/public/vendor/three-vrm/three-vrm.module.min.js +983 -0
- package/public/vendor/uikit-icons.min.js +1 -0
- package/public/vendor/uikit.min.css +1 -0
- package/public/vendor/uikit.min.js +1 -0
- package/public/vendor/webship-icons/ui/bookmark-outlined.svg +1 -0
- package/public/vendor/webship-icons/ui/headset.svg +1 -0
- package/public/vendor/webship-icons/ui/menu.svg +1 -0
- package/public/vendor/webship-icons/ui/pause.svg +1 -0
- package/public/vendor/webship-icons/ui/play.svg +1 -0
- package/public/vendor/webship-icons/ui/share.svg +1 -0
- package/public/vendor/webship-icons/ui/sound-off.svg +1 -0
- package/public/vendor/webship-icons/ui/sound-on.svg +1 -0
- package/public/vendor/webship-icons/ui/stop.svg +1 -0
- package/public/vendor/xterm/addon-fit.js +2 -0
- package/public/vendor/xterm/addon-unicode11.js +2 -0
- package/public/vendor/xterm/addon-web-links.js +2 -0
- package/public/vendor/xterm/xterm.css +218 -0
- package/public/vendor/xterm/xterm.js +2 -0
- package/rag.js +169 -0
- package/reader.js +132 -0
- package/repo-blobs.js +66 -0
- package/report-pdf.js +337 -0
- package/row-layout.js +1439 -0
- package/screenrc +37 -0
- package/screens.js +1063 -0
- package/scripts/code-server-start.sh +190 -0
- package/scripts/images-claude-finish.js +107 -0
- package/scripts/screen-host.sh +114 -0
- package/scripts/screen-runner.js +362 -0
- package/scripts/screen-then.js +47 -0
- package/scripts/update.sh +350 -0
- package/server.js +7923 -0
- package/settings-file.js +423 -0
- package/speech.js +297 -0
- package/tab-modal.js +75 -0
- package/test-reports.js +260 -0
- package/themes.js +381 -0
- package/transcripts.js +168 -0
- package/urls.js +13 -0
- package/validate.js +18 -0
- package/version-check.js +94 -0
- package/video-edit.js +588 -0
- package/waiting.js +85 -0
- package/workspace-card-tools.js +41 -0
- package/workspace-delete.js +139 -0
- package/workspace-types.js +85 -0
- package/workspaces.js +553 -0
package/workspaces.js
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const yaml = require('js-yaml');
|
|
4
|
+
const { resolveType, typeActions } = require('./workspace-types');
|
|
5
|
+
// What the assistant is called on this workspace's page, and what its composer says when empty.
|
|
6
|
+
// The rule about which level wins lives in speech.js with the rest of the assistant's settings.
|
|
7
|
+
const { assistantIdentity } = require('./speech');
|
|
8
|
+
|
|
9
|
+
// Locate the workspace from this file — core/workspace-app/workspaces.js — so a
|
|
10
|
+
// fresh clone needs no environment setup. The WORKSPACE_* variables still
|
|
11
|
+
// win, which is how the dashboard container points at the bind-mounted tree.
|
|
12
|
+
const APP_ROOT = path.resolve(__dirname, '..', '..');
|
|
13
|
+
const ROOT = process.env.WORKSPACE_ROOT || APP_ROOT;
|
|
14
|
+
const CONFIG_DIR = process.env.WORKSPACE_CONFIG || path.join(ROOT, 'core', 'config');
|
|
15
|
+
|
|
16
|
+
// Presentational-only (icon/subtitle) — everything functional (paths, script names,
|
|
17
|
+
// which workspaces exist) is read live from settings.yml / workspace.<name>.settings.yml
|
|
18
|
+
// and the filesystem, so a new workspace added there shows up with no code change.
|
|
19
|
+
// `icon` is a UIKit icon name (https://getuikit.com/docs/icon), rendered
|
|
20
|
+
// with the vendored uikit-icons.min.js.
|
|
21
|
+
// `noun` is what one item in the folder is called (only real projects are
|
|
22
|
+
// "projects"); `plural` overrides irregular plurals.
|
|
23
|
+
const PRESENTATION = {
|
|
24
|
+
products: { icon: 'tabler:shopping-cart', subtitle: 'Internal & External Products', noun: 'product' },
|
|
25
|
+
dev: { icon: 'tabler:code', subtitle: 'Development Projects', noun: 'project' },
|
|
26
|
+
test: { icon: 'tabler:checkbox', subtitle: 'Testing Environment', noun: 'project' },
|
|
27
|
+
demos: { icon: 'tabler:device-desktop', subtitle: 'Demo Sites', noun: 'demo' },
|
|
28
|
+
sandboxes: { icon: 'tabler:flask', subtitle: 'Experimental Projects', noun: 'sandbox', plural: 'sandboxes' },
|
|
29
|
+
projects: { icon: 'tabler:folder', subtitle: 'Client Projects', noun: 'project' },
|
|
30
|
+
profiles: { icon: 'tabler:world', subtitle: 'Distros', noun: 'profile' },
|
|
31
|
+
'site-templates': { icon: 'tabler:photo', subtitle: 'Drupal CMS Site Templates', label: 'Site Templates', noun: 'site template' },
|
|
32
|
+
themes: { icon: 'tabler:paint', subtitle: 'Contrib & Private Themes', noun: 'theme' },
|
|
33
|
+
modules: { icon: 'tabler:apps', subtitle: 'Contrib & Private Modules', noun: 'module' },
|
|
34
|
+
libraries: { icon: 'tabler:library', subtitle: 'Third-Party Libraries', noun: 'library', plural: 'libraries' },
|
|
35
|
+
designs: { icon: 'tabler:palette', subtitle: 'Figma Sources & Exports', noun: 'design', kind: 'files' },
|
|
36
|
+
images: { icon: 'tabler:photo', subtitle: 'Images, screenshots and generated pictures', noun: 'image', kind: 'files' },
|
|
37
|
+
videos: { icon: 'tabler:player-play', subtitle: 'Recorded Walkthroughs & Demos', noun: 'video', kind: 'files' },
|
|
38
|
+
audio: { icon: 'tabler:headphones', subtitle: 'Voices & Sounds', noun: 'sound', kind: 'files' },
|
|
39
|
+
avatars: { icon: 'tabler:mood-smile', subtitle: 'Talking AI Characters, Their Agents & Voices', noun: 'avatar', kind: 'projects' },
|
|
40
|
+
docs: { icon: 'tabler:file-text', subtitle: 'Documentation Projects', noun: 'doc', kind: 'files' },
|
|
41
|
+
worklogs: { icon: 'tabler:history', subtitle: 'Session Worklogs', noun: 'worklog', kind: 'files' },
|
|
42
|
+
'ai-agents': { icon: 'tabler:mood-smile', subtitle: 'AI Automation', label: 'AI Agents', noun: 'agent', kind: 'files' },
|
|
43
|
+
'ai-skills': { icon: 'tabler:star', subtitle: 'AI Skill Definitions', label: 'AI Skills', noun: 'skill', kind: 'files' },
|
|
44
|
+
'ai-commands': { icon: 'tabler:command', subtitle: 'Claude Code Slash Commands', label: 'AI Commands', noun: 'command', kind: 'files' },
|
|
45
|
+
'ai-prompts': { icon: 'tabler:pencil', subtitle: 'Reusable AI Prompts', label: 'AI Prompts', noun: 'prompt', kind: 'files' },
|
|
46
|
+
recipes: { icon: 'tabler:list-check', subtitle: 'Development Recipes', noun: 'recipe' },
|
|
47
|
+
components: { icon: 'tabler:layout-grid', subtitle: 'SDC, React, Canvas Code, HTMX & Web Components', noun: 'component' },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function loadYaml(file) {
|
|
51
|
+
try {
|
|
52
|
+
return yaml.load(fs.readFileSync(file, 'utf8')) || {};
|
|
53
|
+
} catch (_) {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function loadSettings() {
|
|
59
|
+
return loadYaml(path.join(CONFIG_DIR, 'settings.yml'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The environment, then settings.yml's `sources:`, then the shipped default —
|
|
63
|
+
// the same precedence bootstrap.sh gives the shell side. Read per call so a
|
|
64
|
+
// settings change needs no restart.
|
|
65
|
+
function sourceOf(kind, field, fallback) {
|
|
66
|
+
try {
|
|
67
|
+
const v = (((loadSettings().sources || {})[kind]) || {})[field];
|
|
68
|
+
if (v) return String(v);
|
|
69
|
+
} catch (_) { /* fall through */ }
|
|
70
|
+
return fallback;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// The hub's base domain. Locally this is workspace.ddev.site; set
|
|
74
|
+
// `hub_domain:` in settings.yml (plus matching additional_fqdns + DNS +
|
|
75
|
+
// nginx server_names) to run the whole system as a remote development
|
|
76
|
+
// workspace hub on a public domain, e.g. workspace.example.com →
|
|
77
|
+
// dev.workspace.example.com → myproject.dev.workspace.example.com.
|
|
78
|
+
function hubDomain() {
|
|
79
|
+
return loadSettings().hub_domain || `${process.env.DDEV_PROJECT || 'workspace'}.ddev.site`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Dashboard styling from settings.yml's `style:` block, with the shipped defaults
|
|
83
|
+
// filled in. Read live like everything else, so /settings takes effect on the next
|
|
84
|
+
// page load rather than needing a restart.
|
|
85
|
+
//
|
|
86
|
+
// `mode` is the DEFAULT only: a visitor who has used the theme toggle has their choice
|
|
87
|
+
// in localStorage, and that keeps winning — a default is not an override.
|
|
88
|
+
/**
|
|
89
|
+
* Everything about how the dashboard LOOKS, read from `core/config/style.yml`.
|
|
90
|
+
*
|
|
91
|
+
* Styling is its own concern and now has its own file, beside `theme.<name>.yml`: which palette,
|
|
92
|
+
* which mode, the logos and favicons, the page width, how the assistant is shown. It sat in
|
|
93
|
+
* `settings.yml` under a `style:` key, in a file that is otherwise about where projects are built,
|
|
94
|
+
* who commits, and which API keys this machine holds — so the one file nobody should open casually
|
|
95
|
+
* was also the file you had to open to change a logo.
|
|
96
|
+
*
|
|
97
|
+
* `settings.yml`'s `style:` block still works and fills in anything `style.yml` does not set, so a
|
|
98
|
+
* machine that has not moved its block keeps the appearance it had. The dedicated file wins where
|
|
99
|
+
* the two overlap, because that is the point of having it.
|
|
100
|
+
*/
|
|
101
|
+
function loadStyleFile() {
|
|
102
|
+
try {
|
|
103
|
+
const doc = loadYaml(path.join(CONFIG_DIR, 'style.yml'));
|
|
104
|
+
return doc && typeof doc === 'object' ? doc : {};
|
|
105
|
+
} catch (_) {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function styleSettings() {
|
|
111
|
+
const s = { ...(loadSettings().style || {}), ...loadStyleFile() };
|
|
112
|
+
const one_of = (v, allowed, fallback) => (allowed.includes(String(v)) ? String(v) : fallback);
|
|
113
|
+
return {
|
|
114
|
+
workspaceName: typeof s.workspace_name === 'string' && s.workspace_name.trim() ? s.workspace_name.trim() : 'Workspace',
|
|
115
|
+
mode: one_of(s.mode, ['light', 'dark', 'system'], 'light'),
|
|
116
|
+
editorTheme: one_of(s.editor_theme, ['auto', 'light', 'dark'], 'auto'),
|
|
117
|
+
logoOnLight: s.logo_on_light || 'logo.png',
|
|
118
|
+
logoOnDark: s.logo_on_dark || 'logo.png',
|
|
119
|
+
faviconSvg: s.favicon_svg || '',
|
|
120
|
+
faviconPng: s.favicon_png || 'favicon.png',
|
|
121
|
+
// A separate, larger file: a home-screen icon is 180px, and the 32px tab icon blown up to that
|
|
122
|
+
// is a blurry tile. Both are rendered from the same SVG, so they cannot drift apart.
|
|
123
|
+
touchIcon: s.touch_icon || 'touch-icon.png',
|
|
124
|
+
// Rows before a list pages. A default, not an override: a size picked from a pager
|
|
125
|
+
// wins (it is in a cookie), as with `mode`.
|
|
126
|
+
pageSize: one_of(s.page_size, ['20', '50', '100', '200', 'all'], '50'),
|
|
127
|
+
pageSort: one_of(s.page_sort, ['newest', 'oldest', 'name', 'nameDesc'], 'newest'),
|
|
128
|
+
// Which palette the dashboard wears. Resolved in themes.js — a name that is not a theme
|
|
129
|
+
// falls back to the default, so a typo is an ordinary-looking dashboard rather than an
|
|
130
|
+
// unstyled one.
|
|
131
|
+
theme: typeof s.theme === 'string' && s.theme.trim() ? s.theme.trim() : 'default',
|
|
132
|
+
// How the assistant is shown, and whether it starts open. Defaults, never overrides: a view
|
|
133
|
+
// chosen from the panel's own menu is in localStorage and wins, exactly like `mode`.
|
|
134
|
+
// sidebar — docked left, the width it has always had
|
|
135
|
+
// wide — the same, twice as wide, for a long answer
|
|
136
|
+
// float — a launcher, with the panel over the page
|
|
137
|
+
// full — every page's assistant link goes to /assistant, the whole width
|
|
138
|
+
// How much of the window a page's content may use. comfortable is UIKit's own small container
|
|
139
|
+
// (900px), which is a reading width and the right default; wide and full are offered for the
|
|
140
|
+
// pages that want the room — a command catalog, a settings file, a terminal.
|
|
141
|
+
pageWidth: one_of(s.page_width, ['comfortable', 'wide', 'full'], 'comfortable'),
|
|
142
|
+
assistantView: one_of(s.assistant_view, ['sidebar', 'wide', 'float', 'full'], 'sidebar'),
|
|
143
|
+
// Quoted in the shipped file, because unquoted `yes` is a BOOLEAN in YAML 1.1 and would arrive
|
|
144
|
+
// here as true — which is not in the list and would silently fall back. Both are accepted.
|
|
145
|
+
assistantOpen: one_of(s.assistant_open === true ? 'yes' : s.assistant_open === false ? 'no' : s.assistant_open,
|
|
146
|
+
['yes', 'no'], 'yes'),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Rebuilt with a 2s TTL memo: still effectively live (a settings.yml edit
|
|
151
|
+
// shows up on the next page load) without re-reading ~17 YAML files for the
|
|
152
|
+
// many helper calls within a single request.
|
|
153
|
+
let _wsCache = null;
|
|
154
|
+
let _wsCacheAt = 0;
|
|
155
|
+
function loadWorkspaces() {
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
if (_wsCache && now - _wsCacheAt < 2000) return _wsCache;
|
|
158
|
+
const map = _loadWorkspacesFresh();
|
|
159
|
+
_wsCache = map;
|
|
160
|
+
_wsCacheAt = now;
|
|
161
|
+
return map;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function _loadWorkspacesFresh() {
|
|
165
|
+
const settings = loadSettings();
|
|
166
|
+
const names = Array.isArray(settings.workspaces) ? settings.workspaces : [];
|
|
167
|
+
const backupsRoot = settings.backups || path.join(ROOT, 'backups');
|
|
168
|
+
|
|
169
|
+
const map = {};
|
|
170
|
+
for (const name of names) {
|
|
171
|
+
const wsSettings = loadYaml(path.join(CONFIG_DIR, `workspace.${name}.settings.yml`));
|
|
172
|
+
const doc = wsSettings.doc || {};
|
|
173
|
+
const dir = doc.path || path.join(ROOT, name);
|
|
174
|
+
// The hardcoded table is the default; a workspace's own file overrides it, so a
|
|
175
|
+
// workspace added from the dashboard can carry its icon and description without
|
|
176
|
+
// a code change — which is the only way a new one gets anything but the folder
|
|
177
|
+
// icon.
|
|
178
|
+
const own = wsSettings.presentation || {};
|
|
179
|
+
const pres = { ...(PRESENTATION[name] || { icon: 'tabler:folder', subtitle: name }), ...own };
|
|
180
|
+
|
|
181
|
+
const kind = pres.kind || 'projects';
|
|
182
|
+
// What the folder's projects are FOR — the action policy its rows follow. Chosen in
|
|
183
|
+
// this workspace's own settings file (`type:`), defaulting per shipped folder so an
|
|
184
|
+
// unset file behaves as it always did. See workspace-types.js.
|
|
185
|
+
const type = resolveType(name, wsSettings.type, kind);
|
|
186
|
+
map[name] = {
|
|
187
|
+
key: name,
|
|
188
|
+
dir,
|
|
189
|
+
backupsDir: path.join(backupsRoot, doc.name || name),
|
|
190
|
+
icon: pres.icon,
|
|
191
|
+
subtitle: pres.subtitle,
|
|
192
|
+
label: pres.label || name.charAt(0).toUpperCase() + name.slice(1),
|
|
193
|
+
noun: pres.noun || 'item',
|
|
194
|
+
nounPlural: pres.plural || `${pres.noun || 'item'}s`,
|
|
195
|
+
kind,
|
|
196
|
+
type,
|
|
197
|
+
typeActions: typeActions(type),
|
|
198
|
+
// A workspace that both BUILDS things and HOLDS files — designs runs Penpot instances
|
|
199
|
+
// and keeps the Figma exports beside them, so its page needs the project table and the
|
|
200
|
+
// file list. Off everywhere else: on modules or dev a stray file is noise, not an item.
|
|
201
|
+
alsoFiles: pres.also_files === true,
|
|
202
|
+
// What the COMPOSER menu may do here (`composer:` in this workspace's own file). Only `full` runs
|
|
203
|
+
// install, update and scripts — the site workspaces set it. Anything else, unset included, is
|
|
204
|
+
// `manifest`: composer.json is edited and nothing is installed. See actions/composer.js.
|
|
205
|
+
composerMode: wsSettings.composer === 'full' ? 'full' : 'manifest',
|
|
206
|
+
// The assistant's name and composer placeholder ON THIS WORKSPACE'S PAGE. `assistant:` in
|
|
207
|
+
// settings.yml is the default and `assistant:` here overrides it, so a folder can call its
|
|
208
|
+
// assistant what the work in it is called. Resolved once, here, rather than at each of the
|
|
209
|
+
// five places that name the panel — a heading and a button disagreeing about what the thing
|
|
210
|
+
// is called is worse than either name.
|
|
211
|
+
assistant: assistantIdentity(settings, wsSettings),
|
|
212
|
+
// `assistant: suggestions:` from this workspace's file, unvalidated (assistant-suggestions.js cleans it).
|
|
213
|
+
assistantSuggestions: (wsSettings.assistant || {}).suggestions,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return map;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// After a write to settings.yml the 2s memo would serve the OLD order for up to two seconds —
|
|
220
|
+
// long enough that a dragged card grid re-renders exactly as it was and the drag looks lost.
|
|
221
|
+
function invalidateWorkspaces() {
|
|
222
|
+
_wsCache = null;
|
|
223
|
+
_wsCacheAt = 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function isValidWorkspace(name) {
|
|
227
|
+
return Object.prototype.hasOwnProperty.call(loadWorkspaces(), name);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function workspaceDir(name) {
|
|
231
|
+
const ws = loadWorkspaces()[name];
|
|
232
|
+
return ws ? ws.dir : null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Discover which of a folder's cmd-*.sh scripts serve a given purpose, by filename
|
|
236
|
+
// convention, instead of hand-mapping every workspace to a fixed script name.
|
|
237
|
+
function findScript(dir, patterns) {
|
|
238
|
+
let files = [];
|
|
239
|
+
try {
|
|
240
|
+
files = fs.readdirSync(dir);
|
|
241
|
+
} catch (_) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
for (const re of patterns) {
|
|
245
|
+
const hit = files.find((f) => re.test(f));
|
|
246
|
+
if (hit) return hit;
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function findBackupScript(dir) {
|
|
252
|
+
return findScript(dir, [/^cmd-tools?-backup-.*\.sh$/]);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function findRemoveScript(dir) {
|
|
256
|
+
return findScript(dir, [/^cmd-tools-remove\.sh$/]);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function findRagifyScript(dir) {
|
|
260
|
+
return findScript(dir, [/^cmd-tools?-ragify\.sh$/]);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
// Where a builder sits in the list is a SETTING now — `build-order.js`, one order for the machine and
|
|
265
|
+
// one per workspace, over the order the dashboard ships. Required lazily and inside the call, because
|
|
266
|
+
// build-order.js reads CONFIG_DIR from this module: at require time the cycle would hand it an empty
|
|
267
|
+
// object, and the order would silently fall back to alphabetical.
|
|
268
|
+
function sortBuilders(files, dir) {
|
|
269
|
+
// The scope is the workspace the folder IS. Derived rather than passed, so the ~40 callers that only
|
|
270
|
+
// ever had a directory keep working unchanged.
|
|
271
|
+
const scope = path.basename(dir || '') || 'global';
|
|
272
|
+
try {
|
|
273
|
+
return require('./build-order').sortBuilders(files, scope);
|
|
274
|
+
} catch (_) {
|
|
275
|
+
// A broken or missing order file must not cost you the Build form.
|
|
276
|
+
return [...files].sort();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function findBuilderScripts(dir) {
|
|
281
|
+
let files = [];
|
|
282
|
+
try {
|
|
283
|
+
files = fs.readdirSync(dir);
|
|
284
|
+
} catch (_) {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
files = files
|
|
288
|
+
// cmd-tool-/cmd-tools- scripts are per-project tools (backup, graphify, …), not builders, even
|
|
289
|
+
// when they end in -project.sh.
|
|
290
|
+
//
|
|
291
|
+
// `cmd-automated-testing-*-project.sh` USED to be excluded here as well, and that was wrong:
|
|
292
|
+
// they are builders. Each one builds a whole site and scaffolds its testing stack on top —
|
|
293
|
+
// which is the entire reason the `test` workspace exists — and each carries the same
|
|
294
|
+
// `# workspace-name:` header, takes the same project name and the same --install as any other
|
|
295
|
+
// builder. Hiding them meant the test workspace's Build a new project offered nine of its
|
|
296
|
+
// fourteen builders, and the five you would actually go there for were the missing ones.
|
|
297
|
+
.filter((f) => /^cmd-.*-project\.sh$/.test(f) && !/^cmd-tools?-/.test(f))
|
|
298
|
+
// Not alphabetical: alphabetical put eleven `cmd-automated-testing-…` builds at the top of
|
|
299
|
+
// the test workspace and `cmd-drupalcms2-x-website_starter…` above Drupal and Webship everywhere
|
|
300
|
+
// else, so the first thing in "Build a new project" was never the thing most people came to
|
|
301
|
+
// build. The order is Settings → Build order, over the one the dashboard ships.
|
|
302
|
+
.sort();
|
|
303
|
+
return sortBuilders(files, dir);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Human-readable label for a builder script: the `# workspace-name: ...`
|
|
307
|
+
// header near the top of the cmd-*.sh file, falling back to the filename.
|
|
308
|
+
function builderLabel(dir, script) {
|
|
309
|
+
try {
|
|
310
|
+
const head = fs.readFileSync(path.join(dir, script), 'utf8').slice(0, 500);
|
|
311
|
+
const m = head.match(/^# workspace-name:\s*(.+)$/m);
|
|
312
|
+
if (m) return m[1].trim();
|
|
313
|
+
} catch (_) { /* fall through */ }
|
|
314
|
+
return script;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* A short name for a command, for the line above its description.
|
|
319
|
+
*
|
|
320
|
+
* Short is the point. A builder's `# workspace-name:` is already short — "Webship 11.0.x" — and is
|
|
321
|
+
* used as-is. Everything else gets a name derived from its own file name, which is short by
|
|
322
|
+
* construction and is what the person is looking for in the list anyway:
|
|
323
|
+
*
|
|
324
|
+
* cmd-tools-reclaim.sh -> Reclaim
|
|
325
|
+
* cmd-tools-graphify.sh -> Graphify
|
|
326
|
+
* cmd-all-front-end-build-themes -> All front end build themes
|
|
327
|
+
*
|
|
328
|
+
* The `tool`/`tools` segment is dropped: every one of them has it, so it distinguishes nothing and
|
|
329
|
+
* costs a word in every title. `-project` goes for the same reason — the folder already says these
|
|
330
|
+
* build projects.
|
|
331
|
+
*
|
|
332
|
+
* A long `# workspace-name:` is NOT used. "Sync the workspace tooling from the clone" sitting above
|
|
333
|
+
* "Sync the workspace tooling from the git clone into the live workspace" is the same sentence twice,
|
|
334
|
+
* and the row reads as though it holds two facts when it holds one.
|
|
335
|
+
*/
|
|
336
|
+
function commandTitle(dir, script) {
|
|
337
|
+
const declared = builderLabel(dir, script);
|
|
338
|
+
if (declared !== script) {
|
|
339
|
+
// A BUILDER's declared name is a product name — "Website Starter 1.0.x on Drupal CMS 2 (site
|
|
340
|
+
// template)" — and is the title however long it runs. `cmd-*-project.sh` is what the smoke test
|
|
341
|
+
// enforces that header on, so it is also what identifies one.
|
|
342
|
+
if (/^cmd-.*-project\.sh$/.test(script)) return declared;
|
|
343
|
+
// On anything else the header is being used as a free-text label, and it tends to be a sentence
|
|
344
|
+
// that repeats the description: "Sync the workspace tooling from the clone" above "Sync the
|
|
345
|
+
// workspace tooling from the git clone into the live workspace". Kept only while it stays short.
|
|
346
|
+
if (declared.split(/\s+/).length <= 4) return declared;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const stem = script.replace(/^cmd-/, '').replace(/\.sh$/, '')
|
|
350
|
+
.replace(/^tools?-/, '')
|
|
351
|
+
.replace(/-project$/, '')
|
|
352
|
+
.replace(/[-_]+/g, ' ')
|
|
353
|
+
.trim();
|
|
354
|
+
if (!stem) return script;
|
|
355
|
+
return stem.charAt(0).toUpperCase() + stem.slice(1);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* What a command says about itself.
|
|
360
|
+
*
|
|
361
|
+
* Read from the script, not from a list kept beside it. Every `cmd-*.sh` already declares this —
|
|
362
|
+
* 218 of the 239 carry an `ARGPARSE_DESCRIPTION`, which is the same sentence `--help` prints — so a
|
|
363
|
+
* separate file of descriptions would be a second copy to forget to update, and it would be wrong
|
|
364
|
+
* the first time somebody edited a command without knowing the file existed.
|
|
365
|
+
*
|
|
366
|
+
* Three sources, in the order they are worth reading:
|
|
367
|
+
*
|
|
368
|
+
* 1. `ARGPARSE_DESCRIPTION` — written to be read by someone running `--help`, so it is already a
|
|
369
|
+
* sentence aimed at a person.
|
|
370
|
+
* 2. `# workspace-name:` — the human name of a builder, when there is no description.
|
|
371
|
+
* 3. The leading comment block — the first real sentence of it. Longer and more internal, but a
|
|
372
|
+
* command with neither of the above still has one.
|
|
373
|
+
*
|
|
374
|
+
* `ARGPARSE_DESCRIPTION` often interpolates shell variables the script sets just above it
|
|
375
|
+
* (`Build a ${distribution_title} ${site_version} project`). Those are resolved from the same file
|
|
376
|
+
* rather than shown raw, because `Build a ${distribution_title} project` in a list reads as a bug.
|
|
377
|
+
* Anything still unresolved is dropped along with its surrounding spaces.
|
|
378
|
+
*/
|
|
379
|
+
// The flags a builder actually accepts, read from the args/arg-*.sh file it
|
|
380
|
+
// sources — so the AI context can state the real contract (--install, --launch,
|
|
381
|
+
// --dev, ...) instead of guessing. Empty for scripts without an arg file.
|
|
382
|
+
function commandFlags(dir, script) {
|
|
383
|
+
let text;
|
|
384
|
+
try {
|
|
385
|
+
text = fs.readFileSync(path.join(dir, script), 'utf8');
|
|
386
|
+
} catch (_) { return []; }
|
|
387
|
+
const m = text.match(/args\/(arg-[a-zA-Z0-9_$"{}-]+\.sh)/);
|
|
388
|
+
if (!m) return [];
|
|
389
|
+
// Builders source args/arg-${distribution_name}.sh — resolve the variable from
|
|
390
|
+
// the script's own plain assignments, the way commandDescription does.
|
|
391
|
+
const argFile = m[1].replace(/\$\{?([a-zA-Z_][a-zA-Z0-9_]*)\}?/g, (whole, name) => {
|
|
392
|
+
const assigned = text.match(new RegExp(`^${name}=["']?([^"'\n;]+)["']?`, 'm'));
|
|
393
|
+
return assigned ? assigned[1].trim() : '';
|
|
394
|
+
});
|
|
395
|
+
if (!/^arg-[a-z0-9_-]+\.sh$/.test(argFile)) return [];
|
|
396
|
+
let argText;
|
|
397
|
+
try {
|
|
398
|
+
argText = fs.readFileSync(path.join(ROOT, 'core', 'scripts', 'args', argFile), 'utf8');
|
|
399
|
+
} catch (_) { return []; }
|
|
400
|
+
const flags = [];
|
|
401
|
+
const re = /add_argument\('(-[a-zA-Z])', '(--[a-z-]+)'[\s\S]*?help='([^']*)'/g;
|
|
402
|
+
let match;
|
|
403
|
+
while ((match = re.exec(argText)) !== null) {
|
|
404
|
+
flags.push({ short: match[1], long: match[2], help: match[3] });
|
|
405
|
+
}
|
|
406
|
+
return flags;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function commandDescription(dir, script) {
|
|
410
|
+
let text;
|
|
411
|
+
try {
|
|
412
|
+
text = fs.readFileSync(path.join(dir, script), 'utf8');
|
|
413
|
+
} catch (_) {
|
|
414
|
+
return '';
|
|
415
|
+
}
|
|
416
|
+
const head = text.slice(0, 4000);
|
|
417
|
+
|
|
418
|
+
const resolve = (value) => value.replace(/\$\{?([a-zA-Z_][a-zA-Z0-9_]*)\}?/g, (whole, name) => {
|
|
419
|
+
// The script assigns these as plain `name=value` or `name="value"` before using them.
|
|
420
|
+
const assigned = head.match(new RegExp(`^${name}=["']?([^"'\n]+)["']?`, 'm'));
|
|
421
|
+
return assigned ? assigned[1].trim() : '\u0000';
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const argparse = head.match(/^ARGPARSE_DESCRIPTION=["'](.+?)["']\s*;?\s*$/m);
|
|
425
|
+
if (argparse) {
|
|
426
|
+
const out = resolve(argparse[1]).replace(/\s*\u0000\s*/g, ' ').replace(/\s+/g, ' ').trim();
|
|
427
|
+
if (out) return out;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const name = head.match(/^# workspace-name:\s*(.+)$/m);
|
|
431
|
+
if (name) return `Build a ${name[1].trim()} project`;
|
|
432
|
+
|
|
433
|
+
// The leading comment block, minus the shebang and any `# workspace-name:` line. Its first
|
|
434
|
+
// sentence is the one written to say what the command is for.
|
|
435
|
+
const lines = head.split('\n').slice(1);
|
|
436
|
+
const comment = [];
|
|
437
|
+
for (const line of lines) {
|
|
438
|
+
if (/^#\s?/.test(line)) {
|
|
439
|
+
const body = line.replace(/^#\s?/, '').trim();
|
|
440
|
+
if (!body) { if (comment.length) break; continue; }
|
|
441
|
+
if (/^workspace-name:/.test(body)) continue;
|
|
442
|
+
comment.push(body);
|
|
443
|
+
} else if (comment.length) break;
|
|
444
|
+
else if (line.trim()) break;
|
|
445
|
+
}
|
|
446
|
+
const joined = comment.join(' ').trim();
|
|
447
|
+
if (!joined) return '';
|
|
448
|
+
const stop = joined.search(/\.\s|\.$/);
|
|
449
|
+
return (stop > 0 ? joined.slice(0, stop + 1) : joined).trim();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Backup archives for a workspace: ${backups}/<workspace>/*.tar.gz written by the
|
|
453
|
+
// cmd-tool*-backup-*.sh scripts, newest first. It lives here rather than in the page that
|
|
454
|
+
// renders them because the assistant names them too, and neither should own the other.
|
|
455
|
+
function listBackups(key) {
|
|
456
|
+
const meta = loadWorkspaces()[key];
|
|
457
|
+
try {
|
|
458
|
+
return fs.readdirSync(meta.backupsDir)
|
|
459
|
+
.filter((f) => f.endsWith('.tar.gz'))
|
|
460
|
+
.map((f) => {
|
|
461
|
+
const st = fs.statSync(path.join(meta.backupsDir, f));
|
|
462
|
+
return { file: f, size: st.size, mtime: st.mtime };
|
|
463
|
+
})
|
|
464
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
465
|
+
} catch (_) {
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* The release this tree is running, and whether that is knowable.
|
|
472
|
+
*
|
|
473
|
+
* Read from core/workspace-app/.app-version, which install.sh, sync-tooling.sh --pull and
|
|
474
|
+
* update.sh all write. Before they did, only the updater wrote it — so a freshly installed
|
|
475
|
+
* machine answered "(untracked)" for ever and was told to update to the version it already
|
|
476
|
+
* had. Across a team that is the support question nobody can answer.
|
|
477
|
+
*
|
|
478
|
+
* package.json is deliberately NOT the source: it says 1.0.0 and has since the first commit,
|
|
479
|
+
* which is worse than no answer because it looks like one.
|
|
480
|
+
*
|
|
481
|
+
* Returns '' when unknown, and callers must render that as unknown rather than as anything
|
|
482
|
+
* else — a wrong version stops the question being asked.
|
|
483
|
+
*/
|
|
484
|
+
let _appVersion = { at: 0, value: null };
|
|
485
|
+
function appVersion() {
|
|
486
|
+
if (_appVersion.value !== null && Date.now() - _appVersion.at < 10000) return _appVersion.value;
|
|
487
|
+
let v = '';
|
|
488
|
+
try {
|
|
489
|
+
v = fs.readFileSync(path.join(__dirname, '.app-version'), 'utf8').trim().split('\n')[0];
|
|
490
|
+
} catch (_) { v = ''; }
|
|
491
|
+
_appVersion = { at: Date.now(), value: v };
|
|
492
|
+
return v;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Is this workspace reachable from the network, and therefore dangerous?
|
|
497
|
+
*
|
|
498
|
+
* There is no authentication anywhere in this dashboard. It runs shell commands, serves a
|
|
499
|
+
* browser terminal at /terminal/<session>, and a Web IDE with write access to the whole tree
|
|
500
|
+
* and a terminal of its own, in a container holding the Docker socket. That is defensible
|
|
501
|
+
* ONLY because everything binds to loopback.
|
|
502
|
+
*
|
|
503
|
+
* Two settings end that, and both get changed for a good-looking reason: DDEV's
|
|
504
|
+
* router_bind_all_interfaces (documented for Let's Encrypt, and for reaching a project from a
|
|
505
|
+
* phone) and a public hub_domain (the README says the same system runs as a remote hub).
|
|
506
|
+
*
|
|
507
|
+
* The installer warns, but the person who flips the setting a month later is not running the
|
|
508
|
+
* installer — so the dashboard says it too, where they are.
|
|
509
|
+
*/
|
|
510
|
+
let _exposure = { at: 0, value: null };
|
|
511
|
+
function exposureRisk() {
|
|
512
|
+
if (_exposure.value && Date.now() - _exposure.at < 30000) return _exposure.value;
|
|
513
|
+
const reasons = [];
|
|
514
|
+
try {
|
|
515
|
+
const gc = fs.readFileSync(path.join(process.env.HOME || '', '.ddev', 'global_config.yaml'), 'utf8');
|
|
516
|
+
const m = gc.match(/^router_bind_all_interfaces:\s*(\S+)/m);
|
|
517
|
+
if (m && /^(true|yes)$/i.test(m[1])) {
|
|
518
|
+
reasons.push('DDEV\u2019s router is bound to every interface (router_bind_all_interfaces)');
|
|
519
|
+
}
|
|
520
|
+
} catch (_) { /* no global config readable: say nothing rather than guess */ }
|
|
521
|
+
try {
|
|
522
|
+
const hub = hubDomain();
|
|
523
|
+
if (hub && !/\.ddev\.site$|^localhost$|^127\.0\.0\.1$/.test(hub)) {
|
|
524
|
+
reasons.push(`hub_domain is ${hub}, which is not a local address`);
|
|
525
|
+
}
|
|
526
|
+
} catch (_) { /* ignore */ }
|
|
527
|
+
_exposure = { at: Date.now(), value: reasons };
|
|
528
|
+
return reasons;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
module.exports = {
|
|
532
|
+
appVersion, exposureRisk,
|
|
533
|
+
sourceOf,
|
|
534
|
+
commandDescription,
|
|
535
|
+
commandFlags,
|
|
536
|
+
commandTitle,
|
|
537
|
+
ROOT,
|
|
538
|
+
loadSettings,
|
|
539
|
+
styleSettings,
|
|
540
|
+
hubDomain,
|
|
541
|
+
CONFIG_DIR,
|
|
542
|
+
loadWorkspaces,
|
|
543
|
+
invalidateWorkspaces,
|
|
544
|
+
isValidWorkspace,
|
|
545
|
+
workspaceDir,
|
|
546
|
+
findBackupScript,
|
|
547
|
+
findRemoveScript,
|
|
548
|
+
findScript,
|
|
549
|
+
findRagifyScript,
|
|
550
|
+
findBuilderScripts,
|
|
551
|
+
builderLabel,
|
|
552
|
+
listBackups,
|
|
553
|
+
};
|