nobadfonts-cli 1.0.0

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/PUBLISHING.md ADDED
@@ -0,0 +1,34 @@
1
+ # How to Publish NoBadFonts CLI
2
+
3
+ You have the source code for the CLI tool in this folder. To make it available to everyone via `npx nobadfonts`, you need to publish it to npm.
4
+
5
+ ## Prerequisites
6
+
7
+ 1. Create an account on [npmjs.com](https://www.npmjs.com/).
8
+ 2. Login to npm in your terminal:
9
+ ```bash
10
+ npm login
11
+ ```
12
+
13
+ ## publishing
14
+
15
+ 1. Navigate to this folder:
16
+ ```bash
17
+ cd nobadfonts-cli
18
+ ```
19
+ 2. Update the `package.json` name if `nobadfonts-cli` is taken (it might be!). heavily recommended to scope it e.g. `@yourusername/nobadfonts` or just `nobadfonts-official`.
20
+ - If you change the name, update the `bin` command in `package.json` if you want the command to differ.
21
+ 3. Publish:
22
+ ```bash
23
+ npm publish --access public
24
+ ```
25
+
26
+ ## Testing Locally (Without Publishing)
27
+
28
+ You can test the CLI globally on your machine:
29
+
30
+ ```bash
31
+ npm link
32
+ ```
33
+
34
+ Now you can run `nobadfonts` anywhere in your terminal.
package/index.js ADDED
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fetch from 'node-fetch';
4
+ import fs from 'fs-extra';
5
+ import path from 'path';
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ const SUPABASE_URL = 'https://wcegdxhvgwbeskaidlxr.supabase.co';
11
+ const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6IndjZWdkeGh2Z3diZXNrYWlkbHhyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njk1MjI3OTQsImV4cCI6MjA4NTA5ODc5NH0.P_JY0RF6wVdPCDfWLlcor5l1CP3g4bLE5y4JWmZVOig';
12
+
13
+ const args = process.argv.slice(2);
14
+ const command = args[0];
15
+ const targetFont = args[1];
16
+
17
+ if (!command) {
18
+ console.log(chalk.yellow('Usage: npx nobadfonts add <font-name>'));
19
+ process.exit(1);
20
+ }
21
+
22
+ if (command === 'add') {
23
+ if (!targetFont) {
24
+ console.log(chalk.red('Please specify a font name. Example: npx nobadfonts add "Inter"'));
25
+ process.exit(1);
26
+ }
27
+ addFont(targetFont);
28
+ } else if (command === 'list') {
29
+ listFonts();
30
+ } else {
31
+ console.log(chalk.red(`Unknown command: ${command}`));
32
+ console.log(chalk.yellow('Available commands: add <font>, list'));
33
+ process.exit(1);
34
+ }
35
+
36
+ async function listFonts() {
37
+ const spinner = ora('Fetching available fonts...').start();
38
+ try {
39
+ const res = await fetch(`${SUPABASE_URL}/rest/v1/fonts?select=name,category&limit=50&order=downloads.desc`, {
40
+ headers: {
41
+ 'apikey': SUPABASE_ANON_KEY,
42
+ 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`
43
+ }
44
+ });
45
+ if (!res.ok) throw new Error(res.statusText);
46
+ const data = await res.json();
47
+ spinner.stop();
48
+
49
+ console.log(chalk.bold.cyan('\nAvailable Fonts (Top 50):'));
50
+ data.forEach(f => {
51
+ console.log(`${chalk.green('•')} ${chalk.white(f.name)} ${chalk.gray(`(${f.category})`)}`);
52
+ });
53
+ console.log('\nTo install: ' + chalk.yellow('npx nobadfonts add "Font Name"'));
54
+
55
+ } catch (e) {
56
+ spinner.fail('Error fetching fonts: ' + e.message);
57
+ }
58
+ }
59
+
60
+ async function addFont(fontName) {
61
+ const spinner = ora(`Searching for font: ${fontName}...`).start();
62
+
63
+ try {
64
+ // Search for font by name or slug
65
+ const searchUrl = `${SUPABASE_URL}/rest/v1/fonts?or=(name.ilike.*${fontName}*,slug.ilike.*${fontName}*)&select=*,font_variants(*)&limit=1`;
66
+
67
+ const response = await fetch(searchUrl, {
68
+ headers: {
69
+ 'apikey': SUPABASE_ANON_KEY,
70
+ 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`
71
+ }
72
+ });
73
+
74
+ if (!response.ok) {
75
+ throw new Error(`Failed to fetch font data: ${response.statusText}`);
76
+ }
77
+
78
+ const data = await response.json();
79
+
80
+ if (!data || data.length === 0) {
81
+ spinner.fail(chalk.red(`Font "${fontName}" not found.`));
82
+ return;
83
+ }
84
+
85
+ const font = data[0];
86
+ spinner.succeed(chalk.green(`Found font: ${font.name}`));
87
+
88
+ // download files
89
+ const downloadDir = path.join(process.cwd(), 'public', 'fonts', font.slug);
90
+ await fs.ensureDir(downloadDir);
91
+
92
+ const cssContent = [];
93
+ const cssPath = path.join(process.cwd(), 'src', 'index.css'); // Default assumption, maybe configure later? Or creates dedicated file.
94
+ let imports = '';
95
+
96
+ // Function to download file
97
+ const downloadFile = async (url, filename) => {
98
+ if (!url) return null;
99
+ try {
100
+ const res = await fetch(url);
101
+ if (!res.ok) throw new Error(`Failed to download ${url}`);
102
+ const buffer = await res.buffer();
103
+ const filePath = path.join(downloadDir, filename);
104
+ await fs.writeFile(filePath, buffer);
105
+ return filePath;
106
+ } catch (e) {
107
+ console.error(chalk.red(`Error downloading ${filename}: ${e.message}`));
108
+ return null;
109
+ }
110
+ };
111
+
112
+ spinner.start('Downloading font files...');
113
+
114
+ // Download Main Font
115
+ // We prefer woff2 -> woff -> ttf -> otf
116
+ const mainFiles = [
117
+ { url: font.woff2_url, name: `${font.slug}.woff2`, format: 'woff2' },
118
+ { url: font.woff_url, name: `${font.slug}.woff`, format: 'woff' },
119
+ { url: font.ttf_url, name: `${font.slug}.ttf`, format: 'truetype' },
120
+ { url: font.otf_url, name: `${font.slug}.otf`, format: 'opentype' }
121
+ ];
122
+
123
+ let mainFileDownloaded = false;
124
+ let mainCssSrc = [];
125
+
126
+ // Prioritize formats for main font
127
+ for (const f of mainFiles) {
128
+ if (f.url) {
129
+ await downloadFile(f.url, f.name);
130
+ mainCssSrc.push(`url('/fonts/${font.slug}/${f.name}') format('${f.format}')`);
131
+ mainFileDownloaded = true;
132
+ }
133
+ }
134
+
135
+ if (mainFileDownloaded) {
136
+ cssContent.push(`
137
+ @font-face {
138
+ font-family: '${font.name}';
139
+ src: ${mainCssSrc.join(',\n ')};
140
+ font-weight: normal;
141
+ font-style: normal;
142
+ font-display: swap;
143
+ }
144
+ `);
145
+ }
146
+
147
+ // Download Variants
148
+ if (font.font_variants && font.font_variants.length > 0) {
149
+ for (const variant of font.font_variants) {
150
+ const vFiles = [
151
+ { url: variant.woff2_url, name: `${font.slug}-${variant.variant_name}.woff2`, format: 'woff2' },
152
+ { url: variant.woff_url, name: `${font.slug}-${variant.variant_name}.woff`, format: 'woff' },
153
+ { url: variant.ttf_url, name: `${font.slug}-${variant.variant_name}.ttf`, format: 'truetype' },
154
+ { url: variant.otf_url, name: `${font.slug}-${variant.variant_name}.otf`, format: 'opentype' }
155
+ ];
156
+
157
+ let vCssSrc = [];
158
+ // Determine weight/style from name (simple heuristic)
159
+ let weight = 400;
160
+ let style = 'normal';
161
+ const nameLower = variant.variant_name.toLowerCase();
162
+
163
+ if (nameLower.includes('bold')) weight = 700;
164
+ if (nameLower.includes('light')) weight = 300;
165
+ if (nameLower.includes('medium')) weight = 500;
166
+ if (nameLower.includes('black')) weight = 900;
167
+ if (nameLower.includes('thin')) weight = 100;
168
+ if (nameLower.includes('extra')) weight = 800; // heuristic
169
+
170
+ if (nameLower.includes('italic')) style = 'italic';
171
+
172
+ for (const f of vFiles) {
173
+ if (f.url) {
174
+ await downloadFile(f.url, f.name);
175
+ vCssSrc.push(`url('/fonts/${font.slug}/${f.name}') format('${f.format}')`);
176
+ }
177
+ }
178
+
179
+ if (vCssSrc.length > 0) {
180
+ cssContent.push(`
181
+ @font-face {
182
+ font-family: '${font.name}';
183
+ src: ${vCssSrc.join(',\n ')};
184
+ font-weight: ${weight};
185
+ font-style: ${style};
186
+ font-display: swap;
187
+ }
188
+ `);
189
+ }
190
+ }
191
+ }
192
+
193
+ spinner.succeed('Files downloaded successfully.');
194
+
195
+ // Write CSS
196
+ // Create specific CSS file
197
+ const outputCssPath = path.join(process.cwd(), 'src', 'fonts.css');
198
+
199
+ // Check if file exists to append or create
200
+ // If we append, we check if font-family is already defined to avoid duplicates?
201
+ // For simplicity, just append.
202
+
203
+ spinner.start('Updating CSS...');
204
+
205
+ let existingCss = '';
206
+ if (await fs.pathExists(outputCssPath)) {
207
+ existingCss = await fs.readFile(outputCssPath, 'utf8');
208
+ }
209
+
210
+ const newCssBlock = `
211
+ /* Font: ${font.name} (Added via NoBadFonts CLI) */
212
+ ${cssContent.join('\n')}
213
+ `;
214
+
215
+ if (!existingCss.includes(`Font: ${font.name}`)) {
216
+ await fs.outputFile(outputCssPath, existingCss + newCssBlock);
217
+ spinner.succeed(chalk.green(`Updated src/fonts.css with @font-face definitions.`));
218
+ console.log(chalk.blue(`\nSuccess! To use the font, add this to your CSS:`));
219
+ console.log(chalk.cyan(`font-family: '${font.name}', sans-serif;`));
220
+ console.log(chalk.gray(`(Make sure to import './fonts.css' in your main execution file)`));
221
+ } else {
222
+ spinner.info(chalk.yellow(`Font ${font.name} definitions already exist in src/fonts.css`));
223
+ }
224
+
225
+ } catch (error) {
226
+ spinner.fail(chalk.red(`Error: ${error.message}`));
227
+ }
228
+ }
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "nobadfonts-cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool to instantly install fonts from NoBadFonts",
5
+ "bin": {
6
+ "nobadfonts": "index.js"
7
+ },
8
+ "dependencies": {
9
+ "chalk": "^4.1.2",
10
+ "node-fetch": "^2.6.7",
11
+ "ora": "^5.4.1",
12
+ "fs-extra": "^10.0.0"
13
+ },
14
+ "main": "index.js",
15
+ "type": "module",
16
+ "scripts": {
17
+ "start": "node index.js"
18
+ }
19
+ }
package/test_list.js ADDED
@@ -0,0 +1,16 @@
1
+
2
+ import fetch from 'node-fetch';
3
+
4
+ const SUPABASE_URL = 'https://wcegdxhvgwbeskaidlxr.supabase.co';
5
+ const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6IndjZWdkeGh2Z3diZXNrYWlkbHhyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njk1MjI3OTQsImV4cCI6MjA4NTA5ODc5NH0.P_JY0RF6wVdPCDfWLlcor5l1CP3g4bLE5y4JWmZVOig';
6
+
7
+ (async () => {
8
+ const res = await fetch(`${SUPABASE_URL}/rest/v1/fonts?select=name&limit=10`, {
9
+ headers: {
10
+ 'apikey': SUPABASE_ANON_KEY,
11
+ 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`
12
+ }
13
+ });
14
+ const data = await res.json();
15
+ console.log(JSON.stringify(data, null, 2));
16
+ })();