qrdx-cli 0.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 +90 -0
- package/dist/index.js +564 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# qrdx-cli
|
|
2
|
+
|
|
3
|
+
QR code generator CLI for developers. Generate styled, scannable QR codes as SVG or PNG — fully non-interactive, CI-friendly.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g qrdx-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run without installing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx qrdx-cli generate "https://example.com" -o qr.svg
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
qrdx generate <data> [options]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Options**
|
|
24
|
+
|
|
25
|
+
| Flag | Short | Default | Description |
|
|
26
|
+
|------|-------|---------|-------------|
|
|
27
|
+
| `--output` | `-o` | — | Output path — extension sets format (`.svg` or `.png`) |
|
|
28
|
+
| `--size` | | `512` | Output pixel size |
|
|
29
|
+
| `--level` | `-l` | `Q` | Error correction: `L` `M` `Q` `H` |
|
|
30
|
+
| `--body` | | `square` | Body dot pattern |
|
|
31
|
+
| `--eye` | | `square` | Corner eye pattern |
|
|
32
|
+
| `--dot` | | `square` | Corner dot pattern |
|
|
33
|
+
| `--fg` | | `#000000` | Foreground color |
|
|
34
|
+
| `--bg` | | `#ffffff` | Background color |
|
|
35
|
+
| `--eye-color` | | — | Corner eye color override |
|
|
36
|
+
| `--dot-color` | | — | Corner dot color override |
|
|
37
|
+
| `--logo` | | — | Center logo URL |
|
|
38
|
+
| `--margin` | | `0` | Quiet zone margin (modules) |
|
|
39
|
+
|
|
40
|
+
## Examples
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Basic SVG
|
|
44
|
+
qrdx generate "https://qrdx.dev" -o qr.svg
|
|
45
|
+
|
|
46
|
+
# High-res PNG for print
|
|
47
|
+
qrdx generate "https://qrdx.dev" -o qr.png --size 2048 --level H
|
|
48
|
+
|
|
49
|
+
# Branded style
|
|
50
|
+
qrdx generate "https://qrdx.dev" \
|
|
51
|
+
--body circle --eye rounded \
|
|
52
|
+
--fg "#1a1a2e" --bg "#f5f5f5" \
|
|
53
|
+
-o branded.svg
|
|
54
|
+
|
|
55
|
+
# With center logo
|
|
56
|
+
qrdx generate "https://qrdx.dev" \
|
|
57
|
+
--logo "https://qrdx.dev/logo.png" --level H \
|
|
58
|
+
-o logo-qr.svg
|
|
59
|
+
|
|
60
|
+
# Wi-Fi QR
|
|
61
|
+
qrdx generate "WIFI:T:WPA;S:MyNetwork;P:MyPassword;;" -o wifi.svg
|
|
62
|
+
|
|
63
|
+
# Interactive mode (TTY)
|
|
64
|
+
qrdx generate
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Patterns
|
|
68
|
+
|
|
69
|
+
**Body** (`--body`): `square` `circle` `circle-large` `diamond` `circle-mixed` `pacman` `rounded` `small-square` `vertical-line`
|
|
70
|
+
|
|
71
|
+
**Eye** (`--eye`): `square` `rounded` `gear` `circle` `diya` `cushion` `boxy` `pointed`
|
|
72
|
+
|
|
73
|
+
**Dot** (`--dot`): `square` `rounded` `circle` `heart` `diamond` `star`
|
|
74
|
+
|
|
75
|
+
## Interactive mode
|
|
76
|
+
|
|
77
|
+
When run in a TTY without flags, `qrdx generate` starts a guided prompt flow — output path, then optionally advanced customizations (patterns, colors, logo).
|
|
78
|
+
|
|
79
|
+
## Requirements
|
|
80
|
+
|
|
81
|
+
Node.js ≥ 18
|
|
82
|
+
|
|
83
|
+
## Links
|
|
84
|
+
|
|
85
|
+
- [qrdx.dev](https://qrdx.dev) — web app
|
|
86
|
+
- [github.com/qrdx/qrdx](https://github.com/qrdx/qrdx) — source
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var fs = require('fs');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
var p = require('@clack/prompts');
|
|
7
|
+
var qrdx = require('qrdx');
|
|
8
|
+
var react = require('react');
|
|
9
|
+
var server = require('react-dom/server');
|
|
10
|
+
|
|
11
|
+
function _interopNamespace(e) {
|
|
12
|
+
if (e && e.__esModule) return e;
|
|
13
|
+
var n = Object.create(null);
|
|
14
|
+
if (e) {
|
|
15
|
+
Object.keys(e).forEach(function (k) {
|
|
16
|
+
if (k !== 'default') {
|
|
17
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
18
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () { return e[k]; }
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
n.default = e;
|
|
26
|
+
return Object.freeze(n);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var p__namespace = /*#__PURE__*/_interopNamespace(p);
|
|
30
|
+
|
|
31
|
+
function buildSVGString(props) {
|
|
32
|
+
const element = react.createElement(qrdx.QRCodeSVG, {
|
|
33
|
+
size: props.size ?? 512,
|
|
34
|
+
...props
|
|
35
|
+
});
|
|
36
|
+
return server.renderToStaticMarkup(element);
|
|
37
|
+
}
|
|
38
|
+
async function saveOutput(options) {
|
|
39
|
+
const { outputPath, format, ...qrProps } = options;
|
|
40
|
+
const svgString = buildSVGString(qrProps);
|
|
41
|
+
if (format === "svg") {
|
|
42
|
+
const svgContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
43
|
+
${svgString}`;
|
|
44
|
+
fs.writeFileSync(outputPath, svgContent, "utf-8");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const sharp = (await import('sharp')).default;
|
|
48
|
+
const svgBuffer = Buffer.from(svgString, "utf-8");
|
|
49
|
+
const size = qrProps.size ?? 512;
|
|
50
|
+
await sharp(svgBuffer).resize(size, size).png().toFile(outputPath);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/commands/generate.ts
|
|
54
|
+
var RESET = "\x1B[0m";
|
|
55
|
+
var TEXT = "\x1B[38;5;145m";
|
|
56
|
+
var GREEN = "\x1B[38;5;114m";
|
|
57
|
+
var DIM = "\x1B[38;5;102m";
|
|
58
|
+
var IS_TTY = Boolean(process.stdin.isTTY);
|
|
59
|
+
function parseFlags(args) {
|
|
60
|
+
const flags = {};
|
|
61
|
+
let i = 0;
|
|
62
|
+
while (i < args.length) {
|
|
63
|
+
const arg = args[i];
|
|
64
|
+
if (!arg.startsWith("-")) {
|
|
65
|
+
if (!flags.value) {
|
|
66
|
+
flags.value = arg;
|
|
67
|
+
}
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const next = args[i + 1];
|
|
72
|
+
switch (arg) {
|
|
73
|
+
case "-o":
|
|
74
|
+
case "--output":
|
|
75
|
+
flags.output = next;
|
|
76
|
+
i += 2;
|
|
77
|
+
break;
|
|
78
|
+
case "-l":
|
|
79
|
+
case "--level":
|
|
80
|
+
flags.level = next;
|
|
81
|
+
i += 2;
|
|
82
|
+
break;
|
|
83
|
+
case "--body":
|
|
84
|
+
flags.body = next;
|
|
85
|
+
i += 2;
|
|
86
|
+
break;
|
|
87
|
+
case "--eye":
|
|
88
|
+
flags.eye = next;
|
|
89
|
+
i += 2;
|
|
90
|
+
break;
|
|
91
|
+
case "--dot":
|
|
92
|
+
flags.dot = next;
|
|
93
|
+
i += 2;
|
|
94
|
+
break;
|
|
95
|
+
case "--fg":
|
|
96
|
+
flags.fg = next;
|
|
97
|
+
i += 2;
|
|
98
|
+
break;
|
|
99
|
+
case "--bg":
|
|
100
|
+
flags.bg = next;
|
|
101
|
+
i += 2;
|
|
102
|
+
break;
|
|
103
|
+
case "--eye-color":
|
|
104
|
+
flags.eyeColor = next;
|
|
105
|
+
i += 2;
|
|
106
|
+
break;
|
|
107
|
+
case "--dot-color":
|
|
108
|
+
flags.dotColor = next;
|
|
109
|
+
i += 2;
|
|
110
|
+
break;
|
|
111
|
+
case "--logo":
|
|
112
|
+
flags.logo = next;
|
|
113
|
+
i += 2;
|
|
114
|
+
break;
|
|
115
|
+
case "--margin":
|
|
116
|
+
flags.margin = next ? Number.parseInt(next, 10) : 0;
|
|
117
|
+
i += 2;
|
|
118
|
+
break;
|
|
119
|
+
case "--size":
|
|
120
|
+
flags.size = next ? Number.parseInt(next, 10) : 512;
|
|
121
|
+
i += 2;
|
|
122
|
+
break;
|
|
123
|
+
case "--no-logo":
|
|
124
|
+
flags.noLogo = true;
|
|
125
|
+
i++;
|
|
126
|
+
break;
|
|
127
|
+
default:
|
|
128
|
+
i++;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return flags;
|
|
132
|
+
}
|
|
133
|
+
function isCancel2(value) {
|
|
134
|
+
return p__namespace.isCancel(value);
|
|
135
|
+
}
|
|
136
|
+
function handleCancel() {
|
|
137
|
+
p__namespace.cancel("Cancelled.");
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
function inferFormat(outputPath) {
|
|
141
|
+
const ext = path.extname(outputPath).toLowerCase();
|
|
142
|
+
if (ext === ".png") {
|
|
143
|
+
return "png";
|
|
144
|
+
}
|
|
145
|
+
return "svg";
|
|
146
|
+
}
|
|
147
|
+
async function runGenerate(args) {
|
|
148
|
+
const flags = parseFlags(args);
|
|
149
|
+
const nonInteractive = !IS_TTY;
|
|
150
|
+
const hasAdvancedFlags = Boolean(
|
|
151
|
+
flags.body || flags.eye || flags.dot || flags.fg || flags.bg || flags.eyeColor || flags.dotColor || flags.logo || flags.level || flags.margin !== void 0
|
|
152
|
+
);
|
|
153
|
+
p__namespace.intro(`${TEXT}qrdx${RESET} ${DIM}\u2014 QR Code Generator${RESET}`);
|
|
154
|
+
let value = flags.value;
|
|
155
|
+
if (!value) {
|
|
156
|
+
if (nonInteractive) {
|
|
157
|
+
p__namespace.log.error("No data provided. Pass a URL or text as an argument.");
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
const res = await p__namespace.text({
|
|
161
|
+
message: "URL or text to encode",
|
|
162
|
+
placeholder: "https://qrdx.dev",
|
|
163
|
+
validate(v) {
|
|
164
|
+
if (!v.trim()) {
|
|
165
|
+
return "Please enter a value to encode.";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
if (isCancel2(res)) {
|
|
170
|
+
handleCancel();
|
|
171
|
+
}
|
|
172
|
+
value = res;
|
|
173
|
+
}
|
|
174
|
+
let outputPath = flags.output;
|
|
175
|
+
const size = flags.size ?? 512;
|
|
176
|
+
if (!(outputPath || nonInteractive)) {
|
|
177
|
+
const pathRes = await p__namespace.text({
|
|
178
|
+
message: "Output file (.svg or .png)",
|
|
179
|
+
placeholder: "qr.svg",
|
|
180
|
+
defaultValue: "qr.svg",
|
|
181
|
+
validate(v) {
|
|
182
|
+
if (!v.trim()) {
|
|
183
|
+
return "Please enter an output path.";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
if (isCancel2(pathRes)) {
|
|
188
|
+
handleCancel();
|
|
189
|
+
}
|
|
190
|
+
outputPath = pathRes || "qr.svg";
|
|
191
|
+
}
|
|
192
|
+
let wantAdvanced = hasAdvancedFlags;
|
|
193
|
+
if (!(hasAdvancedFlags || nonInteractive)) {
|
|
194
|
+
const res = await p__namespace.confirm({
|
|
195
|
+
message: "Advanced customizations?",
|
|
196
|
+
initialValue: false
|
|
197
|
+
});
|
|
198
|
+
if (isCancel2(res)) {
|
|
199
|
+
handleCancel();
|
|
200
|
+
}
|
|
201
|
+
wantAdvanced = res;
|
|
202
|
+
}
|
|
203
|
+
let level = flags.level ?? "Q";
|
|
204
|
+
let body = flags.body ?? "square";
|
|
205
|
+
let eye = flags.eye ?? "square";
|
|
206
|
+
let dot = flags.dot ?? "square";
|
|
207
|
+
let fg = flags.fg ?? "#000000";
|
|
208
|
+
let bg = flags.bg ?? "#ffffff";
|
|
209
|
+
let eyeColor = flags.eyeColor;
|
|
210
|
+
let dotColor = flags.dotColor;
|
|
211
|
+
let logo = flags.logo;
|
|
212
|
+
let margin = flags.margin ?? 0;
|
|
213
|
+
if (wantAdvanced && !nonInteractive) {
|
|
214
|
+
if (!flags.level) {
|
|
215
|
+
const res = await p__namespace.select({
|
|
216
|
+
message: "Error correction level",
|
|
217
|
+
options: [
|
|
218
|
+
{
|
|
219
|
+
value: "Q",
|
|
220
|
+
label: "Q \u2013 Quartile (recommended)",
|
|
221
|
+
hint: "~25% data recovery"
|
|
222
|
+
},
|
|
223
|
+
{ value: "L", label: "L \u2013 Low", hint: "~7% data recovery" },
|
|
224
|
+
{ value: "M", label: "M \u2013 Medium", hint: "~15% data recovery" },
|
|
225
|
+
{ value: "H", label: "H \u2013 High", hint: "~30% data recovery" }
|
|
226
|
+
],
|
|
227
|
+
initialValue: "Q"
|
|
228
|
+
});
|
|
229
|
+
if (isCancel2(res)) {
|
|
230
|
+
handleCancel();
|
|
231
|
+
}
|
|
232
|
+
level = res;
|
|
233
|
+
}
|
|
234
|
+
if (!flags.body) {
|
|
235
|
+
const res = await p__namespace.select({
|
|
236
|
+
message: "Body dot pattern",
|
|
237
|
+
options: qrdx.BODY_PATTERN.map((b) => ({ value: b, label: b })),
|
|
238
|
+
initialValue: "square"
|
|
239
|
+
});
|
|
240
|
+
if (isCancel2(res)) {
|
|
241
|
+
handleCancel();
|
|
242
|
+
}
|
|
243
|
+
body = res;
|
|
244
|
+
}
|
|
245
|
+
if (!flags.eye) {
|
|
246
|
+
const res = await p__namespace.select({
|
|
247
|
+
message: "Corner eye pattern",
|
|
248
|
+
options: qrdx.CORNER_EYE_PATTERNS.map((e) => ({ value: e, label: e })),
|
|
249
|
+
initialValue: "square"
|
|
250
|
+
});
|
|
251
|
+
if (isCancel2(res)) {
|
|
252
|
+
handleCancel();
|
|
253
|
+
}
|
|
254
|
+
eye = res;
|
|
255
|
+
}
|
|
256
|
+
if (!flags.dot) {
|
|
257
|
+
const res = await p__namespace.select({
|
|
258
|
+
message: "Corner dot pattern",
|
|
259
|
+
options: qrdx.CORNER_EYE_DOT_PATTERNS.map((d) => ({ value: d, label: d })),
|
|
260
|
+
initialValue: "square"
|
|
261
|
+
});
|
|
262
|
+
if (isCancel2(res)) {
|
|
263
|
+
handleCancel();
|
|
264
|
+
}
|
|
265
|
+
dot = res;
|
|
266
|
+
}
|
|
267
|
+
if (!flags.fg) {
|
|
268
|
+
const res = await p__namespace.text({
|
|
269
|
+
message: "Foreground color",
|
|
270
|
+
placeholder: "#000000",
|
|
271
|
+
defaultValue: "#000000",
|
|
272
|
+
validate(v) {
|
|
273
|
+
if (v && !/^#[0-9a-fA-F]{3,8}$/.test(v)) {
|
|
274
|
+
return "Enter a valid hex color (e.g. #000000)";
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
if (isCancel2(res)) {
|
|
279
|
+
handleCancel();
|
|
280
|
+
}
|
|
281
|
+
fg = res || "#000000";
|
|
282
|
+
}
|
|
283
|
+
if (!flags.bg) {
|
|
284
|
+
const res = await p__namespace.text({
|
|
285
|
+
message: "Background color",
|
|
286
|
+
placeholder: "#ffffff",
|
|
287
|
+
defaultValue: "#ffffff",
|
|
288
|
+
validate(v) {
|
|
289
|
+
if (v && !/^#[0-9a-fA-F]{3,8}$/.test(v)) {
|
|
290
|
+
return "Enter a valid hex color (e.g. #ffffff)";
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
if (isCancel2(res)) {
|
|
295
|
+
handleCancel();
|
|
296
|
+
}
|
|
297
|
+
bg = res || "#ffffff";
|
|
298
|
+
}
|
|
299
|
+
if (eyeColor === void 0) {
|
|
300
|
+
const res = await p__namespace.text({
|
|
301
|
+
message: `Corner eye color ${DIM}(leave blank to match foreground)${RESET}`,
|
|
302
|
+
placeholder: fg,
|
|
303
|
+
validate(v) {
|
|
304
|
+
if (v && !/^#[0-9a-fA-F]{3,8}$/.test(v)) {
|
|
305
|
+
return "Enter a valid hex color or leave blank";
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
if (isCancel2(res)) {
|
|
310
|
+
handleCancel();
|
|
311
|
+
}
|
|
312
|
+
eyeColor = res || void 0;
|
|
313
|
+
}
|
|
314
|
+
if (dotColor === void 0) {
|
|
315
|
+
const res = await p__namespace.text({
|
|
316
|
+
message: `Corner dot color ${DIM}(leave blank to match foreground)${RESET}`,
|
|
317
|
+
placeholder: fg,
|
|
318
|
+
validate(v) {
|
|
319
|
+
if (v && !/^#[0-9a-fA-F]{3,8}$/.test(v)) {
|
|
320
|
+
return "Enter a valid hex color or leave blank";
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
if (isCancel2(res)) {
|
|
325
|
+
handleCancel();
|
|
326
|
+
}
|
|
327
|
+
dotColor = res || void 0;
|
|
328
|
+
}
|
|
329
|
+
if (logo === void 0 && !flags.noLogo) {
|
|
330
|
+
const addLogo = await p__namespace.confirm({
|
|
331
|
+
message: "Add a logo to the center?",
|
|
332
|
+
initialValue: false
|
|
333
|
+
});
|
|
334
|
+
if (isCancel2(addLogo)) {
|
|
335
|
+
handleCancel();
|
|
336
|
+
}
|
|
337
|
+
if (addLogo) {
|
|
338
|
+
const res = await p__namespace.text({
|
|
339
|
+
message: "Logo URL",
|
|
340
|
+
placeholder: "https://qrdx.dev/logo.png",
|
|
341
|
+
validate(v) {
|
|
342
|
+
if (!v.trim()) {
|
|
343
|
+
return "Please enter a URL for the logo.";
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
new URL(v);
|
|
347
|
+
} catch {
|
|
348
|
+
return "Please enter a valid URL.";
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
if (isCancel2(res)) {
|
|
353
|
+
handleCancel();
|
|
354
|
+
}
|
|
355
|
+
logo = res;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (flags.margin === void 0) {
|
|
359
|
+
const res = await p__namespace.text({
|
|
360
|
+
message: "Quiet zone margin (modules)",
|
|
361
|
+
placeholder: "0",
|
|
362
|
+
defaultValue: "0",
|
|
363
|
+
validate(v) {
|
|
364
|
+
if (v && Number.isNaN(Number.parseInt(v, 10))) {
|
|
365
|
+
return "Enter a number";
|
|
366
|
+
}
|
|
367
|
+
if (v && Number.parseInt(v, 10) < 0) {
|
|
368
|
+
return "Must be 0 or greater";
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
if (isCancel2(res)) {
|
|
373
|
+
handleCancel();
|
|
374
|
+
}
|
|
375
|
+
margin = Number.parseInt(res || "0", 10);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const spinner2 = p__namespace.spinner();
|
|
379
|
+
spinner2.start("Generating QR code...");
|
|
380
|
+
try {
|
|
381
|
+
const qrProps = {
|
|
382
|
+
value,
|
|
383
|
+
level,
|
|
384
|
+
bodyPattern: body,
|
|
385
|
+
cornerEyePattern: eye,
|
|
386
|
+
cornerEyeDotPattern: dot,
|
|
387
|
+
fgColor: fg,
|
|
388
|
+
bgColor: bg,
|
|
389
|
+
...eyeColor ? { eyeColor } : {},
|
|
390
|
+
...dotColor ? { dotColor } : {},
|
|
391
|
+
margin,
|
|
392
|
+
size,
|
|
393
|
+
...logo ? {
|
|
394
|
+
imageSettings: {
|
|
395
|
+
src: logo,
|
|
396
|
+
height: Math.round(size * 0.2),
|
|
397
|
+
width: Math.round(size * 0.2),
|
|
398
|
+
excavate: true
|
|
399
|
+
}
|
|
400
|
+
} : {}
|
|
401
|
+
};
|
|
402
|
+
if (outputPath) {
|
|
403
|
+
const format = inferFormat(outputPath);
|
|
404
|
+
await saveOutput({ ...qrProps, outputPath, format });
|
|
405
|
+
spinner2.stop(
|
|
406
|
+
`${GREEN}\u2714${RESET} Saved to ${TEXT}${outputPath}${RESET} ${DIM}(${size}\xD7${size})${RESET}`
|
|
407
|
+
);
|
|
408
|
+
} else {
|
|
409
|
+
spinner2.stop("QR code ready");
|
|
410
|
+
}
|
|
411
|
+
} catch (err) {
|
|
412
|
+
spinner2.stop("Failed to generate QR code");
|
|
413
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
414
|
+
p__namespace.log.error(msg);
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
p__namespace.outro(`${DIM}Done!${RESET}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/index.ts
|
|
421
|
+
var RESET2 = "\x1B[0m";
|
|
422
|
+
var BOLD = "\x1B[1m";
|
|
423
|
+
var DIM2 = "\x1B[38;5;102m";
|
|
424
|
+
var TEXT2 = "\x1B[38;5;145m";
|
|
425
|
+
var GRAYS = [
|
|
426
|
+
"\x1B[38;5;250m",
|
|
427
|
+
"\x1B[38;5;248m",
|
|
428
|
+
"\x1B[38;5;245m",
|
|
429
|
+
"\x1B[38;5;243m",
|
|
430
|
+
"\x1B[38;5;240m",
|
|
431
|
+
"\x1B[38;5;238m"
|
|
432
|
+
];
|
|
433
|
+
var LOGO_LINES = [
|
|
434
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557",
|
|
435
|
+
"\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2588\u2588\u2557\u2588\u2588\u2554\u255D",
|
|
436
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2554\u255D ",
|
|
437
|
+
"\u2588\u2588\u2551\u2584\u2584 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2588\u2588\u2557 ",
|
|
438
|
+
"\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2554\u255D \u2588\u2588\u2557",
|
|
439
|
+
" \u255A\u2550\u2550\u2550\u2588\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D"
|
|
440
|
+
];
|
|
441
|
+
function getVersion() {
|
|
442
|
+
try {
|
|
443
|
+
const pkgPath = path.join(__dirname, "..", "package.json");
|
|
444
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
445
|
+
return pkg.version;
|
|
446
|
+
} catch {
|
|
447
|
+
return "0.0.1";
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
var VERSION = getVersion();
|
|
451
|
+
function showLogo() {
|
|
452
|
+
console.log();
|
|
453
|
+
LOGO_LINES.forEach((line, i) => {
|
|
454
|
+
console.log(`${GRAYS[i] ?? GRAYS[5]}${line}${RESET2}`);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
function showBanner() {
|
|
458
|
+
showLogo();
|
|
459
|
+
console.log();
|
|
460
|
+
console.log(`${DIM2}QR code generator for developers${RESET2}`);
|
|
461
|
+
console.log();
|
|
462
|
+
console.log(
|
|
463
|
+
` ${DIM2}$${RESET2} ${TEXT2}qrdx generate${RESET2} ${DIM2}Generate a QR code interactively${RESET2}`
|
|
464
|
+
);
|
|
465
|
+
console.log(
|
|
466
|
+
` ${DIM2}$${RESET2} ${TEXT2}qrdx generate${RESET2} ${DIM2}<url>${RESET2} ${DIM2}Generate from URL${RESET2}`
|
|
467
|
+
);
|
|
468
|
+
console.log();
|
|
469
|
+
console.log(
|
|
470
|
+
` ${DIM2}$${RESET2} ${TEXT2}qrdx --help${RESET2} ${DIM2}Show all commands and options${RESET2}`
|
|
471
|
+
);
|
|
472
|
+
console.log(
|
|
473
|
+
` ${DIM2}$${RESET2} ${TEXT2}qrdx --version${RESET2} ${DIM2}Show version${RESET2}`
|
|
474
|
+
);
|
|
475
|
+
console.log();
|
|
476
|
+
console.log(` ${DIM2}try:${RESET2} npx @qrdx/cli generate "https://qrdx.dev"`);
|
|
477
|
+
console.log();
|
|
478
|
+
}
|
|
479
|
+
function showHelp() {
|
|
480
|
+
showLogo();
|
|
481
|
+
console.log(`
|
|
482
|
+
${BOLD}Usage:${RESET2} qrdx <command> [options]
|
|
483
|
+
|
|
484
|
+
${BOLD}Commands:${RESET2}
|
|
485
|
+
generate, gen, g Generate a QR code (fully interactive or via flags)
|
|
486
|
+
|
|
487
|
+
${BOLD}Generate Options:${RESET2}
|
|
488
|
+
<data> URL or text to encode (positional)
|
|
489
|
+
-o, --output Output file path (.svg or .png)
|
|
490
|
+
-l, --level Error correction: L, M, Q, H ${DIM2}(default: Q)${RESET2}
|
|
491
|
+
--body Body dot pattern:
|
|
492
|
+
${DIM2}square, circle, circle-large, diamond,${RESET2}
|
|
493
|
+
${DIM2}circle-mixed, pacman, rounded, small-square, vertical-line${RESET2}
|
|
494
|
+
--eye Corner eye pattern:
|
|
495
|
+
${DIM2}square, rounded, gear, circle, diya,${RESET2}
|
|
496
|
+
${DIM2}extra-rounded, message, pointy, curly${RESET2}
|
|
497
|
+
--dot Corner dot pattern:
|
|
498
|
+
${DIM2}square, rounded, circle, diamond, message,${RESET2}
|
|
499
|
+
${DIM2}message-reverse, diya, diya-reverse,${RESET2}
|
|
500
|
+
${DIM2}rounded-triangle, star, banner${RESET2}
|
|
501
|
+
--fg Foreground color ${DIM2}(default: #000000)${RESET2}
|
|
502
|
+
--bg Background color ${DIM2}(default: #ffffff)${RESET2}
|
|
503
|
+
--eye-color Corner eye color ${DIM2}(default: matches --fg)${RESET2}
|
|
504
|
+
--dot-color Corner dot color ${DIM2}(default: matches --fg)${RESET2}
|
|
505
|
+
--logo Logo URL for center image
|
|
506
|
+
--margin Quiet zone margin in modules ${DIM2}(default: 0)${RESET2}
|
|
507
|
+
--size Output pixel size for SVG/PNG ${DIM2}(default: 512)${RESET2}
|
|
508
|
+
|
|
509
|
+
${BOLD}Global Options:${RESET2}
|
|
510
|
+
--help, -h Show this help message
|
|
511
|
+
--version, -v Show version number
|
|
512
|
+
|
|
513
|
+
${BOLD}Examples:${RESET2}
|
|
514
|
+
${DIM2}$${RESET2} qrdx generate
|
|
515
|
+
${DIM2}$${RESET2} qrdx generate "https://qrdx.dev"
|
|
516
|
+
${DIM2}$${RESET2} qrdx generate "https://qrdx.dev" -o qr.svg
|
|
517
|
+
${DIM2}$${RESET2} qrdx generate "https://qrdx.dev" -o qr.png --body circle --eye gear --dot star
|
|
518
|
+
${DIM2}$${RESET2} qrdx generate "https://qrdx.dev" --fg "#ff0000" --bg "#ffffff" --size 1024
|
|
519
|
+
${DIM2}$${RESET2} qrdx generate "https://qrdx.dev" --logo "https://qrdx.dev/logo.png" -o branded.svg
|
|
520
|
+
|
|
521
|
+
Visit ${TEXT2}https://qrdx.dev${RESET2} for more.
|
|
522
|
+
`);
|
|
523
|
+
}
|
|
524
|
+
async function main() {
|
|
525
|
+
const args = process.argv.slice(2);
|
|
526
|
+
if (args.length === 0) {
|
|
527
|
+
showBanner();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const command = args[0];
|
|
531
|
+
const restArgs = args.slice(1);
|
|
532
|
+
switch (command) {
|
|
533
|
+
case "generate":
|
|
534
|
+
case "gen":
|
|
535
|
+
case "g":
|
|
536
|
+
showLogo();
|
|
537
|
+
console.log();
|
|
538
|
+
await runGenerate(restArgs);
|
|
539
|
+
break;
|
|
540
|
+
case "--help":
|
|
541
|
+
case "-h":
|
|
542
|
+
case "help":
|
|
543
|
+
showHelp();
|
|
544
|
+
break;
|
|
545
|
+
case "--version":
|
|
546
|
+
case "-v":
|
|
547
|
+
console.log(VERSION);
|
|
548
|
+
break;
|
|
549
|
+
default:
|
|
550
|
+
if (command.startsWith("-")) {
|
|
551
|
+
console.log(`Unknown command: ${command}`);
|
|
552
|
+
console.log(`Run ${BOLD}qrdx --help${RESET2} for usage.`);
|
|
553
|
+
} else {
|
|
554
|
+
showLogo();
|
|
555
|
+
console.log();
|
|
556
|
+
await runGenerate(args);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
main().catch((err) => {
|
|
561
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
562
|
+
console.error(`\x1B[38;5;203m\u2717 ${msg}${RESET2}`);
|
|
563
|
+
process.exit(1);
|
|
564
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qrdx-cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "QR code generator CLI for developers",
|
|
5
|
+
"bin": {
|
|
6
|
+
"qrdx": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@clack/prompts": "^0.10.0",
|
|
14
|
+
"picocolors": "^1.1.1",
|
|
15
|
+
"react": "19.2.3",
|
|
16
|
+
"react-dom": "19.2.3",
|
|
17
|
+
"sharp": "^0.34.2",
|
|
18
|
+
"qrdx": "0.0.1"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^25.1.0",
|
|
22
|
+
"@types/react": "19.2.7",
|
|
23
|
+
"@types/react-dom": "19.2.3",
|
|
24
|
+
"tsup": "^8.5.1",
|
|
25
|
+
"typescript": "5.9.3",
|
|
26
|
+
"vitest": "^4.0.18",
|
|
27
|
+
"@repo/typescript-config": "0.0.0"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"dev": "tsup --watch",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest",
|
|
40
|
+
"clean": "git clean -xdf .cache .turbo dist node_modules",
|
|
41
|
+
"typecheck": "tsc --noEmit"
|
|
42
|
+
}
|
|
43
|
+
}
|