create-bl-theme 1.0.2 → 1.0.4

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.
Files changed (3) hide show
  1. package/README.md +31 -7
  2. package/bin/cli.js +190 -8
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -34,23 +34,47 @@ The CLI will prompt you for:
34
34
  ### Validate a theme
35
35
 
36
36
  ```bash
37
- create-bl-theme validate [directory]
37
+ # Validate a local directory
38
+ create-bl-theme validate ./my-theme
39
+
40
+ # Validate directly from a GitHub repository
41
+ create-bl-theme validate https://github.com/username/theme-repo
38
42
  ```
39
43
 
40
- Checks that your theme has all required files and valid metadata.
44
+ The validator checks:
45
+ - Required files (metadata.json, style.css, images/)
46
+ - Valid JSON structure and required fields
47
+ - Image integrity (detects corrupted files)
48
+ - Image dimensions (recommends 1280x720, but other sizes work fine)
41
49
 
42
50
  ## Generated Structure
43
51
 
44
52
  ```
45
53
  my-theme/
46
- ├── metadata.json # Theme metadata (required)
47
- ├── style.css # Your CSS styles (required)
48
- ├── shader.json # Shader config (if enabled)
49
- ├── README.md # Theme documentation
50
- └── images/ # Screenshots (required)
54
+ ├── metadata.json # Theme metadata (required)
55
+ ├── style.css # Your CSS styles (required)
56
+ ├── DESCRIPTION.md # Rich description (optional, takes precedence)
57
+ ├── shader.json # Shader config (if enabled)
58
+ ├── README.md # Theme documentation
59
+ └── images/ # Screenshots (required)
51
60
  └── preview.png
52
61
  ```
53
62
 
63
+ ### Theme Description Options
64
+
65
+ You can provide your theme description in two ways:
66
+
67
+ 1. **`description` field in metadata.json** - Simple, inline description for basic themes
68
+ 2. **`DESCRIPTION.md` file** - For richer descriptions with formatting (recommended for longer descriptions)
69
+
70
+ If both exist, `DESCRIPTION.md` takes precedence. At least one must be present.
71
+
72
+ Better Lyrics supports **GitHub Flavored Markdown (GFM)** in DESCRIPTION.md, so you can use:
73
+ - **Bold**, *italic*, and other text formatting
74
+ - [Links](https://example.com) and images
75
+ - Lists, tables, and code blocks
76
+ - Any other GFM features
77
+
54
78
  ## Theme Development
55
79
 
56
80
  1. **Edit `style.css`** - Add your custom styles. Use browser DevTools to inspect Better Lyrics elements and find the right selectors.
package/bin/cli.js CHANGED
@@ -4,13 +4,23 @@ import prompts from "prompts";
4
4
  import pc from "picocolors";
5
5
  import fs from "fs";
6
6
  import path from "path";
7
+ import os from "os";
7
8
  import { fileURLToPath } from "url";
9
+ import { imageSize } from "image-size";
10
+ import { execSync } from "child_process";
8
11
 
9
12
  const __filename = fileURLToPath(import.meta.url);
10
13
  const __dirname = path.dirname(__filename);
11
14
 
12
15
  const TEMPLATES_DIR = path.join(__dirname, "..", "templates");
13
16
 
17
+ // Recommended image dimensions
18
+ const RECOMMENDED_WIDTH = 1280;
19
+ const RECOMMENDED_HEIGHT = 720;
20
+
21
+ // GitHub URL patterns
22
+ const GITHUB_URL_PATTERN = /^(?:https?:\/\/)?(?:www\.)?github\.com\/([^\/]+)\/([^\/]+)(?:\/)?(?:\.git)?$/;
23
+
14
24
  async function main() {
15
25
  const args = process.argv.slice(2);
16
26
  const command = args[0];
@@ -35,12 +45,13 @@ async function main() {
35
45
 
36
46
  function showHelp() {
37
47
  console.log(`${pc.bold("Usage:")}
38
- ${pc.cyan("create-bl-theme")} [name] Create a new theme
39
- ${pc.cyan("create-bl-theme")} validate [dir] Validate a theme directory
48
+ ${pc.cyan("create-bl-theme")} [name] Create a new theme
49
+ ${pc.cyan("create-bl-theme")} validate [dir|url] Validate a theme (local or GitHub)
40
50
 
41
51
  ${pc.bold("Examples:")}
42
52
  ${pc.dim("$")} create-bl-theme my-awesome-theme
43
53
  ${pc.dim("$")} create-bl-theme validate ./my-theme
54
+ ${pc.dim("$")} create-bl-theme validate https://github.com/user/theme-repo
44
55
  `);
45
56
  }
46
57
 
@@ -81,6 +92,12 @@ async function create(targetDir) {
81
92
  message: "Description:",
82
93
  initial: "A custom theme for Better Lyrics",
83
94
  },
95
+ {
96
+ type: "confirm",
97
+ name: "useDescriptionFile",
98
+ message: "Use DESCRIPTION.md for richer formatting? (recommended for longer descriptions)",
99
+ initial: false,
100
+ },
84
101
  {
85
102
  type: "text",
86
103
  name: "creator",
@@ -138,7 +155,6 @@ async function create(targetDir) {
138
155
  const metadata = {
139
156
  id: response.id,
140
157
  title: response.title,
141
- description: response.description,
142
158
  creators: [response.creator],
143
159
  minVersion: "2.0.5.6",
144
160
  hasShaders: response.hasShaders,
@@ -147,11 +163,55 @@ async function create(targetDir) {
147
163
  images: ["preview.png"],
148
164
  };
149
165
 
166
+ // Only include description in metadata.json if not using DESCRIPTION.md
167
+ if (!response.useDescriptionFile) {
168
+ metadata.description = response.description;
169
+ }
170
+
150
171
  fs.writeFileSync(
151
172
  path.join(fullPath, "metadata.json"),
152
173
  JSON.stringify(metadata, null, 2)
153
174
  );
154
175
 
176
+ // Create DESCRIPTION.md if user opted for it
177
+ if (response.useDescriptionFile) {
178
+ const descriptionMd = `<!--
179
+ Better Lyrics supports GitHub Flavored Markdown (GFM) for theme descriptions.
180
+ You can use all standard GFM features including:
181
+ - **Bold** and *italic* text
182
+ - [Links](https://example.com)
183
+ - Images: ![alt text](https://example.com/image.png)
184
+ - Lists (ordered and unordered)
185
+ - Code blocks with syntax highlighting
186
+ - Tables
187
+ - And more!
188
+
189
+ This file takes precedence over the "description" field in metadata.json.
190
+ Delete this comment block when you're ready to publish.
191
+ -->
192
+
193
+ ## Features
194
+
195
+ - Add your theme features here
196
+ - Describe what makes your theme special
197
+ - Use **bold** for emphasis on key points
198
+
199
+ ## Preview
200
+
201
+ <!-- You can embed images directly in your description -->
202
+ <!-- ![Theme Preview](https://your-image-url.png) -->
203
+
204
+ ## Installation Notes
205
+
206
+ Any special instructions for using this theme.
207
+
208
+ ## Compatibility
209
+
210
+ - Works with Better Lyrics v2.0.5.6+
211
+ `;
212
+ fs.writeFileSync(path.join(fullPath, "DESCRIPTION.md"), descriptionMd);
213
+ }
214
+
155
215
  // Create style.css from template
156
216
  const cssTemplate = fs.readFileSync(
157
217
  path.join(TEMPLATES_DIR, "style.css"),
@@ -205,12 +265,17 @@ MIT
205
265
  console.log(
206
266
  ` ${pc.dim("3.")} Add a preview screenshot to ${pc.cyan("images/preview.png")}`
207
267
  );
268
+ let stepNum = 4;
269
+ if (response.useDescriptionFile) {
270
+ console.log(` ${pc.dim(`${stepNum}.`)} Edit ${pc.cyan("DESCRIPTION.md")} with your theme description`);
271
+ stepNum++;
272
+ }
208
273
  if (response.hasShaders) {
209
- console.log(` ${pc.dim("4.")} Configure ${pc.cyan("shader.json")} for shader effects`);
274
+ console.log(` ${pc.dim(`${stepNum}.`)} Configure ${pc.cyan("shader.json")} for shader effects`);
275
+ stepNum++;
210
276
  }
211
- const submitStep = response.hasShaders ? "5." : "4.";
212
277
  console.log(
213
- ` ${pc.dim(submitStep)} Push to GitHub and submit to the theme store`
278
+ ` ${pc.dim(`${stepNum}.`)} Push to GitHub and submit to the theme store`
214
279
  );
215
280
  console.log(
216
281
  ` ${pc.dim("https://github.com/boidushya/better-lyrics-themes")}`
@@ -223,10 +288,45 @@ MIT
223
288
  }
224
289
 
225
290
  async function validate(dir) {
226
- const fullPath = path.resolve(process.cwd(), dir);
291
+ let fullPath;
292
+ let tempDir = null;
227
293
  let errors = [];
228
294
  let warnings = [];
229
295
 
296
+ // Check if input is a GitHub URL
297
+ const githubMatch = dir.match(GITHUB_URL_PATTERN);
298
+
299
+ if (githubMatch) {
300
+ const [, owner, repo] = githubMatch;
301
+ const repoUrl = `https://github.com/${owner}/${repo}.git`;
302
+
303
+ console.log(pc.dim(`Cloning ${pc.cyan(`${owner}/${repo}`)} from GitHub...\n`));
304
+
305
+ try {
306
+ // Create temp directory
307
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "bl-theme-"));
308
+ fullPath = tempDir;
309
+
310
+ // Clone the repository (shallow clone for speed)
311
+ execSync(`git clone --depth 1 ${repoUrl} "${tempDir}"`, {
312
+ stdio: "pipe",
313
+ });
314
+
315
+ console.log(pc.green(` Cloned successfully!\n`));
316
+ } catch (e) {
317
+ console.log(pc.red(`Error: Could not clone repository "${owner}/${repo}"`));
318
+ console.log(pc.dim(` Make sure the repository exists and is publicly accessible.\n`));
319
+
320
+ if (e.message) {
321
+ console.log(pc.dim(` ${e.message}`));
322
+ }
323
+
324
+ process.exit(1);
325
+ }
326
+ } else {
327
+ fullPath = path.resolve(process.cwd(), dir);
328
+ }
329
+
230
330
  console.log(pc.dim(`Validating theme at ${fullPath}...\n`));
231
331
 
232
332
  // Check directory exists
@@ -235,6 +335,10 @@ async function validate(dir) {
235
335
  process.exit(1);
236
336
  }
237
337
 
338
+ // Check for DESCRIPTION.md
339
+ const descriptionMdPath = path.join(fullPath, "DESCRIPTION.md");
340
+ const hasDescriptionMd = fs.existsSync(descriptionMdPath);
341
+
238
342
  // Check metadata.json
239
343
  const metadataPath = path.join(fullPath, "metadata.json");
240
344
  if (!fs.existsSync(metadataPath)) {
@@ -245,7 +349,6 @@ async function validate(dir) {
245
349
  const required = [
246
350
  "id",
247
351
  "title",
248
- "description",
249
352
  "creators",
250
353
  "minVersion",
251
354
  "hasShaders",
@@ -259,6 +362,20 @@ async function validate(dir) {
259
362
  }
260
363
  }
261
364
 
365
+ // Check for description: either in metadata.json OR in DESCRIPTION.md
366
+ if (!metadata.description && !hasDescriptionMd) {
367
+ errors.push(
368
+ 'Missing description: add "description" field in metadata.json or create DESCRIPTION.md'
369
+ );
370
+ }
371
+
372
+ if (hasDescriptionMd) {
373
+ const descContent = fs.readFileSync(descriptionMdPath, "utf-8").trim();
374
+ if (descContent.length === 0) {
375
+ errors.push("DESCRIPTION.md exists but is empty");
376
+ }
377
+ }
378
+
262
379
  if (metadata.id && !/^[a-z0-9-]+$/.test(metadata.id)) {
263
380
  errors.push(
264
381
  "metadata.json: id must be lowercase letters, numbers, and hyphens only"
@@ -273,6 +390,19 @@ async function validate(dir) {
273
390
  errors.push("metadata.json: images must be an array");
274
391
  }
275
392
 
393
+ // Check if images referenced in metadata.json exist in images/
394
+ if (metadata.images && Array.isArray(metadata.images)) {
395
+ const imagesDir = path.join(fullPath, "images");
396
+ for (const image of metadata.images) {
397
+ const imagePath = path.join(imagesDir, image);
398
+ if (!fs.existsSync(imagePath)) {
399
+ errors.push(
400
+ `metadata.json: image "${image}" not found in images/ directory`
401
+ );
402
+ }
403
+ }
404
+ }
405
+
276
406
  if (!metadata.tags) {
277
407
  warnings.push("metadata.json: consider adding tags for discoverability");
278
408
  }
@@ -302,6 +432,45 @@ async function validate(dir) {
302
432
  .filter((f) => /\.(png|jpg|jpeg|gif|webp)$/i.test(f));
303
433
  if (images.length === 0) {
304
434
  errors.push("images/ directory must contain at least one image");
435
+ } else {
436
+ // Validate each image
437
+ for (const image of images) {
438
+ const imagePath = path.join(imagesDir, image);
439
+
440
+ try {
441
+ const imageBuffer = fs.readFileSync(imagePath);
442
+ const dimensions = imageSize(imageBuffer);
443
+
444
+ if (!dimensions || !dimensions.width || !dimensions.height) {
445
+ errors.push(
446
+ `${pc.bold(image)}: Unable to read image dimensions - the file may be corrupted or in an unsupported format`
447
+ );
448
+ continue;
449
+ }
450
+
451
+ const { width, height } = dimensions;
452
+ const aspectRatio = (width / height).toFixed(2);
453
+ const recommendedAspectRatio = (RECOMMENDED_WIDTH / RECOMMENDED_HEIGHT).toFixed(2);
454
+
455
+ // Only warn if aspect ratio differs from recommended 16:9
456
+ if (aspectRatio !== recommendedAspectRatio) {
457
+ warnings.push(`${pc.bold(image)}: ${width}x${height} (aspect ratio ${aspectRatio})`);
458
+ }
459
+ } catch (e) {
460
+ // Handle corrupted or unreadable images
461
+ if (e.message.includes("unsupported") || e.message.includes("Invalid")) {
462
+ errors.push(
463
+ `${pc.bold(image)}: This image appears to be corrupted or in an unsupported format\n ${pc.dim("Please ensure the file is a valid image (PNG, JPG, GIF, or WebP)")}`
464
+ );
465
+ } else if (e.code === "ENOENT") {
466
+ errors.push(`${pc.bold(image)}: File not found`);
467
+ } else {
468
+ errors.push(
469
+ `${pc.bold(image)}: Could not validate image - ${e.message}\n ${pc.dim("The file may be corrupted or inaccessible")}`
470
+ );
471
+ }
472
+ }
473
+ }
305
474
  }
306
475
  }
307
476
 
@@ -330,12 +499,25 @@ async function validate(dir) {
330
499
  }
331
500
  if (warnings.length > 0) {
332
501
  console.log(pc.yellow(pc.bold("\n Warnings:")));
502
+ console.log(pc.yellow(" Non-standard aspect ratios (recommended: 16:9)"));
333
503
  warnings.forEach((w) => console.log(pc.yellow(` - ${w}`)));
504
+ console.log();
505
+ console.log(pc.dim(pc.yellow(" This is just a suggestion - your images will still work fine!")));
506
+ console.log(pc.dim(pc.yellow(" Different aspect ratios can be intentional for your theme's design.")));
334
507
  }
335
508
  }
336
509
 
337
510
  console.log();
338
511
 
512
+ // Cleanup temp directory if we cloned from GitHub
513
+ if (tempDir) {
514
+ try {
515
+ fs.rmSync(tempDir, { recursive: true, force: true });
516
+ } catch (e) {
517
+ // Ignore cleanup errors
518
+ }
519
+ }
520
+
339
521
  if (errors.length > 0) {
340
522
  process.exit(1);
341
523
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-bl-theme",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "CLI tool to scaffold Better Lyrics themes",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,8 @@
18
18
  "author": "boidushya",
19
19
  "license": "MIT",
20
20
  "dependencies": {
21
- "prompts": "^2.4.2",
22
- "picocolors": "^1.1.1"
21
+ "image-size": "^2.0.2",
22
+ "picocolors": "^1.1.1",
23
+ "prompts": "^2.4.2"
23
24
  }
24
25
  }