Skip to content
Closed
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
2 changes: 1 addition & 1 deletion pixi.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ renv = "rockerc.renv:main"
renvvsc = "rockerc.renvsc:main"
rockervsc = "rockerc.rockervsc:main"
aid = "rockerc.aid:main"
dp = "rockerc.dp:main"
dl = "rockerc.dl:main"

# Environments
[tool.pixi.environments]
Expand Down
4 changes: 3 additions & 1 deletion rockerc/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"# rockerc completion": "# end rockerc completion",
"# renv completion": "# end renv completion",
"# aid completion": "# end aid completion",
"# dp completion": "# end dp completion",
_RC_BLOCK_START: _RC_BLOCK_END,
}

Expand All @@ -24,6 +25,7 @@
"complete -F _renv_completion renv",
"complete -F _renv_completion renvvsc",
"complete -F _aid_completion aid",
"complete -F _dp_completion dp",
}


Expand Down Expand Up @@ -52,7 +54,7 @@ def install_all_completions(rc_path: Optional[pathlib.Path] = None) -> int:
_rockerc_bash_completion_script().rstrip(),
load_completion_script("renv").rstrip(),
load_completion_script("aid").rstrip(),
load_completion_script("dp").rstrip(),
load_completion_script("dl").rstrip(),
]
combined_script = "\n\n".join(combined_script_parts) + "\n"
completion_path.write_text(combined_script, encoding="utf-8")
Expand Down
10 changes: 5 additions & 5 deletions rockerc/completions/dp.bash → rockerc/completions/dl.bash
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# dp completion
_dp_completion() {
# dl completion
_dl_completion() {
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
Expand All @@ -15,7 +15,7 @@ _dp_completion() {
fi

# Cache file location
local cache_file="$HOME/.cache/dp/completions.json"
local cache_file="$HOME/.cache/dl/completions.json"

# Read from cache (fast path)
local workspaces=""
Expand Down Expand Up @@ -74,5 +74,5 @@ _dp_completion() {
return 0
}

complete -F _dp_completion dp
# end dp completion
complete -F _dl_completion dl
# end dl completion
74 changes: 37 additions & 37 deletions rockerc/dp.py → rockerc/dl.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
"""
dp - DevPod CLI Wrapper
dl - DevLaunch CLI

A streamlined CLI for devpod with intuitive autocomplete and fzf fuzzy selection.
Provides an renv-like UX for managing devcontainer workspaces.

Usage:
dp # fzf selector for existing workspaces
dp <workspace> # open/create workspace, attach shell
dp <workspace> <command> # run command in workspace
dp owner/repo # create from git repo (github.com)
dp owner/repo@branch # specific branch
dp ./path # create from local path
dp --ls # list workspaces
dp --stop <workspace> # stop workspace
dp --rm <workspace> # delete workspace
dp --code <workspace> # open in VS Code
dp --install # install completions
dl # fzf selector for existing workspaces
dl <workspace> # open/create workspace, attach shell
dl <workspace> <command> # run command in workspace
dl owner/repo # create from git repo (github.com)
dl owner/repo@branch # specific branch
dl ./path # create from local path
dl --ls # list workspaces
dl --stop <workspace> # stop workspace
dl --rm <workspace> # delete workspace
dl --code <workspace> # open in VS Code
dl --install # install completions
"""

import sys
Expand All @@ -32,7 +32,7 @@
logging.basicConfig(level=logging.INFO, format="%(message)s")

# Cache configuration
CACHE_DIR = pathlib.Path.home() / ".cache" / "dp"
CACHE_DIR = pathlib.Path.home() / ".cache" / "dl"
CACHE_FILE = CACHE_DIR / "completions.json"


Expand Down Expand Up @@ -93,7 +93,7 @@
try:
# pylint: disable=consider-using-with
subprocess.Popen(
[sys.executable, "-m", "rockerc.dp", "--update-cache"],
[sys.executable, "-m", "rockerc.dl", "--update-cache"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
Expand Down Expand Up @@ -151,7 +151,7 @@
if is_git_spec(spec):
return None
# Invalid - provide helpful error
return f"Unknown workspace '{spec}'. Use 'dp --ls' to list workspaces, or specify owner/repo or ./path"
return f"Unknown workspace '{spec}'. Use 'dl --ls' to list workspaces, or specify owner/repo or ./path"


@dataclass
Expand Down Expand Up @@ -351,7 +351,7 @@

workspaces = list_workspaces()
if not workspaces:
logging.info("No workspaces found. Create one with: dp owner/repo or dp ./path")
logging.info("No workspaces found. Create one with: dl owner/repo or dl ./path")

Check warning on line 354 in rockerc/dl.py

View check run for this annotation

Codecov / codecov/patch

rockerc/dl.py#L354

Added line #L354 was not covered by tests
return None

# Format options for display: "id | type | source"
Expand Down Expand Up @@ -415,15 +415,15 @@

def print_help():
"""Print usage help."""
help_text = """dp - DevPod CLI Wrapper
help_text = """dl - DevLaunch CLI

Check warning on line 418 in rockerc/dl.py

View check run for this annotation

Codecov / codecov/patch

rockerc/dl.py#L418

Added line #L418 was not covered by tests

Usage:
dp Interactive workspace selector (fzf)
dp <workspace> Start workspace and attach shell
dp <workspace> <command> Run command in workspace
dp owner/repo Create workspace from GitHub repo
dp owner/repo@branch Create workspace from specific branch
dp ./path Create workspace from local path
dl Interactive workspace selector (fzf)
dl <workspace> Start workspace and attach shell
dl <workspace> <command> Run command in workspace
dl owner/repo Create workspace from GitHub repo
dl owner/repo@branch Create workspace from specific branch
dl ./path Create workspace from local path

Commands:
--ls List all workspaces
Expand All @@ -437,19 +437,19 @@
--help, -h Show this help

Examples:
dp # Select workspace with fzf
dp myproject # Open existing workspace
dp loft-sh/devpod # Create from GitHub
dp blooop/rockerc@main # Create from specific branch
dp ./my-project # Create from local folder
dp --code myproject # Open in VS Code
dp myproject 'make test' # Run command in workspace
dl # Select workspace with fzf
dl myproject # Open existing workspace
dl loft-sh/devpod # Create from GitHub
dl blooop/rockerc@main # Create from specific branch
dl ./my-project # Create from local folder
dl --code myproject # Open in VS Code
dl myproject 'make test' # Run command in workspace
"""
print(help_text)


def main() -> int:
"""Main entry point for dp CLI."""
"""Main entry point for dl CLI."""
args = sys.argv[1:]

# Handle help
Expand Down Expand Up @@ -507,7 +507,7 @@
if len(args) < 2:
workspace = fuzzy_select_workspace()
if not workspace:
logging.error("Usage: dp --stop <workspace>")
logging.error("Usage: dl --stop <workspace>")

Check warning on line 510 in rockerc/dl.py

View check run for this annotation

Codecov / codecov/patch

rockerc/dl.py#L510

Added line #L510 was not covered by tests
return 1
else:
workspace = args[1]
Expand All @@ -517,7 +517,7 @@
if len(args) < 2:
workspace = fuzzy_select_workspace()
if not workspace:
logging.error("Usage: dp --rm <workspace>")
logging.error("Usage: dl --rm <workspace>")

Check warning on line 520 in rockerc/dl.py

View check run for this annotation

Codecov / codecov/patch

rockerc/dl.py#L520

Added line #L520 was not covered by tests
return 1
else:
workspace = args[1]
Expand All @@ -527,7 +527,7 @@
if len(args) < 2:
workspace = fuzzy_select_workspace()
if not workspace:
logging.error("Usage: dp --status <workspace>")
logging.error("Usage: dl --status <workspace>")

Check warning on line 530 in rockerc/dl.py

View check run for this annotation

Codecov / codecov/patch

rockerc/dl.py#L530

Added line #L530 was not covered by tests
return 1
else:
workspace = args[1]
Expand All @@ -537,7 +537,7 @@
if len(args) < 2:
workspace = fuzzy_select_workspace()
if not workspace:
logging.error("Usage: dp --code <workspace>")
logging.error("Usage: dl --code <workspace>")

Check warning on line 540 in rockerc/dl.py

View check run for this annotation

Codecov / codecov/patch

rockerc/dl.py#L540

Added line #L540 was not covered by tests
return 1
else:
workspace = args[1]
Expand All @@ -548,7 +548,7 @@
if len(args) < 2:
workspace = fuzzy_select_workspace()
if not workspace:
logging.error("Usage: dp --recreate <workspace>")
logging.error("Usage: dl --recreate <workspace>")

Check warning on line 551 in rockerc/dl.py

View check run for this annotation

Codecov / codecov/patch

rockerc/dl.py#L551

Added line #L551 was not covered by tests
return 1
else:
workspace = args[1]
Expand All @@ -561,7 +561,7 @@
if len(args) < 2:
workspace = fuzzy_select_workspace()
if not workspace:
logging.error("Usage: dp --reset <workspace>")
logging.error("Usage: dl --reset <workspace>")

Check warning on line 564 in rockerc/dl.py

View check run for this annotation

Codecov / codecov/patch

rockerc/dl.py#L564

Added line #L564 was not covered by tests
return 1
else:
workspace = args[1]
Expand Down
26 changes: 13 additions & 13 deletions test/test_dp.py → test/test_dl.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Tests for dp (DevPod CLI Wrapper) functionality."""
"""Tests for dl (DevLaunch CLI) functionality."""

import json
from unittest.mock import patch, MagicMock

from rockerc.dp import (
from rockerc.dl import (
expand_workspace_spec,
is_path_spec,
is_git_spec,
Expand Down Expand Up @@ -263,7 +263,7 @@ def test_from_json_missing_fields(self):
class TestListWorkspaces:
"""Tests for list_workspaces function."""

@patch("rockerc.dp.run_devpod")
@patch("rockerc.dl.run_devpod")
def test_list_workspaces_success(self, mock_run):
"""Test successful workspace listing."""
mock_result = MagicMock()
Expand Down Expand Up @@ -294,7 +294,7 @@ def test_list_workspaces_success(self, mock_run):
assert workspaces[0].id == "ws1"
assert workspaces[1].id == "ws2"

@patch("rockerc.dp.run_devpod")
@patch("rockerc.dl.run_devpod")
def test_list_workspaces_empty(self, mock_run):
"""Test empty workspace list."""
mock_result = MagicMock()
Expand All @@ -305,7 +305,7 @@ def test_list_workspaces_empty(self, mock_run):
workspaces = list_workspaces()
assert workspaces == []

@patch("rockerc.dp.run_devpod")
@patch("rockerc.dl.run_devpod")
def test_list_workspaces_error(self, mock_run):
"""Test handling of devpod error."""
mock_result = MagicMock()
Expand All @@ -316,7 +316,7 @@ def test_list_workspaces_error(self, mock_run):
workspaces = list_workspaces()
assert workspaces == []

@patch("rockerc.dp.run_devpod")
@patch("rockerc.dl.run_devpod")
def test_list_workspaces_invalid_json(self, mock_run):
"""Test handling of invalid JSON output."""
mock_result = MagicMock()
Expand All @@ -331,7 +331,7 @@ def test_list_workspaces_invalid_json(self, mock_run):
class TestGetWorkspaceIds:
"""Tests for get_workspace_ids function."""

@patch("rockerc.dp.list_workspaces")
@patch("rockerc.dl.list_workspaces")
def test_get_workspace_ids(self, mock_list):
"""Test getting workspace IDs."""
mock_list.return_value = [
Expand All @@ -342,7 +342,7 @@ def test_get_workspace_ids(self, mock_list):
ids = get_workspace_ids()
assert ids == ["ws1", "ws2"]

@patch("rockerc.dp.list_workspaces")
@patch("rockerc.dl.list_workspaces")
def test_get_workspace_ids_empty(self, mock_list):
"""Test getting workspace IDs when empty."""
mock_list.return_value = []
Expand Down Expand Up @@ -401,7 +401,7 @@ def test_discover_from_git_workspace(self):
repos = discover_repos_from_workspaces(workspaces)
assert repos == {"owner": ["repo"]}

@patch("rockerc.dp.get_git_remote_url")
@patch("rockerc.dl.get_git_remote_url")
def test_discover_from_local_workspace(self, mock_remote):
"""Test discovering repo from local workspace with git remote."""
mock_remote.return_value = "git@github.com:blooop/python_template.git"
Expand All @@ -411,7 +411,7 @@ def test_discover_from_local_workspace(self, mock_remote):
repos = discover_repos_from_workspaces(workspaces)
assert repos == {"blooop": ["python_template"]}

@patch("rockerc.dp.get_git_remote_url")
@patch("rockerc.dl.get_git_remote_url")
def test_discover_multiple_repos(self, mock_remote):
"""Test discovering multiple repos from different owners."""
mock_remote.side_effect = [
Expand All @@ -427,7 +427,7 @@ def test_discover_multiple_repos(self, mock_remote):
repos = discover_repos_from_workspaces(workspaces)
assert repos == {"owner1": ["repo1", "repo3"], "owner2": ["repo2"]}

@patch("rockerc.dp.get_git_remote_url")
@patch("rockerc.dl.get_git_remote_url")
def test_discover_no_remote(self, mock_remote):
"""Test workspace without git remote is skipped."""
mock_remote.return_value = None
Expand All @@ -441,7 +441,7 @@ def test_discover_no_remote(self, mock_remote):
class TestGetKnownRepos:
"""Tests for get_known_repos function."""

@patch("rockerc.dp.list_workspaces")
@patch("rockerc.dl.list_workspaces")
def test_get_known_repos(self, mock_list):
"""Test getting known repos as sorted list."""
mock_list.return_value = [
Expand All @@ -451,7 +451,7 @@ def test_get_known_repos(self, mock_list):
repos = get_known_repos()
assert repos == ["aowner/arepo", "zowner/zrepo"]

@patch("rockerc.dp.list_workspaces")
@patch("rockerc.dl.list_workspaces")
def test_get_known_repos_empty(self, mock_list):
"""Test getting known repos when no workspaces."""
mock_list.return_value = []
Expand Down