feat: Add dl CLI - DevLaunch devpod wrapper - #2
Merged
Merged
Conversation
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>
Reviewer's GuideImplements a new Sequence diagram for dl workspace startup and validationsequenceDiagram
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
Sequence diagram for dl completion cache and shell completionsequenceDiagram
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
Class diagram for new devlaunch CLI and completion modulesclassDiagram
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
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
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.pymodule 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
jqbeing 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 ofjqand print a clear hint to the user duringdl --install. - The completion cache is hardcoded to
$HOME/.cache/dl/completions.json; consider honoringXDG_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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
- 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>
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 ☂️ |
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>
blooop
force-pushed
the
feature/devlaunch
branch
from
January 17, 2026 16:14
b2cdcdc to
42f5c96
Compare
This was referenced Aug 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
dlCLI from rockerc to standalone devlaunch packageowner/repo->github.com/owner/repo)owner/repo@branchsyntaxdl --installCommands
dldl <workspace>dl owner/repodl --lsdl --stopdl --rmdl --codedl --installTest plan
pixi installand verifydlcommand worksdl --installsets up shell completions🤖 Generated with Claude Code
Summary by Sourcery
Introduce the
dlDevLaunch CLI as a devpod-focused workspace wrapper with fuzzy selection, smart GitHub/path handling, and shell autocompletion.New Features:
dlcommand-line interface for managing devpod workspaces, including starting, attaching, stopping, deleting, and opening workspaces in VS Code.owner/repoandowner/repo@branchsyntax with automatic URL expansion.dlis invoked without arguments.dlconsole script entry point via the package configuration.Enhancements:
dl, including a dedicated bash completion script and installer that updates shell rc files.dlmodule.Build:
pyproject.tomlmetadata to reflect the new CLI purpose, add theiterfzfdependency, register thedlconsole script, and adjust linting configuration for the expanded codebase.Documentation:
dlCLI, including installation, usage examples, features, and development workflows.Tests:
dlmodule covering workspace spec parsing, owner/repo URL handling, workspace discovery, and workspace listing utilities, replacing the old basic class tests.