Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
6 changes: 6 additions & 0 deletions .changes/unreleased/optimization-20260322-103653.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: optimization
body: Replace config-based mode setting with runtime detection for interactive/command-line mode
time: 2026-03-22T10:36:53.000000000Z
custom:
Author: ayeshurun
AuthorLink: https://github.com/ayeshurun
12 changes: 6 additions & 6 deletions .github/instructions/test.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ This guide defines how **every test** in Fabric CLI should be designed, implemen
## 2) Structure & naming

- **File names**: `test_<area>_<verb>.py` (e.g., `test_items_ls.py`, `test_gateways_get.py`)
- **Test names**: `test_<behavior>__<condition>` (double underscore between behavior and condition)
- **Test names**: `test_<behavior>_<condition>` (single underscore between behavior and condition)
- **Parametrization**: prefer `@pytest.mark.parametrize` for path variations (absolute, relative, nested, hidden)

---
Expand Down Expand Up @@ -88,7 +88,7 @@ from fabric_cli.__main__ import main # or an entrypoint that dispatches argv
from fabric_cli import constants as fab_const

@responses.activate
def test_ls_semantic_models_under_workspace__json_output(capsys, tmp_home, auth_stub):
def test_ls_semantic_models_under_workspace_json_output(capsys, tmp_home, auth_stub):
# Mock list items under workspace
responses.add(
responses.GET,
Expand Down Expand Up @@ -123,7 +123,7 @@ vcr_recorder = vcr.VCR(
)

@pytest.mark.playback
def test_ls_capacities_hidden_collection__table_output(capsys, tmp_home, auth_stub):
def test_ls_capacities_hidden_collection_table_output(capsys, tmp_home, auth_stub):
with vcr_recorder.use_cassette("ls_capacities_hidden.yaml"):
argv = ["ls", "-a", ".capacities", "-o", "table"]
rc = main(argv) or 0
Expand Down Expand Up @@ -179,7 +179,7 @@ pytest -q tests/test_core tests/test_utils
pytest -q tests/test_commands --playback

# Optional: run a single test
pytest -q tests/test_commands/test_items_ls.py::test_ls_semantic_models_under_workspace__json_output
pytest -q tests/test_commands/test_items_ls.py::test_ls_semantic_models_under_workspace_json_output

```

Expand All @@ -193,7 +193,7 @@ import pytest
from fabric_cli.commands.items.list import attach_items_parsers
from argparse import ArgumentParser

def test_items_ls_parser__has_all_flag_and_output_modes():
def test_items_ls_parser_has_all_flag_and_output_modes():
parser = ArgumentParser()
subs = parser.add_subparsers()
attach_items_parsers(subs)
Expand All @@ -210,7 +210,7 @@ import pytest, responses
from fabric_cli.__main__ import main

@responses.activate
def test_get_item__404_maps_to_fabric_api_error(capsys):
def test_get_item_404_maps_to_fabric_api_error(capsys):
responses.add(
responses.GET,
"https://api.fabric.microsoft.com/v1/items/does-not-exist",
Expand Down
47 changes: 20 additions & 27 deletions docs/essentials/modes.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,40 @@
# CLI Modes

The Fabric CLI supports two primary modes to accommodate a variety of workflows: **command line** and **interactive**. The selected mode is preserved between sessions. If you exit and login to the CLI later, it will resume in the same mode you last used.
The Fabric CLI supports two modes: **command-line** and **REPL** (interactive). The active mode is determined automatically at runtime — no configuration is required.

Use the following command to see the current stored mode setting:
## Command-Line Mode

```
fab config get mode
```

## Command Line Mode
Command-line mode is best suited for scripted tasks, automation, or when you prefer running single commands without a persistent prompt.

Command line mode is best suited for scripted tasks, automation, or when you prefer running single commands without a prompt.

Typing commands directly in the terminal replicates typical UNIX-style usage.

Use the following command to switch the CLI into command line mode:
Invoke any command directly from your terminal with the `fab` prefix:

```
fab config set mode command_line
fab ls /
fab get /myworkspace.Workspace/mynotebook.Notebook
```

You will be required to log in again after switching modes.

## Interactive Mode
## REPL Mode

Interactive mode provides a shell-like environment in which you can run Fabric CLI commands directly without the `fab` prefix.
REPL mode provides a shell-like interactive environment. Run `fab` without any arguments to enter REPL mode:

Upon entering interactive mode, you see a `fab:/$` prompt. Commands are executed one by one without needing to type `fab` before each command, giving you a more guided experience.
```
fab
```

Use the following command to switch the CLI into interactive mode:
Upon entering REPL mode, you see a `fab:/$` prompt. Commands are executed one by one without needing to type `fab` before each command:

```
fab config set mode interactive
fab:/$ ls
fab:/$ cd myworkspace.Workspace
fab:/myworkspace.Workspace$ get mynotebook.Notebook
fab:/myworkspace.Workspace$ quit
```

You will be required to log in again after switching modes.
Type `help` for a list of available commands, and `quit` or `exit` to leave REPL mode.

## Switching Between Modes

To switch from one mode to the other, enter:

```
fab config set mode <desired_mode>
```
There is no explicit mode switch command. The mode is determined by how you invoke the CLI:

where `<desired_mode>` is either `command_line` or `interactive`. Because the Fabric CLI needs to establish new authentication for each mode, you must re-authenticate after switching. The mode choice then remains in effect until you change it again.
- **Command-line mode** — run `fab <command>` with one or more arguments.
- **REPL mode** — run `fab` with no arguments.
5 changes: 2 additions & 3 deletions docs/essentials/settings.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Settings

The Fabric CLI provides a comprehensive set of configuration settings that allow you to customize its behavior, performance, and default values. All settings persist across CLI sessions, except for `mode` and `encryption_fallback_enabled`.
The Fabric CLI provides a comprehensive set of configuration settings that allow you to customize its behavior, performance, and default values. All settings persist across CLI sessions, except for `encryption_fallback_enabled`.

## Available Settings

Expand All @@ -9,11 +9,10 @@ The Fabric CLI provides a comprehensive set of configuration settings that allow
| `cache_enabled` | Toggles caching of CLI HTTP responses | `BOOLEAN` | `true` |
| `check_cli_version_updates` | Enables automatic update notifications on login | `BOOLEAN` | `true` |
| `debug_enabled` | Toggles additional diagnostic logs for troubleshooting | `BOOLEAN` | `false` |
| `context_persistence_enabled` | Persists CLI navigation context in command line mode across sessions | `BOOLEAN` | `false` |
| `context_persistence_enabled` | Persists CLI navigation context in command-line mode across sessions | `BOOLEAN` | `false` |
| `encryption_fallback_enabled` | Permits storing tokens in plain text if secure encryption is unavailable | `BOOLEAN` | `false` |
| `job_cancel_ontimeout` | Cancels job runs that exceed the timeout period | `BOOLEAN` | `true` |
| `local_definition_labels` | Indicates the local JSON file path for label definitions mapping | `VARCHAR` | |
| `mode` | Determines the CLI mode (`interactive` or `command_line`) | `VARCHAR` | `command_line` |
| `output_item_sort_criteria` | Defines items output order (`byname` or `bytype`) | `VARCHAR` | `byname`|
| `show_hidden` | Displays all Fabric elements | `BOOLEAN` | `false` |
| `default_az_admin` | Defines the default Fabric administrator email for capacities | `VARCHAR` | |
Expand Down
14 changes: 14 additions & 0 deletions src/fabric_cli/commands/config/fab_config_get.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@

def exec_command(args: Namespace) -> None:
key = args.key.lower()

# Backward compatibility: 'mode' is no longer a configurable setting.
# Phase 1: warn but still return the runtime mode so existing scripts don't break.
if key == fab_constant.FAB_MODE:
utils_ui.print_warning(
"The 'mode' setting is deprecated and will be removed in a future release. "
"Run 'fab' without arguments to enter REPL mode, "
"or use 'fab <command>' for command-line mode."
)
from fabric_cli.core.fab_context import Context

utils_ui.print_output_format(args, data=Context().get_runtime_mode())
return

if key not in fab_constant.FAB_CONFIG_KEYS_TO_VALID_VALUES:
raise FabricCLIError(
ErrorMessages.Config.unknown_configuration_key(key),
Expand Down
46 changes: 15 additions & 31 deletions src/fabric_cli/commands/config/fab_config_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ def exec_command(args: Namespace) -> None:
key = args.key.lower()
value = args.value.strip().strip("'").strip('"')

# Backward compatibility: 'mode' is no longer a configurable setting.
# Phase 1: warn but still honour the request so existing scripts don't break.
if key == fab_constant.FAB_MODE:
utils_ui.print_warning(
"The 'mode' setting is deprecated and will be removed in a future release. "
"Run 'fab' without arguments to enter REPL mode, "
"or use 'fab <command>' for command-line mode."
)
if value == fab_constant.FAB_MODE_INTERACTIVE:
from fabric_cli.core.fab_interactive import start_interactive_mode

start_interactive_mode()
return

if key not in fab_constant.FAB_CONFIG_KEYS_TO_VALID_VALUES:
raise FabricCLIError(
ErrorMessages.Config.unknown_configuration_key(key),
Expand Down Expand Up @@ -62,16 +76,12 @@ def _set_config(args: Namespace, key: str, value: Any, verbose: bool = True) ->
fab_constant.ERROR_INVALID_PATH,
)

previous_mode = fab_state_config.get_config(key)
fab_state_config.set_config(key, value)
if verbose:
utils_ui.print_output_format(
args, message=f"Configuration '{key}' set to '{value}'"
)

if key == fab_constant.FAB_MODE:
_handle_fab_config_mode(previous_mode, value)


def _set_capacity(args: Namespace, value: str) -> None:
value = utils.remove_dot_suffix(value, ".Capacity")
Expand All @@ -89,30 +99,4 @@ def _set_capacity(args: Namespace, value: str) -> None:
raise FabricCLIError(
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:
# Show deprecation warning
utils_ui.print_warning(
"Mode configuration is deprecated. Running 'fab' now automatically enters interactive mode."
)
utils_ui.print("Starting interactive mode...")
from fabric_cli.core.fab_interactive import start_interactive_mode
start_interactive_mode()

elif current_mode == fab_constant.FAB_MODE_COMMANDLINE:
# Show deprecation warning with better messaging
utils_ui.print_warning(
"Mode configuration is deprecated. Running 'fab' now automatically enters interactive mode."
)
utils_ui.print("Configuration saved for backward compatibility.")

if previous_mode == fab_constant.FAB_MODE_INTERACTIVE:
utils_ui.print("Exiting interactive mode. Goodbye!")
os._exit(0)
)
3 changes: 1 addition & 2 deletions src/fabric_cli/core/fab_constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@
FAB_ENCRYPTION_FALLBACK_ENABLED: ["false", "true"],
FAB_JOB_CANCEL_ONTIMEOUT: ["false", "true"],
FAB_LOCAL_DEFINITION_LABELS: [],
FAB_MODE: [FAB_MODE_INTERACTIVE, FAB_MODE_COMMANDLINE],
FAB_OUTPUT_ITEM_SORT_CRITERIA: ["byname", "bytype"],
FAB_SHOW_HIDDEN: ["false", "true"],
FAB_DEFAULT_AZ_SUBSCRIPTION_ID: [],
Expand All @@ -123,7 +122,6 @@
}

CONFIG_DEFAULT_VALUES = {
FAB_MODE: FAB_MODE_COMMANDLINE,
FAB_CACHE_ENABLED: "true",
FAB_CONTEXT_PERSISTENCE_ENABLED: "false",
FAB_JOB_CANCEL_ONTIMEOUT: "true",
Expand Down Expand Up @@ -344,3 +342,4 @@

# Invalid query parameters for set command across all fabric resources
SET_COMMAND_INVALID_QUERIES = ["id", "type", "workspaceId", "folderId"]

12 changes: 10 additions & 2 deletions src/fabric_cli/core/fab_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,21 @@ class Context:
def __init__(self):
self._context: FabricElement = None
self._command: str = None
self._runtime_mode: str = fab_constant.FAB_MODE_COMMANDLINE
session_id = self._get_context_session_id()
self._context_file = os.path.join(
fab_state_config.config_location(), f"context-{session_id}.json"
)
self._loading_context = False

def set_runtime_mode(self, mode: str) -> None:
"""Set the current runtime mode. Called when entering or leaving the REPL."""
self._runtime_mode = mode

def get_runtime_mode(self) -> str:
"""Return the current runtime mode (FAB_MODE_INTERACTIVE or FAB_MODE_COMMANDLINE)."""
return self._runtime_mode

@property
def context(self) -> FabricElement:
if self._context is None:
Expand Down Expand Up @@ -126,12 +135,11 @@ def _load_context(self) -> None:

def _should_use_context_file(self) -> bool:
"""Determine if the context file should be used based on the current mode and persistence settings."""
mode = fab_state_config.get_config(fab_constant.FAB_MODE)
persistence_enabled = fab_state_config.get_config(
fab_constant.FAB_CONTEXT_PERSISTENCE_ENABLED
)
return (
mode == fab_constant.FAB_MODE_COMMANDLINE
self.get_runtime_mode() == fab_constant.FAB_MODE_COMMANDLINE
and persistence_enabled == "true"
and not self._loading_context
)
Expand Down
2 changes: 2 additions & 0 deletions src/fabric_cli/core/fab_interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def __init__(self, parser=None, subparsers=None):

self.parser = parser
self.parser.set_mode(fab_constant.FAB_MODE_INTERACTIVE)
Context().set_runtime_mode(fab_constant.FAB_MODE_INTERACTIVE)
self.subparsers = subparsers
self.history = InMemoryHistory()
self.session = self.init_session(self.history)
Expand Down Expand Up @@ -147,6 +148,7 @@ def start_interactive(self):
utils_ui.print(fab_constant.INTERACTIVE_EXIT_MESSAGE)
finally:
self._is_running = False
Context().set_runtime_mode(fab_constant.FAB_MODE_COMMANDLINE)


def start_interactive_mode():
Expand Down
4 changes: 4 additions & 0 deletions src/fabric_cli/core/fab_state_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ def init_defaults():
current_config = read_config(config_file)
changed = False

# Migration: remove the deprecated 'mode' key (mode is now detected at runtime)
if fab_constant.FAB_MODE in current_config:
del current_config[fab_constant.FAB_MODE]

for key in fab_constant.FAB_CONFIG_KEYS_TO_VALID_VALUES:
old_key = f"fab_{key}"
if old_key in current_config:
Expand Down
21 changes: 6 additions & 15 deletions src/fabric_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@
from fabric_cli.core.fab_parser_setup import get_global_parser_and_subparsers
from fabric_cli.parsers import fab_auth_parser as auth_parser
from fabric_cli.utils import fab_ui
from fabric_cli.utils.fab_commands import COMMANDS


def main():
parser, subparsers = get_global_parser_and_subparsers()

argcomplete.autocomplete(parser, default_completer=None)

args = parser.parse_args()
Expand All @@ -31,15 +30,8 @@ def main():
if args.command == "auth" and args.auth_command == "login":
from fabric_cli.commands.auth import fab_auth

if fab_auth.init(args):
if (
fab_state_config.get_config(fab_constant.FAB_MODE)
== fab_constant.FAB_MODE_INTERACTIVE
):
from fabric_cli.core.fab_interactive import start_interactive_mode

start_interactive_mode()
return
fab_auth.init(args)
return

if args.command == "auth" and args.auth_command == "logout":
from fabric_cli.commands.auth import fab_auth
Expand Down Expand Up @@ -119,11 +111,11 @@ def _handle_unexpected_error(err, args):
error_message = str(err.args[0]) if err.args else str(err)
except:
error_message = "An unexpected error occurred"

fab_ui.print_output_error(
FabricCLIError(error_message, fab_constant.ERROR_UNEXPECTED_ERROR),
FabricCLIError(error_message, fab_constant.ERROR_UNEXPECTED_ERROR),
output_format_type=args.output_format,
)
)
sys.exit(fab_constant.EXIT_CODE_ERROR)


Expand All @@ -145,4 +137,3 @@ def _execute_command(args, subparsers, parser):

if __name__ == "__main__":
main()

Loading
Loading