Skip to content

feat: Add dl CLI - DevLaunch devpod wrapper - #2

Merged
blooop merged 7 commits into
mainfrom
feature/devlaunch
Jan 17, 2026
Merged

feat: Add dl CLI - DevLaunch devpod wrapper#2
blooop merged 7 commits into
mainfrom
feature/devlaunch

Conversation

@blooop

@blooop blooop commented Jan 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • Port dl CLI from rockerc to standalone devlaunch package
  • Add fzf-based fuzzy workspace selection
  • Add smart tab completion for workspaces, owner/repo format, and paths
  • Add GitHub shorthand expansion (owner/repo -> github.com/owner/repo)
  • Add branch support with owner/repo@branch syntax
  • Add background cache updates for fast (~3ms) autocomplete
  • Add shell completion installation via dl --install

Commands

Command Description
dl Interactive workspace selector (fzf)
dl <workspace> Start and attach to workspace
dl owner/repo Create from GitHub
dl --ls List workspaces
dl --stop Stop workspace
dl --rm Delete workspace
dl --code Open in VS Code
dl --install Install completions

Test plan

  • All 61 tests pass
  • CI passes (format, lint, coverage)
  • Install with pixi install and verify dl command works
  • Verify dl --install sets up shell completions

🤖 Generated with Claude Code

Summary by Sourcery

Introduce the dl DevLaunch CLI as a devpod-focused workspace wrapper with fuzzy selection, smart GitHub/path handling, and shell autocompletion.

New Features:

  • Add dl command-line interface for managing devpod workspaces, including starting, attaching, stopping, deleting, and opening workspaces in VS Code.
  • Support workspace creation from local paths and GitHub repositories using owner/repo and owner/repo@branch syntax with automatic URL expansion.
  • Provide fzf-based interactive workspace selection when dl is invoked without arguments.
  • Expose a dl console script entry point via the package configuration.

Enhancements:

  • Revise the README to describe DevLaunch as a devpod-centric CLI, documenting installation methods, core usage patterns, commands, and feature highlights.
  • Add shell completion installation and management for dl, including a dedicated bash completion script and installer that updates shell rc files.
  • Implement a caching mechanism and helpers to speed up shell completion by reusing workspace and repository metadata.
  • Expand the example script to demonstrate the new workspace spec helpers from the dl module.

Build:

  • Update pyproject.toml metadata to reflect the new CLI purpose, add the iterfzf dependency, register the dl console script, and adjust linting configuration for the expanded codebase.

Documentation:

  • Rewrite the README from a generic Python template description to focused documentation for the DevLaunch dl CLI, including installation, usage examples, features, and development workflows.

Tests:

  • Add an extensive test suite for the dl module covering workspace spec parsing, owner/repo URL handling, workspace discovery, and workspace listing utilities, replacing the old basic class tests.

Port the dl CLI from rockerc to standalone devlaunch package:
- dl command with fzf fuzzy workspace selection
- Smart tab completion for workspaces, owner/repo, and paths
- GitHub shorthand expansion (owner/repo -> github.com/owner/repo)
- Branch support with owner/repo@branch syntax
- Background cache updates for fast (~3ms) autocomplete
- Shell completion installation via dl --install

Commands: --ls, --stop, --rm, --code, --status, --recreate, --reset

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Jan 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements a new dl CLI around devpod, including workspace selection, GitHub shorthand handling, and shell autocompletion with a background-populated cache, and wires it into packaging/README while removing the old template scaffolding.

Sequence diagram for dl workspace startup and validation

sequenceDiagram
    actor User
    participant dl_CLI as dl_main
    participant devpod as devpod_binary
    participant FS as Filesystem_cache

    User->>dl_CLI: dl raw_spec [command]
    dl_CLI->>dl_CLI: get_workspace_ids()
    dl_CLI->>devpod: devpod list --output json
    devpod-->>dl_CLI: JSON workspace list
    dl_CLI->>dl_CLI: validate_workspace_spec(raw_spec, existing_ids)
    alt spec_invalid
        dl_CLI-->>User: log error Unknown workspace
        %% return
    else spec_valid
        dl_CLI->>dl_CLI: expand_workspace_spec(raw_spec)
        dl_CLI->>devpod: devpod up workspace
        devpod-->>dl_CLI: exit code
        alt up_failed
            dl_CLI-->>User: return error code
            %% return
        else up_ok
            dl_CLI->>devpod: devpod ssh workspace [--command command]
            devpod-->>dl_CLI: ssh exit code
            dl_CLI->>dl_CLI: update_cache_background()
            dl_CLI->>FS: spawn devlaunch.dl --update-cache
            FS-->>dl_CLI: cache updated asynchronously
            dl_CLI-->>User: exit
        end
    end
Loading

Sequence diagram for dl completion cache and shell completion

sequenceDiagram
    actor User
    participant Shell as Bash_shell
    participant Completion as dl_completion_function
    participant dl_CLI as dl_main
    participant FS as Filesystem_cache
    participant devpod as devpod_binary

    User->>dl_CLI: dl --install
    dl_CLI->>Completion: install_completions(rc_path)
    Completion->>FS: write completions.sh
    Completion->>FS: update shell rc with source completions.sh block
    Completion-->>User: instructions to source rc file

    loop Every_tab_press_for_dl
        User->>Shell: press Tab after dl ...
        Shell->>Completion: _dl_completion
        Completion->>FS: read ~/.cache/dl/completions.json
        alt cache_exists
            FS-->>Completion: workspaces, repos, owners
            Completion-->>Shell: COMPREPLY from cached data
        else cache_missing
            FS-->>Completion: cache not found
            Completion-->>Shell: limited or no completions
        end
    end

    par Background_cache_refresh
        dl_CLI->>dl_CLI: update_cache_background()
        dl_CLI->>dl_CLI: spawn dl --update-cache
        dl_CLI->>devpod: devpod list --output json
        devpod-->>dl_CLI: JSON workspaces
        dl_CLI->>dl_CLI: discover_repos_from_workspaces()
        dl_CLI->>FS: write_completion_cache(workspaces, repos, owners)
    end
Loading

Class diagram for new devlaunch CLI and completion modules

classDiagram
    class Workspace {
        +str id
        +str source_type
        +str source
        +str last_used
        +str provider
        +str ide
        +from_json(data Dict)
    }

    class dl_module {
        +CACHE_DIR : pathlib_Path
        +CACHE_FILE : pathlib_Path
        +get_cache_path() pathlib_Path
        +read_completion_cache() Dict
        +write_completion_cache(data Dict) void
        +update_completion_cache() Dict
        +update_cache_background() void
        +is_path_spec(spec str) bool
        +is_git_spec(spec str) bool
        +expand_workspace_spec(spec str) str
        +validate_workspace_spec(spec str, existing_ids List~str~) Optional~str~
        +parse_owner_repo_from_url(url str) Optional~tuple~
        +get_git_remote_url(path str) Optional~str~
        +get_git_branches(path str) List~str~
        +discover_repos_from_workspaces(workspaces List~Workspace~) Dict
        +get_known_repos() List~str~
        +run_devpod(args List~str~, capture bool) CompletedProcess
        +list_workspaces() List~Workspace~
        +get_workspace_ids() List~str~
        +print_workspaces() void
        +fuzzy_select_workspace() Optional~str~
        +workspace_up(workspace str, ide Optional~str~, recreate bool, reset bool)
        +workspace_ssh(workspace str, command Optional~str~) int
        +workspace_stop(workspace str) int
        +workspace_delete(workspace str) int
        +workspace_status(workspace str) int
        +print_help() void
        +main() int
    }

    class completion_module {
        +_RC_BLOCK_START : str
        +_RC_BLOCK_END : str
        +_LEGACY_BLOCKS : dict
        +_LEGACY_SINGLE_LINES : set
        +_completion_file_path() pathlib_Path
        +install_completions(rc_path Optional~pathlib_Path~) int
    }

    class completion_loader_module {
        +load_completion_script(name str) str
    }

    class dl_bash_completion_script {
        +_dl_completion() Bash_function
    }

    dl_module --> Workspace : uses
    dl_module --> completion_module : calls_install_completions
    completion_module --> completion_loader_module : load_completion_script
    completion_module --> dl_bash_completion_script : writes_to_file
Loading

File-Level Changes

Change Details Files
Introduce dl CLI for devpod workspace management with workspace listing, fuzzy selection, GitHub owner/repo handling, and workspace lifecycle commands.
  • Add devlaunch.dl module implementing CLI entrypoint, argument parsing, and main command dispatch for dl.
  • Implement workspace abstraction and devpod integration (list, up, ssh, stop, delete, status).
  • Add helpers to classify/expand workspace specs (path vs git vs existing workspace) and validate specs before use.
  • Add fzf-based interactive selector via iterfzf and error-handling/logging around devpod and fzf invocations.
  • Wire dl as a console script entry point in pyproject.toml and add iterfzf dependency.
devlaunch/dl.py
pyproject.toml
Add autocompletion support for the dl CLI, including installation into shell rc files and cached completion data for fast suggestions.
  • Add completion installer that writes a generated completion script to a config path and injects/removes managed blocks in the user shell rc file, cleaning up legacy markers.
  • Embed a bash completion script for dl that uses a JSON cache plus jq to complete flags, workspaces, owner/repo shorthands, and paths.
  • Add loader utilities to read the embedded completion script as a package resource.
  • Implement a JSON completion cache in dl (with background updater and helpers) used by both the CLI and completion script.
devlaunch/completion.py
devlaunch/completions/dl.bash
devlaunch/completion_loader.py
devlaunch/dl.py
Extend tests to cover dl CLI helper logic, workspace parsing, and repo discovery while removing old template tests.
  • Add unit tests for spec parsing/validation, regex matching, workspace parsing, devpod-list handling, repo discovery, and known-repos derivation.
  • Use unittest.mock to stub devpod invocations and git helpers so tests run without devpod/git state.
  • Remove legacy BasicClass implementation and its tests that are no longer relevant.
test/test_dl.py
devlaunch/basic_class.py
test/test_basic.py
Rewrite project documentation and metadata to describe DevLaunch as a devpod CLI instead of a generic Python template, including installation, usage, and development workflow for dl.
  • Replace README template content with dl-focused description, install instructions, example commands, feature overview, and development commands based on pixi.
  • Update project metadata (version reset, description, scripts) and pylint config tweaks to accommodate new CLI structure.
  • Ensure packaging includes the new modules and console script while keeping hatch/pixi config intact.
README.md
pyproject.toml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 security issues, 2 other issues, and left some high level feedback:

Security issues:

  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)

General comments:

  • The dl.py module has grown quite large and mixes CLI argument handling, devpod orchestration, caching, and parsing logic; consider splitting it into smaller modules (e.g., cli, workspaces, cache) to keep responsibilities clearer and make future changes easier.
  • The Bash completion script relies on jq being installed and silently degrades if it is missing; you might want to either fall back to a simpler completion path in pure Bash/Python or detect the absence of jq and print a clear hint to the user during dl --install.
  • The completion cache is hardcoded to $HOME/.cache/dl/completions.json; consider honoring XDG_CACHE_HOME (falling back to ~/.cache) so the cache location behaves more predictably across environments that follow the XDG spec.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `dl.py` module has grown quite large and mixes CLI argument handling, devpod orchestration, caching, and parsing logic; consider splitting it into smaller modules (e.g., `cli`, `workspaces`, `cache`) to keep responsibilities clearer and make future changes easier.
- The Bash completion script relies on `jq` being installed and silently degrades if it is missing; you might want to either fall back to a simpler completion path in pure Bash/Python or detect the absence of `jq` and print a clear hint to the user during `dl --install`.
- The completion cache is hardcoded to `$HOME/.cache/dl/completions.json`; consider honoring `XDG_CACHE_HOME` (falling back to `~/.cache`) so the cache location behaves more predictably across environments that follow the XDG spec.

## Individual Comments

### Comment 1
<location> `devlaunch/dl.py:578-584` </location>
<code_context>
+    command = " ".join(args[1:]) if len(args) > 1 else None
+
+    # Validate the workspace spec
+    existing_ids = get_workspace_ids()
+    error = validate_workspace_spec(raw_spec, existing_ids)
+    if error:
+        logging.error(error)
+        return 1
+
+    workspace = expand_workspace_spec(raw_spec)
+
+    # Start the workspace
</code_context>

<issue_to_address>
**issue (bug_risk):** Avoid expanding specs that are already known workspace IDs to prevent accidental rewriting of workspace names.

`validate_workspace_spec` currently allows `spec` values that are in `existing_ids`, but we still pass those through `expand_workspace_spec`. If an existing workspace ID happens to match `OWNER_REPO_PATTERN` (e.g. `owner/repo`), `expand_workspace_spec` will rewrite it to `github.com/owner/repo`, which likely won’t match the real workspace ID. Instead, treat `spec in existing_ids` as a terminal case (use the raw ID and skip expansion), and only call `expand_workspace_spec` for specs that represent new git/path sources.
</issue_to_address>

### Comment 2
<location> `devlaunch/completions/dl.bash:25-28` </location>
<code_context>
+    local known_repos=""
+    local owners=""
+
+    if [[ -f "$cache_file" ]]; then
+        workspaces=$(jq -r '.workspaces[]?' "$cache_file" 2>/dev/null | tr '\n' ' ')
+        known_repos=$(jq -r '.repos[]?' "$cache_file" 2>/dev/null | tr '\n' ' ')
+        owners=$(jq -r '.owners[]?' "$cache_file" 2>/dev/null | tr '\n' ' ')
+    fi
+
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Bash completion has a hard dependency on jq, which may not be installed on target systems.

This shells out to `jq` on every completion and fails silently to an empty result when `jq` isn’t installed, making completion appear broken and adding a hidden dependency.

Since you already expose `dl --completion-data` / `--repos`, consider using that output (or a completion-specific subcommand) and parsing it in bash instead, or at least falling back to that behavior when `jq` is not available. That would remove the hard `jq` dependency and keep completions aligned with the Python CLI.

Suggested implementation:

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

    # Read from cache (fast path) or fall back to CLI (no hard jq dependency)
    local workspaces=""
    local known_repos=""
    local owners=""

    if command -v jq >/dev/null 2>&1 && [[ -f "$cache_file" ]]; then
        # Fast path: use cached JSON if jq is available
        workspaces=$(jq -r '.workspaces[]?' "$cache_file" 2>/dev/null | tr '\n' ' ')
        known_repos=$(jq -r '.repos[]?' "$cache_file" 2>/dev/null | tr '\n' ' ')
        owners=$(jq -r '.owners[]?' "$cache_file" 2>/dev/null | tr '\n' ' ')
    else
        # Fallback: derive values directly from the CLI so completions still work
        if command -v dl >/dev/null 2>&1; then
            # Use `dl --repos` as a source of truth; this keeps completions aligned
            # with the Python CLI without requiring jq.
            local _dl_repos
            _dl_repos=$(dl --repos 2>/dev/null || true)

            if [[ -n "${_dl_repos}" ]]; then
                # Repos: space-separated list
                known_repos=$(printf '%s\n' "${_dl_repos}" | tr '\n' ' ')

                # Owners: unique owner part before the first '/'
                owners=$(printf '%s\n' "${_dl_repos}" \
                    | awk -F'/' 'NF >= 1 { print $1 }' \
                    | sort -u \
                    | tr '\n' ' ')
            fi
            unset _dl_repos
        fi
    fi

    # Command options

```

1. If `dl --completion-data` (or a more completion-friendly subcommand) is available and has a stable format, you may want to replace the `dl --repos` fallback with parsing of `dl --completion-data` to also populate `workspaces`. That would look similar to the current `jq` path but using pure bash parsing of the `dl` output format.
2. Ensure any later code that relies on `workspaces`, `known_repos`, or `owners` can tolerate them being empty (e.g., when `dl` is not installed or returns no repos).
3. If the CLI guarantees a different format for `--repos` (e.g., JSON instead of plain `owner/repo` lines), adjust the fallback parsing accordingly (e.g., `dl --repos --plain` or using appropriate flags that already exist in your CLI).
</issue_to_address>

### Comment 3
<location> `devlaunch/dl.py:296` </location>
<code_context>
        return subprocess.run(cmd, capture_output=True, text=True, check=False)
</code_context>

<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

### Comment 4
<location> `devlaunch/dl.py:297` </location>
<code_context>
    return subprocess.run(cmd, check=False)
</code_context>

<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread devlaunch/dl.py Outdated
Comment thread devlaunch/completions/dl.bash Outdated
- Skip workspace spec expansion for existing workspace IDs to prevent
  owner/repo-style IDs from being incorrectly rewritten
- Add jq fallback in bash completion - falls back to dl CLI when jq is
  not available
- Honor XDG_CACHE_HOME for cache directory location in both Python and
  bash completion
- Add security note documenting why subprocess.run with list args is
  safe from command injection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New security issues found

Comment thread devlaunch/dl.py
Comment thread devlaunch/dl.py
@codecov

codecov Bot commented Jan 17, 2026

Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

Thanks for integrating Codecov - We've got you covered ☂️

blooop and others added 5 commits January 17, 2026 15:36
The static analysis warnings are false positives since subprocess.run
is called with a list (not shell=True), which prevents command injection.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add write_bash_completion_cache() that writes a sourceable bash file
- Completion script now sources ~/.cache/dl/completions.bash directly
- No external dependencies (jq) needed for tab completion
- Keep JSON cache for --completion-data programmatic access

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The cache file wasn't being created until workspace operations were run,
causing tab completion to not work immediately after install.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Avoid multiline f-string that formats differently between ruff versions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Devpod creates workspaces with just the repo name (e.g., colcon-runner),
not the full spec (e.g., blooop/colcon-runner). Added spec_to_workspace_id()
to derive the correct workspace ID for ssh operations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant