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 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.css # Your CSS styles (required)
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.css`** - Add your custom styles. Use browser DevTools to inspect Better Lyrics elements and find the right selectors.
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 [better-lyrics-themes](https://github.com/better-lyrics/better-lyrics-themes)
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.css from template
216
- const cssTemplate = fs.readFileSync(
217
- path.join(TEMPLATES_DIR, "style.css"),
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.css"), cssTemplate);
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.css")} with your theme styles`);
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
- ` ${pc.dim("https://github.com/boidushya/better-lyrics-themes")}`
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 stylePath = path.join(fullPath, "style.css");
416
- if (!fs.existsSync(stylePath)) {
417
- errors.push("style.css is missing");
418
- } else {
419
- const css = fs.readFileSync(stylePath, "utf-8");
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.4",
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
+ */
@@ -1,6 +0,0 @@
1
- /*
2
- * Better Lyrics Theme
3
- *
4
- * For available selectors and styling guide, see:
5
- * https://github.com/better-lyrics/better-lyrics/blob/master/STYLING.md
6
- */