From d492afc462c55729193072dd6172dd031beb64a3 Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Mon, 3 Aug 2026 17:53:07 +0700 Subject: [PATCH] Support checking multiple endings --- README.md | 28 +++++++++++++++- bin/moshpit-name.mjs | 76 +++++++++++++++++++++++++++++++++++--------- test/cli.test.mjs | 52 ++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 925bbf9..7ae0fc0 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ catch drift between two copies is a good sign the copies should be one thing. ## CLI ```sh -moshpit-name check [--json] can this ending be claimed, and if not why +moshpit-name check [--json] + can these endings be claimed, and if not why moshpit-name parse [--json] split a name into its label and ending moshpit-name list [-] [--limit N] [--json] parse up to N pasted entries; - reads stdin @@ -46,6 +47,27 @@ moshpit-name prices [--json] what an ending and a name cost $ moshpit-name check .420 .420 — claimable +$ 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 1.420 — not a Moshpit name (one label and one ending; both numeric reads as an address) @@ -85,6 +107,10 @@ $ moshpit-name check .420 --json } ``` +For compatibility, `check --json` returns the established bare result +object shown above. Passing two or more endings returns the batch wrapper with +counts and a `results` array. + `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 diff --git a/bin/moshpit-name.mjs b/bin/moshpit-name.mjs index db8059a..65295fb 100755 --- a/bin/moshpit-name.mjs +++ b/bin/moshpit-name.mjs @@ -12,7 +12,8 @@ import { const USAGE = `moshpit-name — the Moshpit namespace rules - moshpit-name check [--json] can this ending be claimed, and if not why + moshpit-name check [--json] + can these endings be claimed, and if not why moshpit-name parse [--json] split a name into its label and ending moshpit-name list [-] [--limit N] [--json] parse up to N pasted entries; - reads stdin @@ -22,15 +23,28 @@ const USAGE = `moshpit-name — the Moshpit namespace rules Pure rules, no network. The same answers the registry gives, without asking it.`; const [sub, ...rawRest] = process.argv.slice(2); -const json = rawRest.includes("--json"); +let json = false; const rest = []; let limit = MAX_BULK_TLDS; let limitValue; 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 === "--limit") { + if (sub !== "list") { + optionError ??= 'unknown option "--limit"'; + continue; + } limitFlags++; const candidate = rawRest[index + 1]; if (candidate !== undefined && !candidate.startsWith("--")) { @@ -39,6 +53,10 @@ for (let index = 0; index < rawRest.length; index++) { } continue; } + if (parsingOptions && arg.startsWith("--")) { + optionError ??= `unknown option "${arg}"`; + continue; + } rest.push(arg); } const out = console.log; @@ -55,19 +73,47 @@ if (!sub || sub === "help" || sub === "--help") { process.exit(0); } +if (optionError) { + 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 = rest.length ? rest : [undefined]; + 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 (json) { + if (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") { diff --git a/test/cli.test.mjs b/test/cli.test.mjs index f94420d..6f1e798 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -54,6 +54,58 @@ test("check --json describes claimable, reserved, and malformed endings", () => }); }); +test("check validates multiple endings in one invocation", () => { + const result = run(["check", ".420", ".bank", "two.labels", "--json"]); + + 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)", + }, + ], + }); + + const human = run(["check", "eggs", ".bank", "two.labels"]); + assert.equal(human.status, 1); + assert.equal(human.stderr, ""); + assert.equal(human.stdout, + ".eggs — claimable\n" + + ".bank — that name is reserved\n" + + ".two.labels — not a valid ending (letters, digits and dashes only, no dots)\n"); + + const empty = run(["check"]); + assert.equal(empty.status, 1); + assert.equal(empty.stderr, ""); + assert.equal(empty.stdout, + ". — not a valid ending (letters, digits and dashes only, no dots)\n"); +}); + +test("check rejects unknown options and supports an end-of-options separator", () => { + const limit = run(["check", ".eggs", "--limit", ".bank"]); + assert.equal(limit.status, 1); + assert.equal(limit.stdout, ""); + assert.equal(limit.stderr, 'moshpit-name: unknown option "--limit"\n'); + + const typo = run(["check", ".eggs", "--jsn", "--json"]); + assert.equal(typo.status, 1); + assert.deepEqual(output(typo), { error: 'unknown option "--jsn"' }); + + const literal = run(["check", "--", "--jsn"]); + assert.equal(literal.status, 1); + assert.equal(literal.stderr, ""); + assert.equal(literal.stdout, + ".--jsn — not a valid ending (letters, digits and dashes only, no dots)\n"); +}); + test("parse --json returns normalized fields and structured invalid output", () => { const valid = run(["parse", " Blue.EGGS ", "--json"]); assert.equal(valid.status, 0);