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.
- package/README.md +31 -7
- package/bin/cli.js +190 -8
- 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
|
-
|
|
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
|
-
|
|
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
|
|
47
|
-
├── style.css
|
|
48
|
-
├──
|
|
49
|
-
├──
|
|
50
|
-
|
|
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]
|
|
39
|
-
${pc.cyan("create-bl-theme")} validate [dir]
|
|
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: 
|
|
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
|
+
<!--  -->
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
}
|