Skip to content
Closed
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
43 changes: 37 additions & 6 deletions drove-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,51 @@ fallback invocation prefix.

---

## Step 1 β€” Load the Full Command Reference
## Step 1 β€” Bootstrap Session for Token-Efficient Usage

Run this once to bring every command and sub-command into context:
Do these two things at the start of every session:

### 1a. Enable compact data output

```bash
export DROVE_COMPACT=1
```

This makes **all data-producing commands** return token-optimised output
automatically β€” TSV tables (tab-separated, no padding), flat `key=value`
dicts, and single-line JSON.

Affected commands: `apps list`, `apps summary`, `cluster summary`,
`executor list`, `localservices list/summary`, `lsinstances list/info`,
`tasks list/show`, `describe *`, `appinstances list/info`.

You can also pass `--compact` per-command instead of the env var.
`--compact` is a **global flag** β€” it goes before the plugin name:

```bash
drove --compact apps list # βœ… correct
drove apps list --compact # ❌ won't work
```

> `DROVE_COMPACT` / `--compact` does **not** affect `--full-help` output
> (that has its own compact format by default).

### 1b. Load the command reference

```bash
drove --full-help
```

This prints the complete, always-up-to-date help for every command and
sub-command in one shot. Use it to look up flags, argument names, and
available options β€” no need to memorise them.
Prints a **compact one-line-per-command** reference (~3 KB).
Use it to look up flags, argument names, and available options.

For the full verbose argparse output (human-readable), add `--verbose`:

```bash
drove --full-help --verbose # ~35 KB, human-readable
```

> **Full documentation:** https://phonepe.github.io/drove-orchestrator/cli/
> Refer to this whenever the built-in help isn't enough or something is unclear.

---

Expand Down
36 changes: 2 additions & 34 deletions drove.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,45 +19,13 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--password", "-p", help="Drove cluster password")
parser.add_argument("--debug", "-d", help="Print details of errors", default=False, action="store_true")
parser.add_argument("--full-help", help="Show help for every command and sub-command", default=False, action="store_true")
parser.add_argument("--compact", help="Token-optimised compact output", default=False, action="store_true")
parser.add_argument("--verbose", help="Verbose output (use with --full-help for detailed help)", default=False, action="store_true")
parser.add_argument("--print-completion", choices=["bash", "zsh", "tcsh"], help="Print shell completion script for the given shell")
return parser

def get_parser():
parser = build_parser()
client = None
client = drovecli.DroveCli(parser)
return client.parser


def run():
parser = build_parser()
client = None
try:
client = drovecli.DroveCli(parser)
client.run()
except (BrokenPipeError, IOError, KeyboardInterrupt):
pass
except droveclient.DroveException as e:
debug = True if client is not None and client.debug else False
droveutils.print_drove_error(e, debug)
if debug:
traceback.print_exc()

except Exception as e:
print("Drove CLI error: " + str(e))
debug = True if client is not None and client.debug else False
if debug:
traceback.print_exc()
else:
parser.print_help()

if __name__ == '__main__':
run()


def get_parser():
parser = build_parser()
client = None
client = drovecli.DroveCli(parser)
return client.parser

Expand Down
104 changes: 103 additions & 1 deletion drovecli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import argparse
import os
import droveclient
import droveutils
from plugins import DrovePlugin
from types import SimpleNamespace
import shtab
Expand Down Expand Up @@ -32,6 +34,99 @@ def _print_full_help(parser: argparse.ArgumentParser) -> None:
DroveCli._print_full_help(subparser)
break

@staticmethod
def _print_compact_help(parser: argparse.ArgumentParser) -> None:
"""Print a compact one-line-per-command reference for LLM consumption."""
# Print global options from the root parser
global_opts = DroveCli._format_opts(parser, skip={"full_help", "compact", "verbose", "print_completion"})
print(f"drove {global_opts}")

# Walk the parser tree and collect leaf commands grouped by top-level plugin
lines_by_group: dict[str, list[str]] = {}
for action in parser._actions:
if isinstance(action, argparse._SubParsersAction):
for group_name, group_parser in sorted(action.choices.items()):
lines_by_group[group_name] = []
DroveCli._collect_compact_lines(group_name, group_parser, lines_by_group[group_name])
break

for group_name, lines in lines_by_group.items():
print()
for line in lines:
print(line)

@staticmethod
def _collect_compact_lines(path: str, parser: argparse.ArgumentParser, lines: list[str]) -> None:
"""Recursively collect compact lines for a parser into the lines list."""
# Check if this parser has sub-parsers (i.e., it's a group, not a leaf)
sub_action = None
for action in parser._actions:
if isinstance(action, argparse._SubParsersAction):
sub_action = action
break

if sub_action is not None:
# This is a group β€” recurse into its sub-commands
for name, subparser in sorted(sub_action.choices.items()):
DroveCli._collect_compact_lines(f"{path} {name}", subparser, lines)
else:
# This is a leaf command β€” build one compact line
positionals = DroveCli._format_positionals(parser)
opts = DroveCli._format_opts(parser)
parts = [path]
if positionals:
parts.append(positionals)
if opts:
parts.append(opts)
lines.append(" ".join(parts))

@staticmethod
def _format_positionals(parser: argparse.ArgumentParser) -> str:
"""Format positional arguments as <metavar> or <metavar...>."""
parts = []
for action in parser._actions:
if isinstance(action, argparse._SubParsersAction):
continue
if action.option_strings:
continue # skip optionals
if isinstance(action, argparse._HelpAction):
continue
metavar = action.metavar or action.dest
if action.nargs in ("+", "*"):
parts.append(f"<{metavar}...>")
else:
parts.append(f"<{metavar}>")
return " ".join(parts)

@staticmethod
def _format_opts(parser: argparse.ArgumentParser, skip: set = None) -> str:
"""Format optional arguments as [-short VAL] compact notation."""
skip = skip or set()
parts = []
for action in parser._actions:
if isinstance(action, argparse._HelpAction):
continue
if not action.option_strings:
continue # skip positionals
if action.dest in skip:
continue

# Pick the shortest flag (prefer -f over --file)
flags = sorted(action.option_strings, key=len)
flag = flags[0]

if isinstance(action, (argparse._StoreTrueAction, argparse._StoreFalseAction, argparse._CountAction)):
parts.append(f"[{flag}]")
else:
metavar = action.metavar or action.dest.upper()
if isinstance(metavar, tuple):
metavar = " ".join(metavar)
if action.required:
parts.append(f"{flag} {metavar}")
else:
parts.append(f"[{flag} {metavar}]")
return " ".join(parts)

def run(self):
args = self.parser.parse_args()
self.debug = args.debug
Expand All @@ -41,9 +136,16 @@ def run(self):
exit(0)

if args.full_help:
self._print_full_help(self.parser)
if args.verbose:
self._print_full_help(self.parser)
else:
self._print_compact_help(self.parser)
exit(0)

# Set compact mode for data output
compact = args.compact or os.environ.get("DROVE_COMPACT", "") == "1"
droveutils.set_compact(compact)

# Load plugins
if args.debug:
print("Selected plugin: " + args.plugin)
Expand Down
65 changes: 60 additions & 5 deletions droveutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,24 @@
import tabulate
import time

# ---------------------------------------------------------------------------
# Compact mode state
# ---------------------------------------------------------------------------
_compact_mode = False

def is_compact() -> bool:
"""Check if compact output mode is active."""
return _compact_mode

def set_compact(val: bool) -> None:
"""Set compact output mode (called by DroveCli at dispatch time)."""
global _compact_mode
_compact_mode = val

def print_dict(data: dict, level: int = 0):
if is_compact() and level == 0:
_print_dict_compact(data, "")
return
for key, value in data.items():
print(level * 4 * " ", end='')
if type(value) is dict and not len(dict(value)) == 0:
Expand All @@ -18,17 +35,55 @@ def print_dict(data: dict, level: int = 0):
else:
print(f"{key: <30}{value}")

def _print_dict_compact(data: dict, prefix: str):
"""Print dict as key=value lines, flattening nested dicts with dot notation."""
for key, value in data.items():
full_key = f"{prefix}.{key}" if prefix else str(key)
if isinstance(value, dict) and len(value) > 0:
_print_dict_compact(value, full_key)
elif isinstance(value, list) and all(isinstance(n, dict) for n in value):
for i, item in enumerate(value):
_print_dict_compact(item, f"{full_key}[{i}]")
else:
print(f"{full_key}={value}")

def print_json(data: dict):
print(json.dumps(data, indent = 4))
if is_compact():
print(json.dumps(data, separators=(',', ':')))
else:
print(json.dumps(data, indent = 4))

def print_table(headers: list, data: list):
print(tabulate.tabulate(data, headers=headers))
if is_compact():
print("\t".join(str(h) for h in headers))
for row in data:
print("\t".join(str(c) for c in row))
else:
print(tabulate.tabulate(data, headers=headers))

def print_dict_table(data: dict, headers: list = None):
if headers:
print(tabulate.tabulate(data, headers=headers))
if is_compact():
if headers:
header_keys = list(headers.keys()) if isinstance(headers, dict) else headers
header_names = list(headers.values()) if isinstance(headers, dict) else headers
print("\t".join(str(h) for h in header_names))
if isinstance(data, list):
for row in data:
print("\t".join(str(row.get(k, "")) for k in header_keys))
elif isinstance(data, dict):
for row in data.values() if isinstance(data, dict) else data:
print("\t".join(str(row.get(k, "") if isinstance(row, dict) else row) for k in header_keys))
else:
if isinstance(data, list) and len(data) > 0:
keys = list(data[0].keys()) if isinstance(data[0], dict) else []
print("\t".join(keys))
for row in data:
print("\t".join(str(row.get(k, "")) for k in keys))
else:
print(tabulate.tabulate(data, headers="keys"))
if headers:
print(tabulate.tabulate(data, headers=headers))
else:
print(tabulate.tabulate(data, headers="keys"))

def to_date(epoch: int) -> str:
date = datetime.datetime.fromtimestamp(epoch/1000)
Expand Down
Loading
Loading