@webship.co/workspace 0.0.0-stage → 1.0.0-alpha1
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/graphs.js +107 -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/obsidian.js +59 -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/graphs.js +104 -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/obsidian.js +405 -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 +448 -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 +1440 -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 +8140 -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 +557 -0
package/images.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The images workspace: what an image file is, how big it is, and where images live.
|
|
3
|
+
*
|
|
4
|
+
* Dimensions are read from the file's own header, a few kilobytes of it, so a folder of three hundred
|
|
5
|
+
* screenshots lists without starting a process per file.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const { ROOT, workspaceDir, isValidWorkspace } = require('./workspaces');
|
|
12
|
+
|
|
13
|
+
const IMAGE_RE = /\.(png|jpe?g|gif|webp|svg|avif)$/i;
|
|
14
|
+
const TYPES = ['png', 'jpg', 'gif', 'webp', 'svg', 'avif'];
|
|
15
|
+
const typeOf = (name) => {
|
|
16
|
+
const ext = path.extname(name).slice(1).toLowerCase();
|
|
17
|
+
return ext === 'jpeg' ? 'jpg' : ext;
|
|
18
|
+
};
|
|
19
|
+
const NOISE_DIRS = new Set(['node_modules', '.git', 'dev-ai-agents', 'workspace-repo', 'vendor']);
|
|
20
|
+
|
|
21
|
+
// ------------------------------------------------------------- dimensions --
|
|
22
|
+
|
|
23
|
+
function readHead(file, bytes) {
|
|
24
|
+
let fd;
|
|
25
|
+
try {
|
|
26
|
+
fd = fs.openSync(file, 'r');
|
|
27
|
+
const buf = Buffer.alloc(bytes);
|
|
28
|
+
const n = fs.readSync(fd, buf, 0, bytes, 0);
|
|
29
|
+
return buf.subarray(0, n);
|
|
30
|
+
} catch (_) {
|
|
31
|
+
return Buffer.alloc(0);
|
|
32
|
+
} finally {
|
|
33
|
+
if (fd !== undefined) try { fs.closeSync(fd); } catch (_) { /* closed */ }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function pngSize(b) {
|
|
38
|
+
if (b.length < 24 || b.readUInt32BE(0) !== 0x89504e47) return null;
|
|
39
|
+
return { width: b.readUInt32BE(16), height: b.readUInt32BE(20) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function gifSize(b) {
|
|
43
|
+
if (b.length < 10 || b.toString('ascii', 0, 3) !== 'GIF') return null;
|
|
44
|
+
return { width: b.readUInt16LE(6), height: b.readUInt16LE(8) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function jpegSize(b) {
|
|
48
|
+
if (b.length < 4 || b[0] !== 0xff || b[1] !== 0xd8) return null;
|
|
49
|
+
let i = 2;
|
|
50
|
+
while (i + 9 < b.length) {
|
|
51
|
+
if (b[i] !== 0xff) { i += 1; continue; }
|
|
52
|
+
const marker = b[i + 1];
|
|
53
|
+
if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd7) || marker === 0x01) { i += 2; continue; }
|
|
54
|
+
const len = b.readUInt16BE(i + 2);
|
|
55
|
+
// SOF0..SOF15, less the ones that are not frames (DHT, JPG, DAC).
|
|
56
|
+
if (marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker)) {
|
|
57
|
+
return { width: b.readUInt16BE(i + 7), height: b.readUInt16BE(i + 5) };
|
|
58
|
+
}
|
|
59
|
+
i += 2 + len;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function webpSize(b) {
|
|
65
|
+
if (b.length < 30 || b.toString('ascii', 0, 4) !== 'RIFF' || b.toString('ascii', 8, 12) !== 'WEBP') return null;
|
|
66
|
+
const chunk = b.toString('ascii', 12, 16);
|
|
67
|
+
if (chunk === 'VP8 ') return { width: b.readUInt16LE(26) & 0x3fff, height: b.readUInt16LE(28) & 0x3fff };
|
|
68
|
+
if (chunk === 'VP8L') {
|
|
69
|
+
const bits = b.readUInt32LE(21);
|
|
70
|
+
return { width: (bits & 0x3fff) + 1, height: ((bits >> 14) & 0x3fff) + 1 };
|
|
71
|
+
}
|
|
72
|
+
if (chunk === 'VP8X') return { width: b.readUIntLE(24, 3) + 1, height: b.readUIntLE(27, 3) + 1 };
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function avifSize(b) {
|
|
77
|
+
const at = b.indexOf('ispe');
|
|
78
|
+
if (at < 0 || at + 16 > b.length) return null;
|
|
79
|
+
return { width: b.readUInt32BE(at + 8), height: b.readUInt32BE(at + 12) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function svgSize(b) {
|
|
83
|
+
const text = b.toString('utf8');
|
|
84
|
+
const tag = (text.match(/<svg\b[^>]*>/i) || [])[0];
|
|
85
|
+
if (!tag) return null;
|
|
86
|
+
const attr = (n) => { const m = tag.match(new RegExp(`\\s${n}\\s*=\\s*["']([\\d.]+)(px)?["']`, 'i')); return m ? Math.round(Number(m[1])) : 0; };
|
|
87
|
+
let width = attr('width');
|
|
88
|
+
let height = attr('height');
|
|
89
|
+
const vb = tag.match(/viewBox\s*=\s*["'][-\d.]+[\s,]+[-\d.]+[\s,]+([\d.]+)[\s,]+([\d.]+)["']/i);
|
|
90
|
+
if ((!width || !height) && vb) { width = width || Math.round(Number(vb[1])); height = height || Math.round(Number(vb[2])); }
|
|
91
|
+
return width && height ? { width, height } : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const dimCache = new Map();
|
|
95
|
+
const DIM_CACHE_MAX = 5000;
|
|
96
|
+
|
|
97
|
+
/** { width, height } of an image, or null when its header does not say. */
|
|
98
|
+
function dimensions(file, stat) {
|
|
99
|
+
const st = stat || (() => { try { return fs.statSync(file); } catch (_) { return null; } })();
|
|
100
|
+
if (!st) return null;
|
|
101
|
+
const key = `${file}|${st.size}|${st.mtimeMs}`;
|
|
102
|
+
if (dimCache.has(key)) return dimCache.get(key);
|
|
103
|
+
const type = typeOf(file);
|
|
104
|
+
// A JPEG's frame header can sit after a large EXIF block, so it gets a longer read.
|
|
105
|
+
const head = readHead(file, type === 'jpg' ? 262144 : 65536);
|
|
106
|
+
const parse = { png: pngSize, gif: gifSize, jpg: jpegSize, webp: webpSize, avif: avifSize, svg: svgSize }[type];
|
|
107
|
+
let out = null;
|
|
108
|
+
try { out = parse ? parse(head) : null; } catch (_) { out = null; }
|
|
109
|
+
if (dimCache.size >= DIM_CACHE_MAX) dimCache.delete(dimCache.keys().next().value);
|
|
110
|
+
dimCache.set(key, out);
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------- listing --
|
|
115
|
+
|
|
116
|
+
/** Resolve rel inside root, following links, or null when it leads out. */
|
|
117
|
+
function safeIn(root, rel) {
|
|
118
|
+
const clean = String(rel || '').replace(/^\/+|\/+$/g, '');
|
|
119
|
+
if (clean.includes('\0') || clean.split('/').some((s) => s === '..' || s.startsWith('.'))) return null;
|
|
120
|
+
try {
|
|
121
|
+
const base = fs.realpathSync(root);
|
|
122
|
+
const abs = fs.realpathSync(path.join(base, clean));
|
|
123
|
+
if (abs !== base && !abs.startsWith(base + path.sep)) return null;
|
|
124
|
+
return { base, abs, rel: path.relative(base, abs).split(path.sep).join('/') };
|
|
125
|
+
} catch (_) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The sidecar that says how an image was made, when there is one. */
|
|
131
|
+
function sidecarOf(abs) {
|
|
132
|
+
try {
|
|
133
|
+
const j = JSON.parse(fs.readFileSync(`${abs}.json`, 'utf8'));
|
|
134
|
+
return j && typeof j === 'object' ? j : null;
|
|
135
|
+
} catch (_) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** One image, as the gallery and the viewer use it. */
|
|
141
|
+
function describe(abs, rel, stat) {
|
|
142
|
+
const st = stat || fs.statSync(abs);
|
|
143
|
+
const dims = dimensions(abs, st);
|
|
144
|
+
return {
|
|
145
|
+
name: path.basename(abs), rel, type: typeOf(abs), size: st.size, mtime: st.mtime,
|
|
146
|
+
width: dims ? dims.width : 0, height: dims ? dims.height : 0,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The folders and images in one folder of a workspace. */
|
|
151
|
+
function listFolder(root, rel) {
|
|
152
|
+
const where = safeIn(root, rel);
|
|
153
|
+
if (!where) return { ok: false, folders: [], images: [], rel: '' };
|
|
154
|
+
const folders = [];
|
|
155
|
+
const images = [];
|
|
156
|
+
let entries = [];
|
|
157
|
+
try { entries = fs.readdirSync(where.abs, { withFileTypes: true }); } catch (_) { /* unreadable */ }
|
|
158
|
+
for (const e of entries) {
|
|
159
|
+
if (e.name.startsWith('.')) continue;
|
|
160
|
+
const childRel = where.rel ? `${where.rel}/${e.name}` : e.name;
|
|
161
|
+
const abs = path.join(where.abs, e.name);
|
|
162
|
+
if (e.isDirectory()) {
|
|
163
|
+
if (NOISE_DIRS.has(e.name)) continue;
|
|
164
|
+
let count = 0;
|
|
165
|
+
let subs = 0;
|
|
166
|
+
try {
|
|
167
|
+
for (const c of fs.readdirSync(abs, { withFileTypes: true })) {
|
|
168
|
+
if (c.name.startsWith('.')) continue;
|
|
169
|
+
if (c.isDirectory() && !NOISE_DIRS.has(c.name)) subs += 1;
|
|
170
|
+
else if (c.isFile() && IMAGE_RE.test(c.name)) count += 1;
|
|
171
|
+
}
|
|
172
|
+
} catch (_) { /* unreadable */ }
|
|
173
|
+
let mtime = new Date(0);
|
|
174
|
+
try { mtime = fs.statSync(abs).mtime; } catch (_) { /* gone */ }
|
|
175
|
+
folders.push({ folder: true, name: e.name, rel: childRel, count, folders: subs, mtime, size: 0 });
|
|
176
|
+
} else if (e.isFile() && IMAGE_RE.test(e.name)) {
|
|
177
|
+
try { images.push(describe(abs, childRel)); } catch (_) { /* vanished */ }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { ok: true, folders, images, rel: where.rel };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ------------------------------------------------------------- everywhere --
|
|
184
|
+
|
|
185
|
+
// Read-only sources for "All media images". Workspaces of documents and media, never of sites.
|
|
186
|
+
const EVERYWHERE = ['images', 'docs', 'videos', 'audio', 'specs', 'designs'];
|
|
187
|
+
const APP_SHOTS = 'dashboard';
|
|
188
|
+
const appShotsDir = () => path.join(ROOT, 'core', 'workspace-app', 'docs', 'screenshots');
|
|
189
|
+
const SCAN_DEPTH = 4;
|
|
190
|
+
// Each source gets its own share, so one large folder of docs cannot crowd out the rest.
|
|
191
|
+
const SCAN_MAX = 1000;
|
|
192
|
+
const SCAN_TTL = 60 * 1000;
|
|
193
|
+
let scanMemo = { at: 0, list: null };
|
|
194
|
+
|
|
195
|
+
function walk(root, src, out) {
|
|
196
|
+
const start = out.length;
|
|
197
|
+
const full = () => out.length - start >= SCAN_MAX;
|
|
198
|
+
const go = (dir, rel, depth) => {
|
|
199
|
+
if (depth > SCAN_DEPTH || full()) return;
|
|
200
|
+
let entries = [];
|
|
201
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
202
|
+
for (const e of entries) {
|
|
203
|
+
if (full()) return;
|
|
204
|
+
if (e.name.startsWith('.') || NOISE_DIRS.has(e.name)) continue;
|
|
205
|
+
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
206
|
+
const abs = path.join(dir, e.name);
|
|
207
|
+
if (e.isDirectory()) go(abs, childRel, depth + 1);
|
|
208
|
+
else if (e.isFile() && IMAGE_RE.test(e.name)) {
|
|
209
|
+
try { out.push({ ...describe(abs, childRel), src }); } catch (_) { /* vanished */ }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
go(root, '', 0);
|
|
214
|
+
return full();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Every image in the media and document workspaces, and the dashboard's own screenshots. */
|
|
218
|
+
function scanEverywhere({ fresh = false } = {}) {
|
|
219
|
+
if (!fresh && scanMemo.list && Date.now() - scanMemo.at < SCAN_TTL) return scanMemo.list;
|
|
220
|
+
const out = [];
|
|
221
|
+
const capped = [];
|
|
222
|
+
for (const ws of EVERYWHERE) if (isValidWorkspace(ws) && walk(workspaceDir(ws), ws, out)) capped.push(ws);
|
|
223
|
+
if (fs.existsSync(appShotsDir()) && walk(appShotsDir(), APP_SHOTS, out)) capped.push(APP_SHOTS);
|
|
224
|
+
out.capped = capped;
|
|
225
|
+
scanMemo = { at: Date.now(), list: out };
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function forgetScan() { scanMemo = { at: 0, list: null }; }
|
|
230
|
+
|
|
231
|
+
/** Where an image of a source lives on disk, or null. */
|
|
232
|
+
function sourceRoot(src) {
|
|
233
|
+
if (src === APP_SHOTS) return appShotsDir();
|
|
234
|
+
return EVERYWHERE.includes(src) && isValidWorkspace(src) ? workspaceDir(src) : null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ------------------------------------------------------------------ names --
|
|
238
|
+
|
|
239
|
+
/** A file name made safe: letters, digits, dot, dash and underscore, and an image extension kept. */
|
|
240
|
+
function safeName(name) {
|
|
241
|
+
const base = path.basename(String(name || '')).replace(/\s+/g, '-').replace(/[^A-Za-z0-9._-]/g, '').replace(/^[.-]+/, '');
|
|
242
|
+
return base.slice(0, 120);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** The first free name for <stem>.<ext> in dir: stem, stem-2, stem-3… */
|
|
246
|
+
function freeName(dir, stem, ext) {
|
|
247
|
+
const clean = safeName(stem).replace(/\.[^.]*$/, '') || 'image';
|
|
248
|
+
for (let n = 1; n < 10000; n += 1) {
|
|
249
|
+
const name = n === 1 ? `${clean}.${ext}` : `${clean}-${n}.${ext}`;
|
|
250
|
+
if (!fs.existsSync(path.join(dir, name))) return name;
|
|
251
|
+
}
|
|
252
|
+
return `${clean}-${Date.now()}.${ext}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The next version of a file: <stem>-v2.<ext>, -v3… never the original. */
|
|
256
|
+
function versionName(dir, file, ext) {
|
|
257
|
+
const stem = path.basename(file).replace(/\.[^.]*$/, '').replace(/-v\d+$/, '');
|
|
258
|
+
for (let n = 2; n < 10000; n += 1) {
|
|
259
|
+
const name = `${stem}-v${n}.${ext}`;
|
|
260
|
+
if (!fs.existsSync(path.join(dir, name))) return name;
|
|
261
|
+
}
|
|
262
|
+
return `${stem}-v${Date.now()}.${ext}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = {
|
|
266
|
+
IMAGE_RE, TYPES, typeOf, dimensions, safeIn, sidecarOf, describe, listFolder,
|
|
267
|
+
EVERYWHERE, APP_SHOTS, SCAN_MAX, appShotsDir, scanEverywhere, forgetScan, sourceRoot,
|
|
268
|
+
safeName, freeName, versionName,
|
|
269
|
+
_parsers: { pngSize, gifSize, jpegSize, webpSize, avifSize, svgSize },
|
|
270
|
+
};
|