From b1743ad19f43421a78ff24b53d3814a28d1a72f0 Mon Sep 17 00:00:00 2001 From: Axel Meyer Date: Sat, 23 May 2026 13:44:10 +0200 Subject: [PATCH] Add CLI dispatcher and bin entry Top-level run(argv,{out,err}) accepts an injectable output sink so handlers can be unit-tested without spawning a subprocess. Handler map is empty for now; subcommands are wired in as they land. Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/mapper.js | 5 +++++ src/cli.js | 43 +++++++++++++++++++++++++++++++++++++++++++ tests/cli.test.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 bin/mapper.js create mode 100644 src/cli.js create mode 100644 tests/cli.test.js diff --git a/bin/mapper.js b/bin/mapper.js new file mode 100644 index 0000000..7029653 --- /dev/null +++ b/bin/mapper.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node +'use strict'; + +const { run } = require('../src/cli'); +process.exit(run(process.argv.slice(2), { out: process.stdout, err: process.stderr })); diff --git a/src/cli.js b/src/cli.js new file mode 100644 index 0000000..edcda16 --- /dev/null +++ b/src/cli.js @@ -0,0 +1,43 @@ +'use strict'; + +const USAGE = `usage: sporel-mapper [args...] + +Subcommands: + encode [rotation] + Print the packed-u32 GID for the given components. + + decode + Print the (atlas, tile, rotation) components of a GID. + + build [--atlas-dir ...] + Compile a text-DSL map spec into a v2 map JSON file. + + inspect [--atlas-dir ...] + Print a human-readable stats report for a v2 map. + +Options: + -h, --help Show this message. +`; + +function run(argv, { out, err }) { + if (argv.length === 0) { + err.write(USAGE); + return 1; + } + const [head, ...rest] = argv; + if (head === '-h' || head === '--help') { + out.write(USAGE); + return 0; + } + const handlers = { + // populated in subsequent tasks + }; + const handler = handlers[head]; + if (!handler) { + err.write(`unknown subcommand: ${head}\n${USAGE}`); + return 1; + } + return handler.run(rest, { out, err }); +} + +module.exports = { run }; diff --git a/tests/cli.test.js b/tests/cli.test.js new file mode 100644 index 0000000..154895c --- /dev/null +++ b/tests/cli.test.js @@ -0,0 +1,46 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { run } = require('../src/cli'); + +class Sink { + constructor() { this.chunks = []; } + write(s) { this.chunks.push(s); } + get text() { return this.chunks.join(''); } +} + +test('cli: no args prints usage to stderr, exit 1', () => { + const out = new Sink(); const err = new Sink(); + const code = run([], { out, err }); + assert.equal(code, 1); + assert.match(err.text, /usage/i); + assert.match(err.text, /encode/); + assert.match(err.text, /decode/); + assert.match(err.text, /build/); + assert.match(err.text, /inspect/); + assert.equal(out.text, ''); +}); + +test('cli: --help prints usage to stdout, exit 0', () => { + const out = new Sink(); const err = new Sink(); + const code = run(['--help'], { out, err }); + assert.equal(code, 0); + assert.match(out.text, /usage/i); + assert.equal(err.text, ''); +}); + +test('cli: -h alias for --help', () => { + const out = new Sink(); const err = new Sink(); + const code = run(['-h'], { out, err }); + assert.equal(code, 0); + assert.match(out.text, /usage/i); +}); + +test('cli: unknown subcommand goes to stderr, exit 1', () => { + const out = new Sink(); const err = new Sink(); + const code = run(['frobnicate'], { out, err }); + assert.equal(code, 1); + assert.match(err.text, /unknown subcommand/i); + assert.match(err.text, /frobnicate/); +});