@gallop.software/studio 2.3.173 → 2.4.1

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @gallop.software/studio
2
2
 
3
- Standalone media manager for Gallop templates. Upload, process, and sync images to Cloudflare R2 CDN. Manage fonts with visual tools.
3
+ Standalone media manager for Gallop templates. Upload, process, and sync images to Cloudflare R2 CDN. Manage fonts with visual tools. Run headless via CLI.
4
4
 
5
5
  ## Features
6
6
 
@@ -8,13 +8,15 @@ Standalone media manager for Gallop templates. Upload, process, and sync images
8
8
 
9
9
  - **Standalone dev server** - runs on its own port, doesn't affect your app
10
10
  - **Upload any file type** with drag-and-drop support and progress tracking
11
- - **Automatic thumbnail generation** for images
11
+ - **Automatic thumbnail generation** for images (sm, md, lg, full)
12
12
  - **Browse folders** with grid and list views
13
13
  - **Multi-select** for batch operations (delete, move, download)
14
14
  - **Push to CDN** (Cloudflare R2) with automatic local cleanup
15
15
  - **Cache purge** for custom CDN domains
16
16
  - **Blurhash** generation for image placeholders
17
17
  - **Image editing** - crop, resize, rotate, and adjust quality
18
+ - **Favicon generation** from any image
19
+ - **Featured image** generation with customizable options
18
20
 
19
21
  ### Font Management
20
22
 
@@ -26,6 +28,13 @@ Standalone media manager for Gallop templates. Upload, process, and sync images
26
28
  - **Folder status badges** - visual indicators for TTF-only, WOFF2-ready, and assigned folders
27
29
  - **Batch operations** - rename folders, delete files, with streaming progress
28
30
 
31
+ ### CLI Commands
32
+
33
+ - **Headless operation** - run scan, process, push, and download without the UI
34
+ - **Prefix filtering** - target specific folders (e.g. `studio process portfolio`)
35
+ - **Font conversion** - convert TTF/OTF to WOFF2 from the command line
36
+ - **Font assignment** - generate font config files without the UI
37
+
29
38
  ## Installation
30
39
 
31
40
  ```bash
@@ -34,25 +43,7 @@ npm install @gallop.software/studio --save-dev
34
43
 
35
44
  ## Quick Start
36
45
 
37
- ### 1. Create `.env.studio`
38
-
39
- Create a `.env.studio` file in your project root:
40
-
41
- ```bash
42
- # Dev site link (opens in new tab from Studio header)
43
- STUDIO_DEV_SITE_URL=http://localhost:3000
44
-
45
- # Cloudflare R2 Storage
46
- CLOUDFLARE_R2_ACCOUNT_ID=your_account_id
47
- CLOUDFLARE_R2_ACCESS_KEY_ID=your_access_key
48
- CLOUDFLARE_R2_SECRET_ACCESS_KEY=your_secret_key
49
- CLOUDFLARE_R2_BUCKET_NAME=your_bucket
50
- CLOUDFLARE_R2_PUBLIC_URL=https://your-cdn.example.com
51
- ```
52
-
53
- Add `.env.studio` to your `.gitignore`.
54
-
55
- ### 2. Add script to package.json
46
+ ### 1. Add script to package.json
56
47
 
57
48
  ```json
58
49
  {
@@ -62,7 +53,7 @@ Add `.env.studio` to your `.gitignore`.
62
53
  }
63
54
  ```
64
55
 
65
- ### 3. Run Studio
56
+ ### 2. Run Studio
66
57
 
67
58
  ```bash
68
59
  npm run studio
@@ -70,11 +61,82 @@ npm run studio
70
61
 
71
62
  Studio opens in your browser on an available port (default 3001).
72
63
 
64
+ R2 credentials and other environment variables are loaded from `.env.local` in your project root.
65
+
66
+ ## CLI Commands
67
+
68
+ All commands accept `--workspace <path>` to target a specific project (defaults to current directory).
69
+
70
+ ### `studio`
71
+
72
+ Start the web UI server.
73
+
74
+ ```bash
75
+ studio # Start on default port 3001
76
+ studio --port 4000 # Custom port
77
+ studio --open # Auto-open browser
78
+ studio --workspace ~/my-project # Target specific project
79
+ ```
80
+
81
+ ### `studio scan`
82
+
83
+ Scan the filesystem for new files not yet tracked in `_data/_studio.json`. Detects orphaned thumbnails in `public/images/`.
84
+
85
+ ```bash
86
+ studio scan
87
+ ```
88
+
89
+ ### `studio process [prefix]`
90
+
91
+ Generate thumbnails (sm, md, lg, full) for unprocessed images. Optionally filter by path prefix.
92
+
93
+ ```bash
94
+ studio process # Process all unprocessed images
95
+ studio process portfolio # Process only images in /portfolio/
96
+ ```
97
+
98
+ ### `studio push [prefix]`
99
+
100
+ Upload local images and thumbnails to Cloudflare R2 CDN. Optionally filter by path prefix.
101
+
102
+ ```bash
103
+ studio push # Push all local images to CDN
104
+ studio push portfolio # Push only /portfolio/ images
105
+ ```
106
+
107
+ ### `studio download [prefix]`
108
+
109
+ Download cloud images from R2 to local storage. Optionally filter by path prefix.
110
+
111
+ ```bash
112
+ studio download # Download all cloud images
113
+ studio download portfolio # Download only /portfolio/ images
114
+ ```
115
+
116
+ ### `studio fonts woff2 <folder>`
117
+
118
+ Convert TTF/OTF fonts in `_fonts/<folder>/` to WOFF2 format.
119
+
120
+ ```bash
121
+ studio fonts woff2 inter # Convert _fonts/inter/ to woff2
122
+ ```
123
+
124
+ ### `studio fonts assign <folder> --name <name>`
125
+
126
+ Generate a `src/fonts/<name>.ts` configuration file from a font folder's WOFF2 files.
127
+
128
+ ```bash
129
+ studio fonts assign inter --name heading # Generate src/fonts/heading.ts
130
+ ```
131
+
73
132
  ## Environment Variables
74
133
 
134
+ Variables are loaded from `.env.local` in your project root.
135
+
75
136
  | Variable | Required | Description |
76
137
  |----------|----------|-------------|
77
138
  | `STUDIO_DEV_SITE_URL` | No | URL to your dev site (shown as link in header) |
139
+ | `NEXT_PUBLIC_PRODUCTION_URL` | No | Fallback for `STUDIO_DEV_SITE_URL` if not set |
78
140
  | `CLOUDFLARE_R2_ACCOUNT_ID` | For CDN | Your Cloudflare account ID |
79
141
  | `CLOUDFLARE_R2_ACCESS_KEY_ID` | For CDN | R2 API access key |
80
142
  | `CLOUDFLARE_R2_SECRET_ACCESS_KEY` | For CDN | R2 API secret key |
@@ -132,7 +194,120 @@ export const body = localFont({
132
194
  })
133
195
  ```
134
196
 
135
- ## Metadata
197
+ ## Architecture
198
+
199
+ ### Directory Structure
200
+
201
+ ```
202
+ studio/
203
+ ├── bin/
204
+ │ └── studio.mjs # CLI entry point (parses args, routes to server or CLI)
205
+ ├── client/
206
+ │ └── main.tsx # React app entry (standalone mode)
207
+ ├── src/
208
+ │ ├── cli/ # CLI subcommands
209
+ │ │ ├── index.ts # Command dispatcher + progress helpers
210
+ │ │ ├── scan.ts # runScan()
211
+ │ │ ├── process.ts # runProcess()
212
+ │ │ ├── push.ts # runPush()
213
+ │ │ ├── download.ts # runDownload()
214
+ │ │ └── fonts.ts # runFonts() → woff2, assign
215
+ │ ├── components/ # React UI
216
+ │ │ ├── StudioUI.tsx # Main container
217
+ │ │ ├── StudioContext.tsx # State provider
218
+ │ │ ├── useStudioActions.tsx # Action dispatchers
219
+ │ │ ├── useStreamingOperation.ts # SSE consumption hook
220
+ │ │ ├── tokens.ts # Design tokens (colors, fonts, sizes)
221
+ │ │ └── ... # Toolbar, FileGrid, DetailView, Modals, Fonts
222
+ │ ├── config/
223
+ │ │ └── workspace.ts # Path resolution (getPublicPath, getDataPath, etc.)
224
+ │ ├── handlers/ # HTTP handlers (Fetch API Request/Response)
225
+ │ │ ├── files.ts # Upload, delete, rename, move, create folder
226
+ │ │ ├── images.ts # Sync, reprocess, unprocess, download, push
227
+ │ │ ├── list.ts # Browse, search, folder listing
228
+ │ │ ├── scan.ts # Filesystem scan, orphan detection
229
+ │ │ ├── import.ts # URL import, CDN management
230
+ │ │ ├── fonts.ts # Font CRUD, woff2 conversion, assignment
231
+ │ │ ├── edit-image.ts # Crop, resize, rotate
232
+ │ │ ├── favicon.ts # Favicon generation
233
+ │ │ ├── featured-image.ts # Featured image generation
234
+ │ │ └── utils/ # Shared utilities
235
+ │ │ ├── meta.ts # loadMeta/saveMeta (atomic writes)
236
+ │ │ ├── response.ts # jsonResponse, streamResponse, createSSEStream
237
+ │ │ ├── cancellation.ts # cancelOperation, isOperationCancelled
238
+ │ │ ├── cdn.ts # R2 upload/download/delete/copy
239
+ │ │ ├── thumbnails.ts # Image processing with sharp
240
+ │ │ ├── files.ts # Slugify, file type detection
241
+ │ │ ├── folders.ts # Empty folder cleanup
242
+ │ │ └── errors.ts # isFileNotFound helper
243
+ │ ├── lib/
244
+ │ │ └── api.ts # Typed API client (used by React components)
245
+ │ ├── server/
246
+ │ │ └── index.ts # Express 5 server, route registration
247
+ │ └── types.ts # TypeScript interfaces (MetaEntry, FileItem, etc.)
248
+ └── package.json
249
+ ```
250
+
251
+ ### Server
252
+
253
+ Express 5 app (`src/server/index.ts`) that:
254
+
255
+ 1. Sets `STUDIO_WORKSPACE` env var from `--workspace` flag
256
+ 2. Loads `.env.local` from the workspace
257
+ 3. Registers API routes at `/api/studio/*`
258
+ 4. Serves static files from `<workspace>/public/`
259
+ 5. Serves the React client with injected globals (`__STUDIO_WORKSPACE__`, `__STUDIO_SITE_URL__`)
260
+
261
+ Handlers use **Web API `Request`/`Response`** (not Express req/res). The server wraps them with `wrapHandler()` which converts between Express and Fetch API conventions.
262
+
263
+ ### Client
264
+
265
+ React 19 + Emotion (CSS-in-JS via `css` prop). Component hierarchy:
266
+
267
+ ```
268
+ StudioUI
269
+ ├── StudioToolbar (search, view toggle, actions)
270
+ ├── StudioFileGrid / StudioFileList (browsing)
271
+ ├── StudioDetailView (file info, thumbnails, CDN status)
272
+ ├── FontsSection
273
+ │ ├── FontsToolbar
274
+ │ └── FontsGrid / FontsList
275
+ └── Modals (Confirm, Input, Alert, Progress)
276
+ ```
277
+
278
+ State is managed via `StudioContext` (React context) with `useStudioActions` for dispatch.
279
+
280
+ ### CLI
281
+
282
+ `bin/studio.mjs` parses args and either starts the server or dispatches to `src/cli/index.ts`. CLI commands reuse the same utility functions as HTTP handlers — no code duplication.
283
+
284
+ ### Streaming
285
+
286
+ Batch operations use **Server-Sent Events (SSE)** via `createSSEStream()`. Events follow this protocol:
287
+
288
+ ```json
289
+ { "type": "start", "total": 10 }
290
+ { "type": "progress", "current": 1, "total": 10, "message": "Processing photo.jpg" }
291
+ { "type": "cleanup", "message": "Removing temp files..." }
292
+ { "type": "complete", "processed": 10, "errors": 0 }
293
+ { "type": "error", "message": "Failed to process image" }
294
+ ```
295
+
296
+ The client consumes these via `useStreamingOperation` hook, which manages progress UI and cancellation.
297
+
298
+ ### Configuration
299
+
300
+ Workspace resolution in `src/config/workspace.ts`:
301
+
302
+ | Function | Returns |
303
+ |----------|---------|
304
+ | `getWorkspace()` | `STUDIO_WORKSPACE` env var or `cwd()` |
305
+ | `getPublicPath(...)` | `<workspace>/public/<segments>` |
306
+ | `getDataPath(...)` | `<workspace>/_data/<segments>` |
307
+ | `getSrcAppPath(...)` | `<workspace>/src/app/<segments>` |
308
+ | `getWorkspacePath(...)` | `<workspace>/<segments>` |
309
+
310
+ ## Metadata Schema
136
311
 
137
312
  Studio stores image metadata in `_data/_studio.json`:
138
313
 
@@ -151,13 +326,115 @@ Studio stores image metadata in `_data/_studio.json`:
151
326
  }
152
327
  ```
153
328
 
154
- | Property | Description |
155
- |----------|-------------|
156
- | `o` | Original dimensions `{ w, h }` |
157
- | `b` | Blurhash string |
158
- | `sm/md/lg/f` | Thumbnail dimensions (small/medium/large/full) |
159
- | `c` | CDN index (references `_cdns` array) |
160
- | `u` | Update pending flag (local file overrides cloud) |
329
+ ### Property Reference
330
+
331
+ | Property | Type | Description |
332
+ |----------|------|-------------|
333
+ | `_cdns` | `string[]` | Array of CDN base URLs |
334
+ | `o` | `{ w, h }` | Original dimensions |
335
+ | `b` | `string` | Blurhash string for placeholder |
336
+ | `sm` | `{ w, h }` | Small thumbnail dimensions (300px width) |
337
+ | `md` | `{ w, h }` | Medium thumbnail dimensions (700px width) |
338
+ | `lg` | `{ w, h }` | Large thumbnail dimensions (1400px width) |
339
+ | `f` | `{ w, h }` | Full size dimensions (capped at 2560px width) |
340
+ | `c` | `number` | CDN index — references `_cdns` array |
341
+ | `u` | `1` | Update pending — local file overrides cloud version |
342
+
343
+ ### File Lifecycle
344
+
345
+ 1. **Upload** — file added to `public/`, entry created with `o` (dimensions)
346
+ 2. **Process** — thumbnails generated in `public/images/`, entry gains `sm`/`md`/`lg`/`f`
347
+ 3. **Push** — thumbnails uploaded to R2, entry gains `c` (CDN index)
348
+ 4. **Cloud** — local thumbnails can be removed; CDN serves the images
349
+
350
+ Files with `u: 1` have local changes that haven't been pushed to CDN yet.
351
+
352
+ ## Thumbnail Sizes
353
+
354
+ | Size | Max Width | Suffix | Path Convention |
355
+ |------|-----------|--------|-----------------|
356
+ | sm | 300px | `-sm` | `/images/{path}-sm.{ext}` |
357
+ | md | 700px | `-md` | `/images/{path}-md.{ext}` |
358
+ | lg | 1400px | `-lg` | `/images/{path}-lg.{ext}` |
359
+ | full | 2560px | *(none)* | `/images/{path}.{ext}` |
360
+
361
+ - PNG inputs produce PNG thumbnails; all others produce JPEG (quality 85)
362
+ - Thumbnails are stored in `public/images/` mirroring the original path structure
363
+
364
+ ## API Reference
365
+
366
+ All endpoints are prefixed with `/api/studio/`.
367
+
368
+ ### Browsing
369
+
370
+ | Method | Path | Description |
371
+ |--------|------|-------------|
372
+ | GET | `/list` | List files/folders at a path |
373
+ | GET | `/search` | Search files by query |
374
+ | GET | `/list-folders` | List all folders |
375
+ | GET | `/count-images` | Count images by status |
376
+ | GET | `/folder-images` | Count images per folder |
377
+
378
+ ### File Operations
379
+
380
+ | Method | Path | Streaming | Description |
381
+ |--------|------|-----------|-------------|
382
+ | POST | `/upload` | | Upload file (FormData) |
383
+ | POST | `/create-folder` | | Create new folder |
384
+ | POST | `/rename` | | Rename file/folder |
385
+ | POST | `/rename-stream` | SSE | Rename with progress |
386
+ | POST | `/delete` | | Delete files |
387
+ | POST | `/delete-stream` | SSE | Delete with progress |
388
+ | POST | `/move` | SSE | Move files to folder |
389
+
390
+ ### Image Processing
391
+
392
+ | Method | Path | Streaming | Description |
393
+ |--------|------|-----------|-------------|
394
+ | POST | `/sync` | | Process single image |
395
+ | POST | `/sync-stream` | SSE | Process with progress |
396
+ | POST | `/reprocess-stream` | SSE | Regenerate thumbnails |
397
+ | POST | `/unprocess-stream` | SSE | Remove local thumbnails |
398
+ | POST | `/edit-image` | | Crop/resize/rotate |
399
+ | POST | `/scan` | SSE | Scan filesystem for changes |
400
+ | POST | `/delete-orphans` | | Remove orphaned thumbnails |
401
+
402
+ ### CDN Operations
403
+
404
+ | Method | Path | Streaming | Description |
405
+ |--------|------|-----------|-------------|
406
+ | POST | `/push-updates-stream` | SSE | Push to R2 CDN |
407
+ | POST | `/download-stream` | SSE | Download from R2 |
408
+ | POST | `/cancel-stream` | | Cancel running operation |
409
+ | POST | `/cancel-updates` | | Cancel pending updates |
410
+ | GET | `/cdns` | | Get CDN URLs |
411
+ | POST | `/cdns` | | Update CDN URLs |
412
+ | POST | `/import` | SSE | Import from external URLs |
413
+
414
+ ### Fonts
415
+
416
+ | Method | Path | Streaming | Description |
417
+ |--------|------|-----------|-------------|
418
+ | GET | `/fonts/list` | | List font folders |
419
+ | POST | `/fonts/upload` | | Upload font file (FormData) |
420
+ | POST | `/fonts/create-folder` | | Create font folder |
421
+ | POST | `/fonts/delete` | | Delete font file |
422
+ | POST | `/fonts/delete-stream` | SSE | Delete with progress |
423
+ | POST | `/fonts/rename` | | Rename font file/folder |
424
+ | POST | `/fonts/rename-stream` | SSE | Rename with progress |
425
+ | POST | `/fonts/scan` | | Scan font folders |
426
+ | GET | `/fonts/assignments` | | List font assignments |
427
+ | POST | `/fonts/assign-stream` | SSE | Generate font config |
428
+ | POST | `/fonts/delete-assignment` | | Delete font assignment |
429
+
430
+ ### Other
431
+
432
+ | Method | Path | Streaming | Description |
433
+ |--------|------|-----------|-------------|
434
+ | POST | `/generate-favicon` | SSE | Generate favicon from image |
435
+ | POST | `/generate-featured-image` | SSE | Generate featured image |
436
+ | GET | `/check-featured-image` | | Check featured image status |
437
+ | GET | `/featured-image-options` | | Get featured image options |
161
438
 
162
439
  ## License
163
440
 
package/bin/studio.mjs CHANGED
@@ -24,6 +24,15 @@ Studio - Media Manager
24
24
 
25
25
  Usage:
26
26
  studio [options]
27
+ studio <command> [options]
28
+
29
+ Commands:
30
+ scan Scan for new media files and update metadata
31
+ process [prefix] Generate thumbnails for unprocessed images
32
+ push [prefix] Upload local images to CDN (R2)
33
+ download [prefix] Download cloud images to local storage
34
+ fonts woff2 <folder> Convert TTF/OTF to woff2
35
+ fonts assign <folder> --name <n> Generate src/fonts/<n>.ts
27
36
 
28
37
  Options:
29
38
  --workspace <path> Path to the project workspace (default: current directory)
@@ -32,9 +41,15 @@ Options:
32
41
  --help, -h Show this help message
33
42
 
34
43
  Examples:
35
- studio # Run in current directory
36
- studio --workspace ~/my-project # Run for specific project
37
- studio --port 3002 --open # Use custom port and open browser
44
+ studio # Start the web UI
45
+ studio --workspace ~/my-project # Start for specific project
46
+ studio scan # Scan for new files
47
+ studio process # Process all unprocessed images
48
+ studio process portfolio # Process images in /portfolio/
49
+ studio push # Push all local images to CDN
50
+ studio download # Download all cloud images
51
+ studio fonts woff2 inter # Convert _fonts/inter/ to woff2
52
+ studio fonts assign inter --name heading # Generate src/fonts/heading.ts
38
53
  `)
39
54
  process.exit(0)
40
55
  }
@@ -46,22 +61,52 @@ if (!existsSync(workspace)) {
46
61
  process.exit(1)
47
62
  }
48
63
 
49
- // Check for public folder
50
- const publicPath = resolve(workspace, 'public')
51
- if (!existsSync(publicPath)) {
52
- console.error(`Error: No 'public' folder found in workspace: ${workspace}`)
53
- console.error('Studio requires a public folder to manage media files.')
54
- process.exit(1)
55
- }
64
+ // Check for CLI subcommands
65
+ const knownCommands = ['scan', 'process', 'push', 'download', 'fonts']
66
+ const command = args.find(a => !a.startsWith('-') && knownCommands.includes(a))
67
+
68
+ if (command) {
69
+ // Remove command from args, also remove --workspace and its value, --port and its value
70
+ const subArgs = []
71
+ let skipNext = false
72
+ for (const a of args) {
73
+ if (skipNext) {
74
+ skipNext = false
75
+ continue
76
+ }
77
+ if (a === command) continue
78
+ if (a === '--workspace' || a === '--port') {
79
+ skipNext = true
80
+ continue
81
+ }
82
+ if (a === '--open' || a === '-o') continue
83
+ subArgs.push(a)
84
+ }
56
85
 
57
- // Start the server
58
- import('../dist/server/index.js').then((mod) => {
59
- mod.startServer({
60
- port,
61
- workspace,
62
- open: shouldOpen,
86
+ import('../dist/cli/index.js').then(mod => {
87
+ mod.run(command, workspace, subArgs)
88
+ }).catch(error => {
89
+ console.error('CLI command failed:', error)
90
+ process.exit(1)
63
91
  })
64
- }).catch((error) => {
65
- console.error('Failed to start Studio server:', error)
66
- process.exit(1)
67
- })
92
+ } else {
93
+ // Check for public folder (only needed for server mode)
94
+ const publicPath = resolve(workspace, 'public')
95
+ if (!existsSync(publicPath)) {
96
+ console.error(`Error: No 'public' folder found in workspace: ${workspace}`)
97
+ console.error('Studio requires a public folder to manage media files.')
98
+ process.exit(1)
99
+ }
100
+
101
+ // Start the server
102
+ import('../dist/server/index.js').then((mod) => {
103
+ mod.startServer({
104
+ port,
105
+ workspace,
106
+ open: shouldOpen,
107
+ })
108
+ }).catch((error) => {
109
+ console.error('Failed to start Studio server:', error)
110
+ process.exit(1)
111
+ })
112
+ }