Skip to content

Commit 0ee6c95

Browse files
committed
Merge branch 'main' into fix-text-file-busy-while-tracj
2 parents 3751999 + 0809cdd commit 0ee6c95

File tree

133 files changed

+8847
-2196
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

133 files changed

+8847
-2196
lines changed

.github/config/auto_assign.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
addAssignees: author
2+
runOnDraft: true

.github/scripts/cli_scraper.py

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
import subprocess
2+
import json
3+
import re
4+
5+
def replace_angle_brackets(text):
6+
"""
7+
Replace any text within angle brackets with backticks to prevent Markdown rendering issues.
8+
Example: "<snapshotName>" becomes "`snapshotName`"
9+
"""
10+
return re.sub(r'<(.*?)>', r'`\1`', text)
11+
12+
def generate_anchor_id(cli_tool, command_chain):
13+
"""
14+
Generate a unique anchor ID based on the entire command chain.
15+
16+
Example:
17+
cli_tool = "avalanche"
18+
command_chain = ["blockchain", "create"]
19+
-> anchor_id = "avalanche-blockchain-create"
20+
"""
21+
full_chain = [cli_tool] + command_chain
22+
anchor_str = '-'.join(full_chain)
23+
# Remove invalid characters for anchors, and lowercase
24+
anchor_str = re.sub(r'[^\w\-]', '', anchor_str.lower())
25+
return anchor_str
26+
27+
def get_command_structure(cli_tool, command_chain=None, max_depth=10, current_depth=0, processed_commands=None):
28+
"""
29+
Recursively get a dictionary of commands, subcommands, flags (with descriptions),
30+
and descriptions for a given CLI tool by parsing its --help output.
31+
"""
32+
if command_chain is None:
33+
command_chain = []
34+
if processed_commands is None:
35+
processed_commands = {}
36+
37+
current_command = [cli_tool] + command_chain
38+
command_key = ' '.join(current_command)
39+
40+
# Prevent re-processing of the same command
41+
if command_key in processed_commands:
42+
return processed_commands[command_key]
43+
44+
# Prevent going too deep
45+
if current_depth > max_depth:
46+
return None
47+
48+
command_structure = {
49+
"description": "",
50+
"flags": [],
51+
"subcommands": {}
52+
}
53+
54+
print(f"Processing command: {' '.join(current_command)}")
55+
56+
# Run `<command> --help`
57+
try:
58+
help_output = subprocess.run(
59+
current_command + ["--help"],
60+
stdout=subprocess.PIPE,
61+
stderr=subprocess.STDOUT,
62+
text=True,
63+
timeout=10,
64+
stdin=subprocess.DEVNULL
65+
)
66+
output = help_output.stdout
67+
# Some CLIs return a non-zero exit code but still provide help text, so no strict check here
68+
except subprocess.TimeoutExpired:
69+
print(f"[ERROR] Timeout expired for command: {' '.join(current_command)}")
70+
return None
71+
except Exception as e:
72+
print(f"[ERROR] Exception while running: {' '.join(current_command)} -> {e}")
73+
return None
74+
75+
if not output.strip():
76+
print(f"[WARNING] No output for command: {' '.join(current_command)}")
77+
return None
78+
79+
# --- Extract Description ------------------------------------------------------
80+
description_match = re.search(r"(?s)^\s*(.*?)\n\s*Usage:", output)
81+
if description_match:
82+
description = description_match.group(1).strip()
83+
command_structure['description'] = replace_angle_brackets(description)
84+
85+
# --- Extract Flags (including Global Flags) -----------------------------------
86+
flags = []
87+
# "Flags:" section
88+
flags_match = re.search(r"(?sm)^Flags:\n(.*?)(?:\n\n|^\S|\Z)", output)
89+
if flags_match:
90+
flags_text = flags_match.group(1)
91+
flags.extend(re.findall(
92+
r"^\s+(-{1,2}[^\s,]+(?:,\s*-{1,2}[^\s,]+)*)\s+(.*)$",
93+
flags_text,
94+
re.MULTILINE
95+
))
96+
97+
# "Global Flags:" section
98+
global_flags_match = re.search(r"(?sm)^Global Flags:\n(.*?)(?:\n\n|^\S|\Z)", output)
99+
if global_flags_match:
100+
global_flags_text = global_flags_match.group(1)
101+
flags.extend(re.findall(
102+
r"^\s+(-{1,2}[^\s,]+(?:,\s*-{1,2}[^\s,]+)*)\s+(.*)$",
103+
global_flags_text,
104+
re.MULTILINE
105+
))
106+
107+
if flags:
108+
command_structure["flags"] = [
109+
{
110+
"flag": f[0].strip(),
111+
"description": replace_angle_brackets(f[1].strip())
112+
}
113+
for f in flags
114+
]
115+
116+
# --- Extract Subcommands ------------------------------------------------------
117+
subcommands_match = re.search(
118+
r"(?sm)(?:^Available Commands?:\n|^Commands?:\n)(.*?)(?:\n\n|^\S|\Z)",
119+
output
120+
)
121+
if subcommands_match:
122+
subcommands_text = subcommands_match.group(1)
123+
# Lines like: " create Create a new something"
124+
subcommand_lines = re.findall(r"^\s+([^\s]+)\s+(.*)$", subcommands_text, re.MULTILINE)
125+
126+
for subcmd, sub_desc in sorted(set(subcommand_lines)):
127+
sub_desc_clean = replace_angle_brackets(sub_desc.strip())
128+
sub_structure = get_command_structure(
129+
cli_tool,
130+
command_chain + [subcmd],
131+
max_depth,
132+
current_depth + 1,
133+
processed_commands
134+
)
135+
if sub_structure is not None:
136+
if not sub_structure.get('description'):
137+
sub_structure['description'] = sub_desc_clean
138+
command_structure["subcommands"][subcmd] = sub_structure
139+
else:
140+
command_structure["subcommands"][subcmd] = {
141+
"description": sub_desc_clean,
142+
"flags": [],
143+
"subcommands": {}
144+
}
145+
146+
processed_commands[command_key] = command_structure
147+
return command_structure
148+
149+
def generate_markdown(cli_structure, cli_tool, file_path):
150+
"""
151+
Generate a Markdown file from the CLI structure JSON object in a developer-friendly format.
152+
No top-level subcommand bullet list.
153+
"""
154+
# Define a set of known type keywords. Adjust as needed.
155+
known_types = {
156+
"string", "bool", "int", "uint", "float", "duration",
157+
"strings", "uint16", "uint32", "uint64", "int16", "int32", "int64",
158+
"float32", "float64"
159+
}
160+
161+
def write_section(structure, file, command_chain=None):
162+
if command_chain is None:
163+
command_chain = []
164+
165+
# If at root level, do not print a heading or bullet list, just go straight
166+
# to recursing through subcommands.
167+
if command_chain:
168+
# Determine heading level (but max out at H6)
169+
heading_level = min(1 + len(command_chain), 6)
170+
171+
# Build heading text:
172+
if len(command_chain) == 1:
173+
heading_text = f"{cli_tool} {command_chain[0]}"
174+
else:
175+
heading_text = ' '.join(command_chain[1:])
176+
177+
# Insert a single anchor before writing the heading
178+
anchor = generate_anchor_id(cli_tool, command_chain)
179+
file.write(f'<a id="{anchor}"></a>\n')
180+
file.write(f"{'#' * heading_level} {heading_text}\n\n")
181+
182+
# Write description
183+
if structure.get('description'):
184+
file.write(f"{structure['description']}\n\n")
185+
186+
# Write usage
187+
full_command = f"{cli_tool} {' '.join(command_chain)}"
188+
file.write("**Usage:**\n")
189+
file.write(f"```bash\n{full_command} [subcommand] [flags]\n```\n\n")
190+
191+
# Subcommands index
192+
subcommands = structure.get('subcommands', {})
193+
if subcommands:
194+
file.write("**Subcommands:**\n\n")
195+
for subcmd in sorted(subcommands.keys()):
196+
sub_desc = subcommands[subcmd].get('description', '')
197+
sub_anchor = generate_anchor_id(cli_tool, command_chain + [subcmd])
198+
file.write(f"- [`{subcmd}`](#{sub_anchor}): {sub_desc}\n")
199+
file.write("\n")
200+
else:
201+
subcommands = structure.get('subcommands', {})
202+
203+
# Flags (only if we have a command chain)
204+
if command_chain and structure.get('flags'):
205+
file.write("**Flags:**\n\n")
206+
flag_lines = []
207+
for flag_dict in structure['flags']:
208+
flag_names = flag_dict['flag']
209+
description = flag_dict['description'].strip()
210+
211+
# Attempt to parse a recognized "type" from the first word.
212+
desc_parts = description.split(None, 1) # Split once on whitespace
213+
if len(desc_parts) == 2:
214+
first_word, rest = desc_parts
215+
# Check if the first word is in known_types
216+
if first_word.lower() in known_types:
217+
flag_type = first_word
218+
flag_desc = rest
219+
else:
220+
flag_type = ""
221+
flag_desc = description
222+
else:
223+
flag_type = ""
224+
flag_desc = description
225+
226+
if flag_type:
227+
flag_line = f"{flag_names} {flag_type}"
228+
else:
229+
flag_line = flag_names
230+
231+
flag_lines.append((flag_line, flag_desc))
232+
233+
# Determine formatting width
234+
max_len = max(len(fl[0]) for fl in flag_lines) if flag_lines else 0
235+
file.write("```bash\n")
236+
for fl, fd in flag_lines:
237+
file.write(f"{fl.ljust(max_len)} {fd}\n")
238+
file.write("```\n\n")
239+
240+
# Recurse into subcommands
241+
subcommands = structure.get('subcommands', {})
242+
for subcmd in sorted(subcommands.keys()):
243+
write_section(subcommands[subcmd], file, command_chain + [subcmd])
244+
245+
with open(file_path, "w", encoding="utf-8") as f:
246+
write_section(cli_structure, f)
247+
248+
def main():
249+
cli_tool = "avalanche" # Adjust if needed
250+
max_depth = 10
251+
252+
# Build the nested command structure
253+
cli_structure = get_command_structure(cli_tool, max_depth=max_depth)
254+
if cli_structure:
255+
# Generate Markdown
256+
generate_markdown(cli_structure, cli_tool, "cmd/commands.md")
257+
print("Markdown documentation saved to cmd/commands.md")
258+
else:
259+
print("[ERROR] Failed to retrieve CLI structure")
260+
261+
if __name__ == "__main__":
262+
main()

0 commit comments

Comments
 (0)