Skip to content
Merged
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
38 changes: 23 additions & 15 deletions devlaunch/completions/dl.bash
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@ _dl_completion() {
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"

# Command options
opts="--ls --repos --stop --rm --code --status --recreate --reset --install --help"
# Global command options (only valid as first arg)
local global_opts="--ls --install --help -h --version"

# Flag completion
if [[ ${cur} == -* ]]; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# Workspace subcommands
local ws_cmds="stop rm code restart recreate reset --"

# Cache file location (honors XDG_CACHE_HOME)
local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/dl"
Expand All @@ -28,16 +25,14 @@ _dl_completion() {
source "$cache_file"
fi

# Commands that need workspace completion
if [[ "$prev" == "--stop" || "$prev" == "--rm" || "$prev" == "--code" || "$prev" == "--status" || "$prev" == "--recreate" || "$prev" == "--reset" ]]; then
if [[ -n "$DL_WORKSPACES" ]]; then
COMPREPLY=( $(compgen -W "${DL_WORKSPACES}" -- ${cur}) )
# First argument: global flags, workspaces, repos, owners, or paths
if [[ ${COMP_CWORD} -eq 1 ]]; then
# Global flags
if [[ ${cur} == -* ]]; then
COMPREPLY=( $(compgen -W "${global_opts}" -- ${cur}) )
return 0
fi
return 0
fi

# First positional argument: workspace, owner/repo, or path
if [[ ${COMP_CWORD} -eq 1 ]]; then
# Don't add space after completion to allow @branch suffix
compopt -o nospace

Expand Down Expand Up @@ -71,6 +66,19 @@ _dl_completion() {
return 0
fi

# Second argument (after workspace): subcommands
if [[ ${COMP_CWORD} -eq 2 ]]; then
# Don't complete after global flags
local first="${COMP_WORDS[1]}"
if [[ "$first" == --* ]]; then
return 0
fi

COMPREPLY=( $(compgen -W "${ws_cmds}" -- ${cur}) )
return 0
fi

# After "--": no completion (user types shell command)
return 0
}

Expand Down
222 changes: 109 additions & 113 deletions devlaunch/dl.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,21 @@
import os
import pathlib
import re
from importlib.metadata import version as pkg_version, PackageNotFoundError
from typing import List, Optional, Dict, Any
from dataclasses import dataclass

from .completion import install_completions


def get_version() -> str:
"""Get the package version."""
try:
return pkg_version("devlaunch")
except PackageNotFoundError:
return "unknown"


logging.basicConfig(level=logging.INFO, format="%(message)s")


Expand Down Expand Up @@ -485,32 +495,41 @@
help_text = """dl - DevLaunch CLI

Usage:
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
--stop <workspace> Stop a workspace
--rm <workspace> Delete a workspace
--code <workspace> Open workspace in VS Code
--status <workspace> Show workspace status
--recreate <workspace> Recreate workspace container
--reset <workspace> Reset workspace (clean slate)
--install Install shell completions
--help, -h Show this help
dl Interactive workspace selector (fzf)
dl <user/repo> Start workspace and attach shell
dl <user/repo> <cmd> Run workspace command (stop, code, etc.)
dl <user/repo> -- <shell> Run shell command in workspace

Workspace sources:
dl myproject Existing workspace by name
dl user/repo Create from GitHub repo
dl user/repo@branch Create from specific branch
dl ./path Create from local path

Workspace commands:
dl <user/repo> stop Stop the workspace
dl <user/repo> rm, prune Delete the workspace
dl <user/repo> code Open in VS Code
dl <user/repo> restart Stop and start (no rebuild)
dl <user/repo> recreate Recreate container
dl <user/repo> reset Clean slate (remove all, recreate)
dl <user/repo> -- <command> Run shell command in workspace

Global commands:
dl --ls List all workspaces
dl --install Install shell completions
dl --help, -h Show this help
dl --version Show version

Examples:
dl # Select workspace with fzf
dl myproject # Open existing workspace
dl loft-sh/devpod # Create from GitHub
dl blooop/devlaunch@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
dl # Select workspace with fzf
dl devpod # Open existing workspace
dl loft-sh/devpod # Create from GitHub
dl blooop/devlaunch@main # Create from specific branch
dl ./my-project # Create from local folder
dl blooop/devlaunch code # Open in VS Code
dl blooop/devlaunch -- make test # Run command in workspace
dl blooop/devlaunch stop # Stop workspace
"""
print(help_text)

Expand All @@ -519,20 +538,24 @@
"""Main entry point for dl CLI."""
args = sys.argv[1:]

# Handle help
if not args or (len(args) == 1 and args[0] in ("--help", "-h")):
if not args:
# No args - try fzf selection
selected = fuzzy_select_workspace()
if not selected:
print_help()
return 1
workspace_up(selected)
return workspace_ssh(selected)
# No args - try fzf selection
if not args:
selected = fuzzy_select_workspace()
if not selected:
print_help()
return 1
workspace_up(selected)
return workspace_ssh(selected)

Check warning on line 548 in devlaunch/dl.py

View check run for this annotation

Codecov / codecov/patch

devlaunch/dl.py#L543-L548

Added lines #L543 - L548 were not covered by tests

# Global commands (no workspace required)
if args[0] in ("--help", "-h"):
print_help()
return 0

# Handle flags
if args[0] == "--version":
print(f"dl {get_version()}")
return 0

if args[0] == "--ls":
print_workspaces()
return 0
Expand Down Expand Up @@ -572,80 +595,9 @@
update_completion_cache()
return install_completions(rc_path)

if args[0] == "--stop":
if len(args) < 2:
workspace = fuzzy_select_workspace()
if not workspace:
logging.error("Usage: dl --stop <workspace>")
return 1
else:
workspace = args[1]
return workspace_stop(workspace)

if args[0] == "--rm":
if len(args) < 2:
workspace = fuzzy_select_workspace()
if not workspace:
logging.error("Usage: dl --rm <workspace>")
return 1
else:
workspace = args[1]
return workspace_delete(workspace)

if args[0] == "--status":
if len(args) < 2:
workspace = fuzzy_select_workspace()
if not workspace:
logging.error("Usage: dl --status <workspace>")
return 1
else:
workspace = args[1]
return workspace_status(workspace)

if args[0] == "--code":
if len(args) < 2:
workspace = fuzzy_select_workspace()
if not workspace:
logging.error("Usage: dl --code <workspace>")
return 1
else:
workspace = args[1]
result = workspace_up(workspace, ide="vscode")
return result.returncode

if args[0] == "--recreate":
if len(args) < 2:
workspace = fuzzy_select_workspace()
if not workspace:
logging.error("Usage: dl --recreate <workspace>")
return 1
else:
workspace = args[1]
workspace_spec = expand_workspace_spec(workspace)
workspace_id = spec_to_workspace_id(workspace)
result = workspace_up(workspace_spec, recreate=True)
if result.returncode != 0:
return result.returncode
return workspace_ssh(workspace_id)

if args[0] == "--reset":
if len(args) < 2:
workspace = fuzzy_select_workspace()
if not workspace:
logging.error("Usage: dl --reset <workspace>")
return 1
else:
workspace = args[1]
workspace_spec = expand_workspace_spec(workspace)
workspace_id = spec_to_workspace_id(workspace)
result = workspace_up(workspace_spec, reset=True)
if result.returncode != 0:
return result.returncode
return workspace_ssh(workspace_id)

# Default: workspace name and optional command
# Workspace commands: dl <workspace> [subcommand] [-- command]
raw_spec = args[0]
command = " ".join(args[1:]) if len(args) > 1 else None
subcommand = args[1] if len(args) > 1 else None

# Validate the workspace spec
existing_ids = get_workspace_ids()
Expand All @@ -654,22 +606,66 @@
logging.error(error)
return 1

# Use raw spec as-is if it's an existing workspace ID, otherwise expand
# This prevents owner/repo-style workspace IDs from being rewritten
# Resolve workspace spec and ID
if raw_spec in existing_ids:
workspace_spec = raw_spec
workspace_id = raw_spec
else:
workspace_spec = expand_workspace_spec(raw_spec)
workspace_id = spec_to_workspace_id(raw_spec)

# Start the workspace
# Handle workspace subcommands
if subcommand == "stop":
return workspace_stop(workspace_id)

if subcommand in ("rm", "prune"):
return workspace_delete(workspace_id)

if subcommand == "code":
result = workspace_up(workspace_spec, ide="vscode")
return result.returncode

if subcommand == "recreate":
result = workspace_up(workspace_spec, recreate=True)
if result.returncode != 0:
return result.returncode

Check warning on line 631 in devlaunch/dl.py

View check run for this annotation

Codecov / codecov/patch

devlaunch/dl.py#L631

Added line #L631 was not covered by tests
return workspace_ssh(workspace_id)

if subcommand == "restart":
# Stop and start without rebuilding
stop_ret = workspace_stop(workspace_id)
if stop_ret != 0:
return stop_ret

Check warning on line 638 in devlaunch/dl.py

View check run for this annotation

Codecov / codecov/patch

devlaunch/dl.py#L638

Added line #L638 was not covered by tests
result = workspace_up(workspace_spec)
if result.returncode != 0:
return result.returncode

Check warning on line 641 in devlaunch/dl.py

View check run for this annotation

Codecov / codecov/patch

devlaunch/dl.py#L641

Added line #L641 was not covered by tests
return workspace_ssh(workspace_id)
Comment thread
sourcery-ai[bot] marked this conversation as resolved.

if subcommand == "reset":
# Clean slate - remove everything and recreate
result = workspace_up(workspace_spec, reset=True)
if result.returncode != 0:
return result.returncode

Check warning on line 648 in devlaunch/dl.py

View check run for this annotation

Codecov / codecov/patch

devlaunch/dl.py#L648

Added line #L648 was not covered by tests
return workspace_ssh(workspace_id)

# Check for shell command (after --)
shell_command = None
if subcommand == "--" and len(args) > 2:
shell_command = " ".join(args[2:])
elif subcommand is not None and subcommand != "--":
# Unknown subcommand - treat as error
logging.error(
f"Unknown command '{subcommand}'. Use 'dl {raw_spec} -- {subcommand}' to run a shell command."
)
return 1

# Default: start workspace and attach shell
result = workspace_up(workspace_spec)
if result.returncode != 0:
return result.returncode

# Attach to workspace using the ID (not the full spec)
ret = workspace_ssh(workspace_id, command)
# Attach to workspace
ret = workspace_ssh(workspace_id, shell_command)

# Update cache in background after workspace operations
update_cache_background()
Expand Down
4 changes: 2 additions & 2 deletions pixi.lock

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

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "devlaunch"
version = "0.0.1"
version = "0.0.2"
authors = [{ name = "Austin Gregg-Smith", email = "blooop@gmail.com" }]
description = "DevLaunch - A streamlined CLI for devpod workspaces"
readme = "README.md"
Expand Down Expand Up @@ -109,7 +109,7 @@ extension-pkg-whitelist = ["numpy"]
jobs = 16 #detect number of cores

[tool.pylint.'MESSAGES CONTROL']
disable = "C,logging-fstring-interpolation,line-too-long,fixme,broad-exception-caught,missing-module-docstring,too-many-instance-attributes,too-few-public-methods,too-many-arguments,too-many-locals,too-many-branches,too-many-statements,too-many-return-statements,use-dict-literal,cyclic-import,duplicate-code,too-many-public-methods"
disable = "C,logging-fstring-interpolation,line-too-long,fixme,broad-exception-caught,missing-module-docstring,too-many-instance-attributes,too-few-public-methods,too-many-arguments,too-many-positional-arguments,too-many-locals,too-many-branches,too-many-statements,too-many-return-statements,use-dict-literal,cyclic-import,duplicate-code,too-many-public-methods"
enable = "no-else-return,consider-using-in"

[tool.coverage.run]
Expand Down
Loading