create-bl-theme 1.0.4 → 1.0.6
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/PLAN.md +399 -0
- package/README.md +35 -4
- package/bin/cli.js +274 -14
- package/package.json +3 -2
- package/templates/style.rics +11 -0
- package/templates/style.css +0 -6
package/PLAN.md
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# create-bl-theme - Implementation Plan
|
|
2
|
+
|
|
3
|
+
This document outlines changes needed to support the new theme registry system.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
create-bl-theme is a CLI tool that:
|
|
8
|
+
|
|
9
|
+
- Scaffolds new Better Lyrics themes
|
|
10
|
+
- Validates theme structure and files
|
|
11
|
+
- Provides RICS syntax validation
|
|
12
|
+
|
|
13
|
+
This plan adds:
|
|
14
|
+
|
|
15
|
+
- `publish` command to help creators set up auto-publishing
|
|
16
|
+
- Updated validation for vendored structure
|
|
17
|
+
- Semver validation improvements
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Current Structure
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
create-bl-theme/
|
|
25
|
+
├── bin/
|
|
26
|
+
│ └── cli.js # Main CLI implementation
|
|
27
|
+
├── templates/
|
|
28
|
+
│ ├── style.rics # RICS template
|
|
29
|
+
│ └── shader.json # Shader template
|
|
30
|
+
├── package.json
|
|
31
|
+
└── README.md
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Current Commands
|
|
35
|
+
|
|
36
|
+
| Command | Purpose |
|
|
37
|
+
|----------------------------|-----------------------------|
|
|
38
|
+
| `create-bl-theme [name]` | Scaffold new theme |
|
|
39
|
+
| `create-bl-theme validate` | Validate theme structure |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## New Command: `publish`
|
|
44
|
+
|
|
45
|
+
Help creators set up auto-publishing via GitHub App.
|
|
46
|
+
|
|
47
|
+
### Usage
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# In theme repo directory
|
|
51
|
+
create-bl-theme publish
|
|
52
|
+
|
|
53
|
+
# Or specify path
|
|
54
|
+
create-bl-theme publish ./my-theme
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### What It Does
|
|
58
|
+
|
|
59
|
+
1. **Check prerequisites**
|
|
60
|
+
- Validate theme structure first
|
|
61
|
+
- Check if repo is a git repository
|
|
62
|
+
- Check if repo has a remote
|
|
63
|
+
|
|
64
|
+
2. **Guide through setup**
|
|
65
|
+
- Check if GitHub App is installed
|
|
66
|
+
- Provide installation link if not
|
|
67
|
+
- Verify theme is registered in better-lyrics/themes
|
|
68
|
+
|
|
69
|
+
3. **Verify publishing works**
|
|
70
|
+
- Simulate what would happen on push
|
|
71
|
+
- Show current version and what would be published
|
|
72
|
+
|
|
73
|
+
### Implementation
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
// bin/cli.js (add to existing)
|
|
77
|
+
|
|
78
|
+
async function publishCommand(args) {
|
|
79
|
+
const themePath = args[0] || '.';
|
|
80
|
+
|
|
81
|
+
console.log(chalk.blue('Checking theme for publishing...'));
|
|
82
|
+
|
|
83
|
+
// 1. Validate theme first
|
|
84
|
+
const validationResult = await validateTheme(themePath);
|
|
85
|
+
if (!validationResult.success) {
|
|
86
|
+
console.log(chalk.red('Theme validation failed. Fix errors before publishing.'));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 2. Check git repo
|
|
91
|
+
const isGitRepo = await checkIsGitRepo(themePath);
|
|
92
|
+
if (!isGitRepo) {
|
|
93
|
+
console.log(chalk.red('Not a git repository. Initialize git first:'));
|
|
94
|
+
console.log(chalk.gray(' git init'));
|
|
95
|
+
console.log(chalk.gray(' git remote add origin https://github.com/username/theme-name.git'));
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 3. Get remote info
|
|
100
|
+
const remote = await getGitRemote(themePath);
|
|
101
|
+
if (!remote) {
|
|
102
|
+
console.log(chalk.red('No git remote found. Add a remote:'));
|
|
103
|
+
console.log(chalk.gray(' git remote add origin https://github.com/username/theme-name.git'));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 4. Parse repo from remote
|
|
108
|
+
const repoMatch = remote.match(/github\.com[:/](.+?)(?:\.git)?$/);
|
|
109
|
+
if (!repoMatch) {
|
|
110
|
+
console.log(chalk.red('Could not parse GitHub repo from remote:', remote));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
const repo = repoMatch[1];
|
|
114
|
+
|
|
115
|
+
// 5. Read metadata
|
|
116
|
+
const metadata = JSON.parse(fs.readFileSync(path.join(themePath, 'metadata.json'), 'utf8'));
|
|
117
|
+
|
|
118
|
+
console.log();
|
|
119
|
+
console.log(chalk.bold('Theme Info:'));
|
|
120
|
+
console.log(` ID: ${metadata.id}`);
|
|
121
|
+
console.log(` Title: ${metadata.title}`);
|
|
122
|
+
console.log(` Version: ${metadata.version}`);
|
|
123
|
+
console.log(` Repo: ${repo}`);
|
|
124
|
+
console.log();
|
|
125
|
+
|
|
126
|
+
// 6. Check if registered
|
|
127
|
+
const isRegistered = await checkThemeRegistered(repo);
|
|
128
|
+
|
|
129
|
+
if (!isRegistered) {
|
|
130
|
+
console.log(chalk.yellow('Theme is not registered in the theme store.'));
|
|
131
|
+
console.log();
|
|
132
|
+
console.log(chalk.bold('To register your theme:'));
|
|
133
|
+
console.log(' 1. Fork https://github.com/better-lyrics/themes');
|
|
134
|
+
console.log(` 2. Add { "repo": "${repo}" } to index.json`);
|
|
135
|
+
console.log(' 3. Open a pull request');
|
|
136
|
+
console.log();
|
|
137
|
+
console.log('After your PR is merged, install the GitHub App for auto-updates:');
|
|
138
|
+
console.log(chalk.cyan(' https://github.com/apps/better-lyrics-themes'));
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 7. Check if GitHub App is installed
|
|
143
|
+
console.log(chalk.green('Theme is registered in the theme store.'));
|
|
144
|
+
console.log();
|
|
145
|
+
console.log(chalk.bold('Auto-publishing Setup:'));
|
|
146
|
+
console.log();
|
|
147
|
+
console.log('To enable automatic updates when you push:');
|
|
148
|
+
console.log(' 1. Install the GitHub App on your repo:');
|
|
149
|
+
console.log(chalk.cyan(' https://github.com/apps/better-lyrics-themes'));
|
|
150
|
+
console.log();
|
|
151
|
+
console.log(' 2. Push changes to your repo:');
|
|
152
|
+
console.log(chalk.gray(' git add .'));
|
|
153
|
+
console.log(chalk.gray(' git commit -m "feat: update theme"'));
|
|
154
|
+
console.log(chalk.gray(' git push'));
|
|
155
|
+
console.log();
|
|
156
|
+
console.log('The registry will automatically:');
|
|
157
|
+
console.log(' - Validate your theme');
|
|
158
|
+
console.log(' - Update the lockfile');
|
|
159
|
+
console.log(' - Vendor your theme files');
|
|
160
|
+
console.log(' - Show a commit status on your push');
|
|
161
|
+
console.log();
|
|
162
|
+
|
|
163
|
+
// 8. Check current lockfile status
|
|
164
|
+
const lockStatus = await checkLockStatus(repo);
|
|
165
|
+
if (lockStatus) {
|
|
166
|
+
console.log(chalk.bold('Current Registry Status:'));
|
|
167
|
+
console.log(` Locked Version: ${lockStatus.version}`);
|
|
168
|
+
console.log(` Locked At: ${lockStatus.locked}`);
|
|
169
|
+
console.log(` Commit: ${lockStatus.commit.slice(0, 7)}`);
|
|
170
|
+
|
|
171
|
+
if (metadata.version === lockStatus.version) {
|
|
172
|
+
console.log();
|
|
173
|
+
console.log(chalk.yellow('Your local version matches the registry.'));
|
|
174
|
+
console.log('Bump the version in metadata.json to publish an update.');
|
|
175
|
+
} else {
|
|
176
|
+
const semver = require('semver');
|
|
177
|
+
if (semver.gt(metadata.version, lockStatus.version)) {
|
|
178
|
+
console.log();
|
|
179
|
+
console.log(chalk.green(`Ready to publish: ${lockStatus.version} -> ${metadata.version}`));
|
|
180
|
+
console.log('Push to your repo to trigger an update.');
|
|
181
|
+
} else {
|
|
182
|
+
console.log();
|
|
183
|
+
console.log(chalk.red(`Version ${metadata.version} is not greater than ${lockStatus.version}`));
|
|
184
|
+
console.log('Versions must increase. Update metadata.json with a higher version.');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function checkThemeRegistered(repo) {
|
|
191
|
+
try {
|
|
192
|
+
const response = await fetch(
|
|
193
|
+
'https://raw.githubusercontent.com/better-lyrics/themes/main/index.json'
|
|
194
|
+
);
|
|
195
|
+
const index = await response.json();
|
|
196
|
+
return index.themes?.some(t => t.repo === repo) ?? false;
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function checkLockStatus(repo) {
|
|
203
|
+
try {
|
|
204
|
+
const response = await fetch(
|
|
205
|
+
'https://raw.githubusercontent.com/better-lyrics/themes/main/index.lock.json'
|
|
206
|
+
);
|
|
207
|
+
const lock = await response.json();
|
|
208
|
+
return lock.themes?.find(t => t.repo === repo) ?? null;
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function checkIsGitRepo(themePath) {
|
|
215
|
+
try {
|
|
216
|
+
execSync('git rev-parse --git-dir', { cwd: themePath, stdio: 'ignore' });
|
|
217
|
+
return true;
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function getGitRemote(themePath) {
|
|
224
|
+
try {
|
|
225
|
+
return execSync('git remote get-url origin', { cwd: themePath, encoding: 'utf8' }).trim();
|
|
226
|
+
} catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Updated Validation
|
|
235
|
+
|
|
236
|
+
### Add Semver Validation
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
// In validateTheme function
|
|
240
|
+
|
|
241
|
+
// Validate version format
|
|
242
|
+
const version = metadata.version;
|
|
243
|
+
const semverRegex = /^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/;
|
|
244
|
+
|
|
245
|
+
if (!semverRegex.test(version)) {
|
|
246
|
+
errors.push({
|
|
247
|
+
type: 'error',
|
|
248
|
+
message: `Invalid version format: "${version}". Must be semver (e.g., 1.0.0, 1.0.0-beta.1)`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Validate Required Files for Registry
|
|
254
|
+
|
|
255
|
+
```javascript
|
|
256
|
+
// Ensure files needed for vendoring exist
|
|
257
|
+
|
|
258
|
+
const requiredFiles = ['metadata.json'];
|
|
259
|
+
const styleFiles = ['style.rics', 'style.css'];
|
|
260
|
+
|
|
261
|
+
// Check style file exists
|
|
262
|
+
const hasStyleFile = styleFiles.some(f => fs.existsSync(path.join(themePath, f)));
|
|
263
|
+
if (!hasStyleFile) {
|
|
264
|
+
errors.push({
|
|
265
|
+
type: 'error',
|
|
266
|
+
message: 'Missing style file. Create style.rics or style.css',
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Check images directory
|
|
271
|
+
const imagesDir = path.join(themePath, 'images');
|
|
272
|
+
if (!fs.existsSync(imagesDir)) {
|
|
273
|
+
errors.push({
|
|
274
|
+
type: 'error',
|
|
275
|
+
message: 'Missing images/ directory. Add at least one screenshot.',
|
|
276
|
+
});
|
|
277
|
+
} else {
|
|
278
|
+
const images = fs.readdirSync(imagesDir).filter(f =>
|
|
279
|
+
/\.(png|jpg|jpeg|gif|webp)$/i.test(f)
|
|
280
|
+
);
|
|
281
|
+
if (images.length === 0) {
|
|
282
|
+
errors.push({
|
|
283
|
+
type: 'error',
|
|
284
|
+
message: 'No images found in images/ directory. Add at least one screenshot.',
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Validate images array matches actual files
|
|
290
|
+
const declaredImages = metadata.images || [];
|
|
291
|
+
for (const img of declaredImages) {
|
|
292
|
+
if (!fs.existsSync(path.join(imagesDir, img))) {
|
|
293
|
+
errors.push({
|
|
294
|
+
type: 'error',
|
|
295
|
+
message: `Image "${img}" declared in metadata.json but not found in images/`,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Check shader.json if hasShaders is true
|
|
301
|
+
if (metadata.hasShaders && !fs.existsSync(path.join(themePath, 'shader.json'))) {
|
|
302
|
+
errors.push({
|
|
303
|
+
type: 'error',
|
|
304
|
+
message: 'hasShaders is true but shader.json not found',
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Check description exists (metadata or DESCRIPTION.md)
|
|
309
|
+
const hasDescription = metadata.description || fs.existsSync(path.join(themePath, 'DESCRIPTION.md'));
|
|
310
|
+
if (!hasDescription) {
|
|
311
|
+
errors.push({
|
|
312
|
+
type: 'error',
|
|
313
|
+
message: 'Missing description. Add "description" to metadata.json or create DESCRIPTION.md',
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Validate Cover Image
|
|
319
|
+
|
|
320
|
+
```javascript
|
|
321
|
+
// Check cover.png or first image in images array
|
|
322
|
+
const hasCoverPng = fs.existsSync(path.join(themePath, 'cover.png'));
|
|
323
|
+
const hasImagesArray = metadata.images && metadata.images.length > 0;
|
|
324
|
+
|
|
325
|
+
if (!hasCoverPng && !hasImagesArray) {
|
|
326
|
+
warnings.push({
|
|
327
|
+
type: 'warning',
|
|
328
|
+
message: 'No cover image. Add cover.png or images to the images/ folder.',
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Updated CLI Help
|
|
336
|
+
|
|
337
|
+
```javascript
|
|
338
|
+
function showHelp() {
|
|
339
|
+
console.log(`
|
|
340
|
+
${chalk.bold('create-bl-theme')} - CLI for Better Lyrics themes
|
|
341
|
+
|
|
342
|
+
${chalk.bold('Usage:')}
|
|
343
|
+
create-bl-theme [theme-name] Create a new theme
|
|
344
|
+
create-bl-theme validate [path] Validate a theme
|
|
345
|
+
create-bl-theme publish [path] Check publishing status
|
|
346
|
+
|
|
347
|
+
${chalk.bold('Examples:')}
|
|
348
|
+
create-bl-theme my-theme
|
|
349
|
+
create-bl-theme validate ./my-theme
|
|
350
|
+
create-bl-theme publish
|
|
351
|
+
|
|
352
|
+
${chalk.bold('Theme Structure:')}
|
|
353
|
+
my-theme/
|
|
354
|
+
├── metadata.json # Required - Theme metadata
|
|
355
|
+
├── style.rics # Required - Styles (or style.css)
|
|
356
|
+
├── shader.json # Optional - If hasShaders: true
|
|
357
|
+
├── DESCRIPTION.md # Optional - Rich description
|
|
358
|
+
├── cover.png # Optional - Cover image
|
|
359
|
+
└── images/ # Required - Screenshots
|
|
360
|
+
└── preview.png
|
|
361
|
+
|
|
362
|
+
${chalk.bold('Documentation:')}
|
|
363
|
+
https://github.com/better-lyrics/themes
|
|
364
|
+
`);
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Package.json Updates
|
|
371
|
+
|
|
372
|
+
```json
|
|
373
|
+
{
|
|
374
|
+
"dependencies": {
|
|
375
|
+
"semver": "^7.5.0"
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Action Items
|
|
383
|
+
|
|
384
|
+
- [ ] Add `publish` command
|
|
385
|
+
- [ ] Add semver validation to validate command
|
|
386
|
+
- [ ] Validate all required files for vendoring
|
|
387
|
+
- [ ] Add cover.png validation
|
|
388
|
+
- [ ] Update help text
|
|
389
|
+
- [ ] Add semver dependency
|
|
390
|
+
- [ ] Update README with publish command docs
|
|
391
|
+
- [ ] Test with existing themes
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Related Documents
|
|
396
|
+
|
|
397
|
+
- [better-lyrics-themes PLAN.md](../better-lyrics-themes/PLAN.md)
|
|
398
|
+
- [store-api PLAN.md](../store-api/PLAN.md)
|
|
399
|
+
- [better-lyrics PLAN.md](../better-lyrics/PLAN.md)
|
package/README.md
CHANGED
|
@@ -42,7 +42,8 @@ create-bl-theme validate https://github.com/username/theme-repo
|
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
The validator checks:
|
|
45
|
-
- Required files (metadata.json, style.css, images/)
|
|
45
|
+
- Required files (metadata.json, style.rics or style.css, images/)
|
|
46
|
+
- RICS syntax validation (for .rics files)
|
|
46
47
|
- Valid JSON structure and required fields
|
|
47
48
|
- Image integrity (detects corrupted files)
|
|
48
49
|
- Image dimensions (recommends 1280x720, but other sizes work fine)
|
|
@@ -52,7 +53,7 @@ The validator checks:
|
|
|
52
53
|
```
|
|
53
54
|
my-theme/
|
|
54
55
|
├── metadata.json # Theme metadata (required)
|
|
55
|
-
├── style.
|
|
56
|
+
├── style.rics # Your styles in RICS format (required)
|
|
56
57
|
├── DESCRIPTION.md # Rich description (optional, takes precedence)
|
|
57
58
|
├── shader.json # Shader config (if enabled)
|
|
58
59
|
├── README.md # Theme documentation
|
|
@@ -60,6 +61,33 @@ my-theme/
|
|
|
60
61
|
└── preview.png
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
## RICS
|
|
65
|
+
|
|
66
|
+
Themes use [RICS](https://github.com/better-lyrics/rics) - a lightweight CSS preprocessor with full CSS parity. RICS adds variables, nesting, and mixins while staying close to standard CSS.
|
|
67
|
+
|
|
68
|
+
**Any valid CSS is also valid RICS**, so you can write plain CSS if you prefer.
|
|
69
|
+
|
|
70
|
+
```scss
|
|
71
|
+
$accent: #ff6b6b;
|
|
72
|
+
|
|
73
|
+
.lyrics-container {
|
|
74
|
+
background: rgba(0, 0, 0, 0.8);
|
|
75
|
+
|
|
76
|
+
.lyrics-line {
|
|
77
|
+
color: $accent;
|
|
78
|
+
|
|
79
|
+
&.active {
|
|
80
|
+
font-weight: bold;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- **Playground:** https://rics.boidu.dev
|
|
87
|
+
- **RICS Docs:** https://github.com/better-lyrics/rics
|
|
88
|
+
|
|
89
|
+
> **Note:** Both `.rics` and `.css` files are supported. The validator prefers `.rics` if both exist.
|
|
90
|
+
|
|
63
91
|
### Theme Description Options
|
|
64
92
|
|
|
65
93
|
You can provide your theme description in two ways:
|
|
@@ -77,7 +105,7 @@ Better Lyrics supports **GitHub Flavored Markdown (GFM)** in DESCRIPTION.md, so
|
|
|
77
105
|
|
|
78
106
|
## Theme Development
|
|
79
107
|
|
|
80
|
-
1. **Edit `style.
|
|
108
|
+
1. **Edit `style.rics`** - Add your custom styles using RICS or plain CSS. Use browser DevTools to inspect Better Lyrics elements and find the right selectors.
|
|
81
109
|
|
|
82
110
|
2. **Add screenshots** - Place at least one preview image in `images/`. Recommended size: 1280x720 (16:9 aspect ratio).
|
|
83
111
|
|
|
@@ -85,10 +113,13 @@ Better Lyrics supports **GitHub Flavored Markdown (GFM)** in DESCRIPTION.md, so
|
|
|
85
113
|
|
|
86
114
|
4. **Test locally** - Install your theme via "Install from URL" in Better Lyrics using your local path or GitHub repo URL.
|
|
87
115
|
|
|
116
|
+
**Resources:**
|
|
117
|
+
- [Styling Guide](https://github.com/better-lyrics/better-lyrics/blob/master/STYLING.md) - Available selectors and styling reference
|
|
118
|
+
|
|
88
119
|
## Submitting to Theme Store
|
|
89
120
|
|
|
90
121
|
1. Push your theme to a GitHub repository
|
|
91
|
-
2. Fork [
|
|
122
|
+
2. Fork [themes](https://github.com/better-lyrics/themes)
|
|
92
123
|
3. Add your theme to `index.json`:
|
|
93
124
|
```json
|
|
94
125
|
{
|
package/bin/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import os from "os";
|
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
9
|
import { imageSize } from "image-size";
|
|
10
10
|
import { execSync } from "child_process";
|
|
11
|
+
import { compileWithDetails } from "rics";
|
|
11
12
|
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
14
|
const __dirname = path.dirname(__filename);
|
|
@@ -35,6 +36,11 @@ async function main() {
|
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
if (command === "publish") {
|
|
40
|
+
await publish(args[1] || ".");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
39
45
|
showHelp();
|
|
40
46
|
return;
|
|
@@ -47,11 +53,26 @@ function showHelp() {
|
|
|
47
53
|
console.log(`${pc.bold("Usage:")}
|
|
48
54
|
${pc.cyan("create-bl-theme")} [name] Create a new theme
|
|
49
55
|
${pc.cyan("create-bl-theme")} validate [dir|url] Validate a theme (local or GitHub)
|
|
56
|
+
${pc.cyan("create-bl-theme")} publish [dir] Check publishing status
|
|
50
57
|
|
|
51
58
|
${pc.bold("Examples:")}
|
|
52
59
|
${pc.dim("$")} create-bl-theme my-awesome-theme
|
|
53
60
|
${pc.dim("$")} create-bl-theme validate ./my-theme
|
|
54
61
|
${pc.dim("$")} create-bl-theme validate https://github.com/user/theme-repo
|
|
62
|
+
${pc.dim("$")} create-bl-theme publish
|
|
63
|
+
|
|
64
|
+
${pc.bold("Theme Structure:")}
|
|
65
|
+
my-theme/
|
|
66
|
+
├── metadata.json ${pc.dim("# Required - Theme metadata")}
|
|
67
|
+
├── style.rics ${pc.dim("# Required - Styles (or style.css)")}
|
|
68
|
+
├── shader.json ${pc.dim("# Optional - If hasShaders: true")}
|
|
69
|
+
├── DESCRIPTION.md ${pc.dim("# Optional - Rich description")}
|
|
70
|
+
├── cover.png ${pc.dim("# Optional - Cover image")}
|
|
71
|
+
└── images/ ${pc.dim("# Required - Screenshots")}
|
|
72
|
+
└── preview.png
|
|
73
|
+
|
|
74
|
+
${pc.bold("Documentation:")}
|
|
75
|
+
${pc.cyan("https://github.com/better-lyrics/themes")}
|
|
55
76
|
`);
|
|
56
77
|
}
|
|
57
78
|
|
|
@@ -212,12 +233,12 @@ Any special instructions for using this theme.
|
|
|
212
233
|
fs.writeFileSync(path.join(fullPath, "DESCRIPTION.md"), descriptionMd);
|
|
213
234
|
}
|
|
214
235
|
|
|
215
|
-
// Create style.
|
|
216
|
-
const
|
|
217
|
-
path.join(TEMPLATES_DIR, "style.
|
|
236
|
+
// Create style.rics from template
|
|
237
|
+
const ricsTemplate = fs.readFileSync(
|
|
238
|
+
path.join(TEMPLATES_DIR, "style.rics"),
|
|
218
239
|
"utf-8"
|
|
219
240
|
);
|
|
220
|
-
fs.writeFileSync(path.join(fullPath, "style.
|
|
241
|
+
fs.writeFileSync(path.join(fullPath, "style.rics"), ricsTemplate);
|
|
221
242
|
|
|
222
243
|
// Create shader.json if needed
|
|
223
244
|
if (response.hasShaders) {
|
|
@@ -261,7 +282,7 @@ MIT
|
|
|
261
282
|
console.log();
|
|
262
283
|
console.log(pc.bold(" Next steps:"));
|
|
263
284
|
console.log(` ${pc.dim("1.")} cd ${dir}`);
|
|
264
|
-
console.log(` ${pc.dim("2.")} Edit ${pc.cyan("style.
|
|
285
|
+
console.log(` ${pc.dim("2.")} Edit ${pc.cyan("style.rics")} with your theme styles`);
|
|
265
286
|
console.log(
|
|
266
287
|
` ${pc.dim("3.")} Add a preview screenshot to ${pc.cyan("images/preview.png")}`
|
|
267
288
|
);
|
|
@@ -277,9 +298,15 @@ MIT
|
|
|
277
298
|
console.log(
|
|
278
299
|
` ${pc.dim(`${stepNum}.`)} Push to GitHub and submit to the theme store`
|
|
279
300
|
);
|
|
280
|
-
console.log(
|
|
281
|
-
|
|
282
|
-
);
|
|
301
|
+
console.log();
|
|
302
|
+
console.log(pc.bold(" Resources:"));
|
|
303
|
+
console.log(` ${pc.cyan("RICS")} is a lightweight CSS preprocessor with full CSS parity.`);
|
|
304
|
+
console.log(` It adds variables, nesting, and mixins - but plain CSS works too!`);
|
|
305
|
+
console.log();
|
|
306
|
+
console.log(` ${pc.dim("Playground:")} https://rics.boidu.dev`);
|
|
307
|
+
console.log(` ${pc.dim("RICS Docs:")} https://github.com/better-lyrics/rics`);
|
|
308
|
+
console.log(` ${pc.dim("Styling Guide:")} https://github.com/better-lyrics/better-lyrics/blob/master/STYLING.md`);
|
|
309
|
+
console.log(` ${pc.dim("Submit Theme:")} https://github.com/better-lyrics/themes`);
|
|
283
310
|
console.log();
|
|
284
311
|
console.log(
|
|
285
312
|
pc.dim(" Validate your theme with: ") + pc.cyan(`npx create-bl-theme@latest validate ${dir}`)
|
|
@@ -362,6 +389,16 @@ async function validate(dir) {
|
|
|
362
389
|
}
|
|
363
390
|
}
|
|
364
391
|
|
|
392
|
+
// Validate version format (semver)
|
|
393
|
+
if (metadata.version) {
|
|
394
|
+
const semverRegex = /^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/;
|
|
395
|
+
if (!semverRegex.test(metadata.version)) {
|
|
396
|
+
errors.push(
|
|
397
|
+
`metadata.json: invalid version format "${metadata.version}". Must be semver (e.g., 1.0.0, 1.0.0-beta.1)`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
365
402
|
// Check for description: either in metadata.json OR in DESCRIPTION.md
|
|
366
403
|
if (!metadata.description && !hasDescriptionMd) {
|
|
367
404
|
errors.push(
|
|
@@ -411,12 +448,41 @@ async function validate(dir) {
|
|
|
411
448
|
}
|
|
412
449
|
}
|
|
413
450
|
|
|
414
|
-
// Check style.css
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
451
|
+
// Check for style.rics or style.css (prefer .rics)
|
|
452
|
+
const ricsPath = path.join(fullPath, "style.rics");
|
|
453
|
+
const cssPath = path.join(fullPath, "style.css");
|
|
454
|
+
const hasRics = fs.existsSync(ricsPath);
|
|
455
|
+
const hasCss = fs.existsSync(cssPath);
|
|
456
|
+
|
|
457
|
+
if (!hasRics && !hasCss) {
|
|
458
|
+
errors.push("Missing required file: style.rics or style.css");
|
|
459
|
+
} else if (hasRics) {
|
|
460
|
+
const ricsSource = fs.readFileSync(ricsPath, "utf-8");
|
|
461
|
+
if (ricsSource.trim().length === 0) {
|
|
462
|
+
warnings.push("style.rics is empty");
|
|
463
|
+
} else {
|
|
464
|
+
// Validate RICS syntax
|
|
465
|
+
try {
|
|
466
|
+
const result = compileWithDetails(ricsSource);
|
|
467
|
+
if (result.errors && result.errors.length > 0) {
|
|
468
|
+
for (const err of result.errors) {
|
|
469
|
+
const location = err.start
|
|
470
|
+
? ` (line ${err.start.line}, column ${err.start.column})`
|
|
471
|
+
: "";
|
|
472
|
+
errors.push(`style.rics: ${err.message}${location}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
476
|
+
for (const warn of result.warnings) {
|
|
477
|
+
warnings.push(`style.rics: ${warn.message || warn}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
} catch (e) {
|
|
481
|
+
errors.push(`style.rics: Failed to compile - ${e.message}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
} else if (hasCss) {
|
|
485
|
+
const css = fs.readFileSync(cssPath, "utf-8");
|
|
420
486
|
if (css.trim().length === 0) {
|
|
421
487
|
warnings.push("style.css is empty");
|
|
422
488
|
}
|
|
@@ -489,6 +555,21 @@ async function validate(dir) {
|
|
|
489
555
|
}
|
|
490
556
|
}
|
|
491
557
|
|
|
558
|
+
// Check for cover image (cover.png or first image in images array)
|
|
559
|
+
if (fs.existsSync(metadataPath)) {
|
|
560
|
+
try {
|
|
561
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
|
|
562
|
+
const hasCoverPng = fs.existsSync(path.join(fullPath, "cover.png"));
|
|
563
|
+
const hasImagesArray = metadata.images && metadata.images.length > 0;
|
|
564
|
+
|
|
565
|
+
if (!hasCoverPng && !hasImagesArray) {
|
|
566
|
+
warnings.push("No cover image found. Add cover.png or images to the images/ folder.");
|
|
567
|
+
}
|
|
568
|
+
} catch (e) {
|
|
569
|
+
// Already reported JSON error above
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
492
573
|
// Print results
|
|
493
574
|
if (errors.length === 0 && warnings.length === 0) {
|
|
494
575
|
console.log(pc.green(" All checks passed!"));
|
|
@@ -523,6 +604,185 @@ async function validate(dir) {
|
|
|
523
604
|
}
|
|
524
605
|
}
|
|
525
606
|
|
|
607
|
+
async function publish(dir) {
|
|
608
|
+
const fullPath = path.resolve(process.cwd(), dir);
|
|
609
|
+
|
|
610
|
+
console.log(pc.dim(`Checking theme for publishing...\n`));
|
|
611
|
+
|
|
612
|
+
// Check directory exists
|
|
613
|
+
if (!fs.existsSync(fullPath)) {
|
|
614
|
+
console.log(pc.red(`Error: Directory "${dir}" does not exist.`));
|
|
615
|
+
process.exit(1);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// 1. Validate theme first (simple check, not full validation)
|
|
619
|
+
const metadataPath = path.join(fullPath, "metadata.json");
|
|
620
|
+
if (!fs.existsSync(metadataPath)) {
|
|
621
|
+
console.log(pc.red("Error: metadata.json not found."));
|
|
622
|
+
console.log(pc.dim("Run validation first: create-bl-theme validate"));
|
|
623
|
+
process.exit(1);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
let metadata;
|
|
627
|
+
try {
|
|
628
|
+
metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
|
|
629
|
+
} catch (e) {
|
|
630
|
+
console.log(pc.red(`Error: Invalid metadata.json - ${e.message}`));
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// 2. Check git repo
|
|
635
|
+
if (!checkIsGitRepo(fullPath)) {
|
|
636
|
+
console.log(pc.red("Not a git repository. Initialize git first:"));
|
|
637
|
+
console.log(pc.dim(" git init"));
|
|
638
|
+
console.log(pc.dim(" git remote add origin https://github.com/username/theme-name.git"));
|
|
639
|
+
process.exit(1);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// 3. Get remote info
|
|
643
|
+
const remote = getGitRemote(fullPath);
|
|
644
|
+
if (!remote) {
|
|
645
|
+
console.log(pc.red("No git remote found. Add a remote:"));
|
|
646
|
+
console.log(pc.dim(" git remote add origin https://github.com/username/theme-name.git"));
|
|
647
|
+
process.exit(1);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// 4. Parse repo from remote
|
|
651
|
+
const repoMatch = remote.match(/github\.com[:/](.+?)(?:\.git)?$/);
|
|
652
|
+
if (!repoMatch) {
|
|
653
|
+
console.log(pc.red("Could not parse GitHub repo from remote:"), remote);
|
|
654
|
+
process.exit(1);
|
|
655
|
+
}
|
|
656
|
+
const repo = repoMatch[1].replace(/\.git$/, "");
|
|
657
|
+
|
|
658
|
+
// 5. Display theme info
|
|
659
|
+
console.log(pc.bold("Theme Info:"));
|
|
660
|
+
console.log(` ID: ${metadata.id}`);
|
|
661
|
+
console.log(` Title: ${metadata.title}`);
|
|
662
|
+
console.log(` Version: ${metadata.version}`);
|
|
663
|
+
console.log(` Repo: ${repo}`);
|
|
664
|
+
console.log();
|
|
665
|
+
|
|
666
|
+
// 6. Check if registered
|
|
667
|
+
const isRegistered = await checkThemeRegistered(repo);
|
|
668
|
+
|
|
669
|
+
if (!isRegistered) {
|
|
670
|
+
console.log(pc.yellow("Theme is not registered in the theme store."));
|
|
671
|
+
console.log();
|
|
672
|
+
console.log(pc.bold("To register your theme:"));
|
|
673
|
+
console.log(" 1. Fork https://github.com/better-lyrics/themes");
|
|
674
|
+
console.log(` 2. Add { "repo": "${repo}" } to index.json`);
|
|
675
|
+
console.log(" 3. Open a pull request");
|
|
676
|
+
console.log();
|
|
677
|
+
console.log("After your PR is merged, install the GitHub App for auto-updates:");
|
|
678
|
+
console.log(pc.cyan(" https://github.com/marketplace/better-lyrics-themes"));
|
|
679
|
+
process.exit(0);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// 7. Theme is registered
|
|
683
|
+
console.log(pc.green("Theme is registered in the theme store."));
|
|
684
|
+
console.log();
|
|
685
|
+
console.log(pc.bold("Auto-publishing Setup:"));
|
|
686
|
+
console.log();
|
|
687
|
+
console.log("To enable automatic updates when you push:");
|
|
688
|
+
console.log(" 1. Install the GitHub App on your repo:");
|
|
689
|
+
console.log(pc.cyan(" https://github.com/marketplace/better-lyrics-themes"));
|
|
690
|
+
console.log();
|
|
691
|
+
console.log(" 2. Push changes to your repo:");
|
|
692
|
+
console.log(pc.dim(" git add ."));
|
|
693
|
+
console.log(pc.dim(' git commit -m "feat: update theme"'));
|
|
694
|
+
console.log(pc.dim(" git push"));
|
|
695
|
+
console.log();
|
|
696
|
+
console.log("The registry will automatically:");
|
|
697
|
+
console.log(" - Validate your theme");
|
|
698
|
+
console.log(" - Update the lockfile");
|
|
699
|
+
console.log(" - Vendor your theme files");
|
|
700
|
+
console.log(" - Show a commit status on your push");
|
|
701
|
+
console.log();
|
|
702
|
+
|
|
703
|
+
// 8. Check current lockfile status
|
|
704
|
+
const lockStatus = await checkLockStatus(repo);
|
|
705
|
+
if (lockStatus) {
|
|
706
|
+
console.log(pc.bold("Current Registry Status:"));
|
|
707
|
+
console.log(` Locked Version: ${lockStatus.version}`);
|
|
708
|
+
console.log(` Locked At: ${lockStatus.locked}`);
|
|
709
|
+
console.log(` Commit: ${lockStatus.commit.slice(0, 7)}`);
|
|
710
|
+
|
|
711
|
+
if (metadata.version === lockStatus.version) {
|
|
712
|
+
console.log();
|
|
713
|
+
console.log(pc.yellow("Your local version matches the registry."));
|
|
714
|
+
console.log("Bump the version in metadata.json to publish an update.");
|
|
715
|
+
} else {
|
|
716
|
+
// Compare versions
|
|
717
|
+
const localParts = metadata.version.split(".").map(Number);
|
|
718
|
+
const remoteParts = lockStatus.version.split(".").map(Number);
|
|
719
|
+
let isGreater = false;
|
|
720
|
+
|
|
721
|
+
for (let i = 0; i < 3; i++) {
|
|
722
|
+
if (localParts[i] > remoteParts[i]) {
|
|
723
|
+
isGreater = true;
|
|
724
|
+
break;
|
|
725
|
+
} else if (localParts[i] < remoteParts[i]) {
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (isGreater) {
|
|
731
|
+
console.log();
|
|
732
|
+
console.log(pc.green(`Ready to publish: ${lockStatus.version} -> ${metadata.version}`));
|
|
733
|
+
console.log("Push to your repo to trigger an update.");
|
|
734
|
+
} else {
|
|
735
|
+
console.log();
|
|
736
|
+
console.log(pc.red(`Version ${metadata.version} is not greater than ${lockStatus.version}`));
|
|
737
|
+
console.log("Versions must increase. Update metadata.json with a higher version.");
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
console.log();
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function checkIsGitRepo(themePath) {
|
|
746
|
+
try {
|
|
747
|
+
execSync("git rev-parse --git-dir", { cwd: themePath, stdio: "ignore" });
|
|
748
|
+
return true;
|
|
749
|
+
} catch {
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function getGitRemote(themePath) {
|
|
755
|
+
try {
|
|
756
|
+
return execSync("git remote get-url origin", { cwd: themePath, encoding: "utf8" }).trim();
|
|
757
|
+
} catch {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function checkThemeRegistered(repo) {
|
|
763
|
+
try {
|
|
764
|
+
const response = await fetch(
|
|
765
|
+
"https://raw.githubusercontent.com/better-lyrics/themes/main/index.json"
|
|
766
|
+
);
|
|
767
|
+
const index = await response.json();
|
|
768
|
+
return index.themes?.some((t) => t.repo === repo) ?? false;
|
|
769
|
+
} catch {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
async function checkLockStatus(repo) {
|
|
775
|
+
try {
|
|
776
|
+
const response = await fetch(
|
|
777
|
+
"https://raw.githubusercontent.com/better-lyrics/themes/main/index.lock.json"
|
|
778
|
+
);
|
|
779
|
+
const lock = await response.json();
|
|
780
|
+
return lock.themes?.find((t) => t.repo === repo) ?? null;
|
|
781
|
+
} catch {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
526
786
|
main().catch((err) => {
|
|
527
787
|
console.error(pc.red(err.message));
|
|
528
788
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-bl-theme",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "CLI tool to scaffold Better Lyrics themes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"image-size": "^2.0.2",
|
|
22
22
|
"picocolors": "^1.1.1",
|
|
23
|
-
"prompts": "^2.4.2"
|
|
23
|
+
"prompts": "^2.4.2",
|
|
24
|
+
"rics": "^0.3.7"
|
|
24
25
|
}
|
|
25
26
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Better Lyrics Theme
|
|
3
|
+
*
|
|
4
|
+
* This file uses RICS - a lightweight CSS preprocessor with full CSS parity.
|
|
5
|
+
* RICS adds variables, nesting, and mixins while staying close to standard CSS.
|
|
6
|
+
* Any valid CSS is also valid RICS, so you can write plain CSS if you prefer.
|
|
7
|
+
*
|
|
8
|
+
* RICS Playground: https://rics.boidu.dev
|
|
9
|
+
* RICS Docs: https://github.com/better-lyrics/rics
|
|
10
|
+
* Styling Guide: https://github.com/better-lyrics/better-lyrics/blob/master/STYLING.md
|
|
11
|
+
*/
|