@acutenetwork/cli 0.1.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/LICENSE +21 -0
- package/README.md +161 -0
- package/assets/mark-light.svg +3 -0
- package/assets/mark-purple.svg +3 -0
- package/dist/bin.js +1050 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Acute Network
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# acute
|
|
2
|
+
|
|
3
|
+
The command line for [Acute](https://acute.network).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g @acutenetwork/cli
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
acute --help
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Node 20 or newer. MIT.
|
|
14
|
+
|
|
15
|
+
Today it generates and reads Acute QR codes. More of the platform lands here as
|
|
16
|
+
it ships, and every command follows the same shape: `--json` for scripting,
|
|
17
|
+
`--quiet` for silence, meaningful exit codes.
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
| | |
|
|
22
|
+
| ------------------------- | ---------------------------------------------------------------------- |
|
|
23
|
+
| `acute qr <value>` | Write an Acute QR code |
|
|
24
|
+
| `acute qr decode <image>` | Read the value back out of one |
|
|
25
|
+
| `acute config` | Defaults, so the flags you always pass become the flags you never pass |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## acute qr
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
acute qr https://example.com
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Writes `acute-qr.png`: 1024px, transparent, with the Acute mark inlaid at the
|
|
36
|
+
centre. This is the same code Acute renders in its own app and on its checkout
|
|
37
|
+
and invoice pages, so a code you generate and a code your customer sees are the
|
|
38
|
+
same object.
|
|
39
|
+
|
|
40
|
+
| Flag | What it does | Default |
|
|
41
|
+
| -------------------- | ---------------------------------------------------------------------------- | ---------------- |
|
|
42
|
+
| `-o, --out <path>` | File to write, or a directory to write into | `./acute-qr.png` |
|
|
43
|
+
| `-f, --format <fmt>` | `png`, `svg`, `jpeg`, `webp`. Inferred from `--out` when it has an extension | `png` |
|
|
44
|
+
| `-s, --size <px>` | Width and height, 64 to 8192 | `1024` |
|
|
45
|
+
| `--margin <px>` | Quiet zone around the code | 8% of `--size` |
|
|
46
|
+
| `--fg <colour>` | Module colour | `black` |
|
|
47
|
+
| `--bg <colour>` | Background colour, or `transparent` | `transparent` |
|
|
48
|
+
| `--mark <what>` | `purple`, `light`, `none`, or a path to your own image | `purple` |
|
|
49
|
+
| `--ec <level>` | Error correction: `auto`, `L`, `M`, `Q`, `H` | `auto` |
|
|
50
|
+
| `-p, --print` | Also print the code in the terminal, scannable as-is | |
|
|
51
|
+
| `--stdout` | Write the bytes to stdout instead of a file | |
|
|
52
|
+
| `--open` | Open the file when it is done | |
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
acute qr https://acute.ink/t/yourtag --size 2048 --out ~/Desktop
|
|
56
|
+
acute qr https://example.com --format svg --bg white
|
|
57
|
+
acute qr https://example.com --mark ./your-logo.svg
|
|
58
|
+
acute qr https://example.com --stdout > code.png
|
|
59
|
+
acute qr https://example.com --print # scan it straight off your screen
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`--fg` and `--bg` take any CSS colour, or one of `black`, `white`, `purple`,
|
|
63
|
+
`purple-accent`, `green`.
|
|
64
|
+
|
|
65
|
+
### Two defaults worth knowing
|
|
66
|
+
|
|
67
|
+
**The background is transparent**, because a code is usually an asset that gets
|
|
68
|
+
dropped onto something else, and a baked-in white square is the one thing you
|
|
69
|
+
cannot undo later. Pass `--bg white` when the code has to stand alone: on a dark
|
|
70
|
+
slide, or saved to a phone's photo library, a black code on nothing is
|
|
71
|
+
invisible.
|
|
72
|
+
|
|
73
|
+
**Error correction is `auto`.** The mark covers a fixed _proportion_ of the
|
|
74
|
+
code, so on a short payload it eats relatively more of a low-version code, and
|
|
75
|
+
the thing reading it is usually a cheap phone camera. Payloads of 64 characters
|
|
76
|
+
or fewer get `H`, the highest level; longer ones get `Q`, which is plenty once
|
|
77
|
+
the code is dense. Override it with `--ec`.
|
|
78
|
+
|
|
79
|
+
## acute qr decode
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
acute qr decode ./poster.jpg
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Reads any QR image, not only ones this wrote. Worth running before a code goes
|
|
86
|
+
to print: what a scanner sees is the only thing that matters, and it is not
|
|
87
|
+
always what you typed.
|
|
88
|
+
|
|
89
|
+
## acute config
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
acute config set qr.out ~/Desktop
|
|
93
|
+
acute config set qr.size 2048
|
|
94
|
+
acute config list
|
|
95
|
+
acute config path
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Stored in `~/.config/acute/config.json`, or under `$XDG_CONFIG_HOME` if you set
|
|
99
|
+
it. Keys no command reads cannot be set, so the file never fills with typos.
|
|
100
|
+
|
|
101
|
+
## Scripting it
|
|
102
|
+
|
|
103
|
+
Every command takes `--json` and prints one object to stdout:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
acute qr https://example.com --json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"path": "/Users/you/acute-qr.png",
|
|
112
|
+
"format": "png",
|
|
113
|
+
"size": 1024,
|
|
114
|
+
"bytes": 22431,
|
|
115
|
+
"value": "https://example.com",
|
|
116
|
+
"errorCorrection": "Q",
|
|
117
|
+
"background": "transparent",
|
|
118
|
+
"foreground": "#000000",
|
|
119
|
+
"margin": 82
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Exit codes: `0` it worked, `1` it could not do what you asked, `2` the command
|
|
124
|
+
line was wrong. `--quiet` prints nothing but errors. Colour is dropped when
|
|
125
|
+
stdout is not a terminal, and `NO_COLOR` is honoured.
|
|
126
|
+
|
|
127
|
+
## Contributing
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
git clone https://github.com/acutenetwork/acute-cli
|
|
131
|
+
cd acute-cli
|
|
132
|
+
pnpm install && pnpm build && pnpm test
|
|
133
|
+
pnpm link --global # puts your build on PATH as `acute`
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
A new command is a folder under `src/commands/` exporting a `CommandGroup`, plus
|
|
137
|
+
one line in `src/commands/index.ts`. Routing, help, flag parsing, `--json`,
|
|
138
|
+
`--quiet` and the settings file are all generated from what a `Command`
|
|
139
|
+
declares, so there is nothing else to wire up:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
export const show: Command = {
|
|
143
|
+
name: 'show',
|
|
144
|
+
summary: 'Show a thing.',
|
|
145
|
+
usage: 'thing show <id>',
|
|
146
|
+
options: { verbose: { kind: 'boolean', describe: 'More detail.' } },
|
|
147
|
+
async run({ positionals, io }) {
|
|
148
|
+
io.field('Id', positionals[0]!);
|
|
149
|
+
io.result({ id: positionals[0] }); // printed only under --json
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Three conventions: use `io` rather than `console.log`, since that is what makes
|
|
155
|
+
`--json` and `--quiet` work; throw a `CliError` with a `hint` rather than
|
|
156
|
+
calling `process.exit`; and use `usageError(...)` for a bad command line, so it
|
|
157
|
+
exits 2 rather than 1.
|
|
158
|
+
|
|
159
|
+
## Credits
|
|
160
|
+
|
|
161
|
+
QR rendering by [fey](https://github.com/acutenetwork/fey).
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62.23 49.92">
|
|
2
|
+
<path d="M62.13,47.37L50.03,1.52c-.21-.81-.78-1.32-1.49-1.48-.58-.13-1.26.07-1.8.61L.64,46.42c-.83.82-.83,1.94-.12,2.69.66.7,1.77.83,2.62.13,3.87-3.24,7.98-5.95,12.62-7.96,15.26-6.64,30.8-2.19,43.2,8.07.74.61,1.69.8,2.43.29.67-.47,1.03-1.18.74-2.26ZM30.57,29.88c-.54,0-1.08.01-1.61.03l15.88-15.76,5.28,20.01c-6.36-2.82-12.95-4.27-19.54-4.27Z" fill="#f2effc" />
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62.23 49.92">
|
|
2
|
+
<path d="M62.13,47.37L50.03,1.52c-.21-.81-.78-1.32-1.49-1.48-.58-.13-1.26.07-1.8.61L.64,46.42c-.83.82-.83,1.94-.12,2.69.66.7,1.77.83,2.62.13,3.87-3.24,7.98-5.95,12.62-7.96,15.26-6.64,30.8-2.19,43.2,8.07.74.61,1.69.8,2.43.29.67-.47,1.03-1.18.74-2.26ZM30.57,29.88c-.54,0-1.08.01-1.61.03l15.88-15.76,5.28,20.01c-6.36-2.82-12.95-4.27-19.54-4.27Z" fill="#5200a0" />
|
|
3
|
+
</svg>
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// package.json
|
|
4
|
+
var package_default = {
|
|
5
|
+
name: "@acutenetwork/cli",
|
|
6
|
+
version: "0.1.0",
|
|
7
|
+
description: "The command line for Acute.",
|
|
8
|
+
license: "MIT",
|
|
9
|
+
author: "Acute's Engineering Team <engineering@acute.network>",
|
|
10
|
+
homepage: "https://github.com/acutenetwork/acute-cli#readme",
|
|
11
|
+
repository: {
|
|
12
|
+
type: "git",
|
|
13
|
+
url: "git+https://github.com/acutenetwork/acute-cli.git"
|
|
14
|
+
},
|
|
15
|
+
bugs: {
|
|
16
|
+
url: "https://github.com/acutenetwork/acute-cli/issues"
|
|
17
|
+
},
|
|
18
|
+
keywords: [
|
|
19
|
+
"acute",
|
|
20
|
+
"cli",
|
|
21
|
+
"acute-network",
|
|
22
|
+
"payments",
|
|
23
|
+
"qr",
|
|
24
|
+
"qr-code"
|
|
25
|
+
],
|
|
26
|
+
type: "module",
|
|
27
|
+
bin: {
|
|
28
|
+
acute: "./dist/bin.js"
|
|
29
|
+
},
|
|
30
|
+
files: [
|
|
31
|
+
"dist",
|
|
32
|
+
"assets",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
engines: {
|
|
37
|
+
node: ">=20.0.0"
|
|
38
|
+
},
|
|
39
|
+
publishConfig: {
|
|
40
|
+
access: "public"
|
|
41
|
+
},
|
|
42
|
+
scripts: {
|
|
43
|
+
build: "tsup",
|
|
44
|
+
dev: "tsup --watch",
|
|
45
|
+
test: "vitest run",
|
|
46
|
+
"test:watch": "vitest",
|
|
47
|
+
"type-check": "tsc --noEmit",
|
|
48
|
+
lint: "eslint .",
|
|
49
|
+
"lint:fix": "eslint . --fix",
|
|
50
|
+
format: "prettier --write .",
|
|
51
|
+
clean: "rm -rf dist",
|
|
52
|
+
prepublishOnly: "pnpm clean && pnpm build"
|
|
53
|
+
},
|
|
54
|
+
dependencies: {
|
|
55
|
+
"@acutenetwork/fey": "^0.2.2",
|
|
56
|
+
jsqr: "^1.4.0",
|
|
57
|
+
"qrcode-generator": "^1.4.4",
|
|
58
|
+
sharp: "^0.35.5"
|
|
59
|
+
},
|
|
60
|
+
devDependencies: {
|
|
61
|
+
"@eslint/js": "^9.12.0",
|
|
62
|
+
"@types/node": "^22.10.0",
|
|
63
|
+
eslint: "^9.12.0",
|
|
64
|
+
"eslint-config-prettier": "^9.1.0",
|
|
65
|
+
prettier: "^3.3.3",
|
|
66
|
+
tsup: "^8.4.0",
|
|
67
|
+
typescript: "^5.7.0",
|
|
68
|
+
"typescript-eslint": "^8.8.1",
|
|
69
|
+
vitest: "^3.0.0"
|
|
70
|
+
},
|
|
71
|
+
packageManager: "pnpm@11.7.0"
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/kernel/config.ts
|
|
75
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
76
|
+
import { homedir } from "os";
|
|
77
|
+
import { dirname, join } from "path";
|
|
78
|
+
|
|
79
|
+
// src/kernel/errors.ts
|
|
80
|
+
var CliError = class extends Error {
|
|
81
|
+
exitCode;
|
|
82
|
+
/** One line under the error saying what to do instead. */
|
|
83
|
+
hint;
|
|
84
|
+
constructor(message, options = {}) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = "CliError";
|
|
87
|
+
this.exitCode = options.exitCode ?? ExitCode.Failure;
|
|
88
|
+
this.hint = options.hint;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var ExitCode = {
|
|
92
|
+
Ok: 0,
|
|
93
|
+
/** The command ran and could not do what was asked. */
|
|
94
|
+
Failure: 1,
|
|
95
|
+
/** The command line itself was wrong: unknown flag, missing argument. */
|
|
96
|
+
Usage: 2
|
|
97
|
+
};
|
|
98
|
+
function usageError(message, hint) {
|
|
99
|
+
return new CliError(message, { exitCode: ExitCode.Usage, ...hint ? { hint } : {} });
|
|
100
|
+
}
|
|
101
|
+
function distance(a, b) {
|
|
102
|
+
const rows = a.length + 1;
|
|
103
|
+
const cols = b.length + 1;
|
|
104
|
+
const d = new Array(rows * cols).fill(0);
|
|
105
|
+
for (let i = 0; i < rows; i += 1) d[i * cols] = i;
|
|
106
|
+
for (let j = 0; j < cols; j += 1) d[j] = j;
|
|
107
|
+
for (let i = 1; i < rows; i += 1) {
|
|
108
|
+
for (let j = 1; j < cols; j += 1) {
|
|
109
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
110
|
+
d[i * cols + j] = Math.min(
|
|
111
|
+
d[(i - 1) * cols + j] + 1,
|
|
112
|
+
d[i * cols + (j - 1)] + 1,
|
|
113
|
+
d[(i - 1) * cols + (j - 1)] + cost
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return d[rows * cols - 1];
|
|
118
|
+
}
|
|
119
|
+
function nearest(input, candidates) {
|
|
120
|
+
let best = null;
|
|
121
|
+
for (const name of candidates) {
|
|
122
|
+
const score = distance(input.toLowerCase(), name.toLowerCase());
|
|
123
|
+
if (!best || score < best.score) best = { name, score };
|
|
124
|
+
}
|
|
125
|
+
if (!best || best.score > Math.max(2, Math.floor(input.length / 2))) return null;
|
|
126
|
+
return best.name;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/kernel/config.ts
|
|
130
|
+
var SETTINGS = {
|
|
131
|
+
"qr.size": {
|
|
132
|
+
describe: "Pixel width of a generated code.",
|
|
133
|
+
parse: (raw) => positiveInt("qr.size", raw, 64, 8192)
|
|
134
|
+
},
|
|
135
|
+
"qr.format": {
|
|
136
|
+
describe: "Default output format: png, svg, jpeg or webp.",
|
|
137
|
+
parse: (raw) => oneOf("qr.format", raw, ["png", "svg", "jpeg", "webp"])
|
|
138
|
+
},
|
|
139
|
+
"qr.out": {
|
|
140
|
+
describe: "Directory (or file path) a generated code is written to.",
|
|
141
|
+
parse: (raw) => raw
|
|
142
|
+
},
|
|
143
|
+
"qr.mark": {
|
|
144
|
+
describe: "Centre mark: purple, light, none, or a path to an image.",
|
|
145
|
+
parse: (raw) => raw
|
|
146
|
+
},
|
|
147
|
+
"qr.foreground": {
|
|
148
|
+
describe: "Module colour, as a CSS colour.",
|
|
149
|
+
parse: (raw) => raw
|
|
150
|
+
},
|
|
151
|
+
"qr.background": {
|
|
152
|
+
describe: 'Background colour, or "transparent".',
|
|
153
|
+
parse: (raw) => raw
|
|
154
|
+
},
|
|
155
|
+
"qr.errorCorrection": {
|
|
156
|
+
describe: "Error correction: auto, L, M, Q or H.",
|
|
157
|
+
parse: (raw) => oneOf("qr.errorCorrection", raw, ["auto", "L", "M", "Q", "H"])
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
function positiveInt(key, raw, min, max) {
|
|
161
|
+
const n = Number(raw);
|
|
162
|
+
if (!Number.isInteger(n) || n < min || n > max) {
|
|
163
|
+
throw usageError(`${key} must be a whole number between ${min} and ${max}, got "${raw}".`);
|
|
164
|
+
}
|
|
165
|
+
return n;
|
|
166
|
+
}
|
|
167
|
+
function oneOf(key, raw, allowed) {
|
|
168
|
+
if (!allowed.includes(raw)) {
|
|
169
|
+
throw usageError(`${key} must be one of ${allowed.join(", ")}, got "${raw}".`);
|
|
170
|
+
}
|
|
171
|
+
return raw;
|
|
172
|
+
}
|
|
173
|
+
function settingKeys() {
|
|
174
|
+
return Object.keys(SETTINGS);
|
|
175
|
+
}
|
|
176
|
+
function describeSetting(key) {
|
|
177
|
+
return SETTINGS[key]?.describe;
|
|
178
|
+
}
|
|
179
|
+
function configPath() {
|
|
180
|
+
const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
181
|
+
return join(base, "acute", "config.json");
|
|
182
|
+
}
|
|
183
|
+
function loadConfig() {
|
|
184
|
+
const path2 = configPath();
|
|
185
|
+
const stored = read(path2);
|
|
186
|
+
return {
|
|
187
|
+
path: path2,
|
|
188
|
+
get: (key) => stored[key],
|
|
189
|
+
entries: () => Object.entries(stored).sort(([a], [b]) => a.localeCompare(b)),
|
|
190
|
+
set(key, raw) {
|
|
191
|
+
const setting = SETTINGS[key];
|
|
192
|
+
if (!setting) throw unknownKey(key);
|
|
193
|
+
stored[key] = setting.parse(raw);
|
|
194
|
+
write(path2, stored);
|
|
195
|
+
},
|
|
196
|
+
unset(key) {
|
|
197
|
+
if (!SETTINGS[key]) throw unknownKey(key);
|
|
198
|
+
delete stored[key];
|
|
199
|
+
write(path2, stored);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function unknownKey(key) {
|
|
204
|
+
const suggestion = nearest(key, settingKeys());
|
|
205
|
+
return usageError(
|
|
206
|
+
`"${key}" is not an Acute setting.`,
|
|
207
|
+
suggestion ? `Did you mean ${suggestion}? Run "acute config list" for all of them.` : void 0
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
function read(path2) {
|
|
211
|
+
let raw;
|
|
212
|
+
try {
|
|
213
|
+
raw = readFileSync(path2, "utf8");
|
|
214
|
+
} catch {
|
|
215
|
+
return {};
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const parsed = JSON.parse(raw);
|
|
219
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
220
|
+
throw new Error("not an object");
|
|
221
|
+
return Object.fromEntries(
|
|
222
|
+
Object.entries(parsed).filter(
|
|
223
|
+
([key, value]) => key in SETTINGS && (typeof value === "string" || typeof value === "number")
|
|
224
|
+
)
|
|
225
|
+
);
|
|
226
|
+
} catch {
|
|
227
|
+
throw new CliError(`${path2} is not valid JSON.`, {
|
|
228
|
+
hint: 'Fix it by hand, or delete it and set the values again with "acute config set".'
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function write(path2, value) {
|
|
233
|
+
mkdirSync(dirname(path2), { recursive: true });
|
|
234
|
+
writeFileSync(path2, `${JSON.stringify(value, null, 2)}
|
|
235
|
+
`, "utf8");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/commands/config.ts
|
|
239
|
+
var list = {
|
|
240
|
+
name: "list",
|
|
241
|
+
summary: "Show every setting and what it is set to.",
|
|
242
|
+
usage: "config list",
|
|
243
|
+
options: {},
|
|
244
|
+
async run({ config, io }) {
|
|
245
|
+
const stored = new Map(config.entries());
|
|
246
|
+
const keys = settingKeys();
|
|
247
|
+
const width = Math.max(...keys.map((k) => k.length));
|
|
248
|
+
io.say();
|
|
249
|
+
for (const key of keys) {
|
|
250
|
+
const value = stored.get(key);
|
|
251
|
+
const shown = value === void 0 ? io.style.dim("unset") : String(value);
|
|
252
|
+
io.say(` ${key.padEnd(width + 2)}${shown}`);
|
|
253
|
+
io.say(` ${" ".repeat(width + 2)}${io.style.dim(describeSetting(key) ?? "")}`);
|
|
254
|
+
}
|
|
255
|
+
io.say();
|
|
256
|
+
io.say(` ${io.style.dim(config.path)}`);
|
|
257
|
+
io.say();
|
|
258
|
+
io.result(Object.fromEntries(config.entries()));
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
var get = {
|
|
262
|
+
name: "get",
|
|
263
|
+
summary: "Print one setting.",
|
|
264
|
+
usage: "config get <key>",
|
|
265
|
+
options: {},
|
|
266
|
+
async run({ positionals, config, io }) {
|
|
267
|
+
const key = positionals[0];
|
|
268
|
+
if (!key) throw usageError("Which setting?", `One of: ${settingKeys().join(", ")}`);
|
|
269
|
+
const value = config.get(key);
|
|
270
|
+
if (value === void 0) {
|
|
271
|
+
io.say(io.style.dim("unset"));
|
|
272
|
+
io.result({ key, value: null });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
io.say(String(value));
|
|
276
|
+
io.result({ key, value });
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
var set = {
|
|
280
|
+
name: "set",
|
|
281
|
+
summary: "Change one setting.",
|
|
282
|
+
usage: "config set <key> <value>",
|
|
283
|
+
options: {},
|
|
284
|
+
examples: ["acute config set qr.out ~/Desktop", "acute config set qr.size 2048"],
|
|
285
|
+
async run({ positionals, config, io }) {
|
|
286
|
+
const [key, value] = positionals;
|
|
287
|
+
if (!key) throw usageError("Which setting?", `One of: ${settingKeys().join(", ")}`);
|
|
288
|
+
if (value === void 0) throw usageError(`What should ${key} be?`);
|
|
289
|
+
config.set(key, value);
|
|
290
|
+
io.say(` ${key} = ${value}`);
|
|
291
|
+
io.result({ key, value: config.get(key), path: config.path });
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
var unset = {
|
|
295
|
+
name: "unset",
|
|
296
|
+
summary: "Go back to the default for one setting.",
|
|
297
|
+
usage: "config unset <key>",
|
|
298
|
+
options: {},
|
|
299
|
+
async run({ positionals, config, io }) {
|
|
300
|
+
const key = positionals[0];
|
|
301
|
+
if (!key) throw usageError("Which setting?", `One of: ${settingKeys().join(", ")}`);
|
|
302
|
+
config.unset(key);
|
|
303
|
+
io.say(` ${key} is back to its default`);
|
|
304
|
+
io.result({ key, value: null, path: config.path });
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
var path = {
|
|
308
|
+
name: "path",
|
|
309
|
+
summary: "Print where the settings file lives.",
|
|
310
|
+
usage: "config path",
|
|
311
|
+
options: {},
|
|
312
|
+
async run({ config, io }) {
|
|
313
|
+
io.say(config.path);
|
|
314
|
+
io.result({ path: config.path });
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
var configGroup = {
|
|
318
|
+
name: "config",
|
|
319
|
+
summary: "Defaults, so the flags you always pass become the flags you never pass.",
|
|
320
|
+
commands: [list, get, set, unset, path],
|
|
321
|
+
defaultCommand: "list"
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// src/commands/qr/decode.ts
|
|
325
|
+
import { readFile } from "fs/promises";
|
|
326
|
+
import { isAbsolute, resolve } from "path";
|
|
327
|
+
|
|
328
|
+
// src/qr/decode.ts
|
|
329
|
+
import jsQR from "jsqr";
|
|
330
|
+
import sharp from "sharp";
|
|
331
|
+
async function decodeQr(image) {
|
|
332
|
+
const { data, info } = await sharp(image).flatten({ background: "#FFFFFF" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
333
|
+
const pixels = new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength);
|
|
334
|
+
const found = jsQR(pixels, info.width, info.height);
|
|
335
|
+
return found ? { value: found.data, width: info.width, height: info.height } : null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/commands/qr/decode.ts
|
|
339
|
+
var decode = {
|
|
340
|
+
name: "decode",
|
|
341
|
+
summary: "Read the value back out of a QR image.",
|
|
342
|
+
usage: "qr decode <image>",
|
|
343
|
+
options: {},
|
|
344
|
+
details: [
|
|
345
|
+
"Reads any QR image, not only ones this CLI wrote. It is the check worth running before a code goes on a page or into a deck: what a scanner sees is the only thing that matters, and it is not always what you typed."
|
|
346
|
+
],
|
|
347
|
+
examples: ["acute qr decode ./acute-qr.png", "acute qr decode ~/Desktop/poster.jpg --json"],
|
|
348
|
+
async run({ positionals, io }) {
|
|
349
|
+
const target = positionals[0];
|
|
350
|
+
if (!target) throw usageError("Nothing to decode.", "Pass the image: acute qr decode <image>");
|
|
351
|
+
const path2 = isAbsolute(target) ? target : resolve(process.cwd(), target);
|
|
352
|
+
let file;
|
|
353
|
+
try {
|
|
354
|
+
file = await readFile(path2);
|
|
355
|
+
} catch {
|
|
356
|
+
throw new CliError(`Could not read ${path2}.`);
|
|
357
|
+
}
|
|
358
|
+
const found = await decodeQr(file);
|
|
359
|
+
if (!found) {
|
|
360
|
+
throw new CliError(`No QR code found in ${path2}.`, {
|
|
361
|
+
hint: "A tight crop, a low resolution or a heavy background can all hide one. Try the original file."
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
io.say();
|
|
365
|
+
io.field("Value", found.value);
|
|
366
|
+
io.field("Image", `${found.width}\xD7${found.height}`);
|
|
367
|
+
io.say();
|
|
368
|
+
io.result({ value: found.value, path: path2, width: found.width, height: found.height });
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// src/commands/qr/generate.ts
|
|
373
|
+
import { spawn } from "child_process";
|
|
374
|
+
import { statSync } from "fs";
|
|
375
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
376
|
+
import { dirname as dirname2, extname, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
|
|
377
|
+
|
|
378
|
+
// src/brand.ts
|
|
379
|
+
import { fileURLToPath } from "url";
|
|
380
|
+
var Brand = {
|
|
381
|
+
/** `AcutePurple[500]`, the main brand purple. The mark is filled with this. */
|
|
382
|
+
purple: "#5200A0",
|
|
383
|
+
/** `AcutePurple[300]`, the secondary purple. Readable on a dark surface. */
|
|
384
|
+
purpleAccent: "#9A5CFF",
|
|
385
|
+
/** `AcuteGreen[400]`, the brand green. A fill, never ink. */
|
|
386
|
+
green: "#13FB79",
|
|
387
|
+
/** `Tint.black`, true black. What QR modules are drawn in. */
|
|
388
|
+
black: "#000000",
|
|
389
|
+
white: "#FFFFFF"
|
|
390
|
+
};
|
|
391
|
+
var NAMED = {
|
|
392
|
+
black: Brand.black,
|
|
393
|
+
white: Brand.white,
|
|
394
|
+
purple: Brand.purple,
|
|
395
|
+
"purple-accent": Brand.purpleAccent,
|
|
396
|
+
green: Brand.green
|
|
397
|
+
};
|
|
398
|
+
function resolveColour(raw) {
|
|
399
|
+
if (raw === "transparent") return raw;
|
|
400
|
+
return NAMED[raw.toLowerCase()] ?? raw;
|
|
401
|
+
}
|
|
402
|
+
function markFile(variant) {
|
|
403
|
+
return fileURLToPath(new URL(`../assets/mark-${variant}.svg`, import.meta.url));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/kernel/args.ts
|
|
407
|
+
import { parseArgs } from "util";
|
|
408
|
+
var GLOBAL_OPTIONS = {
|
|
409
|
+
help: { kind: "boolean", short: "h", describe: "Show help for this command." },
|
|
410
|
+
json: { kind: "boolean", describe: "Print the result as JSON instead of prose." },
|
|
411
|
+
quiet: { kind: "boolean", short: "q", describe: "Print nothing but errors." },
|
|
412
|
+
version: { kind: "boolean", short: "v", describe: "Print the CLI's version." }
|
|
413
|
+
};
|
|
414
|
+
function parse(argv, options) {
|
|
415
|
+
const merged = { ...GLOBAL_OPTIONS, ...options };
|
|
416
|
+
const config = Object.fromEntries(
|
|
417
|
+
Object.entries(merged).map(([name, spec]) => [
|
|
418
|
+
name,
|
|
419
|
+
spec.short ? { type: spec.kind, short: spec.short } : { type: spec.kind }
|
|
420
|
+
])
|
|
421
|
+
);
|
|
422
|
+
try {
|
|
423
|
+
const { values, positionals } = parseArgs({
|
|
424
|
+
args: argv,
|
|
425
|
+
options: config,
|
|
426
|
+
allowPositionals: true,
|
|
427
|
+
strict: true
|
|
428
|
+
});
|
|
429
|
+
return { flags: values, positionals };
|
|
430
|
+
} catch (error) {
|
|
431
|
+
throw asUsageError(error, Object.keys(merged));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function asUsageError(error, known) {
|
|
435
|
+
if (!(error instanceof Error)) return usageError("Could not read the command line.");
|
|
436
|
+
const unknownFlag = /Unknown option '(-{1,2}[^']+)'/.exec(error.message);
|
|
437
|
+
if (unknownFlag) {
|
|
438
|
+
const flag = unknownFlag[1];
|
|
439
|
+
const suggestion = nearest(flag.replace(/^-+/, ""), known);
|
|
440
|
+
return usageError(
|
|
441
|
+
`Unknown option ${flag}.`,
|
|
442
|
+
suggestion ? `Did you mean --${suggestion}?` : "Run the command with --help to see its options."
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const missingValue = /Option '(-{1,2}[^']+)[^']*' argument missing/.exec(error.message);
|
|
446
|
+
if (missingValue) {
|
|
447
|
+
return usageError(`${missingValue[1]} needs a value.`);
|
|
448
|
+
}
|
|
449
|
+
return usageError(error.message);
|
|
450
|
+
}
|
|
451
|
+
function stringFlag(flags, name) {
|
|
452
|
+
const value = flags[name];
|
|
453
|
+
if (value === void 0) return void 0;
|
|
454
|
+
if (typeof value !== "string") throw usageError(`--${name} needs a value.`);
|
|
455
|
+
return value;
|
|
456
|
+
}
|
|
457
|
+
function intFlag(flags, name, range) {
|
|
458
|
+
const raw = stringFlag(flags, name);
|
|
459
|
+
if (raw === void 0) return void 0;
|
|
460
|
+
const n = Number(raw);
|
|
461
|
+
if (!Number.isInteger(n) || n < range.min || n > range.max) {
|
|
462
|
+
throw usageError(
|
|
463
|
+
`--${name} must be a whole number between ${range.min} and ${range.max}, got "${raw}".`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
return n;
|
|
467
|
+
}
|
|
468
|
+
function enumFlag(flags, name, allowed) {
|
|
469
|
+
const raw = stringFlag(flags, name);
|
|
470
|
+
if (raw === void 0) return void 0;
|
|
471
|
+
if (!allowed.includes(raw)) {
|
|
472
|
+
const suggestion = nearest(raw, allowed);
|
|
473
|
+
throw usageError(
|
|
474
|
+
`--${name} must be one of ${allowed.join(", ")}, got "${raw}".`,
|
|
475
|
+
suggestion ? `Did you mean --${name} ${suggestion}?` : void 0
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
return raw;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/qr/render.ts
|
|
482
|
+
import { QRCode } from "@acutenetwork/fey/node";
|
|
483
|
+
import sharp3 from "sharp";
|
|
484
|
+
|
|
485
|
+
// src/qr/mark.ts
|
|
486
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
487
|
+
import sharp2 from "sharp";
|
|
488
|
+
async function loadMark(source, size) {
|
|
489
|
+
if (source.kind === "none") return null;
|
|
490
|
+
const file = source.kind === "builtin" ? markFile(source.variant) : source.path;
|
|
491
|
+
let input;
|
|
492
|
+
try {
|
|
493
|
+
input = await readFile2(file);
|
|
494
|
+
} catch {
|
|
495
|
+
throw new CliError(`Could not read the mark image at ${file}.`);
|
|
496
|
+
}
|
|
497
|
+
const target = Math.max(256, Math.round(size * 0.5));
|
|
498
|
+
let png;
|
|
499
|
+
try {
|
|
500
|
+
png = await sharp2(input).resize({ width: target, withoutEnlargement: false }).png().toBuffer();
|
|
501
|
+
} catch (error) {
|
|
502
|
+
throw new CliError(
|
|
503
|
+
`${file} is not an image this can rasterize (${error instanceof Error ? error.message : "unknown error"}).`,
|
|
504
|
+
{ hint: "Use a PNG, JPEG or SVG, or pass --mark none." }
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
const { width, height } = await sharp2(png).metadata();
|
|
508
|
+
if (!width || !height) throw new CliError(`Could not measure the mark image at ${file}.`);
|
|
509
|
+
return { width, height, dataUri: `data:image/png;base64,${png.toString("base64")}` };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/qr/spec.ts
|
|
513
|
+
var QR_DEFAULTS = {
|
|
514
|
+
size: 1024,
|
|
515
|
+
format: "png",
|
|
516
|
+
foreground: Brand.black,
|
|
517
|
+
/**
|
|
518
|
+
* Transparent, like the app. A code is an asset that gets dropped onto
|
|
519
|
+
* whatever surface it is going on, and a baked-in white square is the one
|
|
520
|
+
* thing you cannot undo later. `--bg white` paints a card when the code has
|
|
521
|
+
* to stand alone (a dark slide, a phone's photo library) and a black code on
|
|
522
|
+
* nothing would otherwise be invisible.
|
|
523
|
+
*/
|
|
524
|
+
background: "transparent",
|
|
525
|
+
mark: "purple",
|
|
526
|
+
errorCorrection: "auto"
|
|
527
|
+
};
|
|
528
|
+
function defaultMargin(size) {
|
|
529
|
+
return Math.round(size * 0.08);
|
|
530
|
+
}
|
|
531
|
+
function resolveErrorCorrection(requested, value) {
|
|
532
|
+
if (requested !== "auto") return requested;
|
|
533
|
+
return value.length <= 64 ? "H" : "Q";
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/qr/render.ts
|
|
537
|
+
function buildOptions(spec) {
|
|
538
|
+
const errorCorrectionLevel = resolveErrorCorrection(spec.errorCorrection, spec.value);
|
|
539
|
+
return {
|
|
540
|
+
width: spec.size,
|
|
541
|
+
height: spec.size,
|
|
542
|
+
margin: spec.margin,
|
|
543
|
+
data: spec.value,
|
|
544
|
+
qrOptions: { errorCorrectionLevel },
|
|
545
|
+
imageOptions: { imageSize: 0.25, margin: 6, hideBackgroundDots: true },
|
|
546
|
+
dotsOptions: { color: spec.foreground, type: "extra-rounded", roundSize: false },
|
|
547
|
+
cornersSquareOptions: { type: "extra-rounded", color: spec.foreground },
|
|
548
|
+
cornersDotOptions: { type: "extra-rounded", color: spec.foreground },
|
|
549
|
+
backgroundOptions: { color: spec.background }
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
async function renderSvg(spec) {
|
|
553
|
+
const options = buildOptions(spec);
|
|
554
|
+
const mark = await loadMark(spec.mark, spec.size);
|
|
555
|
+
const qr = new QRCode({
|
|
556
|
+
...options,
|
|
557
|
+
...mark ? { image: "acute-mark", imageLoader: async () => mark } : {}
|
|
558
|
+
});
|
|
559
|
+
return qr.getSVG();
|
|
560
|
+
}
|
|
561
|
+
async function render(spec, format) {
|
|
562
|
+
const svg = await renderSvg(spec);
|
|
563
|
+
const errorCorrection = resolveErrorCorrection(spec.errorCorrection, spec.value);
|
|
564
|
+
if (format === "svg") {
|
|
565
|
+
return { bytes: Buffer.from(svg, "utf8"), format, errorCorrection };
|
|
566
|
+
}
|
|
567
|
+
const image = sharp3(Buffer.from(svg, "utf8"));
|
|
568
|
+
const flattened = format === "jpeg" && spec.background === "transparent" ? image.flatten({ background: "#FFFFFF" }) : image;
|
|
569
|
+
try {
|
|
570
|
+
const bytes = format === "png" ? await flattened.png({ compressionLevel: 9 }).toBuffer() : format === "jpeg" ? await flattened.jpeg({ quality: 92 }).toBuffer() : await flattened.webp({ quality: 92 }).toBuffer();
|
|
571
|
+
return { bytes, format, errorCorrection };
|
|
572
|
+
} catch (error) {
|
|
573
|
+
throw new CliError(
|
|
574
|
+
`Could not rasterize the code: ${error instanceof Error ? error.message : "unknown error"}.`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/qr/terminal.ts
|
|
580
|
+
import qrcode from "qrcode-generator";
|
|
581
|
+
var BLACK = [0, 0, 0];
|
|
582
|
+
var WHITE = [255, 255, 255];
|
|
583
|
+
function toTerminal(value, errorCorrection) {
|
|
584
|
+
const qr = qrcode(0, errorCorrection);
|
|
585
|
+
qr.addData(value);
|
|
586
|
+
qr.make();
|
|
587
|
+
const count = qr.getModuleCount();
|
|
588
|
+
const QUIET = 4;
|
|
589
|
+
const size = count + QUIET * 2;
|
|
590
|
+
const dark = (row, col) => {
|
|
591
|
+
const r = row - QUIET;
|
|
592
|
+
const c = col - QUIET;
|
|
593
|
+
if (r < 0 || c < 0 || r >= count || c >= count) return false;
|
|
594
|
+
return qr.isDark(r, c);
|
|
595
|
+
};
|
|
596
|
+
const lines = [];
|
|
597
|
+
for (let row = 0; row < size; row += 2) {
|
|
598
|
+
let line = "";
|
|
599
|
+
let currentFg = null;
|
|
600
|
+
let currentBg = null;
|
|
601
|
+
for (let col = 0; col < size; col += 1) {
|
|
602
|
+
const top = dark(row, col);
|
|
603
|
+
const bottom = dark(row + 1, col);
|
|
604
|
+
if (top !== currentFg) {
|
|
605
|
+
line += `\x1B[38;2;${(top ? BLACK : WHITE).join(";")}m`;
|
|
606
|
+
currentFg = top;
|
|
607
|
+
}
|
|
608
|
+
if (bottom !== currentBg) {
|
|
609
|
+
line += `\x1B[48;2;${(bottom ? BLACK : WHITE).join(";")}m`;
|
|
610
|
+
currentBg = bottom;
|
|
611
|
+
}
|
|
612
|
+
line += "\u2580";
|
|
613
|
+
}
|
|
614
|
+
lines.push(`${line}\x1B[0m`);
|
|
615
|
+
}
|
|
616
|
+
return lines.join("\n");
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/commands/qr/generate.ts
|
|
620
|
+
var FORMATS = ["png", "svg", "jpeg", "webp"];
|
|
621
|
+
var LEVELS = ["auto", "L", "M", "Q", "H"];
|
|
622
|
+
var EXTENSIONS = {
|
|
623
|
+
".png": "png",
|
|
624
|
+
".svg": "svg",
|
|
625
|
+
".jpg": "jpeg",
|
|
626
|
+
".jpeg": "jpeg",
|
|
627
|
+
".webp": "webp"
|
|
628
|
+
};
|
|
629
|
+
var generate = {
|
|
630
|
+
name: "generate",
|
|
631
|
+
summary: "Write an Acute QR code for a value.",
|
|
632
|
+
usage: "qr <value> [options]",
|
|
633
|
+
options: {
|
|
634
|
+
out: {
|
|
635
|
+
kind: "string",
|
|
636
|
+
short: "o",
|
|
637
|
+
placeholder: "path",
|
|
638
|
+
describe: "File to write, or a directory to write into.",
|
|
639
|
+
defaultsTo: "./acute-qr.png"
|
|
640
|
+
},
|
|
641
|
+
format: {
|
|
642
|
+
kind: "string",
|
|
643
|
+
short: "f",
|
|
644
|
+
placeholder: FORMATS.join("|"),
|
|
645
|
+
describe: "Output format. Taken from --out when it has an extension.",
|
|
646
|
+
defaultsTo: QR_DEFAULTS.format
|
|
647
|
+
},
|
|
648
|
+
size: {
|
|
649
|
+
kind: "string",
|
|
650
|
+
short: "s",
|
|
651
|
+
placeholder: "px",
|
|
652
|
+
describe: "Width and height of the code.",
|
|
653
|
+
defaultsTo: String(QR_DEFAULTS.size)
|
|
654
|
+
},
|
|
655
|
+
margin: {
|
|
656
|
+
kind: "string",
|
|
657
|
+
placeholder: "px",
|
|
658
|
+
describe: "Quiet zone around the code.",
|
|
659
|
+
defaultsTo: "8% of --size"
|
|
660
|
+
},
|
|
661
|
+
fg: {
|
|
662
|
+
kind: "string",
|
|
663
|
+
placeholder: "colour",
|
|
664
|
+
describe: "Module colour. A brand word or any CSS colour.",
|
|
665
|
+
defaultsTo: "black"
|
|
666
|
+
},
|
|
667
|
+
bg: {
|
|
668
|
+
kind: "string",
|
|
669
|
+
placeholder: "colour",
|
|
670
|
+
describe: 'Background colour, a brand word, or "transparent".',
|
|
671
|
+
defaultsTo: "transparent"
|
|
672
|
+
},
|
|
673
|
+
mark: {
|
|
674
|
+
kind: "string",
|
|
675
|
+
placeholder: "purple|light|none|path",
|
|
676
|
+
describe: "The mark inlaid at the centre.",
|
|
677
|
+
defaultsTo: QR_DEFAULTS.mark
|
|
678
|
+
},
|
|
679
|
+
ec: {
|
|
680
|
+
kind: "string",
|
|
681
|
+
placeholder: LEVELS.join("|"),
|
|
682
|
+
describe: "Error correction level.",
|
|
683
|
+
defaultsTo: QR_DEFAULTS.errorCorrection
|
|
684
|
+
},
|
|
685
|
+
print: { kind: "boolean", short: "p", describe: "Also print the code in the terminal." },
|
|
686
|
+
stdout: { kind: "boolean", describe: "Write the bytes to stdout instead of a file." },
|
|
687
|
+
open: { kind: "boolean", describe: "Open the written file when it is done." }
|
|
688
|
+
},
|
|
689
|
+
details: [
|
|
690
|
+
"The styling is the app\u2019s: extra-rounded dots and corners, the Acute mark inlaid at 25%. A code written here and a code the app renders for the same value are the same object.",
|
|
691
|
+
"The background is transparent, like the app\u2019s, so the code drops onto whatever surface it is going on. Pass --bg white when it has to stand alone: a black code on nothing is invisible on a dark slide or in a phone\u2019s photo library.",
|
|
692
|
+
"Error correction defaults to auto: H for a short payload, where the mark covers relatively more of a low-version code, and Q for a long URL, where the code is dense enough that Q is plenty.",
|
|
693
|
+
"--fg and --bg take a brand word (black, white, purple, purple-accent, green) or any CSS colour."
|
|
694
|
+
],
|
|
695
|
+
examples: [
|
|
696
|
+
"acute qr https://checkout.acute.network/c/chk_live_abc",
|
|
697
|
+
"acute qr acu664463900542trf --out ~/Desktop --size 2048",
|
|
698
|
+
'acute qr "https://acute.ink/t/emeka" --bg transparent --margin 0 --format svg',
|
|
699
|
+
"acute qr https://acute.network --stdout > code.png",
|
|
700
|
+
"acute qr acu664463900542trf --print # scan it straight off the screen"
|
|
701
|
+
],
|
|
702
|
+
async run(ctx) {
|
|
703
|
+
const { io, config } = ctx;
|
|
704
|
+
const value = ctx.positionals[0];
|
|
705
|
+
if (!value) {
|
|
706
|
+
throw usageError("Nothing to encode.", "Pass the value: acute qr <value>");
|
|
707
|
+
}
|
|
708
|
+
if (ctx.positionals.length > 1) {
|
|
709
|
+
throw usageError(
|
|
710
|
+
`Expected one value, got ${ctx.positionals.length}.`,
|
|
711
|
+
'Quote it if it contains spaces: acute qr "one value here"'
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
const toStdout = ctx.flags.stdout === true;
|
|
715
|
+
if (toStdout && stringFlag(ctx.flags, "out")) {
|
|
716
|
+
throw usageError("--out and --stdout both say where the bytes go. Pick one.");
|
|
717
|
+
}
|
|
718
|
+
const spec = resolveSpec(value, ctx, config);
|
|
719
|
+
const format = resolveFormat(ctx, config);
|
|
720
|
+
const rendered = await render(spec, format);
|
|
721
|
+
if (ctx.flags.print === true) {
|
|
722
|
+
io.say();
|
|
723
|
+
io.say(toTerminal(spec.value, rendered.errorCorrection));
|
|
724
|
+
}
|
|
725
|
+
if (toStdout) {
|
|
726
|
+
process.stdout.write(rendered.bytes);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const path2 = resolveOut(ctx, config, format);
|
|
730
|
+
await mkdir(dirname2(path2), { recursive: true });
|
|
731
|
+
await writeFile(path2, rendered.bytes);
|
|
732
|
+
io.say();
|
|
733
|
+
io.field("Acute QR", `${spec.size}\xD7${spec.size} ${format}, level ${rendered.errorCorrection}`);
|
|
734
|
+
io.field("Value", spec.value);
|
|
735
|
+
io.field("Written", path2);
|
|
736
|
+
io.say();
|
|
737
|
+
io.result({
|
|
738
|
+
path: path2,
|
|
739
|
+
format,
|
|
740
|
+
size: spec.size,
|
|
741
|
+
bytes: rendered.bytes.byteLength,
|
|
742
|
+
value: spec.value,
|
|
743
|
+
errorCorrection: rendered.errorCorrection,
|
|
744
|
+
background: spec.background,
|
|
745
|
+
foreground: spec.foreground,
|
|
746
|
+
margin: spec.margin
|
|
747
|
+
});
|
|
748
|
+
if (ctx.flags.open === true) openFile(path2, io.warn);
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
function resolveSpec(value, ctx, config) {
|
|
752
|
+
const size = intFlag(ctx.flags, "size", { min: 64, max: 8192 }) ?? config.get("qr.size") ?? QR_DEFAULTS.size;
|
|
753
|
+
const margin = intFlag(ctx.flags, "margin", { min: 0, max: 4096 }) ?? defaultMargin(size);
|
|
754
|
+
if (margin * 2 >= size) {
|
|
755
|
+
throw usageError(`--margin ${margin} leaves no room inside --size ${size}.`);
|
|
756
|
+
}
|
|
757
|
+
const requestedLevel = enumFlag(ctx.flags, "ec", LEVELS) ?? config.get("qr.errorCorrection") ?? QR_DEFAULTS.errorCorrection;
|
|
758
|
+
return {
|
|
759
|
+
value,
|
|
760
|
+
size,
|
|
761
|
+
margin,
|
|
762
|
+
foreground: resolveColour(
|
|
763
|
+
stringFlag(ctx.flags, "fg") ?? config.get("qr.foreground") ?? QR_DEFAULTS.foreground
|
|
764
|
+
),
|
|
765
|
+
background: resolveColour(
|
|
766
|
+
stringFlag(ctx.flags, "bg") ?? config.get("qr.background") ?? QR_DEFAULTS.background
|
|
767
|
+
),
|
|
768
|
+
mark: resolveMark(
|
|
769
|
+
stringFlag(ctx.flags, "mark") ?? config.get("qr.mark") ?? QR_DEFAULTS.mark
|
|
770
|
+
),
|
|
771
|
+
errorCorrection: requestedLevel
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
function resolveMark(raw) {
|
|
775
|
+
if (raw === "none") return { kind: "none" };
|
|
776
|
+
if (raw === "purple" || raw === "light") return { kind: "builtin", variant: raw };
|
|
777
|
+
const path2 = isAbsolute2(raw) ? raw : resolve2(process.cwd(), raw);
|
|
778
|
+
return { kind: "file", path: path2 };
|
|
779
|
+
}
|
|
780
|
+
function resolveFormat(ctx, config) {
|
|
781
|
+
const explicit = enumFlag(ctx.flags, "format", FORMATS);
|
|
782
|
+
const out = stringFlag(ctx.flags, "out");
|
|
783
|
+
const fromExtension = out ? EXTENSIONS[extname(out).toLowerCase()] : void 0;
|
|
784
|
+
if (explicit && fromExtension && explicit !== fromExtension) {
|
|
785
|
+
throw usageError(
|
|
786
|
+
`--format ${explicit} disagrees with the extension on --out ${out}.`,
|
|
787
|
+
"Drop one of them: the extension alone is enough."
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
return explicit ?? fromExtension ?? config.get("qr.format") ?? QR_DEFAULTS.format;
|
|
791
|
+
}
|
|
792
|
+
function resolveOut(ctx, config, format) {
|
|
793
|
+
const requested = stringFlag(ctx.flags, "out") ?? config.get("qr.out");
|
|
794
|
+
const extension = format === "jpeg" ? "jpg" : format;
|
|
795
|
+
if (!requested) return resolve2(process.cwd(), `acute-qr.${extension}`);
|
|
796
|
+
const expanded = requested.startsWith("~/") ? resolve2(process.env.HOME ?? "", requested.slice(2)) : resolve2(process.cwd(), requested);
|
|
797
|
+
const looksLikeFile = extname(expanded).toLowerCase() in EXTENSIONS;
|
|
798
|
+
if (looksLikeFile) return expanded;
|
|
799
|
+
if (isDirectory(expanded) || requested.endsWith("/")) {
|
|
800
|
+
return resolve2(expanded, `acute-qr.${extension}`);
|
|
801
|
+
}
|
|
802
|
+
return `${expanded}.${extension}`;
|
|
803
|
+
}
|
|
804
|
+
function isDirectory(path2) {
|
|
805
|
+
try {
|
|
806
|
+
return statSync(path2).isDirectory();
|
|
807
|
+
} catch {
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function openFile(path2, warn) {
|
|
812
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
813
|
+
try {
|
|
814
|
+
spawn(opener, [path2], { detached: true, stdio: "ignore" }).unref();
|
|
815
|
+
} catch {
|
|
816
|
+
warn(`Wrote the file but could not open it with "${opener}".`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/commands/qr/index.ts
|
|
821
|
+
var qrGroup = {
|
|
822
|
+
name: "qr",
|
|
823
|
+
summary: "Acute QR codes: write one, or read one back.",
|
|
824
|
+
commands: [generate, decode],
|
|
825
|
+
defaultCommand: "generate"
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
// src/commands/index.ts
|
|
829
|
+
var GROUPS = [qrGroup, configGroup];
|
|
830
|
+
|
|
831
|
+
// src/kernel/help.ts
|
|
832
|
+
var BIN = "acute";
|
|
833
|
+
function rootHelp(io, groups, version) {
|
|
834
|
+
const { bold, dim, accent } = io.style;
|
|
835
|
+
io.say();
|
|
836
|
+
io.say(` ${accent(bold("acute"))} ${dim(version)}`);
|
|
837
|
+
io.say(` ${dim("The Acute command line.")}`);
|
|
838
|
+
io.say();
|
|
839
|
+
io.say(` ${bold("USAGE")}`);
|
|
840
|
+
io.say(` ${BIN} <command> [options]`);
|
|
841
|
+
io.say();
|
|
842
|
+
io.say(` ${bold("COMMANDS")}`);
|
|
843
|
+
const width = Math.max(...groups.map((g) => g.name.length));
|
|
844
|
+
for (const group of groups) {
|
|
845
|
+
io.say(` ${group.name.padEnd(width + 2)}${dim(group.summary)}`);
|
|
846
|
+
}
|
|
847
|
+
io.say();
|
|
848
|
+
io.say(` ${bold("GLOBAL OPTIONS")}`);
|
|
849
|
+
printOptions(io, GLOBAL_OPTIONS);
|
|
850
|
+
io.say();
|
|
851
|
+
io.say(` ${dim(`Run "${BIN} <command> --help" for what a command takes.`)}`);
|
|
852
|
+
io.say();
|
|
853
|
+
}
|
|
854
|
+
function groupHelp(io, group) {
|
|
855
|
+
const { bold, dim, accent } = io.style;
|
|
856
|
+
io.say();
|
|
857
|
+
io.say(` ${accent(bold(`${BIN} ${group.name}`))}`);
|
|
858
|
+
io.say(` ${dim(group.summary)}`);
|
|
859
|
+
io.say();
|
|
860
|
+
io.say(` ${bold("COMMANDS")}`);
|
|
861
|
+
const width = Math.max(...group.commands.map((c) => c.name.length));
|
|
862
|
+
for (const command of group.commands) {
|
|
863
|
+
const marker = command.name === group.defaultCommand ? dim(" (default)") : "";
|
|
864
|
+
io.say(` ${command.name.padEnd(width + 2)}${dim(command.summary)}${marker}`);
|
|
865
|
+
}
|
|
866
|
+
io.say();
|
|
867
|
+
io.say(` ${dim(`Run "${BIN} ${group.name} <command> --help" for options.`)}`);
|
|
868
|
+
io.say();
|
|
869
|
+
}
|
|
870
|
+
function commandHelp(io, group, command) {
|
|
871
|
+
const { bold, dim, accent } = io.style;
|
|
872
|
+
io.say();
|
|
873
|
+
io.say(` ${accent(bold(`${BIN} ${group.name} ${command.name}`))}`);
|
|
874
|
+
io.say(` ${dim(command.summary)}`);
|
|
875
|
+
io.say();
|
|
876
|
+
io.say(` ${bold("USAGE")}`);
|
|
877
|
+
io.say(` ${BIN} ${command.usage}`);
|
|
878
|
+
if (Object.keys(command.options).length > 0) {
|
|
879
|
+
io.say();
|
|
880
|
+
io.say(` ${bold("OPTIONS")}`);
|
|
881
|
+
printOptions(io, command.options);
|
|
882
|
+
}
|
|
883
|
+
if (command.details?.length) {
|
|
884
|
+
for (const paragraph of command.details) {
|
|
885
|
+
io.say();
|
|
886
|
+
io.say(wrap(paragraph, 76, " "));
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (command.examples?.length) {
|
|
890
|
+
io.say();
|
|
891
|
+
io.say(` ${bold("EXAMPLES")}`);
|
|
892
|
+
for (const example of command.examples) io.say(` ${dim(example)}`);
|
|
893
|
+
}
|
|
894
|
+
io.say();
|
|
895
|
+
}
|
|
896
|
+
function printOptions(io, options) {
|
|
897
|
+
const rows = Object.entries(options).map(
|
|
898
|
+
([name, spec]) => [flagLabel(name, spec), spec]
|
|
899
|
+
);
|
|
900
|
+
const width = Math.max(...rows.map(([label]) => label.length));
|
|
901
|
+
for (const [label, spec] of rows) {
|
|
902
|
+
const fallback = spec.defaultsTo ? io.style.dim(` [${spec.defaultsTo}]`) : "";
|
|
903
|
+
io.say(` ${label.padEnd(width + 2)}${io.style.dim(spec.describe)}${fallback}`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
function flagLabel(name, spec) {
|
|
907
|
+
const short = spec.short ? `-${spec.short}, ` : " ";
|
|
908
|
+
const value = spec.kind === "string" ? ` <${spec.placeholder ?? "value"}>` : "";
|
|
909
|
+
return `${short}--${name}${value}`;
|
|
910
|
+
}
|
|
911
|
+
function wrap(text, width, indent) {
|
|
912
|
+
const words = text.split(/\s+/);
|
|
913
|
+
const lines = [];
|
|
914
|
+
let line = "";
|
|
915
|
+
for (const word of words) {
|
|
916
|
+
if (line.length + word.length + 1 > width) {
|
|
917
|
+
lines.push(line);
|
|
918
|
+
line = word;
|
|
919
|
+
} else {
|
|
920
|
+
line = line ? `${line} ${word}` : word;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (line) lines.push(line);
|
|
924
|
+
return lines.map((l) => `${indent}${l}`).join("\n");
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// src/kernel/io.ts
|
|
928
|
+
function supportsColor(stream) {
|
|
929
|
+
if (process.env.NO_COLOR) return false;
|
|
930
|
+
if (process.env.FORCE_COLOR) return process.env.FORCE_COLOR !== "0";
|
|
931
|
+
return Boolean(stream.isTTY);
|
|
932
|
+
}
|
|
933
|
+
function makeStyle(enabled) {
|
|
934
|
+
const wrap2 = (open, close) => (s) => enabled ? `${open}${s}${close}` : s;
|
|
935
|
+
return {
|
|
936
|
+
bold: wrap2("\x1B[1m", "\x1B[22m"),
|
|
937
|
+
dim: wrap2("\x1B[2m", "\x1B[22m"),
|
|
938
|
+
// The brand purple is #5200A0, which is 1.7:1 on black and unreadable in a
|
|
939
|
+
// dark terminal. Terminal accents use the secondary purple instead, which
|
|
940
|
+
// is the same hue at a lightness a terminal can actually show.
|
|
941
|
+
accent: wrap2(`\x1B[38;2;${rgb(Brand.purpleAccent).join(";")}m`, "\x1B[39m"),
|
|
942
|
+
red: wrap2("\x1B[31m", "\x1B[39m")
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
function rgb(hex) {
|
|
946
|
+
const n = Number.parseInt(hex.replace("#", ""), 16);
|
|
947
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
948
|
+
}
|
|
949
|
+
function createIo(options) {
|
|
950
|
+
const style = makeStyle(supportsColor(process.stdout));
|
|
951
|
+
const narrate = !options.json && !options.quiet;
|
|
952
|
+
return {
|
|
953
|
+
json: options.json,
|
|
954
|
+
say(line = "") {
|
|
955
|
+
if (narrate) process.stdout.write(`${line}
|
|
956
|
+
`);
|
|
957
|
+
},
|
|
958
|
+
field(label, value) {
|
|
959
|
+
if (narrate) process.stdout.write(` ${style.dim(label.padEnd(10))}${value}
|
|
960
|
+
`);
|
|
961
|
+
},
|
|
962
|
+
result(value) {
|
|
963
|
+
if (options.json) process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
964
|
+
`);
|
|
965
|
+
},
|
|
966
|
+
warn(line) {
|
|
967
|
+
process.stderr.write(`${line}
|
|
968
|
+
`);
|
|
969
|
+
},
|
|
970
|
+
style
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/cli.ts
|
|
975
|
+
var VERSION = package_default.version;
|
|
976
|
+
async function run(argv) {
|
|
977
|
+
const first = argv[0];
|
|
978
|
+
if (!first || first.startsWith("-")) {
|
|
979
|
+
const { flags: flags2 } = parse(argv, {});
|
|
980
|
+
const io2 = createIo({ json: false, quiet: false });
|
|
981
|
+
if (flags2.version === true) {
|
|
982
|
+
process.stdout.write(`${VERSION}
|
|
983
|
+
`);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
rootHelp(io2, GROUPS, VERSION);
|
|
987
|
+
if (!first) return;
|
|
988
|
+
if (flags2.help !== true) throw usageError("No command given.");
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
const group = GROUPS.find((g) => g.name === first);
|
|
992
|
+
if (!group) {
|
|
993
|
+
const suggestion = nearest(
|
|
994
|
+
first,
|
|
995
|
+
GROUPS.map((g) => g.name)
|
|
996
|
+
);
|
|
997
|
+
throw usageError(
|
|
998
|
+
`"${first}" is not an acute command.`,
|
|
999
|
+
suggestion ? `Did you mean "acute ${suggestion}"?` : 'Run "acute --help" for the list.'
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
const rest = argv.slice(1);
|
|
1003
|
+
const named = rest[0] && !rest[0].startsWith("-") ? group.commands.find((c) => c.name === rest[0]) : void 0;
|
|
1004
|
+
const command = named ?? defaultCommandOf(group);
|
|
1005
|
+
const args = named ? rest.slice(1) : rest;
|
|
1006
|
+
const { flags, positionals } = parse(args, command.options);
|
|
1007
|
+
const io = createIo({ json: flags.json === true, quiet: flags.quiet === true });
|
|
1008
|
+
if (flags.help === true) {
|
|
1009
|
+
if (named) commandHelp(io, group, command);
|
|
1010
|
+
else groupHelp(io, group);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
await command.run({ flags, positionals, config: loadConfig(), io });
|
|
1014
|
+
}
|
|
1015
|
+
function defaultCommandOf(group) {
|
|
1016
|
+
const fallback = group.commands.find((c) => c.name === group.defaultCommand);
|
|
1017
|
+
if (!fallback) {
|
|
1018
|
+
throw new Error(`Command group "${group.name}" has no default command.`);
|
|
1019
|
+
}
|
|
1020
|
+
return fallback;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// src/bin.ts
|
|
1024
|
+
process.stdout.on("error", (error) => {
|
|
1025
|
+
if (error.code === "EPIPE") process.exit(0);
|
|
1026
|
+
throw error;
|
|
1027
|
+
});
|
|
1028
|
+
async function main() {
|
|
1029
|
+
try {
|
|
1030
|
+
await run(process.argv.slice(2));
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
const io = createIo({ json: false, quiet: false });
|
|
1033
|
+
const { red, dim } = io.style;
|
|
1034
|
+
if (error instanceof CliError) {
|
|
1035
|
+
io.warn("");
|
|
1036
|
+
io.warn(` ${red("Error")} ${error.message}`);
|
|
1037
|
+
if (error.hint) io.warn(` ${dim(` ${error.hint}`)}`);
|
|
1038
|
+
io.warn("");
|
|
1039
|
+
process.exitCode = error.exitCode;
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
io.warn("");
|
|
1043
|
+
io.warn(` ${red("Unexpected error")} this is a bug in the acute CLI.`);
|
|
1044
|
+
io.warn("");
|
|
1045
|
+
io.warn(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
1046
|
+
io.warn("");
|
|
1047
|
+
process.exitCode = ExitCode.Failure;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
void main();
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@acutenetwork/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The command line for Acute.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Acute's Engineering Team <engineering@acute.network>",
|
|
7
|
+
"homepage": "https://github.com/acutenetwork/acute-cli#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/acutenetwork/acute-cli.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/acutenetwork/acute-cli/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"acute",
|
|
17
|
+
"cli",
|
|
18
|
+
"acute-network",
|
|
19
|
+
"payments",
|
|
20
|
+
"qr",
|
|
21
|
+
"qr-code"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"bin": {
|
|
25
|
+
"acute": "./dist/bin.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"assets",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20.0.0"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@acutenetwork/fey": "^0.2.2",
|
|
41
|
+
"jsqr": "^1.4.0",
|
|
42
|
+
"qrcode-generator": "^1.4.4",
|
|
43
|
+
"sharp": "^0.35.5"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@eslint/js": "^9.12.0",
|
|
47
|
+
"@types/node": "^22.10.0",
|
|
48
|
+
"eslint": "^9.12.0",
|
|
49
|
+
"eslint-config-prettier": "^9.1.0",
|
|
50
|
+
"prettier": "^3.3.3",
|
|
51
|
+
"tsup": "^8.4.0",
|
|
52
|
+
"typescript": "^5.7.0",
|
|
53
|
+
"typescript-eslint": "^8.8.1",
|
|
54
|
+
"vitest": "^3.0.0"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsup",
|
|
58
|
+
"dev": "tsup --watch",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"test:watch": "vitest",
|
|
61
|
+
"type-check": "tsc --noEmit",
|
|
62
|
+
"lint": "eslint .",
|
|
63
|
+
"lint:fix": "eslint . --fix",
|
|
64
|
+
"format": "prettier --write .",
|
|
65
|
+
"clean": "rm -rf dist"
|
|
66
|
+
}
|
|
67
|
+
}
|