bluedither 1.0.0 → 1.0.2
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/cli/bin.js +5 -1
- package/cli/commands/install.js +92 -2
- package/cli/commands/login.js +71 -0
- package/cli/commands/publish.js +109 -16
- package/cli/commands/search.js +64 -0
- package/cli/commands/tune.js +47 -27
- package/cli/lib/credentials.js +26 -0
- package/cli/lib/registry.js +58 -0
- package/fine-tuner/server.js +283 -0
- package/fine-tuner/tuner.css +362 -0
- package/fine-tuner/tuner.js +445 -0
- package/package.json +5 -2
- package/theme/tokens.defaults.json +5 -5
- package/theme/tokens.json +5 -5
package/cli/bin.js
CHANGED
|
@@ -21,7 +21,9 @@ const HELP = `
|
|
|
21
21
|
bluedither install [target-dir] Install theme into a project
|
|
22
22
|
bluedither tune [--port N] Launch fine-tuner dev server
|
|
23
23
|
bluedither extract [dir] [--validate] Validate theme package completeness
|
|
24
|
-
bluedither publish Validate and
|
|
24
|
+
bluedither publish Validate and publish to marketplace
|
|
25
|
+
bluedither login Authenticate with marketplace
|
|
26
|
+
bluedither search <query> Search marketplace for themes
|
|
25
27
|
|
|
26
28
|
Options:
|
|
27
29
|
--help Show help for a command
|
|
@@ -48,6 +50,8 @@ const commands = {
|
|
|
48
50
|
tune: () => import('./commands/tune.js'),
|
|
49
51
|
extract: () => import('./commands/extract.js'),
|
|
50
52
|
publish: () => import('./commands/publish.js'),
|
|
53
|
+
login: () => import('./commands/login.js'),
|
|
54
|
+
search: () => import('./commands/search.js'),
|
|
51
55
|
};
|
|
52
56
|
|
|
53
57
|
if (!commands[command]) {
|
package/cli/commands/install.js
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { readFileSync, writeFileSync, copyFileSync, mkdirSync, existsSync } from 'fs';
|
|
11
|
-
import { resolve, dirname, basename } from 'path';
|
|
11
|
+
import { resolve, dirname, basename, join } from 'path';
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
|
+
import { getDownloadUrl } from '../lib/registry.js';
|
|
13
14
|
|
|
14
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
16
|
const BLUEDITHER_ROOT = resolve(__dirname, '..', '..');
|
|
@@ -44,7 +45,18 @@ export default async function install(args) {
|
|
|
44
45
|
return;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
// Check if first arg is a registry reference (@designer/theme-name)
|
|
49
|
+
const firstArg = args[0] || '.';
|
|
50
|
+
const registryMatch = firstArg.match(/^@([^/]+)\/(.+)$/);
|
|
51
|
+
|
|
52
|
+
if (registryMatch) {
|
|
53
|
+
const slug = registryMatch[2];
|
|
54
|
+
const targetDir = resolve(args[1] || '.');
|
|
55
|
+
await installFromRegistry(slug, targetDir);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const targetDir = resolve(firstArg);
|
|
48
60
|
|
|
49
61
|
if (!existsSync(targetDir)) {
|
|
50
62
|
console.error(`Target directory does not exist: ${targetDir}`);
|
|
@@ -66,6 +78,7 @@ export default async function install(args) {
|
|
|
66
78
|
['theme/tokens.schema.json', 'tokens.schema.json'],
|
|
67
79
|
['theme/structure.json', 'structure.json'],
|
|
68
80
|
['theme/rules.md', 'rules.md'],
|
|
81
|
+
['theme/template/index.html', 'template/index.html'],
|
|
69
82
|
['theme/shaders/bluedither-shader.js', 'shaders/bluedither-shader.js'],
|
|
70
83
|
['theme/shaders/paper-shaders-bundle.js', 'shaders/paper-shaders-bundle.js'],
|
|
71
84
|
['skill.md', 'skill.md'],
|
|
@@ -124,3 +137,80 @@ $ARGUMENTS
|
|
|
124
137
|
Or run: npx bluedither tune
|
|
125
138
|
`);
|
|
126
139
|
}
|
|
140
|
+
|
|
141
|
+
async function installFromRegistry(slug, targetDir) {
|
|
142
|
+
console.log(`\n Installing theme "${slug}" from marketplace...\n`);
|
|
143
|
+
|
|
144
|
+
if (!existsSync(targetDir)) {
|
|
145
|
+
console.error(`Target directory does not exist: ${targetDir}`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Get download URL from marketplace
|
|
150
|
+
let downloadUrl;
|
|
151
|
+
try {
|
|
152
|
+
downloadUrl = await getDownloadUrl(slug);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.error(` ✗ ${err.message}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Download ZIP
|
|
159
|
+
console.log(' Downloading theme package...');
|
|
160
|
+
const response = await fetch(downloadUrl);
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
console.error(' ✗ Download failed.');
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
166
|
+
|
|
167
|
+
// Extract ZIP
|
|
168
|
+
console.log(' Extracting...');
|
|
169
|
+
const JSZip = (await import('jszip')).default;
|
|
170
|
+
const zip = await JSZip.loadAsync(buffer);
|
|
171
|
+
|
|
172
|
+
const bdDir = resolve(targetDir, 'bluedither');
|
|
173
|
+
mkdirSync(bdDir, { recursive: true });
|
|
174
|
+
|
|
175
|
+
let extracted = 0;
|
|
176
|
+
for (const [path, entry] of Object.entries(zip.files)) {
|
|
177
|
+
if (entry.dir) {
|
|
178
|
+
mkdirSync(resolve(bdDir, path), { recursive: true });
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const content = await entry.async('nodebuffer');
|
|
182
|
+
const destPath = resolve(bdDir, path);
|
|
183
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
184
|
+
writeFileSync(destPath, content);
|
|
185
|
+
extracted++;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log(` Extracted ${extracted} files to ${bdDir}`);
|
|
189
|
+
|
|
190
|
+
// Detect framework and create Claude Code command
|
|
191
|
+
const { framework, typescript } = detectFramework(targetDir);
|
|
192
|
+
console.log(` Detected framework: ${framework}${typescript ? ' + TypeScript' : ''}`);
|
|
193
|
+
|
|
194
|
+
const claudeDir = resolve(targetDir, '.claude', 'commands');
|
|
195
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
196
|
+
|
|
197
|
+
const commandContent = `---
|
|
198
|
+
description: Apply BlueDither theme to this project
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
Read the skill instructions at bluedither/skill.md and follow them to generate the BlueDither theme for this project.
|
|
202
|
+
|
|
203
|
+
The theme files are in the bluedither/ directory. The target framework is ${framework}${typescript ? ' with TypeScript' : ''}.
|
|
204
|
+
|
|
205
|
+
$ARGUMENTS
|
|
206
|
+
`;
|
|
207
|
+
|
|
208
|
+
writeFileSync(resolve(claudeDir, 'apply-theme.md'), commandContent);
|
|
209
|
+
|
|
210
|
+
console.log(`
|
|
211
|
+
✓ Theme "${slug}" installed!
|
|
212
|
+
|
|
213
|
+
To apply the theme, use Claude Code:
|
|
214
|
+
/apply-theme [description of your site]
|
|
215
|
+
`);
|
|
216
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bluedither login
|
|
3
|
+
*
|
|
4
|
+
* Authenticates via the BlueDither marketplace.
|
|
5
|
+
* Opens browser for OAuth, receives token via local callback server.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createServer } from 'http';
|
|
9
|
+
import { saveCredentials } from '../lib/credentials.js';
|
|
10
|
+
import { REGISTRY_URL } from '../lib/registry.js';
|
|
11
|
+
|
|
12
|
+
export default async function login(args) {
|
|
13
|
+
if (args.includes('--help')) {
|
|
14
|
+
console.log(`
|
|
15
|
+
bluedither login
|
|
16
|
+
|
|
17
|
+
Authenticate with the BlueDither marketplace.
|
|
18
|
+
Opens your browser for GitHub sign-in and saves credentials locally.
|
|
19
|
+
`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return new Promise((resolvePromise, reject) => {
|
|
24
|
+
const server = createServer(async (req, res) => {
|
|
25
|
+
if (req.method === 'POST' && req.url === '/callback') {
|
|
26
|
+
let body = '';
|
|
27
|
+
req.on('data', chunk => { body += chunk; });
|
|
28
|
+
req.on('end', () => {
|
|
29
|
+
try {
|
|
30
|
+
const creds = JSON.parse(body);
|
|
31
|
+
saveCredentials(creds);
|
|
32
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
33
|
+
res.end('{"ok":true}');
|
|
34
|
+
console.log('\n ✓ Logged in successfully!\n');
|
|
35
|
+
server.close();
|
|
36
|
+
resolvePromise();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
res.writeHead(400);
|
|
39
|
+
res.end();
|
|
40
|
+
reject(err);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
} else {
|
|
44
|
+
res.writeHead(404);
|
|
45
|
+
res.end();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
server.listen(0, () => {
|
|
50
|
+
const port = server.address().port;
|
|
51
|
+
const loginUrl = `${REGISTRY_URL}/login?cli_port=${port}`;
|
|
52
|
+
|
|
53
|
+
console.log(`\n Opening browser for authentication...`);
|
|
54
|
+
console.log(` If it doesn't open, visit: ${loginUrl}\n`);
|
|
55
|
+
|
|
56
|
+
// Open browser
|
|
57
|
+
import('child_process').then(({ exec }) => {
|
|
58
|
+
const cmd = process.platform === 'win32' ? 'start' :
|
|
59
|
+
process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
60
|
+
exec(`${cmd} "${loginUrl}"`);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Timeout after 5 minutes
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
console.error('\n ✗ Login timed out.\n');
|
|
66
|
+
server.close();
|
|
67
|
+
reject(new Error('Login timed out'));
|
|
68
|
+
}, 300_000);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
package/cli/commands/publish.js
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* bluedither publish
|
|
3
3
|
*
|
|
4
|
-
* Validates the theme package and
|
|
5
|
-
* Full marketplace upload will be added in Phase 6.
|
|
4
|
+
* Validates the theme package and publishes to the BlueDither marketplace.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
import { existsSync, readFileSync } from 'fs';
|
|
7
|
+
import { existsSync, readFileSync, createReadStream } from 'fs';
|
|
9
8
|
import { resolve } from 'path';
|
|
10
9
|
import { execSync } from 'child_process';
|
|
10
|
+
import { getCredentials } from '../lib/credentials.js';
|
|
11
|
+
import { uploadPackage, publishTheme, REGISTRY_URL } from '../lib/registry.js';
|
|
11
12
|
|
|
12
13
|
export default async function publish(args) {
|
|
13
14
|
if (args.includes('--help')) {
|
|
14
15
|
console.log(`
|
|
15
16
|
bluedither publish
|
|
16
17
|
|
|
17
|
-
Validates
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Marketplace upload will be available in a future version.
|
|
18
|
+
Validates, builds, and publishes your theme to the BlueDither marketplace.
|
|
19
|
+
If you're not logged in, a browser window will open for authentication.
|
|
21
20
|
`);
|
|
22
21
|
return;
|
|
23
22
|
}
|
|
@@ -34,13 +33,18 @@ export default async function publish(args) {
|
|
|
34
33
|
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
35
34
|
console.log(`Publishing: ${config.name}@${config.version}\n`);
|
|
36
35
|
|
|
37
|
-
// Step 1: Build
|
|
36
|
+
// Step 1: Build BlueDither assets (shader + tuner bundles)
|
|
38
37
|
console.log('Step 1: Building...');
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
const buildScript = resolve(dir, 'scripts/build.js');
|
|
39
|
+
if (existsSync(buildScript)) {
|
|
40
|
+
try {
|
|
41
|
+
execSync(`node "${buildScript}"`, { cwd: dir, stdio: 'inherit' });
|
|
42
|
+
} catch {
|
|
43
|
+
console.error('\n✗ Build failed. Fix errors before publishing.');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
console.log(' No build script found — skipping build step.');
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
// Step 2: Validate package completeness
|
|
@@ -76,10 +80,99 @@ export default async function publish(args) {
|
|
|
76
80
|
process.exit(1);
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
console.log('\n✓ Validation passed.\n');
|
|
84
|
+
|
|
85
|
+
// Step 4: Authenticate
|
|
86
|
+
let creds = getCredentials();
|
|
87
|
+
if (!creds?.access_token) {
|
|
88
|
+
console.log('\nStep 4: Authenticating...');
|
|
89
|
+
const { default: login } = await import('./login.js');
|
|
90
|
+
await login([]);
|
|
91
|
+
creds = getCredentials();
|
|
92
|
+
if (!creds?.access_token) {
|
|
93
|
+
console.error('\n✗ Login failed. Cannot publish without authentication.');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log('\nStep 5: Creating package ZIP...');
|
|
99
|
+
const JSZip = (await import('jszip')).default;
|
|
100
|
+
const { readdirSync, statSync } = await import('fs');
|
|
101
|
+
const zip = new JSZip();
|
|
102
|
+
|
|
103
|
+
// Collect files to include
|
|
104
|
+
const includeFiles = [
|
|
105
|
+
config.tokens, config.defaults, config.schema,
|
|
106
|
+
config.structure, config.rules, config.skill,
|
|
107
|
+
'bluedither.config.json',
|
|
108
|
+
];
|
|
109
|
+
if (config.tuner && existsSync(resolve(dir, config.tuner))) includeFiles.push(config.tuner);
|
|
110
|
+
if (config.shaders) includeFiles.push(...config.shaders);
|
|
111
|
+
if (config.shaderSources) includeFiles.push(...config.shaderSources);
|
|
112
|
+
|
|
113
|
+
function addDirToZip(dirPath, zipPath) {
|
|
114
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
const full = resolve(dirPath, entry.name);
|
|
117
|
+
const zp = zipPath ? `${zipPath}/${entry.name}` : entry.name;
|
|
118
|
+
if (entry.isDirectory()) {
|
|
119
|
+
addDirToZip(full, zp);
|
|
120
|
+
} else {
|
|
121
|
+
zip.file(zp, readFileSync(full));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const f of includeFiles) {
|
|
127
|
+
const full = resolve(dir, f);
|
|
128
|
+
if (!existsSync(full)) continue;
|
|
129
|
+
if (statSync(full).isDirectory()) {
|
|
130
|
+
addDirToZip(full, f);
|
|
131
|
+
} else {
|
|
132
|
+
zip.file(f, readFileSync(full));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Include generators directory
|
|
137
|
+
if (config.generators && existsSync(resolve(dir, config.generators))) {
|
|
138
|
+
addDirToZip(resolve(dir, config.generators), config.generators);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Include template directory (check both source and installed layouts)
|
|
142
|
+
for (const tmplPath of ['theme/template', 'template']) {
|
|
143
|
+
const templateDir = resolve(dir, tmplPath);
|
|
144
|
+
if (existsSync(templateDir)) {
|
|
145
|
+
addDirToZip(templateDir, tmplPath);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const zipBuffer = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
151
|
+
const slug = config.name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
152
|
+
|
|
153
|
+
console.log(` ZIP size: ${(zipBuffer.length / 1024).toFixed(1)} KB`);
|
|
154
|
+
|
|
155
|
+
console.log('\nStep 6: Uploading to marketplace...');
|
|
156
|
+
const zipBlob = new Blob([zipBuffer], { type: 'application/zip' });
|
|
157
|
+
const zipFile = new File([zipBlob], `${slug}.zip`, { type: 'application/zip' });
|
|
158
|
+
|
|
159
|
+
const { path: packagePath } = await uploadPackage(zipFile, slug, creds.access_token);
|
|
160
|
+
|
|
161
|
+
console.log('Step 7: Publishing theme...');
|
|
162
|
+
const tokens = JSON.parse(readFileSync(resolve(dir, config.tokens), 'utf-8'));
|
|
163
|
+
|
|
164
|
+
const theme = await publishTheme({
|
|
165
|
+
name: config.name,
|
|
166
|
+
slug,
|
|
167
|
+
description: tokens.content?.subHeadline || '',
|
|
168
|
+
tokens_json: tokens,
|
|
169
|
+
package_path: packagePath,
|
|
170
|
+
}, creds.access_token);
|
|
171
|
+
|
|
79
172
|
console.log(`
|
|
80
|
-
✓
|
|
173
|
+
✓ Published to marketplace!
|
|
81
174
|
|
|
82
|
-
|
|
83
|
-
|
|
175
|
+
View: ${REGISTRY_URL}/themes/${slug}
|
|
176
|
+
Install: npx bluedither install @${slug}
|
|
84
177
|
`);
|
|
85
178
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bluedither search <query>
|
|
3
|
+
*
|
|
4
|
+
* Searches the BlueDither marketplace for themes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { searchThemes } from '../lib/registry.js';
|
|
8
|
+
|
|
9
|
+
export default async function search(args) {
|
|
10
|
+
if (args.includes('--help') || args.length === 0) {
|
|
11
|
+
console.log(`
|
|
12
|
+
bluedither search <query>
|
|
13
|
+
|
|
14
|
+
Search the BlueDither marketplace for themes.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
bluedither search "dark minimal"
|
|
18
|
+
`);
|
|
19
|
+
if (args.length === 0 && !args.includes('--help')) {
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const query = args.join(' ');
|
|
26
|
+
console.log(`\n Searching for "${query}"...\n`);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const themes = await searchThemes(query);
|
|
30
|
+
|
|
31
|
+
if (themes.length === 0) {
|
|
32
|
+
console.log(' No themes found.\n');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Table header
|
|
37
|
+
const nameW = 24, designerW = 18, dlW = 10;
|
|
38
|
+
console.log(
|
|
39
|
+
' ' +
|
|
40
|
+
'Name'.padEnd(nameW) +
|
|
41
|
+
'Designer'.padEnd(designerW) +
|
|
42
|
+
'Downloads'.padEnd(dlW) +
|
|
43
|
+
'Install command'
|
|
44
|
+
);
|
|
45
|
+
console.log(' ' + '-'.repeat(nameW + designerW + dlW + 30));
|
|
46
|
+
|
|
47
|
+
for (const t of themes) {
|
|
48
|
+
const designer = t.profiles?.display_name || 'unknown';
|
|
49
|
+
const slug = t.slug;
|
|
50
|
+
const handle = designer.toLowerCase().replace(/\s+/g, '-');
|
|
51
|
+
console.log(
|
|
52
|
+
' ' +
|
|
53
|
+
t.name.slice(0, nameW - 2).padEnd(nameW) +
|
|
54
|
+
designer.slice(0, designerW - 2).padEnd(designerW) +
|
|
55
|
+
String(t.download_count || 0).padEnd(dlW) +
|
|
56
|
+
`npx bluedither install @${handle}/${slug}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
console.log();
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error(` ✗ ${err.message}\n`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
package/cli/commands/tune.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* bluedither tune [--port N]
|
|
3
3
|
*
|
|
4
4
|
* Launches the fine-tuner dev server.
|
|
5
|
-
* Finds the nearest bluedither
|
|
5
|
+
* Finds the nearest bluedither theme directory — either:
|
|
6
|
+
* - Source repo layout: ./theme/tokens.json + ./fine-tuner/server.js
|
|
7
|
+
* - Installed layout: ./bluedither/tokens.json (from `npx bluedither install`)
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import { existsSync } from 'fs';
|
|
@@ -11,16 +13,7 @@ import { fileURLToPath } from 'url';
|
|
|
11
13
|
import { spawn } from 'child_process';
|
|
12
14
|
|
|
13
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
-
|
|
15
|
-
function findThemeRoot(startDir) {
|
|
16
|
-
let dir = startDir;
|
|
17
|
-
while (dir !== dirname(dir)) {
|
|
18
|
-
if (existsSync(resolve(dir, 'bluedither.config.json'))) return dir;
|
|
19
|
-
if (existsSync(resolve(dir, 'theme', 'tokens.json'))) return dir;
|
|
20
|
-
dir = dirname(dir);
|
|
21
|
-
}
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
16
|
+
const PKG_ROOT = resolve(__dirname, '..', '..');
|
|
24
17
|
|
|
25
18
|
export default async function tune(args) {
|
|
26
19
|
if (args.includes('--help')) {
|
|
@@ -28,7 +21,7 @@ export default async function tune(args) {
|
|
|
28
21
|
bluedither tune [--port N]
|
|
29
22
|
|
|
30
23
|
Launches the fine-tuner dev server for real-time token editing.
|
|
31
|
-
Searches
|
|
24
|
+
Searches for a BlueDither theme in the current directory.
|
|
32
25
|
|
|
33
26
|
Options:
|
|
34
27
|
--port N Port number (default: 3333)
|
|
@@ -36,28 +29,55 @@ export default async function tune(args) {
|
|
|
36
29
|
return;
|
|
37
30
|
}
|
|
38
31
|
|
|
39
|
-
const
|
|
32
|
+
const cwd = process.cwd();
|
|
33
|
+
|
|
34
|
+
// Determine where the theme files live
|
|
35
|
+
let themeDir = null;
|
|
36
|
+
let tokensPath = null;
|
|
40
37
|
|
|
41
|
-
|
|
38
|
+
// Case 1: Source repo layout (theme/ at cwd root)
|
|
39
|
+
if (existsSync(resolve(cwd, 'theme', 'tokens.json'))) {
|
|
40
|
+
themeDir = resolve(cwd, 'theme');
|
|
41
|
+
tokensPath = resolve(themeDir, 'tokens.json');
|
|
42
|
+
}
|
|
43
|
+
// Case 2: Installed layout (bluedither/ subfolder from `npx bluedither install`)
|
|
44
|
+
else if (existsSync(resolve(cwd, 'bluedither', 'tokens.json'))) {
|
|
45
|
+
themeDir = resolve(cwd, 'bluedither');
|
|
46
|
+
tokensPath = resolve(themeDir, 'tokens.json');
|
|
47
|
+
}
|
|
48
|
+
// Case 3: Search upward
|
|
49
|
+
else {
|
|
50
|
+
let dir = cwd;
|
|
51
|
+
while (dir !== dirname(dir)) {
|
|
52
|
+
if (existsSync(resolve(dir, 'theme', 'tokens.json'))) {
|
|
53
|
+
themeDir = resolve(dir, 'theme');
|
|
54
|
+
tokensPath = resolve(themeDir, 'tokens.json');
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
if (existsSync(resolve(dir, 'bluedither', 'tokens.json'))) {
|
|
58
|
+
themeDir = resolve(dir, 'bluedither');
|
|
59
|
+
tokensPath = resolve(themeDir, 'tokens.json');
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
dir = dirname(dir);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!themeDir) {
|
|
42
67
|
console.error('Could not find a BlueDither theme directory.');
|
|
43
|
-
console.error('Run this from a
|
|
68
|
+
console.error('Run this from a project with bluedither/ or theme/ containing tokens.json.');
|
|
44
69
|
process.exit(1);
|
|
45
70
|
}
|
|
46
71
|
|
|
47
|
-
|
|
48
|
-
|
|
72
|
+
// The fine-tuner server lives in the npm package
|
|
73
|
+
const serverPath = resolve(PKG_ROOT, 'fine-tuner', 'server.js');
|
|
49
74
|
if (!existsSync(serverPath)) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (!existsSync(sourceServer)) {
|
|
53
|
-
console.error('Fine-tuner server not found.');
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
56
|
-
const child = spawn('node', [sourceServer, ...args], { cwd: themeRoot, stdio: 'inherit' });
|
|
57
|
-
child.on('exit', (code) => process.exit(code || 0));
|
|
58
|
-
return;
|
|
75
|
+
console.error('Fine-tuner server not found at: ' + serverPath);
|
|
76
|
+
process.exit(1);
|
|
59
77
|
}
|
|
60
78
|
|
|
61
|
-
|
|
79
|
+
// Pass the theme directory to the server via env var
|
|
80
|
+
const env = { ...process.env, BD_THEME_DIR: themeDir };
|
|
81
|
+
const child = spawn('node', [serverPath, ...args], { cwd: dirname(themeDir), env, stdio: 'inherit' });
|
|
62
82
|
child.on('exit', (code) => process.exit(code || 0));
|
|
63
83
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
|
|
5
|
+
const CRED_DIR = resolve(homedir(), '.bluedither');
|
|
6
|
+
const CRED_FILE = resolve(CRED_DIR, 'credentials.json');
|
|
7
|
+
|
|
8
|
+
export function getCredentials() {
|
|
9
|
+
if (!existsSync(CRED_FILE)) return null;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(readFileSync(CRED_FILE, 'utf-8'));
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function saveCredentials(creds) {
|
|
18
|
+
mkdirSync(CRED_DIR, { recursive: true });
|
|
19
|
+
writeFileSync(CRED_FILE, JSON.stringify(creds, null, 2));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function clearCredentials() {
|
|
23
|
+
if (existsSync(CRED_FILE)) {
|
|
24
|
+
writeFileSync(CRED_FILE, '{}');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const REGISTRY_URL = process.env.BLUEDITHER_REGISTRY_URL || 'https://marketplace-five-flame.vercel.app';
|
|
2
|
+
|
|
3
|
+
export async function searchThemes(query) {
|
|
4
|
+
const url = `${REGISTRY_URL}/api/themes?q=${encodeURIComponent(query)}`;
|
|
5
|
+
const res = await fetch(url);
|
|
6
|
+
if (!res.ok) throw new Error(`Search failed: ${res.statusText}`);
|
|
7
|
+
return res.json();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function getTheme(slug) {
|
|
11
|
+
const url = `${REGISTRY_URL}/api/themes/${encodeURIComponent(slug)}`;
|
|
12
|
+
const res = await fetch(url);
|
|
13
|
+
if (!res.ok) throw new Error(`Theme not found: ${slug}`);
|
|
14
|
+
return res.json();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function getDownloadUrl(slug) {
|
|
18
|
+
const url = `${REGISTRY_URL}/api/themes/${encodeURIComponent(slug)}/download`;
|
|
19
|
+
const res = await fetch(url);
|
|
20
|
+
if (!res.ok) throw new Error(`Download failed: ${res.statusText}`);
|
|
21
|
+
const data = await res.json();
|
|
22
|
+
return data.url;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function uploadPackage(file, slug, accessToken) {
|
|
26
|
+
const formData = new FormData();
|
|
27
|
+
formData.append('package', file);
|
|
28
|
+
formData.append('slug', slug);
|
|
29
|
+
|
|
30
|
+
const res = await fetch(`${REGISTRY_URL}/api/upload`, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
33
|
+
body: formData,
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const err = await res.json().catch(() => ({}));
|
|
37
|
+
throw new Error(err.error || `Upload failed: ${res.statusText}`);
|
|
38
|
+
}
|
|
39
|
+
return res.json();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function publishTheme(metadata, accessToken) {
|
|
43
|
+
const res = await fetch(`${REGISTRY_URL}/api/themes`, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: {
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
Authorization: `Bearer ${accessToken}`,
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify(metadata),
|
|
50
|
+
});
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
const err = await res.json().catch(() => ({}));
|
|
53
|
+
throw new Error(err.error || `Publish failed: ${res.statusText}`);
|
|
54
|
+
}
|
|
55
|
+
return res.json();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { REGISTRY_URL };
|