@hubtoolify/icons 1.0.0 → 1.0.1
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/README.md +14 -2
- package/bin/cli.mjs +165 -12
- package/icons.json +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# @hubtoolify/icons
|
|
2
2
|
|
|
3
|
-
> Proposed package name. npm ownership of the `@hubtoolify` scope has not been verified yet.
|
|
4
|
-
|
|
5
3
|
Original HubToolify icons as CSS classes, in three styles. For plain HTML sites and any project
|
|
6
4
|
that serves static files. (React or a bundler? Use `@hubtoolify/icons-react` or
|
|
7
5
|
`@hubtoolify/icons-svg`; see "Bundlers" below.)
|
|
@@ -45,6 +43,20 @@ makes the bundler try to process all of them (Vite fails with `EMFILE: too many
|
|
|
45
43
|
package has **no importable entry** on purpose. Put the folder in your static/public directory as
|
|
46
44
|
above, or use `@hubtoolify/icons-react` / `@hubtoolify/icons-svg`, which import one icon at a time.
|
|
47
45
|
|
|
46
|
+
## HubToolify Pro (private registry)
|
|
47
|
+
|
|
48
|
+
The Pro icon styles are installed from HubToolify's own registry, not from npmjs.com, and need an active Pro plan.
|
|
49
|
+
Sign in once per machine with this package's login command (Windows PowerShell, macOS and Linux, Node 18+):
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
npx @hubtoolify/icons login
|
|
53
|
+
npm install @hubtoolify-pro/icons-react
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
It asks for your HubToolify email and password (the password is not shown), then saves a read-only registry token
|
|
57
|
+
for the `@hubtoolify-pro` scope in your user npm config. The token is never printed. Details: the
|
|
58
|
+
`@hubtoolify-pro/icons` README (after sign-in) and https://hubtoolify.com/icons.
|
|
59
|
+
|
|
48
60
|
## What's in the package
|
|
49
61
|
|
|
50
62
|
- `css/` — `hubtoolify-icons.css` and `.min.css`. Link one.
|
package/bin/cli.mjs
CHANGED
|
@@ -1,17 +1,170 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
/*
|
|
3
|
+
* hubtoolify-icons
|
|
4
|
+
*
|
|
5
|
+
* npx @hubtoolify/icons login sign in to the HubToolify Pro package registry (Pro plan)
|
|
6
|
+
* npx hubtoolify-icons <folder> copy the CSS kit (css/ and svg/) into a static folder
|
|
7
|
+
*
|
|
8
|
+
* login: asks for your HubToolify email and password (the password is not shown), requests a read-only
|
|
9
|
+
* registry token, and saves it for the @hubtoolify-pro scope in your user-level npm config. The token is
|
|
10
|
+
* never printed. Works the same on Windows (PowerShell, cmd), macOS and Linux; needs Node 18 or newer.
|
|
11
|
+
*/
|
|
12
|
+
import { cp, mkdir, readFile, writeFile, chmod } from 'node:fs/promises';
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import { createInterface } from 'node:readline';
|
|
15
|
+
import os from 'node:os';
|
|
6
16
|
import path from 'node:path';
|
|
7
17
|
import { fileURLToPath } from 'node:url';
|
|
8
18
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
const DEFAULT_REGISTRY = 'https://hubtoolify.com/api/npm/';
|
|
20
|
+
const SCOPE = '@hubtoolify-pro';
|
|
21
|
+
|
|
22
|
+
const [command, ...args] = process.argv.slice(2);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
if (command === 'login') await login(args);
|
|
26
|
+
else await copyKit(command === 'copy' ? args : [command, ...args]);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.error(`\n${e.message}`);
|
|
29
|
+
process.exit(e.exitCode || 1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* ------------------------------------------------------------------ copy */
|
|
33
|
+
|
|
34
|
+
async function copyKit([dest]) {
|
|
35
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
36
|
+
if (!dest || dest.startsWith('-')) {
|
|
37
|
+
throw Object.assign(new Error('Usage:\n npx @hubtoolify/icons login sign in to the Pro registry\n npx hubtoolify-icons <folder> copy the CSS kit, e.g. public/hubtoolify-icons'), { exitCode: 1 });
|
|
38
|
+
}
|
|
39
|
+
// A bundler must never process the stylesheet: it references every icon file. A static folder avoids that.
|
|
40
|
+
await mkdir(dest, { recursive: true });
|
|
41
|
+
for (const dir of ['css', 'svg']) await cp(path.join(root, dir), path.join(dest, dir), { recursive: true });
|
|
42
|
+
console.log(`HubToolify Icons copied to ${dest}. Link ${path.join(dest, 'css', 'hubtoolify-icons.min.css')} in your HTML.`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* ----------------------------------------------------------------- login */
|
|
46
|
+
|
|
47
|
+
async function login(argv) {
|
|
48
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
49
|
+
console.log('Usage: npx @hubtoolify/icons login [--print-token]\n\nSigns in to the HubToolify Pro package registry and saves a read-only token for\n' + `${SCOPE} in your user npm config. Needs an active Pro plan.\n\n--print-token print the token instead of saving it (for storing as a CI secret; treat it like a password)`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const printToken = argv.includes('--print-token');
|
|
53
|
+
const ri = argv.indexOf('--registry');
|
|
54
|
+
const registry = withSlash(ri >= 0 ? argv[ri + 1] : DEFAULT_REGISTRY);
|
|
55
|
+
if (!/^https?:\/\//.test(registry)) throw new Error('--registry needs a URL such as https://hubtoolify.com/api/npm/');
|
|
56
|
+
|
|
57
|
+
const { email, password } = await credentials();
|
|
58
|
+
if (!email || !password) throw new Error('An email and a password are required.');
|
|
59
|
+
|
|
60
|
+
// JSON is built by the serialiser, so quotes or backslashes in a password cannot break the request.
|
|
61
|
+
const url = `${registry}-/user/org.couchdb.user:${encodeURIComponent(email)}`;
|
|
62
|
+
const body = JSON.stringify({ name: email, password });
|
|
63
|
+
let res;
|
|
64
|
+
try {
|
|
65
|
+
res = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body, signal: AbortSignal.timeout(30000) });
|
|
66
|
+
} catch (e) {
|
|
67
|
+
throw new Error(`Could not reach ${new URL(registry).host}: ${e.cause?.code || e.message}. Check your connection and try again.`);
|
|
68
|
+
}
|
|
69
|
+
const data = await res.json().catch(() => ({}));
|
|
70
|
+
if (res.status !== 201 || typeof data.token !== 'string' || data.token.length < 20) {
|
|
71
|
+
const why = data.error || `the server answered ${res.status}`;
|
|
72
|
+
const hint = res.status === 429 ? ' Wait a minute and try again.' : '';
|
|
73
|
+
throw new Error(`Login failed: ${why}${hint}`);
|
|
74
|
+
}
|
|
75
|
+
const token = data.token;
|
|
76
|
+
|
|
77
|
+
// Confirm the registry accepts the token before anything is saved.
|
|
78
|
+
const who = await fetch(`${registry}-/whoami`, { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, signal: AbortSignal.timeout(30000) }).catch(() => null);
|
|
79
|
+
if (!who || who.status !== 200) throw new Error(`The registry signed you in but did not accept the new token (status ${who ? who.status : 'no response'}). Nothing was saved.`);
|
|
80
|
+
|
|
81
|
+
if (printToken) {
|
|
82
|
+
process.stdout.write(`${token}\n`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const file = await userConfigPath();
|
|
87
|
+
await saveToken(file, registry, token);
|
|
88
|
+
console.log(`\nSigned in to HubToolify Pro as ${email}.`);
|
|
89
|
+
console.log(`Saved the registry token for ${SCOPE} in ${file} (it is not shown).`);
|
|
90
|
+
console.log(`\nNext: npm install ${SCOPE}/icons-react`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function withSlash(u) {
|
|
94
|
+
return u.endsWith('/') ? u : `${u}/`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function credentials() {
|
|
98
|
+
const envEmail = process.env.HUBTOOLIFY_EMAIL;
|
|
99
|
+
const envPassword = process.env.HUBTOOLIFY_PASSWORD;
|
|
100
|
+
if (envEmail && envPassword) return { email: envEmail.trim().toLowerCase(), password: envPassword };
|
|
101
|
+
|
|
102
|
+
if (process.stdin.isTTY) {
|
|
103
|
+
const email = (await ask('HubToolify email: ')).trim().toLowerCase();
|
|
104
|
+
const password = await askHidden('Password: ');
|
|
105
|
+
return { email, password };
|
|
106
|
+
}
|
|
107
|
+
// Not a terminal (a pipe or a CI job): two lines, email then password.
|
|
108
|
+
const lines = await readLines(2);
|
|
109
|
+
return { email: (lines[0] || '').trim().toLowerCase(), password: lines[1] || '' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function ask(prompt) {
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
115
|
+
rl.question(prompt, (answer) => { rl.close(); resolve(answer); });
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Password prompt: no echo, backspace works, Ctrl+C aborts. Raw mode works in PowerShell, cmd, macOS and Linux terminals. */
|
|
120
|
+
function askHidden(prompt) {
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
process.stdout.write(prompt);
|
|
123
|
+
const stdin = process.stdin;
|
|
124
|
+
stdin.setRawMode(true);
|
|
125
|
+
stdin.resume();
|
|
126
|
+
stdin.setEncoding('utf8');
|
|
127
|
+
let value = '';
|
|
128
|
+
const done = () => { stdin.setRawMode(false); stdin.pause(); stdin.removeListener('data', onData); process.stdout.write('\n'); };
|
|
129
|
+
const onData = (chunk) => {
|
|
130
|
+
for (const ch of chunk) {
|
|
131
|
+
if (ch === '\r' || ch === '\n') { done(); resolve(value); return; }
|
|
132
|
+
if (ch === '\u0003') { done(); process.exit(130); }
|
|
133
|
+
if (ch === '\u007f' || ch === '\b') value = value.slice(0, -1);
|
|
134
|
+
else if (ch >= ' ') value += ch;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
stdin.on('data', onData);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readLines(n) {
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
const rl = createInterface({ input: process.stdin });
|
|
144
|
+
const lines = [];
|
|
145
|
+
rl.on('line', (l) => { lines.push(l); if (lines.length >= n) rl.close(); });
|
|
146
|
+
rl.on('close', () => resolve(lines));
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The user-level npmrc, exactly where `npm config set` would write. */
|
|
151
|
+
async function userConfigPath() {
|
|
152
|
+
if (process.env.NPM_CONFIG_USERCONFIG) return process.env.NPM_CONFIG_USERCONFIG;
|
|
153
|
+
const r = spawnSync('npm', ['config', 'get', 'userconfig'], { encoding: 'utf8', shell: process.platform === 'win32' });
|
|
154
|
+
const p = r.status === 0 ? r.stdout.trim() : '';
|
|
155
|
+
return p && p !== 'undefined' ? p : path.join(os.homedir(), '.npmrc');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Replace this registry's token line and the scope's registry line; keep everything else in the file. */
|
|
159
|
+
async function saveToken(file, registry, token) {
|
|
160
|
+
const u = new URL(registry);
|
|
161
|
+
const tokenKey = `//${u.host}${u.pathname}:_authToken`;
|
|
162
|
+
const scopeKey = `${SCOPE}:registry`;
|
|
163
|
+
let existing = '';
|
|
164
|
+
try { existing = await readFile(file, 'utf8'); } catch { /* new file */ }
|
|
165
|
+
const kept = existing.split(/\r?\n/).filter((l) => l.trim() !== '' && !l.startsWith(`${tokenKey}=`) && !l.startsWith(`${scopeKey}=`));
|
|
166
|
+
kept.push(`${scopeKey}=${registry}`, `${tokenKey}=${token}`);
|
|
167
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
168
|
+
await writeFile(file, `${kept.join('\n')}\n`, { mode: 0o600 });
|
|
169
|
+
await chmod(file, 0o600).catch(() => {}); // no-op on Windows
|
|
14
170
|
}
|
|
15
|
-
await mkdir(dest, { recursive: true });
|
|
16
|
-
for (const dir of ['css', 'svg']) await cp(path.join(root, dir), path.join(dest, dir), { recursive: true });
|
|
17
|
-
console.log(`HubToolify Icons copied to ${dest}. Link ${path.join(dest, 'css', 'hubtoolify-icons.min.css')} in your HTML.`);
|