orca-gallery-panel 0.0.0-stage → 1.4.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jaime Errázuriz Werth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,225 @@
1
- # Temporary Holding Version
1
+ # Orca Gallery Panel
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/jerrazuw/orca-gallery-panel/actions/workflows/ci.yml/badge.svg)](https://github.com/jerrazuw/orca-gallery-panel/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+
6
+ **Background images for [Orca](https://orca.dev), the agent IDE.** Orca has no setting for a
7
+ wallpaper or background image, so this plugin adds a side panel that shows a rotating gallery of
8
+ your own pictures while your agents work: wallpapers, photos, art, mood boards.
9
+
10
+ You point it at a folder, it packs the images into an Orca panel plugin, and the panel shuffles
11
+ through them with a slideshow, a blurred background, an optional Ken Burns zoom and controls you
12
+ can change without leaving Orca.
13
+
14
+ ![The gallery panel in Orca's dark and light themes, with the display settings menu open](docs/screenshot.png)
15
+
16
+ ## Features
17
+
18
+ - **Slideshow**: shuffles your images and changes them on an interval you choose. Every image
19
+ shows once before any repeats, and the same one never appears twice in a row.
20
+ - **Controls** on hover: previous, pause/resume, next, a position counter, interval, display
21
+ settings and fit/fill.
22
+ - **Settings in the panel**: click the clock to type the seconds between images (`0` stops the
23
+ automatic change), or the sliders to turn on a blurred background, a slow Ken Burns zoom or
24
+ the image's name. Changes last until Orca reloads the panel, because Orca gives panels no
25
+ storage; the defaults come from your build settings.
26
+ - **Blurred background**: an image that doesn't fill the panel sits on a blurred copy of itself
27
+ instead of empty bars.
28
+ - **Several galleries**, each in its own panel, from one config file.
29
+ - **Watch mode**: `--watch` rebuilds whenever you add or remove images, and Orca reloads the
30
+ panel, so a new photo shows up without touching the terminal.
31
+ - **Keyboard**: `←` / `→` to navigate, `Space` to pause, `F` to switch between fit and fill.
32
+ - **Matches Orca's theme**, light or dark, using the design tokens Orca gives every panel.
33
+ - **Any common format**: PNG, JPEG, GIF, WebP, AVIF and TIFF. Animated GIF and WebP stay
34
+ animated, and EXIF rotation is applied.
35
+ - **English and Spanish**, picked from the system language.
36
+ - **Accessible**: labeled buttons, visible focus, and no fade if you have reduced motion turned on.
37
+ - **Private**: the plugin asks Orca for no permissions and makes no network requests.
38
+
39
+ ## Quick start
40
+
41
+ ### 1. Try it from Orca
42
+
43
+ [Open in Orca](https://onorca.dev/plugins/jerrazuw.gallery-panel-demo?catalog=community) takes you
44
+ to **Gallery Panel (demo)** in Orca's plugin directory; the first time, Orca asks to add its
45
+ community catalog. Click **Install**, then open the panel from the right sidebar. It rotates
46
+ through a few sample images and explains, inside the panel, how to show your own.
47
+
48
+ Orca installs plugins from the directory as they are, so the listed one can only carry sample
49
+ images. Your own photos take one command, below.
50
+
51
+ ### 2. Show your own photos
52
+
53
+ You need [Node.js](https://nodejs.org) 22.18 or later and Orca 1.4.160 or later. In a terminal,
54
+ run this with the path to your folder instead of `~/Pictures/wallpapers`:
55
+
56
+ ```bash
57
+ npx orca-gallery-panel ~/Pictures/wallpapers --out ~/orca-gallery --watch
58
+ ```
59
+
60
+ It packs every image in the folder into an Orca plugin in `~/orca-gallery`, then keeps watching
61
+ the folder. In Orca, go to **Settings → Plugins → Development**, click **Add path** and enter the
62
+ full path of that folder (`/Users/<you>/orca-gallery` on a Mac). Open the panel from the right
63
+ sidebar.
64
+
65
+ If you installed the demo, uninstall it from **Settings → Plugins** so you don't have two
66
+ galleries.
67
+
68
+ ### 3. Add photos whenever you like
69
+
70
+ While the command runs, copy photos into your folder or delete them, and the panel updates by
71
+ itself within a few seconds: the plugin is rebuilt and Orca reloads it. Copying 20 photos at once
72
+ triggers a single rebuild. A good place to leave it running is a terminal tab inside Orca.
73
+
74
+ `Ctrl+C` stops it. Next time, run the same command again: it picks up whatever changed in the
75
+ meantime and goes back to watching. Without `--watch`, it builds once and exits, so run it again
76
+ after changing your photos.
77
+
78
+ ### Saving your settings
79
+
80
+ Instead of typing the options every time, put them in a `gallery.config.json` in the folder you
81
+ run the command from (your home folder, for example):
82
+
83
+ ```json
84
+ {
85
+ "$schema": "https://unpkg.com/orca-gallery-panel/gallery.config.schema.json",
86
+ "folder": "~/Pictures/wallpapers",
87
+ "out": "~/orca-gallery",
88
+ "title": "Wallpapers",
89
+ "interval": 60
90
+ }
91
+ ```
92
+
93
+ From then on, `npx orca-gallery-panel --watch` is enough, and editing the file while it runs
94
+ rebuilds the panel too. The `$schema` line gives you autocomplete and validation in editors that
95
+ support JSON Schema. Options passed on the command line still override the file for that run,
96
+ and `--config <file>` reads a different one.
97
+
98
+ If you installed the plugin with **Install plugin → Local folder** instead of **Development**,
99
+ Orca keeps a copy, so you need to install it again after each build.
100
+
101
+ ### Working from a clone
102
+
103
+ To change the code, or to try the bundled samples with `npm run build -- examples/images`:
104
+
105
+ ```bash
106
+ git clone https://github.com/jerrazuw/orca-gallery-panel.git
107
+ cd orca-gallery-panel
108
+ npm install
109
+ npm run build -- ~/Pictures/wallpapers --watch
110
+ ```
111
+
112
+ Here `npm run build --` takes the place of `npx orca-gallery-panel`, and the plugin goes to
113
+ `dist/` unless you pass `--out`. `gallery.config.json` at the root of the clone is ignored by
114
+ git, so your paths stay on your machine; `gallery.config.example.json` is a starting point.
115
+
116
+ ## Options
117
+
118
+ | Option | Config key | Default | Description |
119
+ | ----------------------- | ----------- | --------- | ------------------------------------------------- |
120
+ | `[folder]` | `folder` | `images` | Folder with your images. |
121
+ | `-o, --out <dir>` | `out` | `dist` | Output folder for the plugin. |
122
+ | `-t, --title <text>` | `title` | `Gallery` | Panel title shown in Orca. |
123
+ | `-i, --interval <sec>` | `interval` | `180` | Seconds between images. `0` disables rotation. |
124
+ | `-s, --max-size <px>` | `maxSize` | `800` | Longest side of each image, in pixels. |
125
+ | `-q, --quality <1-100>` | `quality` | `80` | WebP quality. |
126
+ | `--fit <mode>` | `fit` | `contain` | Initial mode: `contain` (whole image) or `cover`. |
127
+ | `--effect <name>` | `effect` | `none` | `none`, or `kenburns` for a slow zoom. |
128
+ | `--no-blur` | `blur` | `true` | Blurred background behind images. |
129
+ | `--caption` | `caption` | `false` | Show the image's file name. |
130
+ | `-r, --recursive` | `recursive` | `false` | Include images in subfolders. |
131
+ | | `galleries` | | Several galleries; see above. |
132
+ | `-w, --watch` | | | Rebuild when images or the config file change. |
133
+ | `-c, --config <file>` | | | Config file to read instead of the default. |
134
+
135
+ Precedence: command line, then the gallery's own settings, then the top level of
136
+ `gallery.config.json`, then the defaults. Paths in the config file are relative to the file, and
137
+ `~` means your home folder. The `fit`, `interval`, `blur`, `effect` and `caption` settings are
138
+ only where the panel starts: you can change them from the panel itself.
139
+
140
+ ### Several galleries
141
+
142
+ List them under `galleries`. Each one becomes its own panel in Orca. Settings at the top level
143
+ apply to every gallery unless it sets its own; the top-level `title` names the plugin, and a
144
+ gallery without a `title` is named after its folder.
145
+
146
+ ```json
147
+ {
148
+ "$schema": "./gallery.config.schema.json",
149
+ "title": "My galleries",
150
+ "interval": 120,
151
+ "galleries": [
152
+ { "folder": "~/Pictures/wallpapers", "title": "Wallpapers", "effect": "kenburns" },
153
+ { "folder": "~/Pictures/art", "interval": 30, "caption": true }
154
+ ]
155
+ }
156
+ ```
157
+
158
+ Each panel has to stay under Orca's 10 MiB limit for a panel, and all of them together under
159
+ its 50 MiB limit for a plugin.
160
+
161
+ ## How it works
162
+
163
+ Orca renders a plugin panel as a sandboxed `srcdoc` iframe whose Content Security Policy only
164
+ allows inline scripts and styles and `data:` images. A panel cannot read files from disk, and
165
+ Orca's plugin API gives it no file or folder access either.
166
+
167
+ So the images are packed into the panel when you build it:
168
+
169
+ 1. `src/images.ts` finds the supported images in your folder, in natural order.
170
+ 2. `src/encode.ts` uses [sharp](https://sharp.pixelplumbing.com) to apply EXIF rotation, shrink
171
+ each image to `--max-size` and re-encode it as WebP. That keeps transparency and animation,
172
+ and is far smaller than base64-encoded PNGs or GIFs.
173
+ 3. `src/html.ts` bundles the panel script (`src/panel/`, TypeScript) with
174
+ [esbuild](https://esbuild.github.io) and inlines it, the CSS and the images into one
175
+ `panel.html`.
176
+ 4. `src/build.ts` checks the result against Orca's 10 MiB limit for a panel. If it's over, it
177
+ names the largest images and suggests what to change.
178
+ 5. `src/manifest.ts` writes `orca-plugin.json`, taking the version from `package.json`, with
179
+ one panel per gallery.
180
+
181
+ At the default settings a typical photo ends up around 40 KB, so the limit fits a couple of
182
+ hundred images. Animated GIFs are much heavier.
183
+
184
+ ## Development
185
+
186
+ ```bash
187
+ npm run check # typecheck, lint, format check and unit tests
188
+ npm run test:e2e # the panel in headless Chromium (first run: npx playwright install chromium)
189
+ npm run preview # writes dist/preview.html: the built panel inside an Orca-like frame
190
+ ```
191
+
192
+ | Path | What it is |
193
+ | ------------------------------ | ----------------------------------------------------------------------------- |
194
+ | `src/cli.ts`, `src/options.ts` | Command-line entry point and argument parsing. |
195
+ | `src/build.ts` | Builds the plugin: find, encode, render, check size, write. |
196
+ | `src/panel/` | The code that runs inside Orca: slideshow, controls, translations and styles. |
197
+ | `src/watch.ts` | Watch mode: reruns the build when the watched folders change. |
198
+ | `src/preview.ts` | Reproduces Orca's panel shell so the panel can be checked in a browser. |
199
+ | `test/` | Unit tests (`node:test`) and browser tests (`test/e2e/`, Playwright). |
200
+ | `examples/images/` | Sample images, made by `scripts/generate-examples.ts`. |
201
+ | `plugin/` | The demo build listed in Orca's plugin directory, by `npm run build:demo`. |
202
+
203
+ TypeScript runs directly on Node through
204
+ [type stripping](https://nodejs.org/api/typescript.html#type-stripping), so there is no compile
205
+ step while you work on it. Node won't strip types inside `node_modules`, so the npm package is
206
+ compiled to `lib/` by `npm run build:lib`, which `npm pack` runs on its own. CI runs on Linux,
207
+ macOS and Windows with Node 22.18, 22 and 24, and installs the packed package in an empty
208
+ project to check that the command works.
209
+
210
+ ## In Orca's plugin directory
211
+
212
+ Orca lists a plugin from a tagged commit of its repository and installs it as it is, with no build
213
+ step, so it cannot pack your photos. The listed plugin is the build in `plugin/`: the sample images
214
+ with a note on how to load your own, under its own id (`gallery-panel-demo`) so it can sit next to
215
+ the plugin you build. `npm run build:demo` writes it, and `npm version` reruns that for every
216
+ release.
217
+
218
+ ## Contributing
219
+
220
+ Issues and pull requests are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md).
221
+
222
+ ## License
223
+
224
+ [MIT](LICENSE). The sample images in `examples/images/` are generated by code in this repository
225
+ and are covered by the same license.
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "./gallery.config.schema.json",
3
+ "folder": "~/Pictures/wallpapers",
4
+ "title": "Wallpapers",
5
+ "interval": 60
6
+ }
@@ -0,0 +1,127 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Orca Gallery Panel settings",
4
+ "description": "Saved options for the gallery build. Command-line options override these.",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "$schema": {
9
+ "type": "string"
10
+ },
11
+ "folder": {
12
+ "type": "string",
13
+ "minLength": 1,
14
+ "description": "Folder with your images. Relative paths start from this file; ~ is your home folder.",
15
+ "default": "images"
16
+ },
17
+ "title": {
18
+ "type": "string",
19
+ "minLength": 1,
20
+ "maxLength": 256,
21
+ "description": "Panel title shown in Orca. With \"galleries\", it names the plugin instead.",
22
+ "default": "Gallery"
23
+ },
24
+ "interval": {
25
+ "type": "integer",
26
+ "minimum": 0,
27
+ "maximum": 86400,
28
+ "description": "Seconds between images. 0 disables automatic rotation.",
29
+ "default": 180
30
+ },
31
+ "maxSize": {
32
+ "type": "integer",
33
+ "minimum": 16,
34
+ "maximum": 8192,
35
+ "description": "Longest side of each image, in pixels.",
36
+ "default": 800
37
+ },
38
+ "quality": {
39
+ "type": "integer",
40
+ "minimum": 1,
41
+ "maximum": 100,
42
+ "description": "WebP quality.",
43
+ "default": 80
44
+ },
45
+ "fit": {
46
+ "enum": ["contain", "cover"],
47
+ "description": "Initial mode: contain shows the whole image, cover fills the panel.",
48
+ "default": "contain"
49
+ },
50
+ "blur": {
51
+ "type": "boolean",
52
+ "description": "Show a blurred copy of the image behind it when it doesn't fill the panel.",
53
+ "default": true
54
+ },
55
+ "effect": {
56
+ "enum": ["none", "kenburns"],
57
+ "description": "none, or kenburns for a slow zoom while each image is shown.",
58
+ "default": "none"
59
+ },
60
+ "caption": {
61
+ "type": "boolean",
62
+ "description": "Show the image's file name at the top of the panel.",
63
+ "default": false
64
+ },
65
+ "recursive": {
66
+ "type": "boolean",
67
+ "description": "Include images in subfolders.",
68
+ "default": false
69
+ },
70
+ "out": {
71
+ "type": "string",
72
+ "minLength": 1,
73
+ "description": "Output folder for the plugin.",
74
+ "default": "dist"
75
+ },
76
+ "galleries": {
77
+ "type": "array",
78
+ "minItems": 1,
79
+ "description": "Several galleries, each in its own panel. Settings at the top level apply to all of them unless a gallery sets its own.",
80
+ "items": {
81
+ "$ref": "#/$defs/gallery"
82
+ }
83
+ }
84
+ },
85
+ "$defs": {
86
+ "gallery": {
87
+ "type": "object",
88
+ "additionalProperties": false,
89
+ "required": ["folder"],
90
+ "properties": {
91
+ "folder": {
92
+ "$ref": "#/properties/folder"
93
+ },
94
+ "title": {
95
+ "type": "string",
96
+ "minLength": 1,
97
+ "maxLength": 256,
98
+ "description": "Panel title. Defaults to the folder's name."
99
+ },
100
+ "interval": {
101
+ "$ref": "#/properties/interval"
102
+ },
103
+ "maxSize": {
104
+ "$ref": "#/properties/maxSize"
105
+ },
106
+ "quality": {
107
+ "$ref": "#/properties/quality"
108
+ },
109
+ "fit": {
110
+ "$ref": "#/properties/fit"
111
+ },
112
+ "blur": {
113
+ "$ref": "#/properties/blur"
114
+ },
115
+ "effect": {
116
+ "$ref": "#/properties/effect"
117
+ },
118
+ "caption": {
119
+ "$ref": "#/properties/caption"
120
+ },
121
+ "recursive": {
122
+ "$ref": "#/properties/recursive"
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
package/lib/build.js ADDED
@@ -0,0 +1,131 @@
1
+ import { mkdir, readdir, rm, stat, writeFile } from 'node:fs/promises';
2
+ import { basename, join } from 'node:path';
3
+ import { encodeImage } from "./encode.js";
4
+ import { renderPanelHtml } from "./html.js";
5
+ import { findImages, labelFor } from "./images.js";
6
+ import { createManifest, isGeneratedPanelFile, MANIFEST_FILE, panelEntries } from "./manifest.js";
7
+ /** Orca rejects a panel entry above this size (PLUGIN_PANEL_ENTRY_MAX_BYTES). */
8
+ export const PANEL_ENTRY_MAX_BYTES = 10 * 1024 * 1024;
9
+ /** Orca rejects a whole plugin above this size. */
10
+ export const PLUGIN_MAX_BYTES = 50 * 1024 * 1024;
11
+ const ENCODE_CONCURRENCY = 4;
12
+ export class BuildError extends Error {
13
+ name = 'BuildError';
14
+ }
15
+ async function mapConcurrent(items, limit, task) {
16
+ const results = new Array(items.length);
17
+ let cursor = 0;
18
+ const worker = async () => {
19
+ while (cursor < items.length) {
20
+ const index = cursor++;
21
+ results[index] = await task(items[index]);
22
+ }
23
+ };
24
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
25
+ return results;
26
+ }
27
+ export function formatBytes(bytes) {
28
+ if (bytes < 1024)
29
+ return `${String(bytes)} B`;
30
+ if (bytes < 1024 * 1024)
31
+ return `${(bytes / 1024).toFixed(0)} KB`;
32
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
33
+ }
34
+ export function oversizeMessage(title, panelBytes, images) {
35
+ const largest = [...images]
36
+ .sort((a, b) => b.bytes - a.bytes)
37
+ .slice(0, 5)
38
+ .map((image) => ` ${formatBytes(image.bytes).padStart(8)} ${basename(image.file)}${image.animated ? ' (animated)' : ''}`);
39
+ return [
40
+ `"${title}" is ${formatBytes(panelBytes)}, over Orca's ${formatBytes(PANEL_ENTRY_MAX_BYTES)} limit for a panel.`,
41
+ 'Largest images:',
42
+ ...largest,
43
+ 'Try a smaller --max-size or --quality, or remove some images.',
44
+ ].join('\n');
45
+ }
46
+ async function assertFolder(folder) {
47
+ const info = await stat(folder).catch(() => undefined);
48
+ if (!info?.isDirectory())
49
+ throw new BuildError(`Image folder not found: ${folder}`);
50
+ }
51
+ async function buildGallery(gallery, logger, demo) {
52
+ await assertFolder(gallery.input);
53
+ const files = await findImages(gallery.input, gallery.recursive);
54
+ const skipped = [];
55
+ const encoded = await mapConcurrent(files, ENCODE_CONCURRENCY, async (file) => {
56
+ try {
57
+ return { file, image: await encodeImage(file, gallery) };
58
+ }
59
+ catch (error) {
60
+ const reason = error instanceof Error ? error.message : String(error);
61
+ skipped.push({ file, reason });
62
+ logger.warn(`Skipping ${basename(file)}: ${reason}`);
63
+ return undefined;
64
+ }
65
+ });
66
+ const ok = encoded.filter((entry) => entry !== undefined);
67
+ if (ok.length === 0) {
68
+ throw new BuildError(files.length === 0
69
+ ? `No supported images in ${gallery.input}${gallery.recursive ? '' : ' (use --recursive to include subfolders)'}`
70
+ : `None of the ${String(files.length)} images in ${gallery.input} could be read`);
71
+ }
72
+ const images = ok.map(({ file, image }) => ({
73
+ src: image.dataUrl,
74
+ label: labelFor(file),
75
+ }));
76
+ const reports = ok.map(({ file, image }) => ({
77
+ file,
78
+ bytes: image.bytes,
79
+ width: image.width,
80
+ height: image.height,
81
+ animated: image.animated,
82
+ }));
83
+ const html = await renderPanelHtml({
84
+ images,
85
+ intervalMs: gallery.intervalSeconds * 1000,
86
+ fit: gallery.fit,
87
+ blur: gallery.blur,
88
+ effect: gallery.effect,
89
+ caption: gallery.caption,
90
+ demo,
91
+ });
92
+ const panelBytes = Buffer.byteLength(html);
93
+ if (panelBytes > PANEL_ENTRY_MAX_BYTES) {
94
+ throw new BuildError(oversizeMessage(gallery.title, panelBytes, reports));
95
+ }
96
+ return { html, images: reports, skipped };
97
+ }
98
+ /** Removes panel files an earlier build wrote for galleries that no longer exist. */
99
+ async function removeStalePanels(outDir, keep) {
100
+ const files = await readdir(outDir);
101
+ await Promise.all(files
102
+ .filter((file) => isGeneratedPanelFile(file) && !keep.has(file))
103
+ .map((file) => rm(join(outDir, file))));
104
+ }
105
+ export async function buildPlugin(options, logger = console) {
106
+ const entries = panelEntries(options.galleries.map((gallery) => gallery.title));
107
+ const built = [];
108
+ const demo = options.demo ?? false;
109
+ for (const gallery of options.galleries)
110
+ built.push(await buildGallery(gallery, logger, demo));
111
+ const totalBytes = built.reduce((sum, { html }) => sum + Buffer.byteLength(html), 0);
112
+ if (totalBytes > PLUGIN_MAX_BYTES) {
113
+ throw new BuildError(`The galleries add up to ${formatBytes(totalBytes)}, over Orca's ${formatBytes(PLUGIN_MAX_BYTES)} limit for a plugin.`);
114
+ }
115
+ await mkdir(options.outDir, { recursive: true });
116
+ await Promise.all([
117
+ ...built.map(({ html }, i) => writeFile(join(options.outDir, entries[i]?.entry ?? ''), html)),
118
+ writeFile(join(options.outDir, MANIFEST_FILE), `${JSON.stringify(createManifest(options.name, entries, { demo }), null, 2)}\n`),
119
+ ]);
120
+ await removeStalePanels(options.outDir, new Set(entries.map(({ entry }) => entry)));
121
+ return {
122
+ outDir: options.outDir,
123
+ galleries: built.map(({ html, images, skipped }, i) => ({
124
+ title: entries[i]?.title ?? '',
125
+ entry: entries[i]?.entry ?? '',
126
+ panelBytes: Buffer.byteLength(html),
127
+ images,
128
+ skipped,
129
+ })),
130
+ };
131
+ }
package/lib/cli.js ADDED
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ import { basename, dirname, relative, resolve, sep } from 'node:path';
3
+ import { pkg } from "./package-info.js";
4
+ import { buildPlugin, BuildError, formatBytes, PANEL_ENTRY_MAX_BYTES, } from "./build.js";
5
+ import { loadConfigFile } from "./config-file.js";
6
+ import { isSupportedImage } from "./images.js";
7
+ import { CONFIG_FILE, HELP, parseCommand, resolveOptions, UsageError, } from "./options.js";
8
+ import { watchLoop } from "./watch.js";
9
+ function configFilePath({ configPath, cwd }) {
10
+ return resolve(cwd, configPath ?? CONFIG_FILE);
11
+ }
12
+ async function loadOptions(invocation) {
13
+ const { settings, configPath, cwd } = invocation;
14
+ const configFile = await loadConfigFile(configFilePath(invocation), {
15
+ required: configPath !== undefined,
16
+ });
17
+ if (configFile)
18
+ console.log(`Using settings from ${configPath ?? CONFIG_FILE}`);
19
+ return resolveOptions([...(configFile ? [configFile] : []), { settings, baseDir: cwd }], cwd);
20
+ }
21
+ function displayPath(path, cwd) {
22
+ const relativePath = relative(cwd, path);
23
+ if (relativePath === '')
24
+ return '.';
25
+ return relativePath.startsWith('..') ? path : relativePath;
26
+ }
27
+ function plural(count, noun) {
28
+ return `${String(count)} ${noun}${count === 1 ? '' : 's'}`;
29
+ }
30
+ function report(options, result, cwd) {
31
+ const where = displayPath(result.outDir, cwd);
32
+ const usage = (bytes) => `${((bytes / PANEL_ENTRY_MAX_BYTES) * 100).toFixed(0)}%`;
33
+ const [only] = result.galleries;
34
+ if (result.galleries.length === 1 && only) {
35
+ console.log(`Built "${only.title}" with ${plural(only.images.length, 'image')}: ` +
36
+ `${formatBytes(only.panelBytes)} (${usage(only.panelBytes)} of Orca's limit) in ${where}`);
37
+ }
38
+ else {
39
+ console.log(`Built "${options.name}" with ${String(result.galleries.length)} panels in ${where}:`);
40
+ const width = Math.max(...result.galleries.map((gallery) => gallery.title.length));
41
+ for (const gallery of result.galleries) {
42
+ console.log(` ${gallery.title.padEnd(width)} ${plural(gallery.images.length, 'image').padStart(10)} ` +
43
+ `${formatBytes(gallery.panelBytes).padStart(8)} (${usage(gallery.panelBytes)} of the limit)`);
44
+ }
45
+ }
46
+ const skipped = result.galleries.reduce((sum, gallery) => sum + gallery.skipped.length, 0);
47
+ if (skipped > 0)
48
+ console.warn(`${String(skipped)} file(s) skipped.`);
49
+ }
50
+ function watchTargets(invocation, options) {
51
+ const config = configFilePath(invocation);
52
+ const targets = [
53
+ {
54
+ path: dirname(config),
55
+ recursive: false,
56
+ accept: (file) => basename(file) === basename(config),
57
+ },
58
+ ];
59
+ for (const gallery of options?.galleries ?? []) {
60
+ targets.push({
61
+ path: gallery.input,
62
+ recursive: true,
63
+ accept: (file) => isSupportedImage(file) && (gallery.recursive || !file.includes(sep)),
64
+ });
65
+ }
66
+ return targets;
67
+ }
68
+ function describe(error) {
69
+ return error instanceof Error ? error.message : String(error);
70
+ }
71
+ async function watchAndBuild(invocation) {
72
+ const controller = new AbortController();
73
+ process.once('SIGINT', () => {
74
+ controller.abort();
75
+ });
76
+ await watchLoop(async () => {
77
+ let options;
78
+ try {
79
+ options = await loadOptions(invocation);
80
+ report(options, await buildPlugin(options), invocation.cwd);
81
+ }
82
+ catch (error) {
83
+ if (!(error instanceof UsageError || error instanceof BuildError))
84
+ throw error;
85
+ console.error(describe(error));
86
+ }
87
+ console.log(`[${new Date().toLocaleTimeString()}] Watching for changes. Ctrl+C to stop.`);
88
+ return watchTargets(invocation, options);
89
+ }, {
90
+ signal: controller.signal,
91
+ onSkip: (target, error) => {
92
+ console.warn(`Not watching ${target.path}: ${describe(error)}`);
93
+ },
94
+ });
95
+ }
96
+ async function main(argv) {
97
+ const command = parseCommand(argv);
98
+ if (command.kind === 'help') {
99
+ process.stdout.write(HELP);
100
+ return 0;
101
+ }
102
+ if (command.kind === 'version') {
103
+ process.stdout.write(`${pkg.version}\n`);
104
+ return 0;
105
+ }
106
+ const invocation = {
107
+ settings: command.settings,
108
+ configPath: command.configPath,
109
+ cwd: process.cwd(),
110
+ };
111
+ if (command.watch) {
112
+ await watchAndBuild(invocation);
113
+ return 0;
114
+ }
115
+ const options = await loadOptions(invocation);
116
+ report(options, await buildPlugin(options), invocation.cwd);
117
+ console.log('Load it in Orca from Settings > Plugins > Development > Add path; it reloads on every build.\n' +
118
+ 'Add --watch to rebuild on its own whenever your images change.');
119
+ return 0;
120
+ }
121
+ main(process.argv.slice(2)).then((code) => {
122
+ process.exitCode = code;
123
+ }, (error) => {
124
+ if (error instanceof UsageError) {
125
+ console.error(`${error.message}\n\nRun with --help for usage.`);
126
+ process.exitCode = 2;
127
+ }
128
+ else if (error instanceof BuildError) {
129
+ console.error(error.message);
130
+ process.exitCode = 1;
131
+ }
132
+ else {
133
+ console.error(error);
134
+ process.exitCode = 1;
135
+ }
136
+ });