@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/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webship.co/workspace",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0-beta1",
|
|
4
|
+
"description": "The Webship Workspace dashboard: a lean core (workspaces, builders, settings, the assistant shell, the plugin system) that every feature joins as a @webship.co/workspace-<name> plugin \u2014 required with npm, then activated, installed and configured.",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/webship/workspace.git",
|
|
9
|
+
"directory": "core/workspace-app"
|
|
10
|
+
},
|
|
11
|
+
"main": "server.js",
|
|
12
|
+
"files": [
|
|
13
|
+
"*.js",
|
|
14
|
+
"actions",
|
|
15
|
+
"commands",
|
|
16
|
+
"docs",
|
|
17
|
+
"public",
|
|
18
|
+
"scripts",
|
|
19
|
+
"plugins/workspace-notes",
|
|
20
|
+
"screenrc",
|
|
21
|
+
"PLUGINS.md",
|
|
22
|
+
"WORKSPACES.md",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"start": "node server.js",
|
|
27
|
+
"test": "npm run test:unit && npm run test:browser",
|
|
28
|
+
"test:unit": "node --test \"tests/unit/*.test.js\" \"plugins/*/tests/*.test.js\"",
|
|
29
|
+
"test:browser": "cucumber-js --config cucumber.js"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@xterm/addon-fit": "^0.10.0",
|
|
33
|
+
"@xterm/addon-unicode11": "^0.9.0",
|
|
34
|
+
"@xterm/addon-web-links": "^0.11.0",
|
|
35
|
+
"@xterm/xterm": "^5.5.0",
|
|
36
|
+
"ace-builds": "^1.44.0",
|
|
37
|
+
"htmx.org": "^4.0.0",
|
|
38
|
+
"js-yaml": "^5.2.1",
|
|
39
|
+
"ws": "^8.21.3"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"webship-js": "^2.0.4"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [],
|
|
48
|
+
"author": ""
|
|
49
|
+
}
|
package/palette-index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the command palette can go to: the workspaces, a few pages, and every project folder of the
|
|
3
|
+
* workspaces that hold projects. One small JSON document, kept for ten seconds, read by public/palette.js.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { loadWorkspaces, hubDomain } = require('./workspaces');
|
|
9
|
+
|
|
10
|
+
const MAX_PROJECTS = 800;
|
|
11
|
+
const NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,100}$/;
|
|
12
|
+
let cache = null;
|
|
13
|
+
let cachedAt = 0;
|
|
14
|
+
|
|
15
|
+
const PAGES = [
|
|
16
|
+
{ label: 'Screens', url: '/screens', hint: 'Detached sessions' },
|
|
17
|
+
{ label: 'Commands', url: '/commands', hint: 'Every command the dashboard can run' },
|
|
18
|
+
{ label: 'Settings', url: '/settings', hint: 'core/config' },
|
|
19
|
+
{ label: 'Help', url: '/help', hint: 'The user guide' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function buildIndex() {
|
|
23
|
+
const hub = hubDomain();
|
|
24
|
+
const workspaces = [];
|
|
25
|
+
const projects = [];
|
|
26
|
+
for (const [key, meta] of Object.entries(loadWorkspaces())) {
|
|
27
|
+
workspaces.push({ key, label: meta.label || key, url: `https://${key}.${hub}/`, kind: meta.kind || 'projects' });
|
|
28
|
+
if (meta.kind === 'files' || projects.length >= MAX_PROJECTS) continue;
|
|
29
|
+
let entries = [];
|
|
30
|
+
try { entries = fs.readdirSync(meta.dir, { withFileTypes: true }); } catch (_) { continue; }
|
|
31
|
+
for (const e of entries) {
|
|
32
|
+
if (!e.isDirectory() || e.name.startsWith('.') || !NAME_RE.test(e.name) || projects.length >= MAX_PROJECTS) continue;
|
|
33
|
+
const ddev = fs.existsSync(path.join(meta.dir, e.name, '.ddev', 'config.yaml'));
|
|
34
|
+
projects.push({ ws: key, name: e.name, ddev, url: ddev ? `https://${e.name}.${key}.${hub}/` : `https://${key}.${hub}/` });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { workspaces, pages: PAGES, projects };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function paletteIndex() {
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
if (!cache || now - cachedAt > 10000) { cache = buildIndex(); cachedAt = now; }
|
|
43
|
+
return cache;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { paletteIndex, buildIndex, PAGES };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser webship-js drives.
|
|
3
|
+
*
|
|
4
|
+
* Not a Playwright test-runner config: webship-js reads `browser`, `launchOptions` and
|
|
5
|
+
* `contextOptions` from here and launches the browser itself — cucumber-js is the runner.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
// Chromium only. The suite asserts behaviour, not rendering differences between engines, and
|
|
10
|
+
// three browsers is three downloads for no extra answer.
|
|
11
|
+
browser: 'chromium',
|
|
12
|
+
launchOptions: {
|
|
13
|
+
// Headless unless something asks otherwise: `HEADLESS=false ddev test-dashboard browser` shows
|
|
14
|
+
// the run, which is the only time watching it is worth the window.
|
|
15
|
+
headless: process.env.HEADLESS !== 'false',
|
|
16
|
+
slowMo: Number(process.env.SLOW_MO || 0),
|
|
17
|
+
},
|
|
18
|
+
contextOptions: {
|
|
19
|
+
viewport: { width: 1440, height: 900 },
|
|
20
|
+
// DDEV signs its own certificates, and a browser launched here has no reason to trust one.
|
|
21
|
+
ignoreHTTPSErrors: true,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installing a plugin into the workspace: placing what its manifest names.
|
|
3
|
+
*
|
|
4
|
+
* node plugin-install.js install <name> place everything, idempotently
|
|
5
|
+
* node plugin-install.js uninstall <name> remove exactly what install placed
|
|
6
|
+
* node plugin-install.js status [<name>] what is placed, what is not, as JSON
|
|
7
|
+
*
|
|
8
|
+
* A package in node_modules is code the dashboard can require; the workspace is a tree of bash
|
|
9
|
+
* scripts, DDEV config and settings files that know nothing about node_modules. This is the
|
|
10
|
+
* bridge: it copies (never links — a link breaks the moment a package is updated or removed)
|
|
11
|
+
* each piece to where the workspace expects it, records every path it wrote in
|
|
12
|
+
* core/config/plugins.installed.yml, and keeps a managed block in .gitignore so a dev clone
|
|
13
|
+
* never commits what an install placed.
|
|
14
|
+
*
|
|
15
|
+
* scripts.functions -> core/scripts/plugins/<name>/functions/ (bootstrap sources them)
|
|
16
|
+
* scripts.args -> core/scripts/plugins/<name>/args/
|
|
17
|
+
* scripts.machine -> core/scripts/plugins/<name>/
|
|
18
|
+
* scripts.hostInstall -> core/scripts/plugins/<name>/host-install.sh
|
|
19
|
+
* ddev.hostMounts -> core/scripts/plugins/<name>/host-mounts.txt (host-mounts.sh reads it)
|
|
20
|
+
* commands.files -> <workspace>/ for every workspace of commands.types (__WORKSPACE__ filled in)
|
|
21
|
+
* ddev.compose -> core/workspace-app/.ddev/docker-compose.plugin-<name>-<file>
|
|
22
|
+
* themes -> core/config/
|
|
23
|
+
* settingsTemplate -> core/config/plugin.<name>.settings.yml, only when absent, carrying the
|
|
24
|
+
* values of the settings.yml block it replaces (legacySettingsKey)
|
|
25
|
+
* features -> core/workspace-app/tests/features/plugins/<name>/ (webship-js runs them)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
const yaml = require('js-yaml');
|
|
31
|
+
|
|
32
|
+
const { pluginByName, installedPlugins } = require('./plugin-manifest');
|
|
33
|
+
// The workspace root as the dashboard knows it — NOT __dirname's parents: inside the container the
|
|
34
|
+
// app runs at /var/www/html while the tree is bind-mounted at its own absolute path, and the record
|
|
35
|
+
// written from one side must be the one read from the other.
|
|
36
|
+
const { ROOT, CONFIG_DIR } = require('./workspaces');
|
|
37
|
+
|
|
38
|
+
const SCRIPTS_DIR = path.join(ROOT, 'core', 'scripts');
|
|
39
|
+
const DDEV_DIR = path.join(ROOT, 'core', 'workspace-app', '.ddev');
|
|
40
|
+
const RECORD = path.join(CONFIG_DIR, 'plugins.installed.yml');
|
|
41
|
+
const GITIGNORE = path.join(ROOT, '.gitignore');
|
|
42
|
+
const BLOCK_START = '# plugins — managed by core/scripts/plugins.sh; do not edit between the markers';
|
|
43
|
+
const BLOCK_END = '# end plugins';
|
|
44
|
+
|
|
45
|
+
const rel = (p) => path.relative(ROOT, p);
|
|
46
|
+
|
|
47
|
+
function readRecord() {
|
|
48
|
+
try { return yaml.load(fs.readFileSync(RECORD, 'utf8')) || {}; } catch (_) { return {}; }
|
|
49
|
+
}
|
|
50
|
+
function writeRecord(data) {
|
|
51
|
+
fs.writeFileSync(RECORD, `# Written by core/scripts/plugins.sh: what each installed plugin placed in this tree.\n${yaml.dump(data)}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Workspaces of the given types, from settings.yml and each workspace.<key>.settings.yml. */
|
|
55
|
+
function workspacesOfTypes(types) {
|
|
56
|
+
const { loadWorkspaces } = require('./workspaces');
|
|
57
|
+
const all = loadWorkspaces();
|
|
58
|
+
return Object.entries(all).filter(([, m]) => types.includes(m.type)).map(([key]) => key);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function copyFile(src, dest, { fill } = {}) {
|
|
62
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
63
|
+
let data = fs.readFileSync(src);
|
|
64
|
+
if (fill) data = Buffer.from(Object.entries(fill).reduce((t, [k, v]) => t.split(k).join(v), data.toString('utf8')));
|
|
65
|
+
fs.writeFileSync(dest, data, { mode: fs.statSync(src).mode & 0o777 });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The managed .gitignore block: one line per placed path, so a dev clone stays clean. */
|
|
69
|
+
function updateGitignore(record) {
|
|
70
|
+
let text = '';
|
|
71
|
+
try { text = fs.readFileSync(GITIGNORE, 'utf8'); } catch (_) { /* a tree with no .gitignore */ }
|
|
72
|
+
const lines = new Set();
|
|
73
|
+
for (const entry of Object.values(record)) for (const p of entry.placed || []) lines.add(`/${p}`);
|
|
74
|
+
lines.add('/core/config/plugins.installed.yml');
|
|
75
|
+
lines.add('/core/scripts/plugins/');
|
|
76
|
+
const block = `${BLOCK_START}\n${[...lines].sort().join('\n')}\n${BLOCK_END}\n`;
|
|
77
|
+
const re = new RegExp(`${BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${BLOCK_END}\\n?`);
|
|
78
|
+
text = re.test(text) ? text.replace(re, block) : `${text.replace(/\n*$/, '\n\n')}${block}`;
|
|
79
|
+
fs.writeFileSync(GITIGNORE, text);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The settings template, carrying over the block it replaces in settings.yml when there is one. */
|
|
83
|
+
function placeSettings(plugin, placed) {
|
|
84
|
+
const m = plugin.manifest;
|
|
85
|
+
if (!m.settingsTemplate) return { restart: false };
|
|
86
|
+
const dest = path.join(CONFIG_DIR, m.settingsFile || `plugin.${plugin.name}.settings.yml`);
|
|
87
|
+
if (fs.existsSync(dest)) return { restart: false };
|
|
88
|
+
let text = fs.readFileSync(path.join(plugin.dir, m.settingsTemplate), 'utf8');
|
|
89
|
+
if (m.legacySettingsKey) {
|
|
90
|
+
try {
|
|
91
|
+
const settings = yaml.load(fs.readFileSync(path.join(CONFIG_DIR, 'settings.yml'), 'utf8')) || {};
|
|
92
|
+
// Dotted keys name a nested block: git.github is settings.git.github.
|
|
93
|
+
const legacy = String(m.legacySettingsKey).split('.').reduce((o, k) => (o && typeof o === 'object' ? o[k] : undefined), settings);
|
|
94
|
+
if (legacy && typeof legacy === 'object') {
|
|
95
|
+
for (const [k, v] of Object.entries(legacy)) {
|
|
96
|
+
if (v == null || v === '' || v === 'CHANGE_ME') continue;
|
|
97
|
+
const line = new RegExp(`^${k}:.*$`, 'm');
|
|
98
|
+
const rendered = yaml.dump({ [k]: v }).trim();
|
|
99
|
+
text = line.test(text) ? text.replace(line, rendered) : `${text.replace(/\n*$/, '\n')}${rendered}\n`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch (_) { /* settings.yml unreadable: the template ships as is */ }
|
|
103
|
+
}
|
|
104
|
+
fs.writeFileSync(dest, text, { mode: 0o600 });
|
|
105
|
+
placed.push(rel(dest));
|
|
106
|
+
return { restart: false };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function install(name) {
|
|
110
|
+
const plugin = pluginByName(name);
|
|
111
|
+
if (!plugin) throw new Error(`no plugin named '${name}' is installed in core/workspace-app (npm install it first)`);
|
|
112
|
+
const m = plugin.manifest;
|
|
113
|
+
const record = readRecord();
|
|
114
|
+
const before = new Set((record[name] && record[name].placed) || []);
|
|
115
|
+
const placed = [];
|
|
116
|
+
let restart = false;
|
|
117
|
+
const scripts = m.scripts || {};
|
|
118
|
+
const into = path.join(SCRIPTS_DIR, 'plugins', name);
|
|
119
|
+
for (const f of scripts.functions || []) { const d = path.join(into, 'functions', path.basename(f)); copyFile(path.join(plugin.dir, f), d); placed.push(rel(d)); }
|
|
120
|
+
for (const f of scripts.args || []) { const d = path.join(into, 'args', path.basename(f)); copyFile(path.join(plugin.dir, f), d); placed.push(rel(d)); }
|
|
121
|
+
for (const f of scripts.machine || []) { const d = path.join(into, path.basename(f)); copyFile(path.join(plugin.dir, f), d); placed.push(rel(d)); }
|
|
122
|
+
if (scripts.hostInstall) { const d = path.join(into, 'host-install.sh'); copyFile(path.join(plugin.dir, scripts.hostInstall), d); placed.push(rel(d)); }
|
|
123
|
+
const ddev = m.ddev || {};
|
|
124
|
+
if ((ddev.hostMounts || []).length) {
|
|
125
|
+
const d = path.join(into, 'host-mounts.txt');
|
|
126
|
+
fs.mkdirSync(into, { recursive: true });
|
|
127
|
+
fs.writeFileSync(d, `${ddev.hostMounts.join('\n')}\n`);
|
|
128
|
+
placed.push(rel(d));
|
|
129
|
+
}
|
|
130
|
+
for (const f of ddev.compose || []) {
|
|
131
|
+
const d = path.join(DDEV_DIR, `docker-compose.plugin-${name}-${path.basename(f).replace(/^docker-compose\./, '')}`);
|
|
132
|
+
const fresh = !fs.existsSync(d) || !fs.readFileSync(d).equals(fs.readFileSync(path.join(plugin.dir, f)));
|
|
133
|
+
copyFile(path.join(plugin.dir, f), d);
|
|
134
|
+
placed.push(rel(d));
|
|
135
|
+
if (fresh) restart = true;
|
|
136
|
+
}
|
|
137
|
+
const commands = m.commands || {};
|
|
138
|
+
if ((commands.files || []).length) {
|
|
139
|
+
// Named workspaces when the manifest lists them (only those that exist), else every one of the types.
|
|
140
|
+
const named = (commands.workspaces || []).filter((ws) => /^[a-z0-9_-]+$/.test(ws) && fs.existsSync(path.join(ROOT, ws)));
|
|
141
|
+
for (const ws of named.length ? named : workspacesOfTypes(commands.types || ['sites'])) {
|
|
142
|
+
for (const f of commands.files) {
|
|
143
|
+
const d = path.join(ROOT, ws, path.basename(f));
|
|
144
|
+
copyFile(path.join(plugin.dir, f), d, { fill: { __WORKSPACE__: ws } });
|
|
145
|
+
placed.push(rel(d));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const f of m.themes || []) { const d = path.join(CONFIG_DIR, path.basename(f)); copyFile(path.join(plugin.dir, f), d); placed.push(rel(d)); }
|
|
150
|
+
// A workspace folder the plugin IS (modules, themes, dev…): the folder, its settings file (placed once,
|
|
151
|
+
// never overwritten — it holds this machine's lists — and kept on uninstall) and its commands.
|
|
152
|
+
// --activate puts the key on settings.yml's workspaces list; --deactivate takes it off.
|
|
153
|
+
const ws = m.workspace || null;
|
|
154
|
+
if (ws && /^[a-z0-9_-]+$/.test(String(ws.key || ''))) {
|
|
155
|
+
const folder = path.join(ROOT, ws.key);
|
|
156
|
+
fs.mkdirSync(folder, { recursive: true });
|
|
157
|
+
if (ws.settingsTemplate) {
|
|
158
|
+
const d = path.join(CONFIG_DIR, `workspace.${ws.key}.settings.yml`);
|
|
159
|
+
// Recorded every time, placed only once: a path an earlier install recorded and this one does not is removed as stale.
|
|
160
|
+
if (!fs.existsSync(d)) copyFile(path.join(plugin.dir, ws.settingsTemplate), d);
|
|
161
|
+
placed.push(rel(d));
|
|
162
|
+
}
|
|
163
|
+
for (const f of ws.files || []) {
|
|
164
|
+
const d = path.join(folder, path.basename(f));
|
|
165
|
+
copyFile(path.join(plugin.dir, f), d, { fill: { __WORKSPACE__: ws.key } });
|
|
166
|
+
placed.push(rel(d));
|
|
167
|
+
}
|
|
168
|
+
// Written once, for a person to edit: what the folder is for.
|
|
169
|
+
for (const f of ws.docs || []) {
|
|
170
|
+
const d = path.join(folder, path.basename(f));
|
|
171
|
+
if (!fs.existsSync(d)) copyFile(path.join(plugin.dir, f), d, { fill: { __WORKSPACE__: ws.key } });
|
|
172
|
+
placed.push(rel(d));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// webship-js features, under tests/features/plugins/<name>/: the dashboard's cucumber paths cover every
|
|
176
|
+
// folder below tests/features, so an installed plugin's scenarios run with `ddev test-dashboard browser`.
|
|
177
|
+
for (const f of m.features || []) { const d = path.join(ROOT, 'core', 'workspace-app', 'tests', 'features', 'plugins', name, path.basename(f)); copyFile(path.join(plugin.dir, f), d); placed.push(rel(d)); }
|
|
178
|
+
placeSettings(plugin, placed);
|
|
179
|
+
// What an earlier install placed and this one did not (a workspace that changed type, a file the
|
|
180
|
+
// manifest dropped) goes away, so the tree never holds a stale copy.
|
|
181
|
+
for (const old of before) if (!placed.includes(old)) { try { fs.rmSync(path.join(ROOT, old)); } catch (_) { /* already gone */ } }
|
|
182
|
+
record[name] = { module: plugin.module, version: plugin.version, installed: new Date().toISOString(), placed };
|
|
183
|
+
writeRecord(record);
|
|
184
|
+
updateGitignore(record);
|
|
185
|
+
return { name, placed, restart: restart || (ddev.hostMounts || []).length > 0 && !before.size };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function uninstall(name) {
|
|
189
|
+
const record = readRecord();
|
|
190
|
+
const entry = record[name];
|
|
191
|
+
if (!entry) return { name, removed: [] };
|
|
192
|
+
const removed = [];
|
|
193
|
+
for (const p of entry.placed || []) {
|
|
194
|
+
// A workspace's own settings file and its docs stay: they hold what a person put there.
|
|
195
|
+
if (/^core\/config\/workspace\.[a-z0-9_-]+\.settings\.yml$/.test(p) || /\/(AGENTS|CLAUDE|README)\.md$/.test(p)) continue;
|
|
196
|
+
try { fs.rmSync(path.join(ROOT, p)); removed.push(p); } catch (_) { /* already gone */ }
|
|
197
|
+
}
|
|
198
|
+
try { fs.rmSync(path.join(SCRIPTS_DIR, 'plugins', name), { recursive: true, force: true }); } catch (_) { /* nothing there */ }
|
|
199
|
+
delete record[name];
|
|
200
|
+
writeRecord(record);
|
|
201
|
+
updateGitignore(record);
|
|
202
|
+
return { name, removed, restart: removed.some((p) => p.includes('/.ddev/')) };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function status(name) {
|
|
206
|
+
const record = readRecord();
|
|
207
|
+
const list = (name ? [pluginByName(name)].filter(Boolean) : installedPlugins());
|
|
208
|
+
return list.map((p) => {
|
|
209
|
+
const entry = record[p.name];
|
|
210
|
+
const missing = entry ? (entry.placed || []).filter((f) => !fs.existsSync(path.join(ROOT, f))) : [];
|
|
211
|
+
return { name: p.name, module: p.module, version: p.version, dir: p.dir, installed: !!entry, placed: entry ? entry.placed.length : 0, missing,
|
|
212
|
+
settingsFile: p.manifest.settingsFile ? fs.existsSync(path.join(CONFIG_DIR, p.manifest.settingsFile)) : null };
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = { install, uninstall, status, RECORD };
|
|
217
|
+
|
|
218
|
+
if (require.main === module) {
|
|
219
|
+
const [verb, name] = process.argv.slice(2);
|
|
220
|
+
try {
|
|
221
|
+
if (verb === 'install' && name) {
|
|
222
|
+
const r = install(name);
|
|
223
|
+
for (const p of r.placed) console.log(` placed ${p}`);
|
|
224
|
+
console.log(r.restart ? 'RESTART_NEEDED' : 'RESTART_NOT_NEEDED');
|
|
225
|
+
} else if (verb === 'uninstall' && name) {
|
|
226
|
+
const r = uninstall(name);
|
|
227
|
+
for (const p of r.removed) console.log(` removed ${p}`);
|
|
228
|
+
console.log(r.restart ? 'RESTART_NEEDED' : 'RESTART_NOT_NEEDED');
|
|
229
|
+
} else if (verb === 'status') {
|
|
230
|
+
console.log(JSON.stringify(status(name), null, 2));
|
|
231
|
+
} else {
|
|
232
|
+
console.error('usage: node plugin-install.js install|uninstall|status [<name>]');
|
|
233
|
+
process.exit(2);
|
|
234
|
+
}
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.error(err.message);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
}
|