diff --git a/.github/workflows/build_tests.yml b/.github/workflows/build_tests.yml new file mode 100644 index 0000000..10597ec --- /dev/null +++ b/.github/workflows/build_tests.yml @@ -0,0 +1,39 @@ +name: Run Build Tests +on: + push: + branches: + - master + pull_request: + branches: + - dev + workflow_dispatch: + +jobs: + build_tests: + strategy: + max-parallel: 2 + matrix: + python-version: [3.8, 3.9, "3.10", "3.11" ] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install Build Tools + run: | + python -m pip install build wheel + - name: Install System Dependencies + run: | + sudo apt-get update + sudo apt install python3-dev swig libssl-dev + - name: Build Source Packages + run: | + python setup.py sdist + - name: Build Distribution Packages + run: | + python setup.py bdist_wheel + - name: Install skill + run: | + pip install . \ No newline at end of file diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml new file mode 100644 index 0000000..b4c67be --- /dev/null +++ b/.github/workflows/unit_tests.yml @@ -0,0 +1,66 @@ +name: Run UnitTests +on: + pull_request: + branches: + - dev + paths-ignore: + - 'version.py' + - 'examples/**' + - '.github/**' + - '.gitignore' + - 'LICENSE' + - 'CHANGELOG.md' + - 'MANIFEST.in' + - 'README.md' + - 'scripts/**' + push: + branches: + - master + paths-ignore: + - 'version.py' + - 'examples/**' + - '.github/**' + - '.gitignore' + - 'LICENSE' + - 'CHANGELOG.md' + - 'MANIFEST.in' + - 'README.md' + - 'scripts/**' + workflow_dispatch: + +jobs: + unit_tests: + strategy: + matrix: + python-version: [3.9, "3.10" ] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install System Dependencies + run: | + sudo apt-get update + sudo apt install python3-dev + python -m pip install build wheel + - name: Install core repo + run: | + pip install . + - name: Install test dependencies + run: | + pip install pytest pytest-timeout pytest-cov + - name: Install System Dependencies + run: | + sudo apt-get update + - name: Install ovos dependencies + run: | + pip install ovos-plugin-manager + - name: Run unittests + run: | + pytest --cov=ovos-skill-cmd --cov-report xml test + - name: Upload coverage + env: + CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} + uses: codecov/codecov-action@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index d011f40..6c273ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,24 @@ # Changelog -## [0.2.1a1](https://github.com/OpenVoiceOS/ovos-skill-cmd/tree/0.2.1a1) (2024-10-15) +## [0.2.2a2](https://github.com/OpenVoiceOS/ovos-skill-cmd/tree/0.2.2a2) (2024-11-15) -[Full Changelog](https://github.com/OpenVoiceOS/ovos-skill-cmd/compare/V0.1.0...0.2.1a1) +[Full Changelog](https://github.com/OpenVoiceOS/ovos-skill-cmd/compare/0.2.2a1...0.2.2a2) **Merged pull requests:** -- semver [\#8](https://github.com/OpenVoiceOS/ovos-skill-cmd/pull/8) ([JarbasAl](https://github.com/JarbasAl)) -- catalan support [\#7](https://github.com/OpenVoiceOS/ovos-skill-cmd/pull/7) ([gitlocalize-app[bot]](https://github.com/apps/gitlocalize-app)) -- accepted addition translation [\#6](https://github.com/OpenVoiceOS/ovos-skill-cmd/pull/6) ([gitlocalize-app[bot]](https://github.com/apps/gitlocalize-app)) +- shell kwarg [\#13](https://github.com/OpenVoiceOS/ovos-skill-cmd/pull/13) ([JarbasAl](https://github.com/JarbasAl)) + +## [0.2.2a1](https://github.com/OpenVoiceOS/ovos-skill-cmd/tree/0.2.2a1) (2024-11-02) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-skill-cmd/compare/V0.2.1...0.2.2a1) + +**Merged pull requests:** + +- da-dk/translate [\#11](https://github.com/OpenVoiceOS/ovos-skill-cmd/pull/11) ([gitlocalize-app[bot]](https://github.com/apps/gitlocalize-app)) + +## [V0.2.1](https://github.com/OpenVoiceOS/ovos-skill-cmd/tree/V0.2.1) (2024-10-15) + +[Full Changelog](https://github.com/OpenVoiceOS/ovos-skill-cmd/compare/0.2.1...V0.2.1) diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..fb05bf3 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +recursive-include locale * +include *.txt \ No newline at end of file diff --git a/README.md b/README.md index ddfedee..c4d34b2 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,23 @@ # Commands Skill -A Simple OVOS skill for running shell scripts and other commands. The commands will run quietly without any confirmation from OVOS. +A simple OVOS skill for running shell scripts and other commands. The commands execute quietly without requiring confirmation from OVOS. + +## Features + +- Run shell scripts or commands by speaking human-readable phrases. +- Configure aliases for easy-to-remember commands. +- Optionally run commands under a specific user's privileges. ## Usage -*Hey Mycroft, launch command echo TEST* +Trigger commands using phrases like: -*Hey Mycroft, run script generate report* +- **"Hey Mycroft, launch command echo TEST"** +- **"Hey Mycroft, run script generate report"** ## Configuration -The skill can be configured to run scripts from easily pronounceable human utterances, such as "generate report" by adding the following to the skill `settings.json` +The skill can be configured to map spoken phrases to scripts or commands in the `settings.json` file. For example: ```json { @@ -20,4 +27,46 @@ The skill can be configured to run scripts from easily pronounceable human utter } ``` -The configuration above will launch `/home/forslund/scripts/generate_report.sh` when "run script generate report" is said by the user. +### Example: +- User says: **"Run script generate report"** +- The skill executes: `/home/forslund/scripts/generate_report.sh` + +### Additional Settings: + +- **`user`** *(optional)*: Specify a username to run commands under their privileges. Example: + ```json + { + "user": "ovos" + } + ``` + +- **`shell`** *(optional)*: Determines whether commands are executed via a shell. Defaults to `true`. Example: + ```json + { + "shell": false + } + ``` + +### Full Configuration Example: +```json +{ + "user": "ovos", + "alias": { + "generate report": "/home/forslund/scripts/generate_report.sh", + "update system": "sudo apt update && sudo apt upgrade -y", + "reboot device": "sudo reboot" + }, + "shell": true +} +``` + +## Security Notes + +1. **Shell Commands**: + - By default, commands are executed via the shell, which allows complex operations but may expose security risks. If your commands don’t require shell features, set `shell` to `false`. + +2. **User Permissions**: + - Commands can run under a specific user by configuring the `user` field. Ensure that the user has appropriate permissions to execute the commands. + +3. **Validation**: + - Avoid configuring dangerous commands like `rm -rf` without additional safeguards. diff --git a/__init__.py b/__init__.py index 388a6fe..356a0a5 100644 --- a/__init__.py +++ b/__init__.py @@ -53,12 +53,13 @@ def initialize(self): def run(self, message): script = message.data.get('Script') script = self.alias.get(script, script) - args = script.split(' ') + shell = self.settings.get('shell', True) + args = script.split(' ') if shell else script try: + LOG.info(f'Running {args}') if self.uid and self.gid: - subprocess.Popen(args, preexec_fn=set_user(self.uid, self.gid)) + subprocess.Popen(args, preexec_fn=set_user(self.uid, self.gid), shell=shell) else: - LOG.info(f'Running {args}') - subprocess.Popen(args) + subprocess.Popen(args, shell=shell) except Exception: LOG.exception('Could not run script ' + script) diff --git a/scripts/bump_alpha.py b/scripts/bump_alpha.py deleted file mode 100644 index 49016a7..0000000 --- a/scripts/bump_alpha.py +++ /dev/null @@ -1,19 +0,0 @@ -"""increases alpha version number""" -import fileinput -from os.path import join, dirname - - -version_file = join(dirname(dirname(__file__)), "version.py") -version_var_name = "VERSION_ALPHA" - -with open(version_file, "r", encoding="utf-8") as v: - for line in v.readlines(): - if line.startswith(version_var_name): - version = int(line.split("=")[-1]) - new_version = int(version) + 1 - -for line in fileinput.input(version_file, inplace=True): - if line.startswith(version_var_name): - print(f"{version_var_name} = {new_version}") - else: - print(line.rstrip('\n')) diff --git a/scripts/bump_build.py b/scripts/bump_build.py deleted file mode 100644 index 61099f8..0000000 --- a/scripts/bump_build.py +++ /dev/null @@ -1,21 +0,0 @@ -import fileinput -from os.path import join, dirname - - -version_file = join(dirname(dirname(__file__)), "version.py") -version_var_name = "VERSION_BUILD" -alpha_var_name = "VERSION_ALPHA" - -with open(version_file, "r", encoding="utf-8") as v: - for line in v.readlines(): - if line.startswith(version_var_name): - version = int(line.split("=")[-1]) - new_version = int(version) + 1 - -for line in fileinput.input(version_file, inplace=True): - if line.startswith(version_var_name): - print(f"{version_var_name} = {new_version}") - elif line.startswith(alpha_var_name): - print(f"{alpha_var_name} = 0") - else: - print(line.rstrip('\n')) diff --git a/scripts/bump_major.py b/scripts/bump_major.py deleted file mode 100644 index 2610fbb..0000000 --- a/scripts/bump_major.py +++ /dev/null @@ -1,27 +0,0 @@ -import fileinput -from os.path import join, dirname - - -version_file = join(dirname(dirname(__file__)), "version.py") -version_var_name = "VERSION_MAJOR" -minor_var_name = "VERSION_MINOR" -build_var_name = "VERSION_BUILD" -alpha_var_name = "VERSION_ALPHA" - -with open(version_file, "r", encoding="utf-8") as v: - for line in v.readlines(): - if line.startswith(version_var_name): - version = int(line.split("=")[-1]) - new_version = int(version) + 1 - -for line in fileinput.input(version_file, inplace=True): - if line.startswith(version_var_name): - print(f"{version_var_name} = {new_version}") - elif line.startswith(minor_var_name): - print(f"{minor_var_name} = 0") - elif line.startswith(build_var_name): - print(f"{build_var_name} = 0") - elif line.startswith(alpha_var_name): - print(f"{alpha_var_name} = 0") - else: - print(line.rstrip('\n')) diff --git a/scripts/bump_minor.py b/scripts/bump_minor.py deleted file mode 100644 index 86dfd9d..0000000 --- a/scripts/bump_minor.py +++ /dev/null @@ -1,24 +0,0 @@ -import fileinput -from os.path import join, dirname - - -version_file = join(dirname(dirname(__file__)), "version.py") -version_var_name = "VERSION_MINOR" -build_var_name = "VERSION_BUILD" -alpha_var_name = "VERSION_ALPHA" - -with open(version_file, "r", encoding="utf-8") as v: - for line in v.readlines(): - if line.startswith(version_var_name): - version = int(line.split("=")[-1]) - new_version = int(version) + 1 - -for line in fileinput.input(version_file, inplace=True): - if line.startswith(version_var_name): - print(f"{version_var_name} = {new_version}") - elif line.startswith(build_var_name): - print(f"{build_var_name} = 0") - elif line.startswith(alpha_var_name): - print(f"{alpha_var_name} = 0") - else: - print(line.rstrip('\n')) diff --git a/scripts/prepare_translations.py b/scripts/prepare_translations.py deleted file mode 100644 index 01a674d..0000000 --- a/scripts/prepare_translations.py +++ /dev/null @@ -1,53 +0,0 @@ -"""this script should run every time the contents of the locale folder change -except if PR originated from @gitlocalize-app -TODO - on commit to dev -""" - -import json -from os.path import dirname -import os - -locale = f"{dirname(dirname(__file__))}/locale" -tx = f"{dirname(dirname(__file__))}/translations" - - -for lang in os.listdir(locale): - intents = {} - dialogs = {} - vocs = {} - regexes = {} - for root, _, files in os.walk(f"{locale}/{lang}"): - b = root.split(f"/{lang}")[-1] - - for f in files: - if b: - fid = f"{b}/{f}" - else: - fid = f - with open(f"{root}/{f}") as fi: - strings = [l.replace("{{", "{").replace("}}", "}") - for l in fi.read().split("\n") if l.strip() - and not l.startswith("#")] - - if fid.endswith(".intent"): - intents[fid] = strings - elif fid.endswith(".dialog"): - dialogs[fid] = strings - elif fid.endswith(".voc"): - vocs[fid] = strings - elif fid.endswith(".rx"): - regexes[fid] = strings - - os.makedirs(f"{tx}/{lang.lower()}", exist_ok=True) - if intents: - with open(f"{tx}/{lang.lower()}/intents.json", "w") as f: - json.dump(intents, f, indent=4) - if dialogs: - with open(f"{tx}/{lang.lower()}/dialogs.json", "w") as f: - json.dump(dialogs, f, indent=4) - if vocs: - with open(f"{tx}/{lang.lower()}/vocabs.json", "w") as f: - json.dump(vocs, f, indent=4) - if regexes: - with open(f"{tx}/{lang.lower()}/regexes.json", "w") as f: - json.dump(regexes, f, indent=4) diff --git a/scripts/remove_alpha.py b/scripts/remove_alpha.py deleted file mode 100644 index fca7342..0000000 --- a/scripts/remove_alpha.py +++ /dev/null @@ -1,13 +0,0 @@ -import fileinput -from os.path import join, dirname - - -version_file = join(dirname(dirname(__file__)), "version.py") - -alpha_var_name = "VERSION_ALPHA" - -for line in fileinput.input(version_file, inplace=True): - if line.startswith(alpha_var_name): - print(f"{alpha_var_name} = 0") - else: - print(line.rstrip('\n')) diff --git a/setup.py b/setup.py index dbebb2f..d80d3d5 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ # Define package information SKILL_CLAZZ = "CmdSkill" # Make sure it matches __init__.py class name -URL = "https://github.com/OVOSHatchery/ovos-skill-cmd" +URL = "https://github.com/OpenVoiceOS/ovos-skill-cmd" AUTHOR = "forslund" EMAIL = "" LICENSE = "Apache2.0" @@ -37,7 +37,7 @@ def get_requirements(requirements_filename: str = "requirements.txt"): # Function to find resource files def find_resource_files(): - resource_base_dirs = ("locale", "ui") + resource_base_dirs = ("locale") base_dir = abspath(dirname(__file__)) package_data = ["*.json"] for res in resource_base_dirs: diff --git a/test/test_plugin.py b/test/test_plugin.py new file mode 100644 index 0000000..bca6307 --- /dev/null +++ b/test/test_plugin.py @@ -0,0 +1,13 @@ +import unittest +from ovos_plugin_manager.skills import find_skill_plugins + + +class TestPlugin(unittest.TestCase): + @classmethod + def setUpClass(self): + self.skill_id = "ovos-skill-cmd.openvoiceos" + + def test_find_plugin(self): + plugins = find_skill_plugins() + self.assertIn(self.skill_id, list(plugins)) + diff --git a/test/test_skill_loading.py b/test/test_skill_loading.py new file mode 100644 index 0000000..6d5143a --- /dev/null +++ b/test/test_skill_loading.py @@ -0,0 +1,54 @@ +import unittest +from os.path import dirname + +from ovos_plugin_manager.skills import find_skill_plugins +from ovos_utils.messagebus import FakeBus +from ovos_workshop.skill_launcher import PluginSkillLoader, SkillLoader +from ovos_skill_cmd import CmdSkill + + +class TestSkillLoading(unittest.TestCase): + @classmethod + def setUpClass(self): + self.skill_id = "ovos-skill-cmd.openvoiceos" + self.path = dirname(dirname(__file__)) + + def test_from_class(self): + bus = FakeBus() + skill = CmdSkill() + skill._startup(bus, self.skill_id) + self.assertEqual(skill.bus, bus) + self.assertEqual(skill.skill_id, self.skill_id) + + def test_from_plugin(self): + bus = FakeBus() + for skill_id, plug in find_skill_plugins().items(): + if skill_id == self.skill_id: + skill = plug() + skill._startup(bus, self.skill_id) + self.assertEqual(skill.bus, bus) + self.assertEqual(skill.skill_id, self.skill_id) + break + else: + raise RuntimeError("plugin not found") + + def test_from_loader(self): + bus = FakeBus() + loader = SkillLoader(bus, self.path) + loader.load() + self.assertEqual(loader.instance.bus, bus) + self.assertEqual(loader.instance.root_dir, self.path) + + def test_from_plugin_loader(self): + bus = FakeBus() + loader = PluginSkillLoader(bus, self.skill_id) + for skill_id, plug in find_skill_plugins().items(): + if skill_id == self.skill_id: + loader.load(plug) + break + else: + raise RuntimeError("plugin not found") + + self.assertEqual(loader.skill_id, self.skill_id) + self.assertEqual(loader.instance.bus, bus) + self.assertEqual(loader.instance.skill_id, self.skill_id) diff --git a/translations/da/vocabs.json b/translations/da/vocabs.json new file mode 100644 index 0000000..3ca9aa1 --- /dev/null +++ b/translations/da/vocabs.json @@ -0,0 +1,5 @@ +{ + "Run.voc": [ + "(kør|udfør|start) (kommando|script)" + ] +} diff --git a/version.py b/version.py index a210c75..2648883 100644 --- a/version.py +++ b/version.py @@ -1,6 +1,6 @@ # START_VERSION_BLOCK VERSION_MAJOR = 0 VERSION_MINOR = 2 -VERSION_BUILD = 1 -VERSION_ALPHA = 0 +VERSION_BUILD = 2 +VERSION_ALPHA = 2 # END_VERSION_BLOCK