Skip to content

Add initial Q Developer customization #9470

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 2, 2025
Merged
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
62 changes: 62 additions & 0 deletions awscli/agentmode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

import argparse

from awscli.argparser import CLIArgParser
from awscli.customizations.q.chat import ChatCommand


class AgentModeDriver(CLIArgParser):
"""
Driver for the CLI to enable the Q plugin when the --agent-mode argument
is passed. This behaves more like a custom command, but is implemented
as a driver similar to autoprompt so we don't need to expose it as an
actual command via the command table.
"""

_AGENT_MODE_ARG = '--agent-mode'

def __init__(self, driver):
super().__init__()
self._driver = driver
self._session = driver.session
self._parser = self._create_parser()

def _create_parser(self):
parser = argparse.ArgumentParser()
parser.add_argument(
'--region',
help='AWS region to use for agent inference.',
default=None,
)
parser.add_argument(
'--profile',
help='Use a specific profile from your credential file.',
default=None,
)
parser.add_argument(
self._AGENT_MODE_ARG,
action='store_true',
help='Enable agentic mode.',
)
return parser

def should_enter_agent_mode(self, args):
return self._AGENT_MODE_ARG in args

def enter_agent_mode(self, args):
parsed_args = self._parser.parse_args(args)

# TODO - handle the agent-mode-specific region and profile
return ChatCommand(self._session)('', parsed_args)
2 changes: 1 addition & 1 deletion awscli/autocomplete/local/indexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class ModelIndexer:
'ddb': 'High level DynamoDB commands',
}

_NON_SERVICE_COMMANDS = ['configure', 'history', 'cli-dev']
_NON_SERVICE_COMMANDS = ['configure', 'history', 'cli-dev', 'q']

_CREATE_CMD_TABLE = """\
CREATE TABLE IF NOT EXISTS command_table (
Expand Down
7 changes: 6 additions & 1 deletion awscli/clidriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from botocore.history import get_global_history_recorder

from awscli import __version__
from awscli.agentmode import AgentModeDriver
from awscli.alias import AliasCommandInjector, AliasLoader
from awscli.argparser import (
ArgTableArgParser,
Expand Down Expand Up @@ -231,7 +232,11 @@ def _do_main(self, args):
driver = create_clidriver(args)
rc = self._run_driver(driver, args, prompt_mode='partial')
else:
rc = self._run_driver(driver, args, prompt_mode='off')
agent_mode_driver = AgentModeDriver(driver)
if agent_mode_driver.should_enter_agent_mode(args):
rc = agent_mode_driver.enter_agent_mode(args)
else:
rc = self._run_driver(driver, args, prompt_mode='off')
return rc


Expand Down
42 changes: 42 additions & 0 deletions awscli/customizations/q/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

from awscli.customizations.commands import BasicCommand
from awscli.customizations.q.chat import ChatCommand
from awscli.customizations.q.uninstall import UninstallCommand
from awscli.customizations.q.update import UpdateCommand


def register_q_commands(event_handlers):
event_handlers.register('building-command-table.main', inject_commands)


def inject_commands(command_table, session, **kwargs):
command_table['q'] = QCommand(session)


class QCommand(BasicCommand):
# 'TODO help prints "System Message: WARNING/2 (<string>:, line 7)"
# but not figuring out yet since naming may still change
NAME = 'q'
SYNOPSIS = 'q <command> [<args>]'
DESCRIPTION = (
'You can use Amazon Q Developer to translate natural language to '
'AWS CLI commands. Run ``aws q chat`` to launch the Q CLI. You can '
'update to the latest version of the Q plugin with ``aws q update``.'
)
SUBCOMMANDS = [
{'name': 'chat', 'command_class': ChatCommand},
{'name': 'update', 'command_class': UpdateCommand},
{'name': 'uninstall', 'command_class': UninstallCommand},
]
39 changes: 39 additions & 0 deletions awscli/customizations/q/chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

import subprocess

from botocore.utils import original_ld_library_path

from awscli.customizations.commands import BasicCommand
from awscli.customizations.q.utils import _get_executable_path_or_download


class ChatCommand(BasicCommand):
NAME = 'chat'
DESCRIPTION = (
'Launch Amazon Q Developer, which can translate natural language '
'to AWS CLI commands.'
)

def __init__(self, session):
super().__init__(session)

def _run_main(self, parsed_args, parsed_globals):
path = _get_executable_path_or_download()
if not path:
raise RuntimeError('Amazon Q extension is not found.')

# TODO pass through credentials and region
with original_ld_library_path():
subprocess.check_call(path)
39 changes: 39 additions & 0 deletions awscli/customizations/q/uninstall.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

import shutil
import sys
from pathlib import Path

from awscli.customizations.commands import BasicCommand
from awscli.customizations.q.utils import _find_extension_paths


class UninstallCommand(BasicCommand):
NAME = 'uninstall'
DESCRIPTION = 'Uninstall the Q CLI extension.'

def __init__(self, session):
super().__init__(session)

def _run_main(self, parsed_args, parsed_globals):
extension_paths = _find_extension_paths()

if not extension_paths:
sys.stdout.write('The Q CLI extension is not installed.\n')
return

for path in extension_paths:
extension_path = Path(path).parent.absolute()
shutil.rmtree(extension_path, ignore_errors=True)
sys.stdout.write(f'Uninstalled {path}\n')
28 changes: 28 additions & 0 deletions awscli/customizations/q/update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

import sys

from awscli.customizations.commands import BasicCommand


class UpdateCommand(BasicCommand):
NAME = 'update'
DESCRIPTION = 'Update the Q CLI extension.'

def __init__(self, session):
super().__init__(session)

def _run_main(self, parsed_args, parsed_globals):
# TODO - implement this
pass
49 changes: 49 additions & 0 deletions awscli/customizations/q/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import os
from pathlib import Path

EXTENSIONS_DIR = Path.home() / '.aws' / 'cli' / 'extensions'
Q_DIRECTORY_NAME = 'q'
Q_EXECUTABLE_NAME = 'q'
DEFAULT_EXTENSION_PATH = EXTENSIONS_DIR / Q_DIRECTORY_NAME / Q_EXECUTABLE_NAME


def _find_extension_paths():
"""Finds all possible paths of the Q extension. For the case where multiple
are found via AWS_DATA_PATH, callers should prefer the first."""

# Prioritize any paths specified by AWS_DATA_PATH ahead of our default path
possible_extension_paths = os.environ.get('AWS_DATA_PATH', '').split(
os.pathsep
)
possible_extension_paths.append(str(EXTENSIONS_DIR))

q_extension_paths = []
for extension_path in possible_extension_paths:
possible_path = (
Path(extension_path) / Q_DIRECTORY_NAME / Q_EXECUTABLE_NAME
)
if Path.is_file(possible_path):
q_extension_paths.append(possible_path)
return q_extension_paths


def _get_executable_path_or_download():
extension_paths = _find_extension_paths()

if extension_paths:
return extension_paths[0]

# TODO - implement download
return None
4 changes: 4 additions & 0 deletions awscli/data/cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
"no-cli-auto-prompt": {
"action": "store_true",
"help": "<p>Disable automatically prompt for CLI input parameters.</p>"
},
"agent-mode": {
"action": "store_true",
"help": "<p>Enter agent mode, which can translate natural language to AWS CLI commands.</p>"
}
}
}
4 changes: 4 additions & 0 deletions awscli/examples/global_options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,7 @@

Disable automatically prompt for CLI input parameters.

``--agent-mode`` (boolean)

Enter agent mode, which can translate natural language to AWS CLI commands.

1 change: 1 addition & 0 deletions awscli/examples/global_synopsis.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@
[--no-cli-pager]
[--cli-auto-prompt]
[--no-cli-auto-prompt]
[--agent-mode]
2 changes: 2 additions & 0 deletions awscli/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
from awscli.customizations.opsworkscm import register_alias_opsworks_cm
from awscli.customizations.paginate import register_pagination
from awscli.customizations.putmetricdata import register_put_metric_data
from awscli.customizations.q import register_q_commands
from awscli.customizations.quicksight import (
register_quicksight_asset_bundle_customizations,
)
Expand Down Expand Up @@ -237,3 +238,4 @@ def awscli_initialize(event_handlers):
register_kinesis_list_streams_pagination_backcompat(event_handlers)
register_quicksight_asset_bundle_customizations(event_handlers)
register_ec2_instance_connect_commands(event_handlers)
register_q_commands(event_handlers)
12 changes: 12 additions & 0 deletions tests/unit/customizations/q/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
Loading