/ tools / r-wrapper / aliases.py
aliases.py
  1  #!/usr/bin/env python3
  2  """
  3  Command alias parser for cspec templates.
  4  Extracts executable aliases from alpha-delta-context/infra/machine/commands/*.cspec
  5  Generates aliases.json for Claude reference.
  6  """
  7  import os
  8  import re
  9  import json
 10  from typing import Dict, Optional, Tuple
 11  
 12  from config import CSPEC_COMMANDS_DIR, ALIASES_JSON_PATH
 13  
 14  
 15  def parse_cspec_value(value: str) -> str:
 16      """Clean a cspec value, handling multi-line strings."""
 17      value = value.strip()
 18      # Remove leading pipe for multi-line
 19      if value.startswith('|'):
 20          value = value[1:].strip()
 21      # Remove quotes
 22      if (value.startswith('"') and value.endswith('"')) or \
 23         (value.startswith("'") and value.endswith("'")):
 24          value = value[1:-1]
 25      return value
 26  
 27  
 28  def parse_cspec_file(filepath: str) -> Dict[str, dict]:
 29      """
 30      Parse a cspec file and extract command definitions.
 31      Returns dict of {alias_name: {command: str, placeholders: list, source: str}}
 32      """
 33      aliases = {}
 34  
 35      if not os.path.exists(filepath):
 36          return aliases
 37  
 38      with open(filepath, 'r', encoding='utf-8') as f:
 39          content = f.read()
 40  
 41      filename = os.path.basename(filepath)
 42      current_section = None
 43      current_key = None
 44      multiline_buffer = []
 45      in_multiline = False
 46  
 47      for line in content.split('\n'):
 48          stripped = line.strip()
 49  
 50          # Skip comments and empty lines
 51          if not stripped or stripped.startswith('#'):
 52              if in_multiline:
 53                  multiline_buffer.append(line)
 54              continue
 55  
 56          # Section header (e.g., "ssh_port:", "source_server:")
 57          section_match = re.match(r'^([a-z_][a-z0-9_]*):(.*)$', stripped, re.IGNORECASE)
 58          if section_match and not line.startswith(' ') and not line.startswith('\t'):
 59              # Save any pending multiline
 60              if in_multiline and current_section and current_key:
 61                  cmd = '\n'.join(multiline_buffer).strip()
 62                  if cmd:
 63                      alias_name = f"{current_section}-{current_key}".replace('_', '-')
 64                      placeholders = re.findall(r'\{\{(\w+)\}\}', cmd)
 65                      aliases[alias_name] = {
 66                          'command': cmd,
 67                          'placeholders': placeholders,
 68                          'source': filename
 69                      }
 70  
 71              current_section = section_match.group(1)
 72              remaining = section_match.group(2).strip()
 73  
 74              # Simple key: value on same line
 75              if remaining and not remaining.startswith('|'):
 76                  # It's a direct command (e.g., ssh_port: 2584)
 77                  # Skip non-command values
 78                  pass
 79  
 80              in_multiline = False
 81              multiline_buffer = []
 82              current_key = None
 83              continue
 84  
 85          # Nested key (e.g., "  ssh: ssh -p 2584...")
 86          nested_match = re.match(r'^(\s+)([a-z_][a-z0-9_]*):(.*)$', stripped if not stripped else line)
 87          if not nested_match:
 88              nested_match = re.match(r'^(\s+)([a-z_][a-z0-9_]*):\s*(.*)$', line)
 89  
 90          if line.startswith(' ') or line.startswith('\t'):
 91              key_match = re.match(r'^\s+([a-z_][a-z0-9_]*):\s*(.*)$', line, re.IGNORECASE)
 92              if key_match:
 93                  # Save any pending multiline
 94                  if in_multiline and current_section and current_key:
 95                      cmd = '\n'.join(multiline_buffer).strip()
 96                      if cmd:
 97                          alias_name = f"{current_section}-{current_key}".replace('_', '-')
 98                          placeholders = re.findall(r'\{\{(\w+)\}\}', cmd)
 99                          aliases[alias_name] = {
100                              'command': cmd,
101                              'placeholders': placeholders,
102                              'source': filename
103                          }
104  
105                  current_key = key_match.group(1)
106                  value = key_match.group(2).strip()
107  
108                  if value == '|':
109                      # Start multiline
110                      in_multiline = True
111                      multiline_buffer = []
112                  elif value:
113                      # Single line command
114                      cmd = parse_cspec_value(value)
115                      if current_section:
116                          alias_name = f"{current_section}-{current_key}".replace('_', '-')
117                          placeholders = re.findall(r'\{\{(\w+)\}\}', cmd)
118                          aliases[alias_name] = {
119                              'command': cmd,
120                              'placeholders': placeholders,
121                              'source': filename
122                          }
123                      in_multiline = False
124                      multiline_buffer = []
125              elif in_multiline:
126                  multiline_buffer.append(line)
127  
128      # Final multiline flush
129      if in_multiline and current_section and current_key:
130          cmd = '\n'.join(multiline_buffer).strip()
131          if cmd:
132              alias_name = f"{current_section}-{current_key}".replace('_', '-')
133              placeholders = re.findall(r'\{\{(\w+)\}\}', cmd)
134              aliases[alias_name] = {
135                  'command': cmd,
136                  'placeholders': placeholders,
137                  'source': filename
138              }
139  
140      return aliases
141  
142  
143  def add_shorthand_aliases(aliases: Dict[str, dict]) -> Dict[str, dict]:
144      """Add convenient shorthand aliases for common commands."""
145      shorthands = {
146          # SSH connections
147          'connect-ci': aliases.get('ci-server-ssh', {}),
148          'connect-source': aliases.get('source-server-ssh', {}),
149  
150          # Runner operations
151          'runner-status': aliases.get('runner-status', {}),
152          'runner-logs': aliases.get('runner-logs', {}),
153          'runner-restart': aliases.get('runner-restart', {}),
154  
155          # Cargo shortcuts
156          'build': aliases.get('cargo-build', {}),
157          'check': aliases.get('cargo-check', {}),
158          'test': aliases.get('cargo-test', {}),
159          'clippy': aliases.get('cargo-clippy', {}),
160          'fmt': aliases.get('cargo-fmt', {}),
161  
162          # Service management
163          'service-status': aliases.get('systemd-status', {}),
164          'service-restart': aliases.get('systemd-restart', {}),
165          'service-logs': aliases.get('systemd-logs', {}),
166      }
167  
168      for name, value in shorthands.items():
169          if value and name not in aliases:
170              aliases[name] = value.copy()
171              aliases[name]['shorthand_for'] = next(
172                  (k for k, v in aliases.items() if v == value), None
173              )
174  
175      return aliases
176  
177  
178  def load_all_aliases() -> Dict[str, dict]:
179      """Load aliases from all cspec files in commands directory."""
180      all_aliases = {}
181  
182      if not os.path.isdir(CSPEC_COMMANDS_DIR):
183          return all_aliases
184  
185      for filename in os.listdir(CSPEC_COMMANDS_DIR):
186          if filename.endswith('.cspec') and filename != 'index.cspec':
187              filepath = os.path.join(CSPEC_COMMANDS_DIR, filename)
188              file_aliases = parse_cspec_file(filepath)
189              all_aliases.update(file_aliases)
190  
191      # Add shorthand aliases
192      all_aliases = add_shorthand_aliases(all_aliases)
193  
194      return all_aliases
195  
196  
197  def resolve_alias(alias: str, args: Dict[str, str] = None) -> Optional[str]:
198      """
199      Resolve an alias to its command, substituting placeholders.
200  
201      Args:
202          alias: The alias name (e.g., 'connect-ci')
203          args: Dict of placeholder values (e.g., {'repo': 'alphavm'})
204  
205      Returns:
206          Resolved command string or None if alias not found
207      """
208      aliases = load_all_aliases()
209  
210      if alias not in aliases:
211          return None
212  
213      cmd = aliases[alias]['command']
214      placeholders = aliases[alias].get('placeholders', [])
215  
216      if args:
217          for key, value in args.items():
218              cmd = cmd.replace(f'{{{{{key}}}}}', value)
219  
220      # Check for unresolved placeholders
221      remaining = re.findall(r'\{\{(\w+)\}\}', cmd)
222      if remaining:
223          return None  # Missing required placeholders
224  
225      return cmd
226  
227  
228  def parse_alias_invocation(args: list) -> Tuple[str, Dict[str, str]]:
229      """
230      Parse alias invocation from command line args.
231  
232      Examples:
233          ['connect-ci'] -> ('connect-ci', {})
234          ['ci-status', 'repo=alphavm'] -> ('ci-status', {'repo': 'alphavm'})
235      """
236      if not args:
237          return ('', {})
238  
239      alias = args[0]
240      params = {}
241  
242      for arg in args[1:]:
243          if '=' in arg:
244              key, value = arg.split('=', 1)
245              params[key] = value
246  
247      return (alias, params)
248  
249  
250  def generate_aliases_json():
251      """Generate aliases.json for Claude reference."""
252      aliases = load_all_aliases()
253  
254      # Simplify for JSON output
255      output = {}
256      for name, data in sorted(aliases.items()):
257          output[name] = {
258              'cmd': data['command'],
259              'args': data.get('placeholders', []),
260              'src': data.get('source', 'unknown')
261          }
262  
263      with open(ALIASES_JSON_PATH, 'w', encoding='utf-8') as f:
264          json.dump(output, f, indent=2)
265  
266      return len(output)
267  
268  
269  def list_aliases() -> str:
270      """Return formatted list of available aliases."""
271      aliases = load_all_aliases()
272  
273      lines = ["Available aliases:\n"]
274  
275      # Group by source file
276      by_source = {}
277      for name, data in sorted(aliases.items()):
278          source = data.get('source', 'unknown')
279          if source not in by_source:
280              by_source[source] = []
281          by_source[source].append((name, data))
282  
283      for source, items in sorted(by_source.items()):
284          lines.append(f"\n[{source}]")
285          for name, data in items:
286              placeholders = data.get('placeholders', [])
287              if placeholders:
288                  args_str = ' ' + ' '.join(f'{{{p}}}' for p in placeholders)
289              else:
290                  args_str = ''
291              lines.append(f"  {name}{args_str}")
292  
293      return '\n'.join(lines)
294  
295  
296  if __name__ == "__main__":
297      import sys
298  
299      if len(sys.argv) > 1 and sys.argv[1] == '--generate':
300          count = generate_aliases_json()
301          print(f"[S:PASS] Generated {ALIASES_JSON_PATH} with {count} aliases.")
302      elif len(sys.argv) > 1 and sys.argv[1] == '--list':
303          print(list_aliases())
304      else:
305          print("Usage:")
306          print("  python aliases.py --generate  # Generate aliases.json")
307          print("  python aliases.py --list      # List all aliases")