Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ catch drift between two copies is a good sign the copies should be one thing.
## CLI

```sh
moshpit-name check <ending...> [--json]
can these endings be claimed, and if not why
moshpit-name parse <name...> [--json]
split names into their labels and endings
moshpit-name check (<ending...> | -) [--json]
can endings be claimed; - reads one per line
moshpit-name parse (<name...> | -) [--json]
split names into labels; - reads one per line
moshpit-name list [-] [--limit N] [--json]
parse up to N pasted entries; - reads stdin
moshpit-name reserved [--json] list endings that cannot be claimed
Expand Down Expand Up @@ -113,6 +113,11 @@ 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.

`list --limit N` stops after `N` unique entries and reports the remainder in
`skipped`. `N` must be an integer from 1 through 1000, the package's bulk
ceiling. The option works with inline arguments or stdin and can appear before
Expand Down
56 changes: 48 additions & 8 deletions bin/moshpit-name.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@
// asked over the network — and the answers are pure, so they are the same
// offline, in CI, and inside a browser extension.

import { createInterface } from "node:readline";

import {
CHILD_PRICE_USD, ENDING_PRICE_USD, MAX_BULK_TLDS, RESERVED_TLDS,
normalizeTld, parseMoshpitName, parseTldList, tldRejection,
} from "../lib/index.mjs";

const USAGE = `moshpit-name — the Moshpit namespace rules

moshpit-name check <ending...> [--json]
can these endings be claimed, and if not why
moshpit-name parse <name...> [--json]
split names into their labels and endings
moshpit-name check (<ending...> | -) [--json]
can endings be claimed; - reads one per line
moshpit-name parse (<name...> | -) [--json]
split names into labels; - reads one per line
moshpit-name list [-] [--limit N] [--json]
parse up to N pasted entries; - reads stdin
moshpit-name reserved [--json] list endings that cannot be claimed
Expand Down Expand Up @@ -69,6 +71,42 @@ const readStdin = async () => {
return data;
};

const readCommandStdin = async () => {
const lines = [];
const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
for await (const line of input) {
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) {
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 (json) outJson({ error });
else console.error(`moshpit-name: ${error}`);
process.exit(1);
};

if (!sub || sub === "help" || sub === "--help") {
out(USAGE);
process.exit(0);
Expand All @@ -81,7 +119,8 @@ if (optionError) {
}

if (sub === "check") {
const inputs = rest.length ? rest : [undefined];
const { inputs, fromStdin, error } = await commandInputs(rest);
if (error) exitInputError(error);
const results = inputs.map((input) => {
const tld = normalizeTld(input);
if (!tld) {
Expand All @@ -97,7 +136,7 @@ if (sub === "check") {
});

if (json) {
if (results.length === 1) outJson(results[0]);
if (!fromStdin && results.length === 1) outJson(results[0]);
else {
const claimableCount = results.filter((result) => result.claimable).length;
outJson({
Expand All @@ -118,7 +157,8 @@ if (sub === "check") {
}

if (sub === "parse") {
const inputs = rest.length ? rest : [undefined];
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);
Expand All @@ -131,7 +171,7 @@ if (sub === "parse") {
});

if (json) {
if (results.length === 1) outJson(results[0]);
if (!fromStdin && results.length === 1) outJson(results[0]);
else {
const validCount = results.filter((result) => result.valid).length;
outJson({
Expand Down
103 changes: 101 additions & 2 deletions test/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ test("commands stay human-readable unless --json is requested", () => {
assert.equal(result.stdout, ".420 — claimable\n");
});

test("help documents the batch parse syntax", () => {
test("help documents batch arguments and stdin", () => {
const result = run(["--help"]);
assert.equal(result.status, 0);
assert.equal(result.stderr, "");
assert.match(result.stdout, /moshpit-name parse <name\.\.\.> \[--json\]/);
assert.match(result.stdout, /moshpit-name check \(<ending\.\.\.> \| -\) \[--json\]/);
assert.match(result.stdout, /moshpit-name parse \(<name\.\.\.> \| -\) \[--json\]/);
});

test("check --json describes claimable, reserved, and malformed endings", () => {
Expand Down Expand Up @@ -96,6 +97,60 @@ test("check validates multiple endings in one invocation", () => {
". — not a valid ending (letters, digits and dashes only, no dots)\n");
});

test("check reads newline-delimited endings from stdin", () => {
const result = run(["check", "-", "--json"], ".420\r\n\r\n.bank\r\ntwo.labels\r\n");

assert.equal(result.status, 1);
assert.deepEqual(output(result), {
count: 3,
claimableCount: 1,
rejectedCount: 2,
results: [
{ input: ".420", tld: "420", claimable: true, reason: null },
{ input: ".bank", tld: "bank", claimable: false, reason: "that name is reserved" },
{
input: "two.labels",
tld: null,
claimable: false,
reason: "not a valid ending (letters, digits and dashes only, no dots)",
},
],
});
});

test("single-line check stdin keeps the batch JSON shape on success", () => {
const result = run(["check", "-", "--json"], ".420\n");

assert.equal(result.status, 0);
assert.deepEqual(output(result), {
count: 1,
claimableCount: 1,
rejectedCount: 0,
results: [
{ input: ".420", tld: "420", claimable: true, reason: null },
],
});
});

test("empty stdin is reported like a missing check input", () => {
const missing = run(["check"]);
const empty = run(["check", "-"], "\n\r\n");
assert.equal(empty.status, 1);
assert.equal(empty.stdout, missing.stdout);
assert.equal(empty.stderr, missing.stderr);
});

test("check rejects stdin beyond the bulk ceiling without a stack trace", () => {
const input = Array.from({ length: MAX_BULK_TLDS + 1 }, () => ".eggs").join("\n");
const result = run(["check", "-", "--json"], input);

assert.equal(result.status, 1);
assert.equal(result.stderr, "");
assert.deepEqual(output(result), {
error: `stdin accepts at most ${MAX_BULK_TLDS} non-empty lines`,
});
});

test("check rejects unknown options and supports an end-of-options separator", () => {
const limit = run(["check", ".eggs", "--limit", ".bank"]);
assert.equal(limit.status, 1);
Expand Down Expand Up @@ -179,6 +234,50 @@ test("parse validates multiple names while preserving input order", () => {
);
});

test("parse reads newline-delimited names from stdin", () => {
const result = run(["parse", "--json", "-"], " Blue.EGGS \n\n1.420\nred.420\n");

assert.equal(result.status, 1);
assert.deepEqual(output(result), {
count: 3,
validCount: 2,
invalidCount: 1,
results: [
{ input: " Blue.EGGS ", valid: true, label: "blue", tld: "eggs", reason: null },
{
input: "1.420",
valid: false,
label: null,
tld: null,
reason: "not a Moshpit name (one label and one ending; both numeric reads as an address)",
},
{ input: "red.420", valid: true, label: "red", tld: "420", reason: null },
],
});
});

test("single-line parse stdin keeps the batch JSON shape on success", () => {
const result = run(["parse", "-", "--json"], "Blue.EGGS\n");

assert.equal(result.status, 0);
assert.deepEqual(output(result), {
count: 1,
validCount: 1,
invalidCount: 0,
results: [
{ input: "Blue.EGGS", valid: true, label: "blue", tld: "eggs", reason: null },
],
});
});

test("empty stdin is reported like a missing parse input", () => {
const missing = run(["parse"]);
const empty = run(["parse", "-"], "");
assert.equal(empty.status, 1);
assert.equal(empty.stdout, missing.stdout);
assert.equal(empty.stderr, missing.stderr);
});

test("list --json emits the complete parse result from stdin", () => {
const result = run(["list", "--json", "-"], ".Eggs\nblue.eggs\nyeah\n");
assert.equal(result.status, 0);
Expand Down
Loading