Add rivinfo tool
diff --git a/.gitignore b/.gitignore
index 563e7e5..72dd777 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,9 +31,6 @@
 *.out
 *.app
 
-# test builds
-dev/test/build/bin/*
-
 # aot snapshots
 dev/bin/*
 
@@ -57,6 +54,8 @@
 
 # Build directories
 build/bin
+dev/test/build/bin
+rivinfo/build/macosx
 
 # Skia dependencies
 skia/dependencies/skia
diff --git a/rivinfo/build.sh b/rivinfo/build.sh
new file mode 100755
index 0000000..e0d0eb5
--- /dev/null
+++ b/rivinfo/build.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+# dir=$(pwd)
+
+# cd ../renderer
+# ./build.sh $@
+
+# cd $dir
+
+cd build
+
+OPTION=$1
+
+if [ "$OPTION" = 'help' ]; then
+    echo build.sh - build debug library
+    echo build.sh clean - clean the build
+    echo build.sh release - build release library
+elif [ "$OPTION" = "clean" ]; then
+    echo Cleaning project ...
+    # TODO: fix premake5 clean to bubble the clean command to dependent projects
+    premake5 gmake && make clean
+elif [ "$OPTION" = "release" ]; then
+    premake5 gmake && make config=release -j7
+else
+    premake5 gmake && make -j7
+fi
diff --git a/rivinfo/build/premake5.lua b/rivinfo/build/premake5.lua
new file mode 100644
index 0000000..0c69b56
--- /dev/null
+++ b/rivinfo/build/premake5.lua
@@ -0,0 +1,73 @@
+workspace "rive"
+configurations {"debug", "release"}
+
+project "rivinfo"
+    kind "ConsoleApp"
+    language "C++"
+    cppdialect "C++17"
+    targetdir "%{cfg.system}/bin/%{cfg.buildcfg}"
+    objdir "%{cfg.system}/obj/%{cfg.buildcfg}"
+    includedirs {
+        "../../include",
+        "../../test",
+        "/usr/local/include",
+        "/usr/include",
+    }
+
+    if os.host() == 'macosx' then 
+        links {
+            "Cocoa.framework",
+            "CoreFoundation.framework",
+            "IOKit.framework",
+            "Security.framework",
+            "bz2",
+            "iconv",
+            "lzma",
+            "rive",
+            "z",  -- lib av format 
+        }
+    else
+        links {
+            "m",
+            "rive",
+            "z",
+            "dl",
+        }
+    end 
+
+    libdirs {
+        "../../build/%{cfg.system}/bin/%{cfg.buildcfg}",
+        "/usr/local/lib",
+        "/usr/lib",
+    }
+
+    files {
+        "../**.cpp",
+        "../../test/no_op_factory.cpp",
+    }
+
+    buildoptions {"-Wall", "-fno-rtti", "-g"}
+
+    filter "configurations:debug"
+    defines {"DEBUG"}
+    symbols "On"
+
+    filter "configurations:release"
+    defines {"RELEASE"}
+    defines {"NDEBUG"}
+    optimize "On"
+
+-- Clean Function --
+newaction {
+    trigger = "clean",
+    description = "clean the build",
+    execute = function()
+        print("clean the build...")
+        os.rmdir("./bin")
+        os.rmdir("./obj")
+        os.remove("Makefile")
+        -- no wildcards in os.remove, so use shell
+        os.execute("rm *.make")
+        print("build cleaned")
+    end
+}
diff --git a/rivinfo/main.cpp b/rivinfo/main.cpp
new file mode 100644
index 0000000..17b70e5
--- /dev/null
+++ b/rivinfo/main.cpp
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "rive/artboard.hpp"
+#include "rive/file.hpp"
+#include "rive/animation/linear_animation_instance.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "no_op_factory.hpp"
+
+class JSoner {
+    std::vector<bool> m_IsArray;
+
+    void tab() {
+        for (int i = 0; i < m_IsArray.size(); ++i) {
+            printf("\t");
+        }
+    }
+    void add_c(const char key[], char c) {
+        this->tab();
+        if (key) {
+            printf("\"%s\": %c\n", key, c);
+        } else {
+            printf("%c\n", c);
+        }
+    }
+
+public:
+    JSoner() {}
+    ~JSoner() {
+        while (!m_IsArray.empty()) {
+            this->pop();
+        }
+    }
+
+    void add(const char key[], const char value[]) {
+        this->tab();
+        printf("\"%s\": \"%s\"\n", key, value);
+    }
+    void pushArray(const char key[] = nullptr) {
+        this->add_c(key, '[');
+        m_IsArray.push_back(true);
+    }
+    void pushStruct(const char key[] = nullptr) {
+        this->add_c(key, '{');
+        m_IsArray.push_back(false);
+    }
+    void pop() {
+        assert(!m_IsArray.empty());
+        char c = m_IsArray.front() ? ']' : '}';
+        m_IsArray.pop_back();
+
+        this->tab();
+        printf("%c\n", c);
+    }
+
+    void add(const char key[], int value) {
+        this->add(key, std::to_string(value).c_str());
+    }
+};
+
+//////////////////////////////////////////////////
+
+static void dump(JSoner& js, rive::LinearAnimationInstance* anim) {
+    js.pushStruct();
+    js.add("name", anim->name().c_str());
+    js.add("duration", std::to_string(anim->durationSeconds()).c_str());
+    js.add("loop", std::to_string(anim->loopValue()).c_str());
+    js.pop();
+}
+
+static void dump(JSoner& js, rive::StateMachineInstance* smi) {
+    js.pushStruct();
+    js.add("name", smi->name().c_str());
+    if (auto count = smi->inputCount()) {
+        js.pushArray("inputs");
+        for (auto i = 0; i < count; ++i) {
+            auto inp = smi->input(i);
+            js.add("name", inp->name().c_str());
+        }
+        js.pop();
+    }
+    js.pop();
+}
+
+static void dump(JSoner& js, rive::ArtboardInstance* abi) {
+    js.pushStruct();
+    js.add("name", abi->name().c_str());
+    if (auto count = abi->animationCount()) {
+        js.pushArray("animations");
+        for (size_t i = 0; i < count; ++i) {
+            dump(js, abi->animationAt(i).get());
+        }
+        js.pop();
+    }
+    if (auto count = abi->stateMachineCount()) {
+        js.pushArray("machines");
+        for (size_t i = 0; i < count; ++i) {
+            dump(js, abi->stateMachineAt(i).get());
+        }
+        js.pop();
+    }
+    js.pop();
+}
+
+static void dump(JSoner& js, rive::File* file) {
+    auto count = file->artboardCount();
+    js.pushArray("artboards");
+    for (size_t i = 0; i < count; ++i) {
+        dump(js, file->artboardAt(i).get());
+    }
+    js.pop();
+}
+
+static std::unique_ptr<rive::File> open_file(const char name[]) {
+    FILE* f = fopen(name, "rb");
+    if (!f) {
+        return nullptr;
+    }
+
+    fseek(f, 0, SEEK_END);
+    auto length = ftell(f);
+    fseek(f, 0, SEEK_SET);
+
+    std::vector<uint8_t> bytes(length);
+
+    if (fread(bytes.data(), 1, length, f) != length) {
+        printf("Failed to read file into bytes array\n");
+        return nullptr;
+    }
+
+    static rive::NoOpFactory gFactory;
+    return rive::File::import(rive::toSpan(bytes), &gFactory);
+}
+
+static bool is_arg(const char arg[], const char target[], const char alt[] = nullptr) {
+    return !strcmp(arg, target) || (arg && !strcmp(arg, alt));
+}
+
+int main(int argc, const char* argv[]) {
+    const char* filename = nullptr;
+
+    for (int i = 1; i < argc; ++i) {
+        if (is_arg(argv[i], "--file", "-f")) {
+            filename = argv[++i];
+            continue;
+        }
+        printf("Unrecognized argument %s\n", argv[i]);
+        return 1;
+    }
+
+    if (!filename) {
+        printf("Need --file filename\n");
+        return 1;
+    }
+
+    auto file = open_file(filename);
+    if (!file) {
+        printf("Can't open %s\n", filename);
+        return 1;
+    }
+
+    JSoner js;
+    js.pushStruct();
+    dump(js, file.get());
+    return 0;
+}