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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,24 @@ irm https://raw.githubusercontent.com/databricks-solutions/ai-dev-kit/main/insta
**Next steps:** Respond to interactive prompts and follow the on-screen instructions.
- Note: Cursor and Copilot require updating settings manually after install.

#### Updating to the Latest Version

After initial install, update to the latest version with:

```bash
bash <(curl -sL https://raw.githubusercontent.com/databricks-solutions/ai-dev-kit/main/install.sh) --update
```

This re-runs the installer using your saved configuration (tools, scope, profile) — no interactive prompts needed. It updates both the MCP server and re-copies the latest skills to your tool directories.

> **Why is `--update` needed?** Skills are copied to tool-specific directories (e.g., `~/.cursor/skills/`) during install. A `git pull` on the repo only updates the MCP server source, not the copied skills. The `--update` flag ensures everything stays in sync.

If you haven't used `--update` before (installed with an older version), use `--force` instead:

```bash
bash <(curl -sL https://raw.githubusercontent.com/databricks-solutions/ai-dev-kit/main/install.sh) --force
```


### Visual Builder App

Expand Down
8 changes: 2 additions & 6 deletions databricks-tools-core/databricks_tools_core/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,7 @@ def get_workspace_client() -> WorkspaceClient:
# Cross-workspace: explicit token overrides env OAuth so tool operations
# target the caller-specified workspace instead of the app's own workspace
if force and host and token:
return tag_client(
WorkspaceClient(host=host, token=token, auth_type="pat", **product_kwargs)
)
return tag_client(WorkspaceClient(host=host, token=token, auth_type="pat", **product_kwargs))

# In Databricks Apps (OAuth credentials in env), explicitly use OAuth M2M.
# Setting auth_type="oauth-m2m" prevents the SDK from also reading
Expand All @@ -185,9 +183,7 @@ def get_workspace_client() -> WorkspaceClient:

# Development mode: use explicit token if provided
if host and token:
return tag_client(
WorkspaceClient(host=host, token=token, auth_type="pat", **product_kwargs)
)
return tag_client(WorkspaceClient(host=host, token=token, auth_type="pat", **product_kwargs))

if host:
return tag_client(WorkspaceClient(host=host, **product_kwargs))
Expand Down
54 changes: 21 additions & 33 deletions databricks-tools-core/tests/unit/test_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,7 @@ def test_executor_without_query_tags_omits_from_api(self, mock_get_client):
assert "query_tags" not in call_kwargs


def _make_warehouse(id, name, state, creator_name="other@example.com",
enable_serverless_compute=False):
def _make_warehouse(id, name, state, creator_name="other@example.com", enable_serverless_compute=False):
"""Helper to create a mock warehouse object."""
w = mock.Mock()
w.id = id
Expand All @@ -141,33 +140,29 @@ class TestSortWithinTier:
def test_serverless_first(self):
"""Serverless warehouses should come before classic ones."""
classic = _make_warehouse("c1", "Classic WH", State.RUNNING)
serverless = _make_warehouse("s1", "Serverless WH", State.RUNNING,
enable_serverless_compute=True)
serverless = _make_warehouse("s1", "Serverless WH", State.RUNNING, enable_serverless_compute=True)
result = _sort_within_tier([classic, serverless], current_user=None)
assert result[0].id == "s1"
assert result[1].id == "c1"

def test_serverless_before_user_owned(self):
"""Serverless should be preferred over user-owned classic."""
classic_owned = _make_warehouse("c1", "My WH", State.RUNNING,
creator_name="me@example.com")
serverless_other = _make_warehouse("s1", "Other WH", State.RUNNING,
creator_name="other@example.com",
enable_serverless_compute=True)
result = _sort_within_tier([classic_owned, serverless_other],
current_user="me@example.com")
classic_owned = _make_warehouse("c1", "My WH", State.RUNNING, creator_name="me@example.com")
serverless_other = _make_warehouse(
"s1", "Other WH", State.RUNNING, creator_name="other@example.com", enable_serverless_compute=True
)
result = _sort_within_tier([classic_owned, serverless_other], current_user="me@example.com")
assert result[0].id == "s1"

def test_serverless_user_owned_first(self):
"""Among serverless, user-owned should come first."""
serverless_other = _make_warehouse("s1", "Other Serverless", State.RUNNING,
creator_name="other@example.com",
enable_serverless_compute=True)
serverless_owned = _make_warehouse("s2", "My Serverless", State.RUNNING,
creator_name="me@example.com",
enable_serverless_compute=True)
result = _sort_within_tier([serverless_other, serverless_owned],
current_user="me@example.com")
serverless_other = _make_warehouse(
"s1", "Other Serverless", State.RUNNING, creator_name="other@example.com", enable_serverless_compute=True
)
serverless_owned = _make_warehouse(
"s2", "My Serverless", State.RUNNING, creator_name="me@example.com", enable_serverless_compute=True
)
result = _sort_within_tier([serverless_other, serverless_owned], current_user="me@example.com")
assert result[0].id == "s2"
assert result[1].id == "s1"

Expand All @@ -177,53 +172,46 @@ def test_empty_list(self):
def test_no_current_user(self):
"""Without a current user, only serverless preference applies."""
classic = _make_warehouse("c1", "Classic", State.RUNNING)
serverless = _make_warehouse("s1", "Serverless", State.RUNNING,
enable_serverless_compute=True)
serverless = _make_warehouse("s1", "Serverless", State.RUNNING, enable_serverless_compute=True)
result = _sort_within_tier([classic, serverless], current_user=None)
assert result[0].id == "s1"


class TestGetBestWarehouseServerless:
"""Tests for serverless preference in get_best_warehouse."""

@mock.patch("databricks_tools_core.sql.warehouse.get_current_username",
return_value="me@example.com")
@mock.patch("databricks_tools_core.sql.warehouse.get_current_username", return_value="me@example.com")
@mock.patch("databricks_tools_core.sql.warehouse.get_workspace_client")
def test_prefers_serverless_within_running_shared(self, mock_client_fn, mock_user):
"""Among running shared warehouses, serverless should be picked."""
classic_shared = _make_warehouse("c1", "Shared WH", State.RUNNING)
serverless_shared = _make_warehouse("s1", "Shared Serverless", State.RUNNING,
enable_serverless_compute=True)
serverless_shared = _make_warehouse("s1", "Shared Serverless", State.RUNNING, enable_serverless_compute=True)
mock_client = mock.Mock()
mock_client.warehouses.list.return_value = [classic_shared, serverless_shared]
mock_client_fn.return_value = mock_client

result = get_best_warehouse()
assert result == "s1"

@mock.patch("databricks_tools_core.sql.warehouse.get_current_username",
return_value="me@example.com")
@mock.patch("databricks_tools_core.sql.warehouse.get_current_username", return_value="me@example.com")
@mock.patch("databricks_tools_core.sql.warehouse.get_workspace_client")
def test_prefers_serverless_within_running_other(self, mock_client_fn, mock_user):
"""Among running non-shared warehouses, serverless should be picked."""
classic = _make_warehouse("c1", "My WH", State.RUNNING)
serverless = _make_warehouse("s1", "Fast WH", State.RUNNING,
enable_serverless_compute=True)
serverless = _make_warehouse("s1", "Fast WH", State.RUNNING, enable_serverless_compute=True)
mock_client = mock.Mock()
mock_client.warehouses.list.return_value = [classic, serverless]
mock_client_fn.return_value = mock_client

result = get_best_warehouse()
assert result == "s1"

@mock.patch("databricks_tools_core.sql.warehouse.get_current_username",
return_value="me@example.com")
@mock.patch("databricks_tools_core.sql.warehouse.get_current_username", return_value="me@example.com")
@mock.patch("databricks_tools_core.sql.warehouse.get_workspace_client")
def test_tier_order_preserved_over_serverless(self, mock_client_fn, mock_user):
"""A running shared classic should still beat a stopped serverless."""
running_shared_classic = _make_warehouse("c1", "Shared WH", State.RUNNING)
stopped_serverless = _make_warehouse("s1", "Fast WH", State.STOPPED,
enable_serverless_compute=True)
stopped_serverless = _make_warehouse("s1", "Fast WH", State.STOPPED, enable_serverless_compute=True)
mock_client = mock.Mock()
mock_client.warehouses.list.return_value = [stopped_serverless, running_shared_classic]
mock_client_fn.return_value = mock_client
Expand Down
61 changes: 55 additions & 6 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ SCOPE="${DEVKIT_SCOPE:-project}"
SCOPE_EXPLICIT=false # Track if --global was explicitly passed
FORCE="${DEVKIT_FORCE:-false}"
IS_UPDATE=false
UPDATE_MODE=false
SILENT="${DEVKIT_SILENT:-false}"
TOOLS="${DEVKIT_TOOLS:-}"
USER_TOOLS=""
Expand Down Expand Up @@ -103,6 +104,7 @@ while [ $# -gt 0 ]; do
--silent) SILENT=true; shift ;;
--tools) USER_TOOLS="$2"; shift 2 ;;
-f|--force) FORCE=true; shift ;;
-u|--update) UPDATE_MODE=true; FORCE=true; shift ;;
-h|--help)
echo "Databricks AI Dev Kit Installer"
echo ""
Expand All @@ -118,6 +120,7 @@ while [ $# -gt 0 ]; do
echo " --silent Silent mode (no output except errors)"
echo " --tools LIST Comma-separated: claude,cursor,copilot,codex,gemini"
echo " -f, --force Force reinstall"
echo " -u, --update Update to latest version using saved install config"
echo " -h, --help Show this help"
echo ""
echo "Environment Variables (alternative to flags):"
Expand Down Expand Up @@ -389,7 +392,6 @@ detect_tools() {
TOOLS=$(echo "$USER_TOOLS" | tr ',' ' ')
return
elif [ -n "$TOOLS" ]; then
# TOOLS env var already set, just normalize it
TOOLS=$(echo "$TOOLS" | tr ',' ' ')
return
fi
Expand Down Expand Up @@ -1011,6 +1013,41 @@ write_mcp_configs() {
done
}

# Save install config for future --update runs
save_config() {
local config_file="$INSTALL_DIR/install.conf"
cat > "$config_file" <<CONF
# Saved by ai-dev-kit installer — used by --update flag
SAVED_TOOLS="$TOOLS"
SAVED_SCOPE="$SCOPE"
SAVED_PROFILE="$PROFILE"
SAVED_BASE_DIR="${1:-}"
CONF
if [ "$SCOPE" = "project" ]; then
mkdir -p ".ai-dev-kit"
cp "$config_file" ".ai-dev-kit/install.conf"
fi
}

# Load saved config for --update mode
load_config() {
local config_file="$INSTALL_DIR/install.conf"
[ -f ".ai-dev-kit/install.conf" ] && config_file=".ai-dev-kit/install.conf"

if [ ! -f "$config_file" ]; then
die "No saved config found at $config_file. Run a full install first, then use --update."
fi

# shellcheck disable=SC1090
source "$config_file"
[ -n "${SAVED_TOOLS:-}" ] && TOOLS="$SAVED_TOOLS"
[ -n "${SAVED_SCOPE:-}" ] && SCOPE="$SAVED_SCOPE"
[ -n "${SAVED_PROFILE:-}" ] && PROFILE="$SAVED_PROFILE"
[ -n "${SAVED_BASE_DIR:-}" ] && UPDATE_BASE_DIR="$SAVED_BASE_DIR"
SILENT=true
msg "${B}Update mode:${N} reusing saved config (tools=$TOOLS, scope=$SCOPE, profile=$PROFILE)"
}

# Save version
save_version() {
# Use -f to fail on HTTP errors (like 404)
Expand Down Expand Up @@ -1181,6 +1218,11 @@ main() {
echo "────────────────────────────────"
fi

# Load saved config if --update mode
if [ "$UPDATE_MODE" = true ]; then
load_config
fi

# Check dependencies
step "Checking prerequisites"
check_deps
Expand Down Expand Up @@ -1234,9 +1276,15 @@ main() {
# ── Step 6: Version check (may exit early if up to date) ──
check_version

# Determine base directory
# Determine base directory (use saved path in update mode for project-scoped installs)
local base_dir
[ "$SCOPE" = "global" ] && base_dir="$HOME" || base_dir="$(pwd)"
if [ -n "${UPDATE_BASE_DIR:-}" ]; then
base_dir="$UPDATE_BASE_DIR"
elif [ "$SCOPE" = "global" ]; then
base_dir="$HOME"
else
base_dir="$(pwd)"
fi

# Setup MCP server
if [ "$INSTALL_MCP" = true ]; then
Expand All @@ -1263,11 +1311,12 @@ main() {
# Write MCP configs
[ "$INSTALL_MCP" = true ] && write_mcp_configs "$base_dir"

# Save version
# Save version and install config
save_version
save_config "$base_dir"

# Prompt to run auth
prompt_auth
# Prompt to run auth (skip in update mode)
[ "$UPDATE_MODE" != true ] && prompt_auth

# Done
summary
Expand Down