@moshcoder/moshpit-name
Advanced tools
+195
-32
@@ -8,2 +8,4 @@ #!/usr/bin/env node | ||
| import { writeFileSync } from "node:fs"; | ||
| import { | ||
@@ -16,5 +18,7 @@ CHILD_PRICE_USD, ENDING_PRICE_USD, MAX_BULK_TLDS, RESERVED_TLDS, | ||
| moshpit-name check <ending> [--json] can this ending be claimed, and if not why | ||
| moshpit-name parse <name> [--json] split a name into its label and ending | ||
| moshpit-name list [-] [--limit N] [--json] | ||
| moshpit-name check (<ending...> | -) [--json | --ndjson] | ||
| can endings be claimed; - reads one per line | ||
| moshpit-name parse (<name...> | -) [--json | --ndjson] | ||
| split names into labels; - reads one per line | ||
| moshpit-name list [-] [--limit N] [--json | --ndjson] | ||
| parse up to N pasted entries; - reads stdin | ||
@@ -27,3 +31,4 @@ moshpit-name reserved [--json] list endings that cannot be claimed | ||
| const [sub, ...rawRest] = process.argv.slice(2); | ||
| const json = rawRest.includes("--json"); | ||
| let json = false; | ||
| let ndjson = false; | ||
| const rest = []; | ||
@@ -33,6 +38,27 @@ let limit = MAX_BULK_TLDS; | ||
| let limitFlags = 0; | ||
| let optionError = null; | ||
| let parsingOptions = true; | ||
| for (let index = 0; index < rawRest.length; index++) { | ||
| const arg = rawRest[index]; | ||
| if (arg === "--json") continue; | ||
| if (arg === "--limit") { | ||
| if (parsingOptions && arg === "--") { | ||
| parsingOptions = false; | ||
| continue; | ||
| } | ||
| if (parsingOptions && arg === "--json") { | ||
| json = true; | ||
| continue; | ||
| } | ||
| if (parsingOptions && arg === "--ndjson") { | ||
| if (!["check", "parse", "list"].includes(sub)) { | ||
| optionError ??= 'unknown option "--ndjson"'; | ||
| } else { | ||
| ndjson = true; | ||
| } | ||
| continue; | ||
| } | ||
| if (parsingOptions && arg === "--limit") { | ||
| if (sub !== "list") { | ||
| optionError ??= 'unknown option "--limit"'; | ||
| continue; | ||
| } | ||
| limitFlags++; | ||
@@ -46,6 +72,23 @@ const candidate = rawRest[index + 1]; | ||
| } | ||
| if (parsingOptions && arg.startsWith("--")) { | ||
| optionError ??= `unknown option "${arg}"`; | ||
| continue; | ||
| } | ||
| rest.push(arg); | ||
| } | ||
| if (json && ndjson) optionError ??= "--json and --ndjson cannot be used together"; | ||
| const out = console.log; | ||
| const outJson = (value) => out(JSON.stringify(value, null, 2)); | ||
| const writeStdout = (value) => { | ||
| try { | ||
| writeFileSync(1, value); | ||
| } catch (error) { | ||
| if (error?.code === "EPIPE") process.exit(0); | ||
| throw error; | ||
| } | ||
| }; | ||
| const outJson = (value) => writeStdout(`${JSON.stringify(value, null, 2)}\n`); | ||
| const outNdjson = (values) => { | ||
| if (values.length) writeStdout(`${values.map((value) => JSON.stringify(value)).join("\n")}\n`); | ||
| }; | ||
| const MAX_STDIN_BYTES = 1024 * 1024; | ||
@@ -58,2 +101,56 @@ const readStdin = async () => { | ||
| const readCommandStdin = async () => { | ||
| const chunks = []; | ||
| let bytes = 0; | ||
| for await (const chunk of process.stdin) { | ||
| const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); | ||
| bytes += data.length; | ||
| if (bytes > MAX_STDIN_BYTES) { | ||
| return { lines: [], error: "stdin accepts at most 1 MiB" }; | ||
| } | ||
| chunks.push(data); | ||
| } | ||
| const lines = []; | ||
| for (const line of Buffer.concat(chunks).toString("utf8").split(/\r?\n/)) { | ||
| if (!line.trim()) continue; | ||
| if (lines.length === MAX_BULK_TLDS) { | ||
| return { | ||
| lines: [], | ||
| error: `stdin accepts at most ${MAX_BULK_TLDS} non-empty lines`, | ||
| }; | ||
| } | ||
| lines.push(line); | ||
| } | ||
| return { lines, error: null }; | ||
| }; | ||
| const commandInputs = async (args) => { | ||
| const fromStdin = args.length === 1 && args[0] === "-"; | ||
| if (!fromStdin && args.includes("-")) { | ||
| return { | ||
| inputs: [], | ||
| fromStdin: false, | ||
| error: '"-" must be the only input when reading from stdin', | ||
| }; | ||
| } | ||
| if (!fromStdin) { | ||
| return { inputs: args.length ? args : [undefined], fromStdin, error: null }; | ||
| } | ||
| const { lines, error } = await readCommandStdin(); | ||
| return { | ||
| inputs: lines.length ? lines : [undefined], | ||
| fromStdin, | ||
| error, | ||
| }; | ||
| }; | ||
| const exitInputError = (error) => { | ||
| if (ndjson) outNdjson([{ error }]); | ||
| else if (json) outJson({ error }); | ||
| else console.error(`moshpit-name: ${error}`); | ||
| process.exit(1); | ||
| }; | ||
| if (!sub || sub === "help" || sub === "--help") { | ||
@@ -64,31 +161,82 @@ out(USAGE); | ||
| if (optionError) { | ||
| if (ndjson) outNdjson([{ error: optionError }]); | ||
| else if (json) outJson({ error: optionError }); | ||
| else console.error(`moshpit-name: ${optionError}`); | ||
| process.exit(1); | ||
| } | ||
| if (sub === "check") { | ||
| const raw = rest[0]; | ||
| const tld = normalizeTld(raw); | ||
| if (!tld) { | ||
| const reason = "not a valid ending (letters, digits and dashes only, no dots)"; | ||
| if (json) outJson({ input: raw ?? null, tld: null, claimable: false, reason }); | ||
| else out(`.${raw ?? ""} — ${reason}`); | ||
| process.exit(1); | ||
| const { inputs, fromStdin, error } = await commandInputs(rest); | ||
| if (error) exitInputError(error); | ||
| const results = inputs.map((input) => { | ||
| const tld = normalizeTld(input); | ||
| if (!tld) { | ||
| return { | ||
| input: input ?? null, | ||
| tld: null, | ||
| claimable: false, | ||
| reason: "not a valid ending (letters, digits and dashes only, no dots)", | ||
| }; | ||
| } | ||
| const reason = tldRejection(tld); | ||
| return { input, tld, claimable: !reason, reason }; | ||
| }); | ||
| if (ndjson) outNdjson(results); | ||
| else if (json) { | ||
| if (!fromStdin && results.length === 1) outJson(results[0]); | ||
| else { | ||
| const claimableCount = results.filter((result) => result.claimable).length; | ||
| outJson({ | ||
| count: results.length, | ||
| claimableCount, | ||
| rejectedCount: results.length - claimableCount, | ||
| results, | ||
| }); | ||
| } | ||
| } else { | ||
| for (const result of results) { | ||
| out(result.claimable | ||
| ? `.${result.tld} — claimable` | ||
| : `.${result.tld ?? result.input ?? ""} — ${result.reason}`); | ||
| } | ||
| } | ||
| const why = tldRejection(tld); | ||
| if (json) outJson({ input: raw, tld, claimable: !why, reason: why }); | ||
| else out(why ? `.${tld} — ${why}` : `.${tld} — claimable`); | ||
| process.exit(why ? 1 : 0); | ||
| process.exit(results.some((result) => !result.claimable) ? 1 : 0); | ||
| } | ||
| if (sub === "parse") { | ||
| const input = rest[0]; | ||
| const parsed = parseMoshpitName(input); | ||
| if (!parsed) { | ||
| // The two ways this fails are worth telling apart: too many labels, and a | ||
| // pair of numbers that reads as an IPv4 literal. | ||
| const reason = "not a Moshpit name (one label and one ending; both numeric reads as an address)"; | ||
| if (json) outJson({ input: input ?? null, valid: false, label: null, tld: null, reason }); | ||
| else out(`${input ?? ""} — ${reason}`); | ||
| process.exit(1); | ||
| const { inputs, fromStdin, error } = await commandInputs(rest); | ||
| if (error) exitInputError(error); | ||
| const reason = "not a Moshpit name (one label and one ending; both numeric reads as an address)"; | ||
| const results = inputs.map((input) => { | ||
| const parsed = parseMoshpitName(input); | ||
| if (!parsed) { | ||
| // The two ways this fails are worth telling apart: too many labels, and | ||
| // a pair of numbers that reads as an IPv4 literal. | ||
| return { input: input ?? null, valid: false, label: null, tld: null, reason }; | ||
| } | ||
| return { input, valid: true, ...parsed, reason: null }; | ||
| }); | ||
| if (ndjson) outNdjson(results); | ||
| else if (json) { | ||
| if (!fromStdin && results.length === 1) outJson(results[0]); | ||
| else { | ||
| const validCount = results.filter((result) => result.valid).length; | ||
| outJson({ | ||
| count: results.length, | ||
| validCount, | ||
| invalidCount: results.length - validCount, | ||
| results, | ||
| }); | ||
| } | ||
| } else { | ||
| for (const result of results) { | ||
| out(result.valid | ||
| ? `${result.label}.${result.tld} label=${result.label} ending=${result.tld}` | ||
| : `${result.input ?? ""} — ${result.reason}`); | ||
| } | ||
| } | ||
| if (json) outJson({ input, valid: true, ...parsed, reason: null }); | ||
| else out(`${parsed.label}.${parsed.tld} label=${parsed.label} ending=${parsed.tld}`); | ||
| process.exit(0); | ||
| process.exit(results.some((result) => !result.valid) ? 1 : 0); | ||
| } | ||
@@ -107,3 +255,4 @@ | ||
| const error = `--limit must be an integer from 1 to ${MAX_BULK_TLDS}`; | ||
| if (json) outJson({ error }); | ||
| if (ndjson) outNdjson([{ error }]); | ||
| else if (json) outJson({ error }); | ||
| else console.error(`moshpit-name: ${error}`); | ||
@@ -114,4 +263,18 @@ process.exit(1); | ||
| const input = rest[0] === "-" || !rest.length ? await readStdin() : rest.join("\n"); | ||
| if (rest.includes("-") && !(rest.length === 1 && rest[0] === "-")) { | ||
| exitInputError('"-" must be the only input when reading from stdin'); | ||
| } | ||
| let input; | ||
| if (rest[0] === "-" || !rest.length) { | ||
| const stdin = await readCommandStdin(); | ||
| if (stdin.error) exitInputError(stdin.error); | ||
| input = stdin.lines.join("\n"); | ||
| } else { | ||
| input = rest.join("\n"); | ||
| } | ||
| const parsed = parseTldList(input, limit); | ||
| if (ndjson) { | ||
| outNdjson(parsed.entries); | ||
| process.exit(0); | ||
| } | ||
| if (json) { | ||
@@ -118,0 +281,0 @@ outJson(parsed); |
+1
-1
| { | ||
| "name": "@moshcoder/moshpit-name", | ||
| "version": "0.4.0", | ||
| "version": "0.5.0", | ||
| "type": "module", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
+48
-3
@@ -37,5 +37,7 @@ # @moshcoder/moshpit-name | ||
| ```sh | ||
| moshpit-name check <ending> [--json] can this ending be claimed, and if not why | ||
| moshpit-name parse <name> [--json] split a name into its label and ending | ||
| moshpit-name list [-] [--limit N] [--json] | ||
| moshpit-name check (<ending...> | -) [--json | --ndjson] | ||
| can endings be claimed; - reads one per line | ||
| moshpit-name parse (<name...> | -) [--json | --ndjson] | ||
| split names into labels; - reads one per line | ||
| moshpit-name list [-] [--limit N] [--json | --ndjson] | ||
| parse up to N pasted entries; - reads stdin | ||
@@ -50,2 +52,23 @@ moshpit-name reserved [--json] list endings that cannot be claimed | ||
| $ moshpit-name check .eggs .bank --json | ||
| { | ||
| "count": 2, | ||
| "claimableCount": 1, | ||
| "rejectedCount": 1, | ||
| "results": [ | ||
| { | ||
| "input": ".eggs", | ||
| "tld": "eggs", | ||
| "claimable": true, | ||
| "reason": null | ||
| }, | ||
| { | ||
| "input": ".bank", | ||
| "tld": "bank", | ||
| "claimable": false, | ||
| "reason": "that name is reserved" | ||
| } | ||
| ] | ||
| } | ||
| $ moshpit-name parse 1.420 | ||
@@ -90,2 +113,24 @@ 1.420 — not a Moshpit name (one label and one ending; both numeric reads as an address) | ||
| For compatibility, `check <ending> --json` and `parse <name> --json` return | ||
| their established bare result objects. Passing two or more inputs returns a | ||
| batch wrapper with counts and a `results` array; results stay in input order and | ||
| any rejected input makes the command exit non-zero. | ||
| Pass `-` as the only input to `check` or `parse` to read up to 1000 non-empty | ||
| lines from stdin. Stdin JSON always uses the batch wrapper, even for one line, | ||
| so a pipeline receives a stable shape. More than 1000 lines is an error. An | ||
| empty stream is treated like a missing input and exits non-zero. | ||
| Use `--ndjson` with `check`, `parse`, or `list` when each result should be a | ||
| compact JSON object on its own line. This keeps input order, writes even a | ||
| single result as one record, and preserves the command's normal exit status. | ||
| For `list`, the records are the parsed entries; an empty list produces no | ||
| records. `--json` and `--ndjson` cannot be combined. | ||
| ```sh | ||
| $ printf '.eggs\n.bank\n' | moshpit-name check - --ndjson | ||
| {"input":".eggs","tld":"eggs","claimable":true,"reason":null} | ||
| {"input":".bank","tld":"bank","claimable":false,"reason":"that name is reserved"} | ||
| ``` | ||
| `list --limit N` stops after `N` unique entries and reports the remainder in | ||
@@ -92,0 +137,0 @@ `skipped`. `N` must be an integer from 1 through 1000, the package's bulk |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
32631
22.89%614
33.48%162
38.46%1
Infinity%