Skip to content
Open
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
28 changes: 28 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Project Guidelines

## Project Context

XTS Core is a Python CLI for running declarative command workflows defined in `.xts` YAML files.
Prioritize the alias-based workflow described in [README.md](../README.md) over direct file execution.
Target Linux environments and Python 3.10+.

## Architecture

- Keep changes aligned with the existing split between CLI entrypoints, alias management, argument parsing, and plugins under `src/xts_core/`.
- Treat `.xts` files as the user-facing workflow definition format and Python modules as the execution/runtime layer.
- Keep plugin work isolated to `src/xts_core/plugins/` and follow the existing `BaseXTSPlugin` abstraction.
- Prefer small, local changes over broad refactors unless the task explicitly requires restructuring.

## Build And Test

- Set up a local environment with `python -m venv .venv`, activate it, then run `pip install -e .`.
- Install runtime dependencies from the project metadata rather than duplicating dependency definitions.
- Run the focused test slice first when possible, then `pytest test/` for broader validation.
- If a change affects CLI behavior, validate it with a targeted `pytest` run before expanding scope.

## Conventions

- Preserve the existing Apache 2.0 header format in Python source files.
- Use helpers from `xts_core.utils` for user-facing terminal output instead of adding ad hoc `print()` calls.
- Keep YAML examples and `.xts` fixtures readable, minimal, and consistent with [examples/hello_world.xts](/home/ubuntu/TEST/test-automation/xts_core/worktrees/bash_completion_AI/examples/hello_world.xts).
- Link contributors to [CONTRIBUTING.md](/home/ubuntu/TEST/test-automation/xts_core/worktrees/bash_completion_AI/CONTRIBUTING.md) for branch naming, issue linkage, and pull request workflow.
14 changes: 14 additions & 0 deletions .github/instructions/python-conventions.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
description: "Use when editing Python source in src/xts_core, adding CLI behavior, extending alias management, or implementing plugins. Covers project-specific Python conventions and library usage."
applyTo: "src/**/*.py"
---

# Python Conventions

- Preserve the existing Apache 2.0 file header when editing or creating Python source files.
- Add a module docstring for new modules and keep public function or class docstrings concise and concrete.
- Prefer project helpers from `xts_core.utils` for user-facing output instead of raw `print()`.
- Follow the existing CLI style: keep argument parsing explicit, keep validation close to the command entrypoint, and avoid hidden side effects.
- Keep plugin implementations under `src/xts_core/plugins/` and subclass `BaseXTSPlugin` when adding new plugins.
- Reuse the project's current libraries and patterns before introducing new dependencies. In particular, stay consistent with `argparse`, `yaml`, `requests`, and `rich` usage already present in the codebase.
- Keep functions focused and local. Prefer straightforward control flow over indirection.
13 changes: 13 additions & 0 deletions .github/instructions/test-conventions.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
description: "Use when adding or updating pytest coverage, CLI tests, alias tests, or plugin tests in the test suite. Covers fixtures, mocking, and repository test patterns."
applyTo: "test/**/*.py"
---

# Test Conventions

- Write tests with `pytest` and the existing fixture style used in the `test/` directory.
- Use `tmp_path` for temporary files and directories instead of writing into real user locations.
- Use `monkeypatch` to isolate environment-dependent behavior such as `HOME` and XTS cache paths.
- Mock external systems and network calls with `unittest.mock.patch` or pytest fixtures so tests stay deterministic.
- Keep tests narrow and behavior-focused. Name them for the observable behavior being validated.
- When adding coverage for a module, prefer extending the nearest existing test file unless a new file gives a clearer split.
14 changes: 14 additions & 0 deletions .github/instructions/xts-yaml-format.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
description: "Use when creating or editing .xts workflow files, examples, or YAML command definitions for XTS. Covers command structure, nesting, passthrough parameters, and formatting expectations."
applyTo: "**/*.xts"
---

# XTS YAML Format

- Follow the command layout shown in [examples/hello_world.xts](../../examples/hello_world.xts).
- Use top-level keys as command groups and nested keys as executable commands or subcommands.
- Provide a `description` for each runnable command so CLI help stays useful.
- Use `command` as either a single shell command string or a list of commands to run in sequence.
- Use `params.passthrough: true` only when the command is intended to forward extra CLI arguments.
- Keep YAML indentation to two spaces and do not use tabs.
- Ignore unrelated data structures unless the task explicitly requires documenting or validating ignored sections.
26 changes: 23 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,18 +1,38 @@
[build-system]
requires = ["setuptools>=64", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "xts"
version = "2.0.0"
dependencies = [
"rich>=13.7.1",
"yaml_runner @ git+https://github.com/rdkcentral/yaml_runner.git@2.1.0",
"pyinstaller>=6.12.0",
"requests>=2.32.3",
"rich-argparse>=1.7.2"
"rich-argparse>=1.7.2",
"importlib_resources>=7.1.0",
"yaml_runner @ git+https://github.com/rdkcentral/yaml_runner.git@2.2.0",
"argparse_completion @ git+https://github.com/rdkcentral/argparse_completion.git@1.0.0"
]
[tool.setuptools.packages.find]
include = ["xts_core", "xts_core.plugins"]
include = ["xts_core*"]
where = ["src"]
namespaces = false

# --- Include non-Python files ---
[tool.setuptools.package-data]
# If your data folder is *inside* xts_core, use this:
xts_core = ["data/**/*"]

# --- Include data outside packages ---
[tool.setuptools]
include-package-data = true

# This ensures data/ is included in sdist/wheel
[tool.setuptools.data-files]
# Installs data into a shared location in site-packages
"xts_core_data" = ["data/**/*"]
Comment on lines +31 to +34

[project.scripts]
xts-install = "xts_core.install:main"
xts = "xts_core.xts:main"
13 changes: 13 additions & 0 deletions src/xts_core/data/xts_bash_completion.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash

function _xts_runner_completion()
{
local IFS='
'
COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \
COMP_CWORD=$COMP_CWORD \
_XTS_COMPLETE=complete_bash $1 ) )
return 0
}

complete -o default -F _xts_runner_completion xts
50 changes: 43 additions & 7 deletions src/xts_core/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,26 @@
#* ******************************************************************************

import datetime
import importlib_resources
import os
from pathlib import Path
import platform
import subprocess
import tempfile
import xts_core

from .utils import info, error, warning

SCRIPT_PATH = Path(__file__).absolute()
SCRIPT_DIR = Path(os.path.dirname(SCRIPT_PATH))

def main():
if 'Linux' in platform.platform():
_linux_install()
else:
error('Unfortunately XTS does not currently support your OS')

def _linux_install():
script_path = Path(__file__).absolute()
script_dir = Path(os.path.dirname(script_path))
user_home = Path('.').home()
xts_dir = user_home.joinpath(Path('.xts'))
bin_dir = xts_dir.joinpath(Path('bin'))
Expand All @@ -50,7 +53,7 @@ def _linux_install():
os.makedirs(log_dir, exist_ok=True)
result = subprocess.run(['pyinstaller',
'--onefile',
f'{script_dir}/xts.py',
f'{SCRIPT_DIR}/xts.py',
'--name',
'xts',
'--distpath',
Expand Down Expand Up @@ -79,10 +82,43 @@ def _linux_install():
'Raise an issue on xts_core for more help:\n'+
'https://github.com/rdkcentral/xts_core/issues/new?template=01-bugs.yml')
else:
info('XTS has been installed successfully.\n'+
'Please run the following commands:\n' +
f'\texport PATH="{bin_dir}:$PATH"\n' +
'To permanently install this, add the export command as a line in your ~/.bashrc file')
shell = os.environ.get('SHELL','')
match shell:
case '/bin/bash':
_install_bash(user_home)
info('XTS has been installed successfully.\n')
case _:
warning('XTS has been installed successfully, but we could not automatically add xts to your PATH.\n' +
'Please add the following line to your shell config file:\n' +
f'export PATH="{user_home.joinpath(Path(".xts/bin"))}:$PATH"\n')

def _install_bash(user_home: Path):
if (bashrc:=user_home.joinpath(Path('.bashrc'))).exists():
with open(user_home.joinpath(Path('.bashrc')), 'a') as f:
f.write('\n# XTS PATH\n')
f.write(f'export PATH="{user_home.joinpath(Path(".xts/bin"))}:$PATH"\n')
_install_bash_completion(bashrc)
elif (bash_profile:=user_home.joinpath(Path('.bash_profile'))).exists():
with open(user_home.joinpath(Path('.bash_profile')), 'a') as f:
f.write('\n# XTS PATH\n')
f.write(f'export PATH="{user_home.joinpath(Path(".xts/bin"))}:$PATH"\n')
_install_bash_completion(bash_profile)
else:
warning('Could not find .bashrc or .bash_profile to add xts to PATH. Please add the following line to your shell config file:\n' +
f'export PATH="{user_home.joinpath(Path(".xts/bin"))}:$PATH"\n')

Comment on lines +95 to +109
def _install_bash_completion(rc_file: Path):
user_home = rc_file.parent
bash_completion_dir = user_home.joinpath(Path('.bash_completion.d'))
if not bash_completion_dir.exists():
os.makedirs(bash_completion_dir)
bash_completion_script = bash_completion_dir.joinpath(Path('xts_bash_completion.sh'))
with importlib_resources.open_text(xts_core,'data/xts_bash_completion.sh', encoding='utf-8') as src, \
open(bash_completion_script, 'w') as dst:
dst.write(src.read())
with open(rc_file, 'a') as f:
f.write('\n# XTS bash completion\n')
f.write(f'source {bash_completion_dir}/xts_bash_completion.sh\n')
Comment on lines +119 to +121

if __name__ == '__main__':
main()
2 changes: 1 addition & 1 deletion src/xts_core/plugins/xts_allocator_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ def _update_slot(self,
state,
owner_email]):
plugin_utils.error('At least one field must be provided for update.')
sys.exit(1)
raise SystemExit(1)
# Required fields
payload = {
'slot_id': slot_id
Expand Down
118 changes: 71 additions & 47 deletions src/xts_core/xts.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
import os
import re
import sys
import json

import yaml
try:
Expand All @@ -47,6 +46,7 @@

import yaml.scanner
from yaml_runner import YamlRunner
import argparse_completion

try:
from .plugins import XTSAllocatorClient
Expand All @@ -68,6 +68,8 @@
except ImportError:
from xts_core.xts_arg_parser import XTSArgumentParser

from xts_core.xts_rich_help_formatter import XTSRichHelpFormatter


class XTS():
"""
Expand Down Expand Up @@ -162,7 +164,7 @@ def _is_command_section(subdict: dict) -> bool:
return command_sections


def _parse_first_arg(self):
def _setup_first_parser(self):
"""
Parse CLI arguments and set up argparse for all commands.
The first argument must be either:
Expand All @@ -174,63 +176,85 @@ def _parse_first_arg(self):
list[str]: Remaining args starting with the command name, e.g. ["run", ...].
Comment on lines 169 to 176
"""
first_arg_parser = XTSArgumentParser(prog='xts',
formatter_class=XTSRichHelpFormatter,
add_help=False)
first_arg_parser.add_argument('--alias',
action='store_true',
help='Add/Remove or list aliases',
dest='alias_option',
default=False)
args, remaining_args = first_arg_parser.parse_known_args(sys.argv[1:])

if args.alias_option:
xts_alias.run_alias_builtin(remaining_args)
raise SystemExit(0)

if not remaining_args:
first_arg_parser.print_help()
raise SystemExit(0)

alias_name = remaining_args[0]
resolved_xts_path = xts_alias.resolve_alias_to_xts_path(alias_name)

if resolved_xts_path is None:
error(
f'Unknown alias "{alias_name}". '
'Use "xts --alias --list" to see available aliases.'
)
raise SystemExit(1)

first_arg_subparsers = first_arg_parser.add_subparsers(dest='alias_name',
metavar='')
known_aliases = sorted(list(xts_alias.load_aliases()))
if len(known_aliases) >= 1:
first_arg_subparsers.add_parser('alias_name',
aliases=known_aliases,
help='Alias to run commands from.',
add_help=False)

alias_parser = first_arg_subparsers.add_parser('alias',
help='Manage aliases (add, list, remove)')
xts_alias.setup_alias_parser(alias_parser)
Comment on lines +190 to +192
return first_arg_parser

def _run_yaml_runner(self, alias:str, arguments:list[str]):
resolved_xts_path = xts_alias.resolve_alias_to_xts_path(alias)
# load xts config remove alias name from argv before parsing
self.xts_config = resolved_xts_path

return remaining_args[1:]

def run(self):
"""Run the XTS app.

Raises:
SystemExit: Raised when unrecogised arguments are given.
"""
args = self._parse_first_arg()

try:
yaml_runner = YamlRunner(
self._command_sections,
program='xts',
program=f'xts {alias}',
hierarchical=True,
fail_fast=True,
parser_class=XTSArgumentParser
)

_, _, exit_code = yaml_runner.run(args)
sys.exit(sorted(exit_code)[-1])

_, _, exit_code = yaml_runner.run(arguments)
raise SystemExit(sorted(exit_code)[-1])
except Exception as e:
error(
'An unrecognised command caused an error\n\n'
f'Command Args: [{" ".join(args)}]\n\n'
f'{str(e)}'
)
error(
'An unrecognised command caused an error\n\n'
f'Command Args: [{" ".join(arguments)}]\n\n'
f'{str(e)}')
Comment on lines 210 to +214

def _run_completion(self, arg_parser:XTSArgumentParser):
os.environ['_ARGPARSE_COMPLETE'] = os.getenv('_XTS_COMPLETE')
completion = argparse_completion.get_completion(arg_parser)
if 'alias_name' in completion:
completion.remove('alias_name')
if len(completion) == 0 and (comp_words:=(os.getenv('COMP_WORDS').split()))[1] in xts_alias.load_aliases().keys():
os.environ['_YAML_RUNNER_COMPLETE'] = os.getenv('_XTS_COMPLETE')
alias = comp_words[1]
comp_words.remove('xts')
comp_words.remove(alias)
os.environ['COMP_WORDS'] = " ".join(comp_words)
self._run_yaml_runner(alias, comp_words)
else:
print('\n'.join(completion))
Comment on lines +217 to +229

def run(self):
"""Run the XTS app.

Raises:
SystemExit: Raised when unrecogised arguments are given.
"""
parser = self._setup_first_parser()
if os.getenv('_XTS_COMPLETE'):
self._run_completion(parser)
raise SystemExit(0)
if len(sys.argv) <= 1:
parser.print_help()
raise SystemExit(0)
args, remaining_args = parser.parse_known_args()
args = vars(args)
alias_name = args.get('alias_name')
match alias_name:
case 'alias':
alias_name_subparser = list(filter(lambda x: x.dest == 'alias_name',parser._actions))[0]
alias_subparser = alias_name_subparser.choices.get('alias')
xts_alias.run_alias_builtin(alias_subparser)
case None|'alias_name':
parser.print_help()
raise SystemExit(0)
case _:
self._run_yaml_runner(alias_name, remaining_args)


def main():
XTS().run()
Expand Down
Loading
Loading