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
42 changes: 10 additions & 32 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,17 @@
# Argus Project Guidelines

## Quick Reference
- **Version**: `gradle.properties` → `argusVersion` (single source of truth, read by JAR manifest)
- **Java**: 21 required (bytecode target). Diagnoses any JVM 11+
- **Build**: `./gradlew :argus-cli:fatJar`
- **Test**: `./gradlew :argus-cli:test` (125+ tests)
- **Install locally**: `cp argus-cli/build/libs/argus-cli-*-all.jar ~/.argus/argus-cli.jar`
- **Test**: `./gradlew :argus-cli:test`
- **Install**: `cp argus-cli/build/libs/argus-cli-*-all.jar ~/.argus/argus-cli.jar`
- **Java**: 21 bytecode target. CLI diagnoses JVM 11+

## Version Management
After ANY version bump: `grep -rn 'OLD_VERSION' --include='*.md' --include='*.html' .`
## Rules
- Version source of truth: `gradle.properties` → `argusVersion`. After bump: `grep -rn 'OLD_VERSION' --include='*.md' --include='*.html' .`
- Commit: `feat:`/`fix:`/`docs:`/`refactor:`/`test:` with `-s` flag. No `Co-Authored-By: Claude`.
- VERSION: read from JAR manifest `Implementation-Version` (never hardcode)

Update locations: `gradle.properties`, `README.md`, `docs/getting-started.md`, `docs/usage.md`, `site/index.html`, `install.sh`
## Adding Commands
See [docs/architecture.md](docs/architecture.md) for full patterns (SPI, Provider, CommandGroup, i18n, TUI).

## Adding a New CLI Command
1. Command class → `argus-cli/.../command/<Name>Command.java` (implements `Command`, override `group()`)
2. Register in `ArgusCli.java` → `register(commands, new <Name>Command())`
3. i18n → `cmd.<name>.desc=...` in all 4 `messages_*.properties`
4. Completions → `completions/argus.bash`, `.zsh`, `.fish`, `.ps1`
5. Test + docs update

For commands with provider pattern (jcmd-based): add Result model, Provider interface, JdkProvider, register in ProviderRegistry.

For SPI server commands: add `DiagnosticCommand` impl in `argus-server/.../impl/`, register in `META-INF/services`.

See [Architecture Guide](docs/architecture.md) for full module structure and code patterns.

## Commit Convention
- `feat:` / `fix:` / `docs:` / `refactor:` / `test:`
- Always use `-s` flag for DCO sign-off
- Do NOT include `Co-Authored-By: Claude` line

## Key Architecture Decisions
- **DiagnosticCommand SPI** — `argus-core` shared interface, ServiceLoader auto-discovery
- **CommandGroup** — shared enum for CLI help categorization + server API grouping
- **JvmSnapshotCollector** — local (MXBean) vs remote (jcmd parsing), routes by PID
- **Doctor rules** — pluggable `HealthRule` interface, each rule independent
- **TUI** — JLine3 + alt screen buffer, 3-phase flow (PS → CMD → OUT)
- **VERSION** — read from JAR manifest `Implementation-Version` (never hardcode)
Checklist: Command class → ArgusCli register → 4x messages_*.properties → completions → test
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public final class CiCommand implements Command {

@Override public String name() { return "ci"; }
@Override public CommandGroup group() { return CommandGroup.PROFILING; }
@Override public CommandMode mode() { return CommandMode.WRITE; }
@Override public boolean supportsTui() { return false; }

@Override
Expand Down
14 changes: 14 additions & 0 deletions argus-cli/src/main/java/io/argus/cli/command/Command.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
*/
public interface Command {

/** Read-only vs mutating/extracting command classification. */
enum CommandMode {
/** Safe, read-only inspection (threads, gc, heap, etc.) */
READ,
/** Mutating or extracting operation (heapdump, gcrun, vmset, profile, etc.) */
WRITE
}

/**
* The command name used to route from the CLI entry point.
*/
Expand All @@ -24,6 +32,12 @@ public interface Command {
*/
default CommandGroup group() { return CommandGroup.MONITORING; }

/**
* Whether this command is read-only or mutating/extracting.
* TUI shows READ commands by default; WRITE commands require explicit toggle.
*/
default CommandMode mode() { return CommandMode.READ; }

/**
* Short description shown in the help/usage output.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public final class CompareCommand implements Command {

@Override public String name() { return "compare"; }
@Override public CommandGroup group() { return CommandGroup.PROFILING; }
@Override public boolean supportsTui() { return false; }
@Override public CommandMode mode() { return CommandMode.WRITE; }

@Override
public String description(Messages messages) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public final class FlameCommand implements Command {

@Override public String name() { return "flame"; }
@Override public CommandGroup group() { return CommandGroup.PROFILING; }
@Override public CommandMode mode() { return CommandMode.WRITE; }

@Override
public String description(Messages messages) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public final class GcLogCommand implements Command {

@Override public String name() { return "gclog"; }
@Override public CommandGroup group() { return CommandGroup.MEMORY; }
@Override public boolean supportsTui() { return false; }
@Override public CommandMode mode() { return CommandMode.WRITE; }

@Override
public String description(Messages messages) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public String name() {
}

@Override public CommandGroup group() { return CommandGroup.MEMORY; }
@Override public CommandMode mode() { return CommandMode.WRITE; }

@Override
public String description(Messages messages) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public final class HeapAnalyzeCommand implements Command {

@Override public String name() { return "heapanalyze"; }
@Override public CommandGroup group() { return CommandGroup.MEMORY; }
@Override public CommandMode mode() { return CommandMode.WRITE; }

@Override
public String description(Messages messages) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public String name() {
}

@Override public CommandGroup group() { return CommandGroup.MEMORY; }
@Override public CommandMode mode() { return CommandMode.WRITE; }
@Override public boolean supportsTui() { return false; }

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public final class JfrAnalyzeCommand implements Command {

private static final int WIDTH = RichRenderer.DEFAULT_WIDTH;

@Override public boolean supportsTui() { return false; }
@Override public CommandMode mode() { return CommandMode.WRITE; }

@Override
public String name() {
return "jfranalyze";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public String name() {
}

@Override public CommandGroup group() { return CommandGroup.PROFILING; }
@Override public CommandMode mode() { return CommandMode.WRITE; }

@Override
public String description(Messages messages) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public final class JmxCommand implements Command {
public String name() { return "jmx"; }

@Override public CommandGroup group() { return CommandGroup.PROCESS; }
@Override public CommandMode mode() { return CommandMode.WRITE; }

@Override
public String description(Messages messages) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public String name() {
}

@Override public CommandGroup group() { return CommandGroup.PROFILING; }
@Override public CommandMode mode() { return CommandMode.WRITE; }

@Override
public String description(Messages messages) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public final class SlowlogCommand implements Command {

@Override public String name() { return "slowlog"; }
@Override public CommandGroup group() { return CommandGroup.PROFILING; }
@Override public CommandMode mode() { return CommandMode.WRITE; }
@Override public boolean supportsTui() { return false; }

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public String name() {
}

@Override public CommandGroup group() { return CommandGroup.THREADS; }
@Override public CommandMode mode() { return CommandMode.WRITE; }

@Override
public String description(Messages messages) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public final class VmLogCommand implements Command {
public String name() { return "vmlog"; }

@Override public CommandGroup group() { return CommandGroup.PROCESS; }
@Override public CommandMode mode() { return CommandMode.WRITE; }

@Override
public String description(Messages messages) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public final class VmSetCommand implements Command {
public String name() { return "vmset"; }

@Override public CommandGroup group() { return CommandGroup.PROCESS; }
@Override public CommandMode mode() { return CommandMode.WRITE; }
@Override public boolean supportsTui() { return false; }

@Override
Expand Down
93 changes: 74 additions & 19 deletions argus-cli/src/main/java/io/argus/cli/tui/TuiApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ private enum Phase { PS, CMD, OUT }
private List<CE> fCmds = new ArrayList<>();
private int cIdx = 0, cScr = 0;
private String sq = ""; private boolean searching = false;
private boolean writePopup = false;

private String output = "", outName = "";
private int oScr = 0;
Expand Down Expand Up @@ -79,7 +80,8 @@ private void buildCmds() {
var g = new LinkedHashMap<CommandGroup, List<Command>>();
for (CommandGroup cg : CommandGroup.values()) g.put(cg, new ArrayList<>());
for (Command c : commands.values())
if (!c.name().equals("tui") && !c.name().equals("init") && c.supportsTui()) g.get(c.group()).add(c);
if (!c.name().equals("tui") && !c.name().equals("init") && c.supportsTui()
&& c.mode() == Command.CommandMode.READ) g.get(c.group()).add(c);
for (var e : g.entrySet()) {
if (e.getValue().isEmpty()) continue;
allCmds.add(new CE(null, e.getKey().displayName(), true));
Expand All @@ -105,31 +107,35 @@ public void run() {
// Pre-load processes before JLine init (saves ~1s)
refreshPs();

System.out.print("\033[?1049h\033[?25l\033[H\033[2J");
System.out.print("\033[?1049h\033[?25l\033[?7l\033[H\033[2J"); // alt screen + hide cursor + no autowrap + clear
for (String l : LOGO) System.out.println("\033[36m " + l + R);
System.out.flush();

try (Terminal t = TerminalBuilder.builder().system(true).jansi(true).build()) {
t.enterRawMode(); NonBlockingReader rd = t.reader(); PrintWriter w = t.writer();
t.enterRawMode(); NonBlockingReader rd = t.reader();
java.io.OutputStream rawOut = t.output();
while (running) {
int H = Math.max(t.getHeight(), 20);
int TW = Math.max(t.getWidth(), 40);
int W = Math.min(TW, 120); // box max 120 chars
int W = Math.min(TW, 120);
int margin = (TW - W) / 2;
String ml = margin > 0 ? " ".repeat(margin) : "";
StringBuilder sb = new StringBuilder("\033[H\033[2J"); // home + clear entire screen
StringBuilder sb = new StringBuilder(H * (W + 40));
sb.append("\033[1;").append(H).append("r\033[H");
switch (phase) {
case PS -> drawPS(sb, W, H, ml);
case CMD -> drawCMD(sb, W, H, ml);
case OUT -> drawOUT(sb, W, H, ml);
}
w.print(sb); w.flush();
int key = rd.read(80);
byte[] frame = sb.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
rawOut.write(frame);
rawOut.flush();
int key = rd.read(50);
if (key == -2) continue; if (key == -1) break;
onKey(key, rd);
}
w.print("\033[?25h\033[?1049l"); w.flush();
} catch (Exception e) { System.out.print("\033[?25h\033[?1049l"); }
rawOut.write("\033[r\033[?7h\033[?25h\033[?1049l".getBytes()); rawOut.flush();
} catch (Exception e) { System.out.print("\033[r\033[?7h\033[?25h\033[?1049l"); System.out.flush(); }
}

private void onKey(int key, NonBlockingReader rd) throws Exception {
Expand All @@ -146,6 +152,7 @@ private void onKey(int key, NonBlockingReader rd) throws Exception {
return;
}
if (langSelect) { langSelect = false; return; }
if (writePopup) { writePopup = false; return; }
back(); return;
}
if (langSelect) {
Expand All @@ -170,6 +177,7 @@ private void onKey(int key, NonBlockingReader rd) throws Exception {
case 'r','R' -> { if (phase == Phase.PS) refreshPs(); }
case 'l','L' -> { langSelect = true; langSelIdx = langIdx; }
case 't','T' -> themeIdx = (themeIdx+1) % THEME_NAMES.length;
case 'w','W' -> { if (phase == Phase.CMD) writePopup = !writePopup; }
case 10, 13 -> enter();
default -> {}
}
Expand Down Expand Up @@ -273,7 +281,7 @@ private void drawPS(StringBuilder s, int W, int H, String ml) {
rows.add(centerColorRow(DIM, "↑↓ select ⏎ connect r refresh l lang t theme q quit", W));
rows.add(botLine(W));

for (String r : rows) s.append(ml).append(r).append("\n");
for (int _i = 0; _i < rows.size(); _i++) { s.append(ml).append(rows.get(_i)).append("\033[K"); if (_i < rows.size()-1) s.append("\n"); }

if (langSelect) drawLangOverlay(s, W, H);
}
Expand All @@ -295,14 +303,13 @@ private void drawCMD(StringBuilder s, int W, int H, String ml) {
rows.add(colorRow(acc(), " ▸ " + e.n, W));
} else {
String desc = e.c != null ? trn(e.c.description(messages), W-26) : "";
String content = " \033[1m" + fg() + pad(e.n, 18) + R + DIM + desc + R;
String plain = " " + pad(e.n, 18) + desc;
if (i == cIdx) {
rows.add(hlRow(" " + pad(e.n, 18) + desc, W));
rows.add(hlRow(" " + pad(e.n, 18) + desc, W));
} else {
// Command name bold+color, description dim
String plain = " " + pad(e.n, 18) + desc;
String colored = fg()+"│"+R+" " + " \033[1m"+fg()+pad(e.n,18)+R+DIM+desc+R;
int plen = plain.length();
rows.add(colored+" ".repeat(Math.max(0,W-3-plen))+fg()+"│"+R);
int padLen = Math.max(0, W - 3 - plain.length());
rows.add(fg() + "│" + R + " " + content + " ".repeat(padLen) + fg() + "│" + R);
}
}
drawn++;
Expand All @@ -311,12 +318,60 @@ private void drawCMD(StringBuilder s, int W, int H, String ml) {

rows.add(midLine(W));
if (searching) rows.add(centerRow("/" + sq + "▏", W));
else rows.add(centerColorRow(DIM, "↑↓ navigate ⏎ execute / search esc back l lang t theme", W));
else rows.add(centerColorRow(DIM, "↑↓ navigate ⏎ execute / search w write cmds esc back l lang t theme", W));
rows.add(botLine(W));

for (String r : rows) s.append(ml).append(r).append("\n");
for (int _i = 0; _i < rows.size(); _i++) { s.append(ml).append(rows.get(_i)).append("\033[K"); if (_i < rows.size()-1) s.append("\n"); }

if (langSelect) drawLangOverlay(s, W, H);
if (writePopup) drawWritePopup(s, W, H);
}

private void drawWritePopup(StringBuilder s, int W, int H) {
// Collect WRITE commands
List<String[]> writeCmds = new ArrayList<>();
for (Command c : commands.values()) {
if (c.mode() == Command.CommandMode.WRITE && c.supportsTui()) {
writeCmds.add(new String[]{c.name(), c.description(messages)});
}
}

int ow = Math.min(W - 4, 62);
int oh = writeCmds.size() + 6;
int ox = (W - ow) / 2;
int oy = (H - oh) / 2;

s.append("\033[").append(oy).append(";").append(ox + 1).append("H");
s.append("\033[33m").append("╭").append("─".repeat(ow - 2)).append("╮").append(R);

s.append("\033[").append(oy + 1).append(";").append(ox + 1).append("H");
String title = "⚠ Write Commands (run from CLI)";
int tpad = ow - 2 - title.length();
s.append("\033[33m│\033[1m ").append(title).append(" ".repeat(Math.max(0, tpad - 1))).append("\033[0m\033[33m│").append(R);

s.append("\033[").append(oy + 2).append(";").append(ox + 1).append("H");
s.append("\033[33m├").append("─".repeat(ow - 2)).append("┤").append(R);

for (int i = 0; i < writeCmds.size(); i++) {
s.append("\033[").append(oy + 3 + i).append(";").append(ox + 1).append("H");
String name = writeCmds.get(i)[0];
String desc = writeCmds.get(i)[1];
String line = " \033[1m" + pad(name, 14) + "\033[0m\033[2m" + trn(desc, ow - 18) + "\033[0m";
int plainLen = 1 + 14 + Math.min(desc.length(), ow - 18);
s.append("\033[33m│").append(line).append(" ".repeat(Math.max(0, ow - 2 - plainLen))).append("\033[33m│").append(R);
}

int footerY = oy + 3 + writeCmds.size();
s.append("\033[").append(footerY).append(";").append(ox + 1).append("H");
s.append("\033[33m├").append("─".repeat(ow - 2)).append("┤").append(R);

s.append("\033[").append(footerY + 1).append(";").append(ox + 1).append("H");
String hint = " Run: argus <command> <pid>";
int hpad = ow - 2 - hint.length();
s.append("\033[33m│\033[2m").append(hint).append(" ".repeat(Math.max(0, hpad))).append("\033[0m\033[33m│").append(R);

s.append("\033[").append(footerY + 2).append(";").append(ox + 1).append("H");
s.append("\033[33m╰").append("─".repeat(ow - 2)).append("╯").append(R);
}

private void drawOUT(StringBuilder s, int W, int H, String ml) {
Expand Down Expand Up @@ -348,7 +403,7 @@ private void drawOUT(StringBuilder s, int W, int H, String ml) {
rows.add(centerColorRow(DIM, hint, W));
rows.add(botLine(W));

for (String r : rows) s.append(ml).append(r).append("\n");
for (int _i = 0; _i < rows.size(); _i++) { s.append(ml).append(rows.get(_i)).append("\033[K"); if (_i < rows.size()-1) s.append("\n"); }
}

private void drawLangOverlay(StringBuilder s, int W, int H) {
Expand Down
Loading