bluedither 1.0.1 → 1.0.3
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/extract.js +46 -25
- package/cli/commands/install.js +91 -2
- package/cli/commands/login.js +71 -0
- package/cli/commands/publish.js +109 -16
- package/cli/commands/search.js +64 -0
- package/cli/lib/credentials.js +26 -0
- package/cli/lib/registry.js +58 -0
- package/package.json +4 -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/extract.js
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* bluedither extract [dir] [--validate]
|
|
3
3
|
*
|
|
4
|
-
* Validates theme directory completeness.
|
|
5
|
-
* Full Paper extraction is planned for Phase 5.
|
|
4
|
+
* Validates theme directory completeness using bluedither.config.json paths.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import { readFileSync, existsSync } from 'fs';
|
|
9
|
-
import { resolve
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
|
-
|
|
12
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
import { resolve } from 'path';
|
|
13
9
|
|
|
14
10
|
export default async function extract(args) {
|
|
15
11
|
if (args.includes('--help')) {
|
|
@@ -30,27 +26,52 @@ export default async function extract(args) {
|
|
|
30
26
|
|
|
31
27
|
console.log(`Checking theme package in: ${dir}\n`);
|
|
32
28
|
|
|
29
|
+
// Read config to get actual paths
|
|
30
|
+
const configPath = resolve(dir, 'bluedither.config.json');
|
|
31
|
+
if (!existsSync(configPath)) {
|
|
32
|
+
console.error('✗ No bluedither.config.json found.');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
37
|
+
|
|
33
38
|
const required = [
|
|
34
|
-
{ path:
|
|
35
|
-
{ path:
|
|
36
|
-
{ path:
|
|
37
|
-
{ path:
|
|
38
|
-
{ path:
|
|
39
|
-
{ path:
|
|
40
|
-
{ path: 'theme/shaders/bluedither-shader.js', label: 'Shader module' },
|
|
41
|
-
{ path: 'theme/shaders/paper-shaders-bundle.js', label: 'Shader bundle' },
|
|
42
|
-
{ path: 'skill.md', label: 'Skill instructions' },
|
|
39
|
+
{ path: config.tokens, label: 'Tokens' },
|
|
40
|
+
{ path: config.defaults, label: 'Default tokens' },
|
|
41
|
+
{ path: config.schema, label: 'Token schema' },
|
|
42
|
+
{ path: config.structure, label: 'Structure contract' },
|
|
43
|
+
{ path: config.rules, label: 'Design rules' },
|
|
44
|
+
{ path: config.skill, label: 'Skill instructions' },
|
|
43
45
|
{ path: 'bluedither.config.json', label: 'Package manifest' },
|
|
44
46
|
];
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
// Add shader sources if present
|
|
49
|
+
if (config.shaderSources) {
|
|
50
|
+
for (const s of config.shaderSources) {
|
|
51
|
+
required.push({ path: s, label: `Shader source (${s})` });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Template directory
|
|
56
|
+
for (const tmplPath of ['theme/template/index.html', 'template/index.html']) {
|
|
57
|
+
if (existsSync(resolve(dir, tmplPath))) {
|
|
58
|
+
required.push({ path: tmplPath, label: 'HTML template' });
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const optional = [];
|
|
64
|
+
if (config.tuner) optional.push({ path: config.tuner, label: 'Tuner bundle' });
|
|
65
|
+
if (config.shaders) {
|
|
66
|
+
for (const s of config.shaders) {
|
|
67
|
+
optional.push({ path: s, label: `Shader bundle (${s})` });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (config.generators) {
|
|
71
|
+
for (const gen of ['vanilla.md', 'react.md', 'vue.md', 'svelte.md']) {
|
|
72
|
+
optional.push({ path: `${config.generators}${gen}`, label: `${gen.replace('.md', '')} generator` });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
54
75
|
|
|
55
76
|
let allPresent = true;
|
|
56
77
|
|
|
@@ -70,8 +91,8 @@ export default async function extract(args) {
|
|
|
70
91
|
// Schema validation
|
|
71
92
|
if (doValidate) {
|
|
72
93
|
console.log('\nSchema validation:');
|
|
73
|
-
const schemaPath = resolve(dir,
|
|
74
|
-
const tokensPath = resolve(dir,
|
|
94
|
+
const schemaPath = resolve(dir, config.schema);
|
|
95
|
+
const tokensPath = resolve(dir, config.tokens);
|
|
75
96
|
|
|
76
97
|
if (!existsSync(schemaPath) || !existsSync(tokensPath)) {
|
|
77
98
|
console.log(' ✗ Cannot validate: schema or tokens file missing');
|
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}`);
|
|
@@ -125,3 +137,80 @@ $ARGUMENTS
|
|
|
125
137
|
Or run: npx bluedither tune
|
|
126
138
|
`);
|
|
127
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
|
+
}
|
|
@@ -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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bluedither",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "A bold, dithered-shader hero theme for Claude Code — skill + fine-tuner",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
],
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@paper-design/shaders": "^0.0.71"
|
|
34
|
+
"@paper-design/shaders": "^0.0.71",
|
|
35
|
+
"archiver": "^7.0.1",
|
|
36
|
+
"jszip": "^3.10.1"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
37
39
|
"ajv": "^8.18.0",
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./tokens.schema.json",
|
|
3
3
|
"meta": {
|
|
4
|
-
"name": "
|
|
4
|
+
"name": "Understanding Moon",
|
|
5
5
|
"version": "1.0.0",
|
|
6
|
-
"description": "A
|
|
6
|
+
"description": "A deep navy theme with teal dithered ripple shader evoking lunar surfaces"
|
|
7
7
|
},
|
|
8
8
|
"layout": {
|
|
9
9
|
"designWidth": 1364
|
|
10
10
|
},
|
|
11
11
|
"colors": {
|
|
12
12
|
"background": "#040037",
|
|
13
|
-
"primary": "#
|
|
13
|
+
"primary": "#005A6A",
|
|
14
14
|
"text": "#FFFFFF",
|
|
15
15
|
"ctaBackground": "#FFFFFF",
|
|
16
16
|
"ctaText": "#14161C",
|
|
17
|
-
"shaderFront": "#
|
|
17
|
+
"shaderFront": "#005A6A",
|
|
18
18
|
"shaderBack": "#00000000"
|
|
19
19
|
},
|
|
20
20
|
"typography": {
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"shader": {
|
|
59
59
|
"speed": 0.41,
|
|
60
|
-
"shape": "
|
|
60
|
+
"shape": "ripple",
|
|
61
61
|
"type": "2x2",
|
|
62
62
|
"size": 4.2,
|
|
63
63
|
"scale": 1,
|
package/theme/tokens.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./tokens.schema.json",
|
|
3
3
|
"meta": {
|
|
4
|
-
"name": "
|
|
4
|
+
"name": "Understanding Moon",
|
|
5
5
|
"version": "1.0.0",
|
|
6
|
-
"description": "A
|
|
6
|
+
"description": "A deep navy theme with teal dithered ripple shader evoking lunar surfaces"
|
|
7
7
|
},
|
|
8
8
|
"layout": {
|
|
9
9
|
"designWidth": 1364
|
|
10
10
|
},
|
|
11
11
|
"colors": {
|
|
12
12
|
"background": "#040037",
|
|
13
|
-
"primary": "#
|
|
13
|
+
"primary": "#005A6A",
|
|
14
14
|
"text": "#FFFFFF",
|
|
15
15
|
"ctaBackground": "#FFFFFF",
|
|
16
16
|
"ctaText": "#14161C",
|
|
17
|
-
"shaderFront": "#
|
|
17
|
+
"shaderFront": "#005A6A",
|
|
18
18
|
"shaderBack": "#00000000"
|
|
19
19
|
},
|
|
20
20
|
"typography": {
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"shader": {
|
|
59
59
|
"speed": 0.41,
|
|
60
|
-
"shape": "
|
|
60
|
+
"shape": "ripple",
|
|
61
61
|
"type": "2x2",
|
|
62
62
|
"size": 4.2,
|
|
63
63
|
"scale": 1,
|