@bcts/seedtool-cli 1.0.0-alpha.22 → 1.0.0-beta.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/dist/index.cjs +87 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +20 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +78 -89
- package/dist/index.mjs.map +1 -1
- package/dist/main.mjs +150 -192
- package/dist/main.mjs.map +1 -1
- package/package.json +18 -18
- package/src/cli.ts +13 -5
- package/src/formats/cards.ts +7 -2
- package/src/formats/multipart.ts +5 -1
- package/src/main.ts +103 -54
- package/src/util.ts +28 -8
package/src/main.ts
CHANGED
|
@@ -13,9 +13,13 @@ import { SSKRGroupSpec } from "@bcts/components";
|
|
|
13
13
|
import { Cli, type InputFormatKey, type OutputFormatKey, type SSKRFormatKey } from "./cli.js";
|
|
14
14
|
import { selectInputFormat, selectOutputFormat } from "./formats/index.js";
|
|
15
15
|
import { DeterministicRandomNumberGenerator } from "./random.js";
|
|
16
|
-
import
|
|
16
|
+
import pkg from "../package.json" with { type: "json" };
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Package version, sourced from `package.json` so the CLI's `--version` output
|
|
20
|
+
* never drifts from the published version.
|
|
21
|
+
*/
|
|
22
|
+
const VERSION: string = pkg.version;
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
25
|
* CLI options parsed from commander.
|
|
@@ -37,6 +41,78 @@ interface CliOptions {
|
|
|
37
41
|
deterministic?: string;
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Build an argParser that validates a value against the given choices and,
|
|
46
|
+
* on failure, emits the clap-style error format used by Rust's seedtool-cli:
|
|
47
|
+
*
|
|
48
|
+
* error: invalid value 'X' for '--<long> <UPPER>'
|
|
49
|
+
* [possible values: a, b, c]
|
|
50
|
+
*
|
|
51
|
+
* For more information, try '--help'.
|
|
52
|
+
*
|
|
53
|
+
* `metavar` is the value-name placeholder (e.g. INPUT_TYPE), normally derived
|
|
54
|
+
* from the option's `<META>` declaration. Used in conjunction with `.choices()`
|
|
55
|
+
* for help-text generation — argParser runs first, so on a bad value we exit
|
|
56
|
+
* before commander's own choice-validation message can fire.
|
|
57
|
+
*/
|
|
58
|
+
function clapChoiceParser<T extends string>(
|
|
59
|
+
longName: string,
|
|
60
|
+
metavar: string,
|
|
61
|
+
choices: readonly T[],
|
|
62
|
+
): (value: string) => T {
|
|
63
|
+
return (value: string): T => {
|
|
64
|
+
if ((choices as readonly string[]).includes(value)) return value as T;
|
|
65
|
+
process.stderr.write(
|
|
66
|
+
`error: invalid value '${value}' for '--${longName} <${metavar}>'\n` +
|
|
67
|
+
` [possible values: ${choices.join(", ")}]\n\n` +
|
|
68
|
+
`For more information, try '--help'.\n`,
|
|
69
|
+
);
|
|
70
|
+
process.exit(2);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Choice arrays match Rust's clap `ValueEnum` declaration order so that
|
|
75
|
+
// `--help` and the `[possible values: …]` block in error output line up
|
|
76
|
+
// byte-identically. See seedtool-cli-rust/src/cli.rs.
|
|
77
|
+
const IN_CHOICES = [
|
|
78
|
+
"random",
|
|
79
|
+
"hex",
|
|
80
|
+
"btw",
|
|
81
|
+
"btwu",
|
|
82
|
+
"btwm",
|
|
83
|
+
"bits",
|
|
84
|
+
"cards",
|
|
85
|
+
"dice",
|
|
86
|
+
"base6",
|
|
87
|
+
"base10",
|
|
88
|
+
"ints",
|
|
89
|
+
"bip39",
|
|
90
|
+
"sskr",
|
|
91
|
+
"envelope",
|
|
92
|
+
"multipart",
|
|
93
|
+
"seed",
|
|
94
|
+
] as const;
|
|
95
|
+
|
|
96
|
+
const OUT_CHOICES = [
|
|
97
|
+
"hex",
|
|
98
|
+
"btw",
|
|
99
|
+
"btwu",
|
|
100
|
+
"btwm",
|
|
101
|
+
"bits",
|
|
102
|
+
"cards",
|
|
103
|
+
"dice",
|
|
104
|
+
"base6",
|
|
105
|
+
"base10",
|
|
106
|
+
"ints",
|
|
107
|
+
"bip39",
|
|
108
|
+
"sskr",
|
|
109
|
+
"envelope",
|
|
110
|
+
"multipart",
|
|
111
|
+
"seed",
|
|
112
|
+
] as const;
|
|
113
|
+
|
|
114
|
+
const SSKR_FORMAT_CHOICES = ["envelope", "btw", "btwm", "btwu", "ur"] as const;
|
|
115
|
+
|
|
40
116
|
function parseLowInt(value: string): number {
|
|
41
117
|
const num = parseInt(value, 10);
|
|
42
118
|
if (isNaN(num) || num < 0 || num > 254) {
|
|
@@ -88,10 +164,10 @@ function main(): void {
|
|
|
88
164
|
"Report bugs to ChristopherA@BlockchainCommons.com.\n" +
|
|
89
165
|
"© 2024 Blockchain Commons.",
|
|
90
166
|
)
|
|
91
|
-
.version(VERSION)
|
|
167
|
+
.version(`@bcts/seedtool-cli ${VERSION}`)
|
|
92
168
|
.argument(
|
|
93
169
|
"[INPUT]",
|
|
94
|
-
"The input to be transformed. If required and not present, it will be read from stdin
|
|
170
|
+
"The input to be transformed. If required and not present, it will be read from stdin",
|
|
95
171
|
)
|
|
96
172
|
.option(
|
|
97
173
|
"-c, --count <COUNT>",
|
|
@@ -101,54 +177,23 @@ function main(): void {
|
|
|
101
177
|
.addOption(
|
|
102
178
|
new Option(
|
|
103
179
|
"-i, --in <INPUT_TYPE>",
|
|
104
|
-
"The input format. If not specified, a new random seed is generated using a secure random number generator
|
|
180
|
+
"The input format. If not specified, a new random seed is generated using a secure random number generator",
|
|
105
181
|
)
|
|
106
|
-
.choices([
|
|
107
|
-
|
|
108
|
-
"hex",
|
|
109
|
-
"btw",
|
|
110
|
-
"btwm",
|
|
111
|
-
"btwu",
|
|
112
|
-
"bits",
|
|
113
|
-
"cards",
|
|
114
|
-
"dice",
|
|
115
|
-
"base6",
|
|
116
|
-
"base10",
|
|
117
|
-
"ints",
|
|
118
|
-
"bip39",
|
|
119
|
-
"sskr",
|
|
120
|
-
"envelope",
|
|
121
|
-
"seed",
|
|
122
|
-
"multipart",
|
|
123
|
-
])
|
|
182
|
+
.choices([...IN_CHOICES])
|
|
183
|
+
.argParser(clapChoiceParser("in", "INPUT_TYPE", IN_CHOICES))
|
|
124
184
|
.default("random"),
|
|
125
185
|
)
|
|
126
186
|
.addOption(
|
|
127
|
-
new Option("-o, --out <OUTPUT_TYPE>", "The output format
|
|
128
|
-
.choices([
|
|
129
|
-
|
|
130
|
-
"btw",
|
|
131
|
-
"btwm",
|
|
132
|
-
"btwu",
|
|
133
|
-
"bits",
|
|
134
|
-
"cards",
|
|
135
|
-
"dice",
|
|
136
|
-
"base6",
|
|
137
|
-
"base10",
|
|
138
|
-
"ints",
|
|
139
|
-
"bip39",
|
|
140
|
-
"sskr",
|
|
141
|
-
"envelope",
|
|
142
|
-
"seed",
|
|
143
|
-
"multipart",
|
|
144
|
-
])
|
|
187
|
+
new Option("-o, --out <OUTPUT_TYPE>", "The output format")
|
|
188
|
+
.choices([...OUT_CHOICES])
|
|
189
|
+
.argParser(clapChoiceParser("out", "OUTPUT_TYPE", OUT_CHOICES))
|
|
145
190
|
.default("hex"),
|
|
146
191
|
)
|
|
147
192
|
.option("--low <LOW>", "The lowest int returned (0-254)", parseLowInt, 0)
|
|
148
193
|
.option("--high <HIGH>", "The highest int returned (1-255), low < high", parseHighInt, 9)
|
|
149
|
-
.option("--name <NAME>", "The name of the seed
|
|
150
|
-
.option("--note <NOTE>", "The note associated with the seed
|
|
151
|
-
.option("--date <DATE>", "The seed's creation date, in ISO-8601 format. May also be `now
|
|
194
|
+
.option("--name <NAME>", "The name of the seed")
|
|
195
|
+
.option("--note <NOTE>", "The note associated with the seed")
|
|
196
|
+
.option("--date <DATE>", "The seed's creation date, in ISO-8601 format. May also be `now`")
|
|
152
197
|
.option(
|
|
153
198
|
"--max-fragment-len <MAX_FRAG_LEN>",
|
|
154
199
|
"For `multipart` output, the UR will be segmented into parts with fragments no larger than MAX_FRAG_LEN",
|
|
@@ -156,7 +201,7 @@ function main(): void {
|
|
|
156
201
|
)
|
|
157
202
|
.option(
|
|
158
203
|
"--additional-parts <NUM_PARTS>",
|
|
159
|
-
"For `multipart` output, the number of additional parts above the minimum to generate using fountain encoding
|
|
204
|
+
"For `multipart` output, the number of additional parts above the minimum to generate using fountain encoding",
|
|
160
205
|
"0",
|
|
161
206
|
)
|
|
162
207
|
.option(
|
|
@@ -167,18 +212,19 @@ function main(): void {
|
|
|
167
212
|
)
|
|
168
213
|
.option(
|
|
169
214
|
"-t, --group-threshold <THRESHOLD>",
|
|
170
|
-
"The number of groups that must meet their threshold. Must be <= the number of group specifications
|
|
215
|
+
"The number of groups that must meet their threshold. Must be <= the number of group specifications",
|
|
171
216
|
parseGroupThreshold,
|
|
172
217
|
1,
|
|
173
218
|
)
|
|
174
219
|
.addOption(
|
|
175
|
-
new Option("-s, --sskr-format <SSKR_FORMAT>", "
|
|
176
|
-
.choices([
|
|
220
|
+
new Option("-s, --sskr-format <SSKR_FORMAT>", "Output format")
|
|
221
|
+
.choices([...SSKR_FORMAT_CHOICES])
|
|
222
|
+
.argParser(clapChoiceParser("sskr-format", "SSKR_FORMAT", SSKR_FORMAT_CHOICES))
|
|
177
223
|
.default("envelope"),
|
|
178
224
|
)
|
|
179
225
|
.option(
|
|
180
226
|
"-d, --deterministic <SEED_STRING>",
|
|
181
|
-
"Use a deterministic random number generator with the given seed string. Output generated from this seed will be the same every time, so generated seeds are only as secure as the seed string
|
|
227
|
+
"Use a deterministic random number generator with the given seed string. Output generated from this seed will be the same every time, so generated seeds are only as secure as the seed string",
|
|
182
228
|
);
|
|
183
229
|
|
|
184
230
|
program.parse();
|
|
@@ -189,12 +235,12 @@ function main(): void {
|
|
|
189
235
|
// Create the CLI state
|
|
190
236
|
const cli = new Cli();
|
|
191
237
|
|
|
192
|
-
// Set input from
|
|
238
|
+
// Set input from positional argv. Stdin is NOT read here — the active input
|
|
239
|
+
// format calls `cli.expectInput()` lazily, mirroring Rust's `expect_input()`.
|
|
240
|
+
// This keeps deterministic flows (--in random, -d <SEED>, etc.) from
|
|
241
|
+
// blocking on stdin in non-TTY contexts (CI, sub-processes).
|
|
193
242
|
if (args.length > 0) {
|
|
194
243
|
cli.input = args[0];
|
|
195
|
-
} else if (process.stdin.isTTY !== true) {
|
|
196
|
-
// Read from stdin if it's piped
|
|
197
|
-
cli.input = fs.readFileSync(process.stdin.fd, "utf-8").trim();
|
|
198
244
|
}
|
|
199
245
|
|
|
200
246
|
// Set options
|
|
@@ -249,6 +295,9 @@ try {
|
|
|
249
295
|
main();
|
|
250
296
|
} catch (error: unknown) {
|
|
251
297
|
const message = error instanceof Error ? error.message : String(error);
|
|
252
|
-
|
|
298
|
+
// Mirrors Rust's anyhow `Error: {msg}` stderr format. Avoid double-prefixing
|
|
299
|
+
// if a deeper layer already emitted "Error: " (e.g. roundtrippability check).
|
|
300
|
+
const prefixed = message.startsWith("Error: ") ? message : `Error: ${message}`;
|
|
301
|
+
console.error(prefixed);
|
|
253
302
|
process.exit(1);
|
|
254
303
|
}
|
package/src/util.ts
CHANGED
|
@@ -19,23 +19,41 @@ export function dataToHex(bytes: Uint8Array): string {
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Convert hex string to bytes.
|
|
22
|
-
*
|
|
22
|
+
*
|
|
23
|
+
* Mirrors the error wording produced by Rust's `hex` crate
|
|
24
|
+
* (`FromHexError::OddLength` and `FromHexError::InvalidHexCharacter`),
|
|
25
|
+
* which seedtool-cli-rust surfaces unchanged through anyhow:
|
|
26
|
+
*
|
|
27
|
+
* "Odd number of digits"
|
|
28
|
+
* "Invalid character '{c}' at position {n}"
|
|
29
|
+
*
|
|
30
|
+
* The outer CLI layer adds the `Error: ` prefix to match Rust's anyhow
|
|
31
|
+
* output.
|
|
23
32
|
*/
|
|
24
33
|
export function hexToData(hex: string): Uint8Array {
|
|
25
34
|
if (hex.length % 2 !== 0) {
|
|
26
|
-
throw new Error("
|
|
35
|
+
throw new Error("Odd number of digits");
|
|
27
36
|
}
|
|
28
37
|
const bytes = new Uint8Array(hex.length / 2);
|
|
29
38
|
for (let i = 0; i < hex.length; i += 2) {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
bytes[i / 2] = byte;
|
|
39
|
+
const hi = hexCharToNibble(hex, i);
|
|
40
|
+
const lo = hexCharToNibble(hex, i + 1);
|
|
41
|
+
bytes[i / 2] = (hi << 4) | lo;
|
|
35
42
|
}
|
|
36
43
|
return bytes;
|
|
37
44
|
}
|
|
38
45
|
|
|
46
|
+
function hexCharToNibble(hex: string, index: number): number {
|
|
47
|
+
const c = hex.charCodeAt(index);
|
|
48
|
+
// 0..9
|
|
49
|
+
if (c >= 0x30 && c <= 0x39) return c - 0x30;
|
|
50
|
+
// a..f
|
|
51
|
+
if (c >= 0x61 && c <= 0x66) return c - 0x61 + 10;
|
|
52
|
+
// A..F
|
|
53
|
+
if (c >= 0x41 && c <= 0x46) return c - 0x41 + 10;
|
|
54
|
+
throw new Error(`Invalid character '${hex[index]}' at position ${index}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
/**
|
|
40
58
|
* Convert byte values to a different base range [0, base-1].
|
|
41
59
|
* Each byte (0-255) is scaled proportionally to the target base.
|
|
@@ -136,7 +154,9 @@ export function digitsToData(inStr: string, low: number, high: number): Uint8Arr
|
|
|
136
154
|
for (const c of inStr) {
|
|
137
155
|
const n = c.charCodeAt(0) - "0".charCodeAt(0);
|
|
138
156
|
if (n < low || n > high) {
|
|
139
|
-
|
|
157
|
+
// Mirrors Rust util.rs:64 (`bail!("Invalid digit.")`). The terser wording
|
|
158
|
+
// is intentional — if Rust's diagnostic is broadened upstream, mirror here.
|
|
159
|
+
throw new Error("Invalid digit.");
|
|
140
160
|
}
|
|
141
161
|
result.push(n);
|
|
142
162
|
}
|