Skip to content
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
3 changes: 3 additions & 0 deletions .changes/unreleased/fixed-20251203-155412.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: fixed
body: Avoid re‑authentication when switching from command‑line to interactive mode.
time: 2025-12-03T15:54:12.961104889Z
30 changes: 17 additions & 13 deletions src/fabric_cli/commands/config/fab_config_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,9 @@ def _set_config(args: Namespace, key: str, value: Any, verbose: bool = True) ->
utils_ui.print_output_format(
args, message=f"Configuration '{key}' set to '{value}'"
)
current_mode = fab_state_config.get_config(fab_constant.FAB_MODE)

# Clean up context files when changing mode
if key == fab_constant.FAB_MODE:
from fabric_cli.core.fab_context import Context

Context().cleanup_context_files(cleanup_all_stale=True, cleanup_current=True)

if (
key == fab_constant.FAB_MODE
and current_mode == fab_constant.FAB_MODE_COMMANDLINE
and previous_mode == fab_constant.FAB_MODE_INTERACTIVE
):
utils_ui.print("Exiting interactive mode. Goodbye!")
os._exit(0)
_handle_fab_config_mode(previous_mode, value)


def _set_capacity(args: Namespace, value: str) -> None:
Expand All @@ -102,3 +90,19 @@ def _set_capacity(args: Namespace, value: str) -> None:
ErrorMessages.Config.invalid_capacity(value),
fab_constant.ERROR_INVALID_INPUT,
)


def _handle_fab_config_mode(previous_mode: str, current_mode: str) -> None:
from fabric_cli.core.fab_context import Context
# Clean up context files when changing mode
Context().cleanup_context_files(cleanup_all_stale=True, cleanup_current=True)

if current_mode == fab_constant.FAB_MODE_INTERACTIVE:
utils_ui.print("Switching to interactive mode...")
from fabric_cli.core.fab_interactive import start_interactive_mode
start_interactive_mode()
Comment thread
aviatco marked this conversation as resolved.

elif (current_mode == fab_constant.FAB_MODE_COMMANDLINE
and previous_mode == fab_constant.FAB_MODE_INTERACTIVE):
utils_ui.print("Exiting interactive mode. Goodbye!")
os._exit(0)
58 changes: 50 additions & 8 deletions src/fabric_cli/core/fab_interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,21 @@


class InteractiveCLI:
def __init__(self, parser, subparsers):
_instance = None

def __new__(cls, parser=None, subparsers=None):
Comment thread
aviatco marked this conversation as resolved.
if cls._instance is None:
cls._instance = super(InteractiveCLI, cls).__new__(cls)
# Initialize the instance immediately after creation
cls._instance._init_instance(parser, subparsers)
return cls._instance

def _init_instance(self, parser=None, subparsers=None):
"""Initialize the singleton instance"""
if parser is None or subparsers is None:
from fabric_cli.core.fab_parser_setup import get_global_parser_and_subparsers
parser, subparsers = get_global_parser_and_subparsers()

self.parser = parser
self.parser.set_mode(fab_constant.FAB_MODE_INTERACTIVE)
self.subparsers = subparsers
Expand All @@ -31,6 +45,21 @@ def __init__(self, parser, subparsers):
("input", "fg:white"), # Input color
]
)
self._is_running = False

def __init__(self, parser=None, subparsers=None):
# __init__ is called after __new__, but we've already initialized in __new__
pass

@classmethod
def get_instance(cls, parser=None, subparsers=None):
"""Get or create the singleton instance"""
return cls(parser, subparsers)

@classmethod
def reset_instance(cls):
Comment thread
aviatco marked this conversation as resolved.
"""Reset the singleton instance (mainly for testing)"""
cls._instance = None

def init_session(self, session_history: InMemoryHistory) -> PromptSession:
return PromptSession(history=session_history)
Expand Down Expand Up @@ -89,11 +118,17 @@ def handle_command(self, command):

def start_interactive(self):
"""Start the interactive mode using prompt_toolkit for input."""
utils_ui.print("\nWelcome to the Fabric CLI ⚑")
utils_ui.print("Type 'help' for help. \n")
if self._is_running:
utils_ui.print("Interactive mode is already running.")
return

while True:
try:
self._is_running = True

try:
Comment thread
aviatco marked this conversation as resolved.
utils_ui.print("\nWelcome to the Fabric CLI ⚑")
utils_ui.print("Type 'help' for help. \n")

while True:
context = Context().context
pwd_context = f"/{context.path.strip('/')}"

Expand All @@ -111,6 +146,13 @@ def start_interactive(self):
if should_exit: # Check if the command was to exit
break

except (EOFError, KeyboardInterrupt):
utils_ui.print(f"\n{fab_constant.INTERACTIVE_EXIT_MESSAGE}")
break
except (EOFError, KeyboardInterrupt):
utils_ui.print(f"\n{fab_constant.INTERACTIVE_EXIT_MESSAGE}")
finally:
self._is_running = False


def start_interactive_mode():
"""Launch interactive mode using singleton pattern"""
interactive_cli = InteractiveCLI.get_instance()
interactive_cli.start_interactive()
239 changes: 239 additions & 0 deletions src/fabric_cli/core/fab_parser_setup.py
Comment thread
aviatco marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

import argparse
import re
import sys

import argcomplete

from fabric_cli.core import fab_constant, fab_logger
from fabric_cli.parsers import fab_acls_parser as acls_parser
from fabric_cli.parsers import fab_api_parser as api_parser
from fabric_cli.parsers import fab_auth_parser as auth_parser
from fabric_cli.parsers import fab_config_parser as config_parser
from fabric_cli.parsers import fab_describe_parser as describe_parser
from fabric_cli.parsers import fab_extension_parser as extension_parser
from fabric_cli.parsers import fab_fs_parser as fs_parser
from fabric_cli.parsers import fab_global_params
from fabric_cli.parsers import fab_jobs_parser as jobs_parser
from fabric_cli.parsers import fab_labels_parser as labels_parser
from fabric_cli.parsers import fab_tables_parser as tables_parser
from fabric_cli.utils import fab_error_parser as utils_error_parser
from fabric_cli.utils import fab_ui
from fabric_cli.utils.fab_commands import COMMANDS


class CustomHelpFormatter(argparse.HelpFormatter):

def __init__(
self,
prog,
fab_examples=None,
fab_aliases=None,
fab_learnmore=None,
*args,
**kwargs,
):
super().__init__(prog, *args, **kwargs)
self.fab_examples = fab_examples or []
self.fab_aliases = fab_aliases or []
self.fab_learnmore = fab_learnmore or []

def _format_args(self, action, default_metavar):
if action.nargs in ("*", "+"):
if action.option_strings:
return ""
else:
# Ensure metavar is lowercase for positional arguments
return f"<{action.dest}>"
return super()._format_args(action, default_metavar)

def _format_action_invocation(self, action):
if not action.metavar and action.nargs in (None, "?"):
# For no metavar and simple arguments
return ", ".join(action.option_strings)
elif action.nargs in ("*", "+"):
metavar = self._format_args(action, action.dest)
return ", ".join(action.option_strings) + metavar
else:
return super()._format_action_invocation(action)

def format_help(self):
help_message = super().format_help()

# Custom output
help_message = help_message.replace("usage:", "Usage:")
help_message = help_message.replace("positional arguments:", "Arg(s):")
help_message = help_message.replace("options:", "Flags:")

help_message = re.sub(
r"\s*-h, --help\s*(Show help for command|show this help message and exit)?",
"",
help_message,
)
help_message = help_message.replace(" -help\n", "")
help_message = help_message.replace("[-h] ", "")
help_message = help_message.replace("[-help] ", "")
help_message = help_message.replace("[-help]", "")

if "Flags:" in help_message:
flags_section = help_message.split("Flags:")[1].strip()
if not flags_section: # If no flags follow the "Flags:" line, remove it
help_message = help_message.replace("\nFlags:\n", "")

# Add aliases
if self.fab_aliases:
help_message += "\nAliases:\n"
for alias in self.fab_aliases:
help_message += f" {alias}\n"

# Add examples
if self.fab_examples:
help_message += "\nExamples:\n"
for example in self.fab_examples:
if "#" in example:
# Grey color
help_message += f" \033[38;5;243m{example}\033[0m\n"
else:
help_message += f" {example}\n"

# Add learn more
if self.fab_learnmore:
help_message += "\nLearn more:\n"
if self.fab_learnmore != ["_"]:
for learn_more in self.fab_learnmore:
help_message += f" {learn_more}\n"
help_message += " For more usage examples, see https://aka.ms/fabric-cli\n"

return help_message + "\n"


class CustomArgumentParser(argparse.ArgumentParser):
def __init__(
self, *args, fab_examples=None, fab_aliases=None, fab_learnmore=None, **kwargs
):
kwargs["formatter_class"] = lambda prog: CustomHelpFormatter(
prog,
fab_examples=fab_examples,
fab_aliases=fab_aliases,
fab_learnmore=fab_learnmore,
)
super().__init__(*args, **kwargs)
# Add custom help and format flags
fab_global_params.add_global_flags(self)
self.fab_mode = fab_constant.FAB_MODE_COMMANDLINE
self.fab_examples = fab_examples or []
self.fab_aliases = fab_aliases or []

def print_help(self, file=None):
command_name = self.prog.split()[-1]

help_functions = {
"acl": lambda: acls_parser.show_help(None),
"job": lambda: jobs_parser.show_help(None),
"label": lambda: labels_parser.show_help(None),
"table": lambda: tables_parser.show_help(None),
"auth": lambda: auth_parser.show_help(None),
"config": lambda: config_parser.show_help(None),
"fab": lambda: fab_ui.display_help(COMMANDS),
}

if command_name in help_functions:
help_functions[command_name]()
else:
super().print_help(file)

def set_mode(self, mode):
self.fab_mode = mode

def get_mode(self):
return self.fab_mode

def error(self, message):
if "invalid choice" in message:
utils_error_parser.invalid_choice(self, message)
elif "unrecognized arguments" in message:
utils_error_parser.unrecognized_arguments(message)
elif "the following arguments are required" in message:
utils_error_parser.missing_required_arguments(message)
else:
# Add more custom error parsers here
fab_logger.log_warning(message)

if self.fab_mode == fab_constant.FAB_MODE_COMMANDLINE:
sys.exit(2)


# Global parser instances
Comment thread
aviatco marked this conversation as resolved.
_global_parser = None
_global_subparsers = None

def create_parser_and_subparsers():
"""Create parser and subparsers for reuse across CLI modes"""
parser = CustomArgumentParser(description="Fabric CLI")

# -c option for command line execution
parser.add_argument(
"-c",
"--command",
action="append", # Allow multiple -c options
help="Run commands in non-interactive mode",
)

# -version and --version
parser.add_argument("-v", "--version", action="store_true")

subparsers = parser.add_subparsers(dest="command", required=False)

# Custom parsers
config_parser.register_parser(subparsers)

# Single parsers
fs_parser.register_ls_parser(subparsers) # ls
fs_parser.register_mkdir_parser(subparsers) # mkdir
fs_parser.register_cd_parser(subparsers) # cd
fs_parser.register_rm_parser(subparsers) # rm
fs_parser.register_mv_parser(subparsers) # mv
fs_parser.register_cp_parser(subparsers) # cp
fs_parser.register_exists_parser(subparsers) # exists
fs_parser.register_pwd_parser(subparsers) # pwd
fs_parser.register_open_parser(subparsers) # open
fs_parser.register_export_parser(subparsers) # export
fs_parser.register_import_parser(subparsers) # import
fs_parser.register_set_parser(subparsers) # set
fs_parser.register_get_parser(subparsers) # get
fs_parser.register_clear_parser(subparsers) # clear
fs_parser.register_ln_parser(subparsers) # ln
fs_parser.register_start_parser(subparsers) # start
fs_parser.register_stop_parser(subparsers) # stop
fs_parser.register_assign_parser(subparsers) # assign
fs_parser.register_unassign_parser(subparsers) # unassign

jobs_parser.register_parser(subparsers) # jobs
tables_parser.register_parser(subparsers) # tables
acls_parser.register_parser(subparsers) # acls
labels_parser.register_parser(subparsers) # labels

api_parser.register_parser(subparsers) # api
auth_parser.register_parser(subparsers) # auth
describe_parser.register_parser(subparsers) # desc
extension_parser.register_parser(subparsers) # extension

# version
version_parser = subparsers.add_parser(
"version", help=fab_constant.COMMAND_VERSION_DESCRIPTION
)
version_parser.set_defaults(func=fab_ui.print_version)

return parser, subparsers

def get_global_parser_and_subparsers():
"""Get singleton parser and subparsers instances"""
global _global_parser, _global_subparsers

if _global_parser is None:
_global_parser, _global_subparsers = create_parser_and_subparsers()

return _global_parser, _global_subparsers

Loading