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
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ would only work in one of those places.
## CLI

```sh
moshpit-resolve [<name...>] [--stdin] [--moshpit] [--clearnet-resolves] [--registry URL] [--console URL] [--parking URL] [--timeout MS] [--concurrency N] [--strict] [--json]
moshpit-resolve [<name...>] [--stdin] [--moshpit] [--clearnet-resolves] [--registry URL] [--console URL] [--parking URL] [--timeout MS] [--concurrency N] [--strict] [--json | --ndjson]
```

```
Expand All @@ -83,6 +83,13 @@ every navigation, and the thing you need when a name goes somewhere unexpected.

Use `--json` when another tool needs the registry answer, decision, reason, and
destination without parsing the human-readable summary.
Use `--ndjson` for batch pipelines that consume one compact JSON object per
line. Input order and exit-status rules are identical to `--json`; an empty
stdin batch emits no records. The two output flags are mutually exclusive.

```sh
moshpit-resolve blue.eggs red.eggs --ndjson | jq -c 'select(.destination)'
```

By default, an unavailable registry still exits successfully after reporting
the clearnet fallback. Scripts can add `--strict` to exit non-zero when any
Expand Down
30 changes: 23 additions & 7 deletions bin/moshpit-resolve.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {

const USAGE = `moshpit-resolve — where a Moshpit name would send you

moshpit-resolve [<name...>] [--stdin] [--moshpit] [--clearnet-resolves] [--registry URL] [--console URL] [--parking URL] [--timeout MS] [--concurrency N] [--strict] [--json]
moshpit-resolve [<name...>] [--stdin] [--moshpit] [--clearnet-resolves] [--registry URL] [--console URL] [--parking URL] [--timeout MS] [--concurrency N] [--strict] [--json | --ndjson]

--moshpit let a registered name beat a clearnet answer
--clearnet-resolves pretend the real internet has an answer for this name
Expand All @@ -22,6 +22,7 @@ const USAGE = `moshpit-resolve — where a Moshpit name would send you
--strict fail when any registry lookup is inconclusive
--stdin append whitespace-delimited names from standard input
--json print a machine-readable resolution decision
--ndjson print one compact JSON decision per line

Prints the destination and the reason for it. No browser, no navigation.`;

Expand All @@ -41,24 +42,39 @@ const value = (n, d) => {
const candidate = i >= 0 ? args[i + 1] : null;
return candidate && !candidate.startsWith("-") ? candidate : d;
};
const raw = flag("json");
const ndjson = flag("ndjson");
const raw = flag("json") || ndjson;
const timeoutValue = value("timeout", null);
const concurrencyValue = value("concurrency", null);
const jsonError = (error) => {
if (names.length === 0) return { error };
if (names.length === 1) return { name, error };
return names.map((requestedName) => ({ name: requestedName, error }));
};
const machineText = (value, space) => {
if (ndjson) {
const records = Array.isArray(value) ? value : [value];
return records.map((record) => JSON.stringify(record)).join("\n");
}
return JSON.stringify(value, null, space);
};
const printMachine = (value) => {
const output = machineText(value, 2);
if (output) console.log(output);
};
const printJsonError = (error) => new Promise((resolve, reject) => {
const output = `${JSON.stringify(
jsonError(error), null, names.length > 1 ? 2 : undefined,
)}\n`;
const output = `${machineText(jsonError(error), names.length > 1 ? 2 : undefined)}\n`;
process.stdout.write(output, (writeError) => {
if (writeError) reject(writeError);
else resolve();
});
});

if (flag("json") && ndjson) {
await printJsonError("--json and --ndjson cannot be used together");
process.exit(1);
}

for (const option of ["registry", "console", "parking"]) {
if (flag(option) && value(option, null) === null) {
const error = `--${option} requires a URL`;
Expand Down Expand Up @@ -186,12 +202,12 @@ if (names.length === 1) {
process.exit(1);
}

if (raw) console.log(JSON.stringify(result, null, 2));
if (raw) printMachine(result);
else printHuman(result);
if (strictFailure(result)) process.exitCode = 1;
} else {
if (raw) {
console.log(JSON.stringify(results, null, 2));
printMachine(results);
} else {
results.forEach((result, index) => {
if (index > 0) console.log("");
Expand Down
69 changes: 69 additions & 0 deletions test/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,70 @@ function run(args, {
});
}

test("--ndjson prints one compact decision per input in order", async () => {
const result = await run([
"mosh.eggs", "localhost", "mosh.apples",
"--console", "https://console.example", "--ndjson",
]);

assert.equal(result.status, 1);
assert.equal(result.stderr, "");
const lines = result.stdout.trimEnd().split("\n");
assert.equal(lines.length, 3);
assert.ok(lines.every((line) => !line.startsWith(" ")));
const records = lines.map((line) => JSON.parse(line));
assert.deepEqual(records.map(({ name }) => name), [
"mosh.eggs", "localhost", "mosh.apples",
]);
assert.equal(records[0].destination, "https://console.example/pit?tld=eggs");
assert.equal(records[1].error, "not a Moshpit name (one label and one ending)");
assert.equal(records[2].destination, "https://console.example/pit?tld=apples");
});

test("--ndjson keeps a single result on one compact line", async () => {
const result = await run([
"mosh.eggs", "--console", "https://console.example", "--ndjson",
]);

assert.equal(result.status, 0, result.stderr || result.stdout);
assert.equal(result.stderr, "");
assert.equal(result.stdout.trimEnd().split("\n").length, 1);
assert.equal(JSON.parse(result.stdout).name, "mosh.eggs");
});

test("--ndjson emits no records for an empty stdin batch", async () => {
const result = await run(["--stdin", "--ndjson"], { stdin: " \n\t " });

assert.equal(result.status, 0);
assert.equal(result.stdout, "");
assert.equal(result.stderr, "");
});

test("machine-readable output flags are mutually exclusive", async () => {
const result = await run(["blue.eggs", "--json", "--ndjson"]);

assert.equal(result.status, 1);
assert.equal(result.stderr, "");
assert.deepEqual(JSON.parse(result.stdout), {
name: "blue.eggs",
error: "--json and --ndjson cannot be used together",
});
});

test("batch option errors use one NDJSON record per requested name", async () => {
const result = await run([
"blue.eggs", "red.eggs", "--registry", "--ndjson",
]);

assert.equal(result.status, 1);
assert.equal(result.stderr, "");
const records = result.stdout.trimEnd().split("\n").map((line) => JSON.parse(line));
assert.deepEqual(records, [
{ name: "blue.eggs", error: "--registry requires a URL" },
{ name: "red.eggs", error: "--registry requires a URL" },
]);
});

test("--json prints the complete machine-readable resolution", async () => {
let requests = 0;
const server = createServer((_request, response) => {
Expand Down Expand Up @@ -88,6 +152,11 @@ test("--json prints the complete machine-readable resolution", async () => {
},
destination: `http://127.0.0.1:${port}/n/blue.eggs`,
});
assert.equal(
result.stdout,
`${JSON.stringify(JSON.parse(result.stdout), null, 2)}\n`,
"--json should preserve its indented output",
);
assert.equal(requests, 1, "the CLI should make one registry request per resolution");
} finally {
await new Promise((resolve) => server.close(resolve));
Expand Down
Loading