From eb255480a159e9e8e42fc02409a95a622f373dd1 Mon Sep 17 00:00:00 2001 From: Axel Meyer Date: Sat, 23 May 2026 16:59:07 +0200 Subject: [PATCH] Add inspect subcommand: read-only stats for v2 map files Reads the map JSON, optionally loads --atlas-dir paths for name resolution, and prints the inspector report to stdout. Missing file, bad JSON, and missing --atlas-dir paths all produce labelled stderr errors with exit 1. --- src/cli.js | 1 + src/commands/inspect.js | 77 +++++++++++++++++++++++++++++++++++++++++ tests/inspect.test.js | 53 ++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 src/commands/inspect.js create mode 100644 tests/inspect.test.js diff --git a/src/cli.js b/src/cli.js index f1b037d..0213891 100644 --- a/src/cli.js +++ b/src/cli.js @@ -33,6 +33,7 @@ function run(argv, { out, err }) { encode: require('./commands/encode'), decode: require('./commands/decode'), build: require('./commands/build'), + inspect: require('./commands/inspect'), }; const handler = handlers[head]; if (!handler) { diff --git a/src/commands/inspect.js b/src/commands/inspect.js new file mode 100644 index 0000000..7a95c55 --- /dev/null +++ b/src/commands/inspect.js @@ -0,0 +1,77 @@ +'use strict'; + +const fs = require('node:fs'); +const { loadAtlasDirs } = require('../atlas-loader'); +const { inspectMap } = require('../inspector'); + +const USAGE = 'usage: sporel-mapper inspect [--atlas-dir ...]\n'; + +function parseArgs(args) { + const positional = []; + const atlasDirs = []; + let i = 0; + while (i < args.length) { + const a = args[i]; + if (a === '--atlas-dir') { + if (i + 1 >= args.length) { + throw new Error(`--atlas-dir requires a path`); + } + atlasDirs.push(args[i + 1]); + i += 2; + } else { + positional.push(a); + i += 1; + } + } + return { positional, atlasDirs }; +} + +function run(args, { out, err }) { + let parsed; + try { + parsed = parseArgs(args); + } catch (e) { + err.write(`error: ${e.message}\n${USAGE}`); + return 1; + } + if (parsed.positional.length !== 1) { + err.write(USAGE); + return 1; + } + const [mapPath] = parsed.positional; + + let raw; + try { + raw = fs.readFileSync(mapPath, 'utf8'); + } catch (e) { + err.write(`error: cannot read ${mapPath}: ${e.message}\n`); + return 1; + } + + let map; + try { + map = JSON.parse(raw); + } catch (e) { + err.write(`error: ${mapPath} is not valid JSON: ${e.message}\n`); + return 1; + } + + let registry = {}; + if (parsed.atlasDirs.length > 0) { + try { + const result = loadAtlasDirs(parsed.atlasDirs); + registry = result.registry; + for (const w of result.warnings) { + err.write(`warning: ${w}\n`); + } + } catch (e) { + err.write(`error: ${e.message}\n`); + return 1; + } + } + + out.write(inspectMap(map, registry)); + return 0; +} + +module.exports = { run }; diff --git a/tests/inspect.test.js b/tests/inspect.test.js new file mode 100644 index 0000000..dc0c504 --- /dev/null +++ b/tests/inspect.test.js @@ -0,0 +1,53 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); +const { run } = require('../src/cli'); + +class Sink { + constructor() { this.chunks = []; } + write(s) { this.chunks.push(s); } + get text() { return this.chunks.join(''); } +} + +const FIX_MAP = path.join(__dirname, 'fixtures', 'maps', 'inspect_sample.map.json'); +const FIX_ATLAS = path.join(__dirname, 'fixtures', 'atlases'); + +test('inspect: prints report to stdout, exit 0', () => { + const out = new Sink(); const err = new Sink(); + const code = run(['inspect', FIX_MAP], { out, err }); + assert.equal(code, 0, err.text); + assert.match(out.text, /Map: inspect_sample/); + assert.match(out.text, /Roof: 4 cells flagged/); +}); + +test('inspect: --atlas-dir enables name resolution', () => { + const out = new Sink(); const err = new Sink(); + const code = run(['inspect', FIX_MAP, '--atlas-dir', FIX_ATLAS], { out, err }); + assert.equal(code, 0); + assert.match(out.text, /atlas_a:grass/); +}); + +test('inspect: missing positional arg prints usage, exit 1', () => { + const out = new Sink(); const err = new Sink(); + const code = run(['inspect'], { out, err }); + assert.equal(code, 1); + assert.match(err.text, /usage: sporel-mapper inspect/); +}); + +test('inspect: missing map file prints error, exit 1', () => { + const out = new Sink(); const err = new Sink(); + const code = run(['inspect', '/no/such/map.json'], { out, err }); + assert.equal(code, 1); + assert.match(err.text, /error: cannot read/); +}); + +test('inspect: invalid JSON prints error, exit 1', () => { + const out = new Sink(); const err = new Sink(); + // Use a non-JSON file as bogus input + const reallyNotJson = path.join(__dirname, '..', 'src', 'cli.js'); + const code = run(['inspect', reallyNotJson], { out, err }); + assert.equal(code, 1); + assert.match(err.text, /error/i); +});