diff --git a/README.md b/README.md index 09fd6c0..fc6d63e 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,42 @@ Health score is based on CPU, memory, disk, temperature, and I/O load, with colo Shortcuts: In `mo status`, press `k` to toggle the cat and save the preference, and `q` to quit. +#### Machine-Readable Output + +Both `mo analyze` and `mo status` support a `--json` flag for scripting and automation. + +`mo status` also auto-detects when its output is piped (not a terminal) and switches to JSON automatically. + +```bash +# Disk analysis as JSON +$ mo analyze --json ~/Documents +{ + "path": "/Users/you/Documents", + "entries": [ + { "name": "Library", "path": "...", "size": 80939438080, "is_dir": true }, + ... + ], + "total_size": 168393441280, + "total_files": 42187 +} + +# System status as JSON +$ mo status --json +{ + "host": "MacBook-Pro", + "health_score": 92, + "cpu": { "usage": 45.2, "logical_cpu": 8, ... }, + "memory": { "total": 25769803776, "used": 15049334784, "used_percent": 58.4 }, + "disks": [ ... ], + "uptime": "3d 12h 45m", + ... +} + +# Auto-detected JSON when piped +$ mo status | jq '.health_score' +92 +``` + ### Project Artifact Purge Clean old build artifacts such as `node_modules`, `target`, `build`, and `dist` to free up disk space. diff --git a/cmd/status/metrics.go b/cmd/status/metrics.go index 75ecd13..d24e3f1 100644 --- a/cmd/status/metrics.go +++ b/cmd/status/metrics.go @@ -255,9 +255,7 @@ func (c *Collector) Collect() (MetricsSnapshot, error) { // Helper to launch concurrent collection. collect := func(fn func() error) { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { defer func() { if r := recover(); r != nil { errMu.Lock() @@ -279,7 +277,7 @@ func (c *Collector) Collect() (MetricsSnapshot, error) { } errMu.Unlock() } - }() + }) } // Launch independent collection tasks. diff --git a/tests/cli.bats b/tests/cli.bats index 44882fd..2da29d8 100644 --- a/tests/cli.bats +++ b/tests/cli.bats @@ -11,6 +11,19 @@ setup_file() { export HOME mkdir -p "$HOME" + + # Build Go binaries from current source for JSON tests. + # Point GOPATH/GOMODCACHE at the real home so go build doesn't write + # module caches into the fake HOME under tests/. + if command -v go > /dev/null 2>&1; then + ANALYZE_BIN="$(mktemp "${TMPDIR:-/tmp}/analyze-go.XXXXXX")" + STATUS_BIN="$(mktemp "${TMPDIR:-/tmp}/status-go.XXXXXX")" + GOPATH="${ORIGINAL_HOME}/go" GOMODCACHE="${ORIGINAL_HOME}/go/pkg/mod" \ + go build -o "$ANALYZE_BIN" "$PROJECT_ROOT/cmd/analyze" 2>/dev/null + GOPATH="${ORIGINAL_HOME}/go" GOMODCACHE="${ORIGINAL_HOME}/go/pkg/mod" \ + go build -o "$STATUS_BIN" "$PROJECT_ROOT/cmd/status" 2>/dev/null + export ANALYZE_BIN STATUS_BIN + fi } teardown_file() { @@ -19,6 +32,7 @@ teardown_file() { if [[ -n "${ORIGINAL_HOME:-}" ]]; then export HOME="$ORIGINAL_HOME" fi + rm -f "${ANALYZE_BIN:-}" "${STATUS_BIN:-}" } create_fake_utils() { @@ -278,3 +292,130 @@ EOF run grep "pam_tid.so" "$pam_file" [ "$status" -ne 0 ] } + +# --- JSON output mode tests --- + +@test "mo analyze --json outputs valid JSON with expected fields" { + if [[ ! -x "${ANALYZE_BIN:-}" ]]; then + skip "analyze binary not available (go not installed?)" + fi + + run "$ANALYZE_BIN" --json /tmp + [ "$status" -eq 0 ] + + # Validate it is parseable JSON + echo "$output" | python3 -c "import sys, json; json.load(sys.stdin)" + + # Check required top-level keys + echo "$output" | python3 -c " +import sys, json +data = json.load(sys.stdin) +assert 'path' in data, 'missing path' +assert 'entries' in data, 'missing entries' +assert 'total_size' in data, 'missing total_size' +assert 'total_files' in data, 'missing total_files' +assert isinstance(data['entries'], list), 'entries is not a list' +" +} + +@test "mo analyze --json entries contain required fields" { + if [[ ! -x "${ANALYZE_BIN:-}" ]]; then + skip "analyze binary not available (go not installed?)" + fi + + run "$ANALYZE_BIN" --json /tmp + [ "$status" -eq 0 ] + + echo "$output" | python3 -c " +import sys, json +data = json.load(sys.stdin) +for entry in data['entries']: + assert 'name' in entry, 'entry missing name' + assert 'path' in entry, 'entry missing path' + assert 'size' in entry, 'entry missing size' + assert 'is_dir' in entry, 'entry missing is_dir' +" +} + +@test "mo analyze --json path reflects target directory" { + if [[ ! -x "${ANALYZE_BIN:-}" ]]; then + skip "analyze binary not available (go not installed?)" + fi + + run "$ANALYZE_BIN" --json /tmp + [ "$status" -eq 0 ] + + echo "$output" | python3 -c " +import sys, json +data = json.load(sys.stdin) +assert data['path'] == '/tmp' or data['path'] == '/private/tmp', \ + f\"unexpected path: {data['path']}\" +" +} + +@test "mo status --json outputs valid JSON with expected fields" { + if [[ ! -x "${STATUS_BIN:-}" ]]; then + skip "status binary not available (go not installed?)" + fi + + run "$STATUS_BIN" --json + [ "$status" -eq 0 ] + + # Validate it is parseable JSON + echo "$output" | python3 -c "import sys, json; json.load(sys.stdin)" + + # Check required top-level keys + echo "$output" | python3 -c " +import sys, json +data = json.load(sys.stdin) +for key in ['cpu', 'memory', 'disks', 'health_score', 'host', 'uptime']: + assert key in data, f'missing key: {key}' +" +} + +@test "mo status --json cpu section has expected structure" { + if [[ ! -x "${STATUS_BIN:-}" ]]; then + skip "status binary not available (go not installed?)" + fi + + run "$STATUS_BIN" --json + [ "$status" -eq 0 ] + + echo "$output" | python3 -c " +import sys, json +data = json.load(sys.stdin) +cpu = data['cpu'] +assert 'usage' in cpu, 'cpu missing usage' +assert 'logical_cpu' in cpu, 'cpu missing logical_cpu' +assert isinstance(cpu['usage'], (int, float)), 'cpu usage is not a number' +" +} + +@test "mo status --json memory section has expected structure" { + if [[ ! -x "${STATUS_BIN:-}" ]]; then + skip "status binary not available (go not installed?)" + fi + + run "$STATUS_BIN" --json + [ "$status" -eq 0 ] + + echo "$output" | python3 -c " +import sys, json +data = json.load(sys.stdin) +mem = data['memory'] +assert 'total' in mem, 'memory missing total' +assert 'used' in mem, 'memory missing used' +assert 'used_percent' in mem, 'memory missing used_percent' +assert mem['total'] > 0, 'memory total should be positive' +" +} + +@test "mo status --json piped to stdout auto-detects JSON mode" { + if [[ ! -x "${STATUS_BIN:-}" ]]; then + skip "status binary not available (go not installed?)" + fi + + # When piped (not a tty), status should auto-detect and output JSON + output=$("$STATUS_BIN" 2>/dev/null) + echo "$output" | python3 -c "import sys, json; json.load(sys.stdin)" +}