Skip to content

Commit dbfc60c

Browse files
authored
feat: migrate CLI from argparse to click with dynamic shell completion (#1)
* Fix: Add new line before to open i * feat: reorder open args, add settings.json config, fix missing projectpath error - devcode open now takes <projectpath> [template] (template optional) - Auto-detects template from container history (running → stopped → default) - Adds settings.json (~/.config/dev-code/settings.json) replacing DEVCODE_TEMPLATE_PATH - settings.json auto-created with defaults on first run (default_template: dev-code) - DEVCODE_CONF_DIR env var overrides config directory - Errors on non-existent projectpath (bug fix) - Updates shell completion: template completes at position 3 (not 2) * feat: migrate CLI from argparse to click with dynamic shell completion - Replace argparse with click>=8.0 throughout src/devcode.py - Add dynamic shell completion for template names (open, edit commands) - Extract _do_open() helper; inline cmd_* logic into click commands - Remove hand-rolled completion subcommand (_BASH_COMPLETION, _ZSH_COMPLETION) - Update entry point: devcode:main → devcode:cli - Update tests: CliRunner for CLI tests, .callback() for unit tests - Add _complete_templates tests; 184 tests passing * fix: add click to tox deps (skip_install=true bypasses pyproject.toml) * fix: handle missing package metadata for --version in tox skip_install env _get_version() falls back to "(dev)" when the package is not installed, preventing PackageNotFoundError when tox runs with skip_install = true. * Revert "fix: handle missing package metadata for --version in tox skip_install env" This reverts commit 99aed26. * fix: proper tox/pytest setup — remove skip_install, split dependency groups - Remove skip_install=true and manual deps list from tox.ini; use dependency_groups = test so tox installs the package (and its deps) from pyproject.toml properly - Split dependency-groups into test (pytest, pytest-cov) and dev (tox, pyyaml, git-cliff); drop unused pyfiglet - Add pythonpath = ["src"] to pytest config as safety net for direct runs * fix: use os.path.join and os.path.isabs in tests for Windows compatibility Path separator is \ on Windows; hardcoded forward-slash assertions failed on windows-latest CI runner. * ci: only run on push/PR to main
1 parent 3faee49 commit dbfc60c

File tree

13 files changed

+806
-1149
lines changed

13 files changed

+806
-1149
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ name: CI
22

33
on:
44
push:
5-
branches: ["**"]
5+
branches: ["main"]
66
pull_request:
7-
branches: ["**"]
7+
branches: ["main"]
88

99
jobs:
1010
test:
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Sample workflow for building and deploying a Jekyll site to GitHub Pages
2-
name: Deploy Jekyll with GitHub Pages dependencies preinstalled
2+
name: Deploy GitHub Pages
33

44
on:
55
workflow_run:
6-
workflows: ["publish"]
6+
workflows: ["Publish to PyPI"]
77
types: [completed]
88

99
# Allows you to run this workflow manually from the Actions tab
@@ -25,6 +25,7 @@ jobs:
2525
# Build job
2626
build:
2727
runs-on: ubuntu-latest
28+
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
2829
steps:
2930
- name: Checkout
3031
uses: actions/checkout@v4
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Publish
1+
name: Publish to PyPi
22

33
on:
44
push:

.gitignore

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,41 @@ pyrightconfig.json
175175

176176
# End of https://www.toptal.com/developers/gitignore/api/python
177177

178+
# Created by https://www.toptal.com/developers/gitignore/api/macos
179+
# Edit at https://www.toptal.com/developers/gitignore?templates=macos
180+
181+
### macOS ###
182+
# General
183+
.DS_Store
184+
.AppleDouble
185+
.LSOverride
186+
187+
# Icon must end with two \r
188+
Icon
189+
190+
191+
# Thumbnails
192+
._*
193+
194+
# Files that might appear in the root of a volume
195+
.DocumentRevisions-V100
196+
.fseventsd
197+
.Spotlight-V100
198+
.TemporaryItems
199+
.Trashes
200+
.VolumeIcon.icns
201+
.com.apple.timemachine.donotpresent
202+
203+
# Directories potentially created on remote AFP share
204+
.AppleDB
205+
.AppleDesktop
206+
Network Trash Folder
207+
Temporary Items
208+
.apdisk
209+
210+
### macOS Patch ###
211+
# iCloud generated files
212+
*.icloud
213+
214+
# End of https://www.toptal.com/developers/gitignore/api/macos
215+
uv.lock

Makefile

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
.PHONY: lint typecheck test all \
2+
changelog changelog-preview changelog-bump release
3+
4+
# ── CI / quality ────────────────────────────────────────────────────────────
5+
6+
lint:
7+
ruff check .
8+
9+
typecheck:
10+
mypy src/
11+
12+
test:
13+
pytest --cov=src --cov-report=term-missing
14+
15+
all: lint typecheck test
16+
17+
# ── Changelog / release ──────────────────────────────────────────────────────
18+
19+
# Preview what the next changelog entry will look like (dry-run, no files changed)
20+
changelog-preview:
21+
git cliff --unreleased --bump
22+
23+
# Show what the next version number will be
24+
changelog-bump:
25+
git cliff --bumped-version
26+
27+
# Prepend new entries to CHANGELOG.md for the next release
28+
changelog:
29+
$(eval VERSION := $(shell git cliff --bumped-version 2>/dev/null))
30+
touch CHANGELOG.md
31+
git cliff --unreleased --prepend CHANGELOG.md --bump
32+
@echo "CHANGELOG.md updated to $(VERSION). Review, then run: make release"
33+
34+
# Full release: update changelog, commit, tag, push
35+
release:
36+
$(eval VERSION := $(shell git cliff --bumped-version 2>/dev/null))
37+
git add CHANGELOG.md
38+
@git commit -m "chore: release $(VERSION)" || { echo "Error: CHANGELOG.md unchanged. Run 'make changelog' first."; exit 1; }
39+
git tag $(VERSION)
40+
git push origin main
41+
git push origin $(VERSION)
42+
@echo "Released $(VERSION)"

README.md

Lines changed: 50 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Reusable Dev Containers for any project — without modifying the repository.
2020

2121
---
2222

23-
![](https://github.com/dacrystal/dev-code/raw/main/demo/demo.gif "Demo gif")
23+
![](assets/demo.gif "Demo gif")
2424

2525
`devcode` is a CLI that opens any project in VS Code Dev Containers using reusable, local templates.
2626

@@ -50,11 +50,11 @@ Typical Dev Container workflows involve:
5050
# Install
5151
pip install dev-code
5252

53-
# Create default template
54-
devcode init
53+
# Open a project (auto-detects template from container history, or uses default)
54+
devcode open ~/projects/my-app
5555

56-
# Open a project
57-
devcode open dev-code ~/projects/my-app
56+
# Open with an explicit template
57+
devcode open ~/projects/my-app dev-code
5858

5959
# Reopen projects later
6060
devcode ps -a -i
@@ -81,14 +81,7 @@ Default location:
8181
~/.local/share/dev-code/templates/
8282
```
8383

84-
Override search paths:
85-
86-
```bash
87-
DEVCODE_TEMPLATE_PATH=/my/templates:/team/templates
88-
```
89-
90-
* The first path is used for writes
91-
* Additional paths are read-only
84+
Override search paths via `settings.json` (see [Configuration](#configuration)).
9285

9386
---
9487

@@ -105,25 +98,29 @@ DEVCODE_TEMPLATE_PATH=/my/templates:/team/templates
10598
### devcode open
10699

107100
```bash
108-
devcode open <template> <path> [options]
101+
devcode open <path> [template] [options]
109102
```
110103

111-
Open a project using a template.
104+
Open a project in VS Code using a devcontainer template.
112105

113106
#### Arguments
114107

115-
* `<template>`
108+
* `<path>` — Project directory (must exist)
109+
110+
* `[template]` *(optional)*
116111

117112
* Template name, or
118113
* Path to a `devcontainer.json`, or
119114
* Path to a directory containing it
120115

121116
Paths must start with `./`, `../`, `/`, or `~/`.
122117

123-
If both a template and directory match, the template takes precedence and a warning is shown.
118+
If both a template name and a local directory match, the template takes precedence and a warning is shown.
124119

125-
* `<path>`
126-
Project directory
120+
**If omitted**, devcode auto-detects the template in this order:
121+
1. Most recently running container for this project path (uses its stored config)
122+
2. Most recently stopped container for this project path
123+
3. `default_template` from `settings.json` (error if not set)
127124

128125
#### Options
129126

@@ -135,16 +132,6 @@ Open a project using a template.
135132

136133
---
137134

138-
### devcode init
139-
140-
```bash
141-
devcode init
142-
```
143-
144-
Creates the default template.
145-
146-
---
147-
148135
### devcode new
149136

150137
```bash
@@ -229,32 +216,54 @@ eval "$(devcode completion bash)"
229216
## Typical Workflow
230217

231218
```bash
232-
devcode init
233219
devcode new python-dev
234220
devcode edit python-dev
235-
devcode open python-dev ~/projects/my-app
221+
devcode open ~/projects/my-app python-dev
236222
```
237223

238224
---
239225

240-
## Template System
226+
## Configuration
241227

242-
### Default Location
228+
devcode reads `settings.json` from:
243229

244230
```
245-
~/.local/share/dev-code/templates/
231+
~/.config/dev-code/settings.json
246232
```
247233

248-
### Custom Paths
234+
Override the config directory:
249235

250236
```bash
251-
DEVCODE_TEMPLATE_PATH=$HOME/my/templates:/team/shared/templates
237+
DEVCODE_CONF_DIR=/custom/path devcode open ~/projects/my-app
238+
```
239+
240+
The file is created automatically with defaults on first run.
241+
242+
### settings.json
243+
244+
```json
245+
{
246+
"template_sources": ["~/.local/share/dev-code/templates"],
247+
"default_template": "dev-code"
248+
}
252249
```
253250

254-
Resolution order:
251+
| Key | Description |
252+
| --- | --- |
253+
| `template_sources` | Ordered list of template directories. First is the write target; rest are read-only. |
254+
| `default_template` | Template used when `devcode open` is called without a template argument and no container history is found. Error if unset. |
255+
256+
---
257+
258+
## Template System
259+
260+
### Default Location
261+
262+
```
263+
~/.local/share/dev-code/templates/
264+
```
255265

256-
1. First directory is the write target
257-
2. Remaining directories are used for lookup
266+
Configure additional paths via `template_sources` in `settings.json`.
258267

259268
---
260269

@@ -348,8 +357,8 @@ Lists containers and allows reopening projects interactively.
348357

349358
## Internal Flow
350359

351-
1. Resolve template
352-
2. Resolve project path
360+
1. Validate project path (must exist)
361+
2. Resolve template (explicit → container history → settings default)
353362
3. Launch VS Code Dev Container
354363
4. Apply file injection rules
355364

assets/demo.gif

384 KB
Loading

cliff.toml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# git-cliff configuration
2+
# https://git-cliff.org/docs/configuration
3+
4+
[changelog]
5+
body = """
6+
{% if version %}\
7+
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
8+
{% else %}\
9+
## [Unreleased]
10+
{% endif %}\
11+
{% for group, commits in commits | group_by(attribute="group") %}
12+
### {{ group | striptags | trim | upper_first }}
13+
{% for commit in commits %}
14+
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
15+
{% if commit.breaking %}[**breaking**] {% endif %}\
16+
{{ commit.message | upper_first }}\
17+
{% endfor %}
18+
{% endfor %}\n
19+
"""
20+
trim = true
21+
render_always = true
22+
postprocessors = [
23+
{ pattern = "<REPO>", replace = "https://github.com/dacrystal/dev-code" },
24+
]
25+
26+
[git]
27+
conventional_commits = true
28+
filter_unconventional = true
29+
split_commits = false
30+
commit_preprocessors = [
31+
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))" },
32+
]
33+
commit_parsers = [
34+
{ message = "^feat", group = "Added" },
35+
{ message = "^fix", group = "Fixed" },
36+
{ message = "^refactor", group = "Changed" },
37+
{ message = "^perf", group = "Changed" },
38+
{ message = "^revert", group = "Fixed" },
39+
{ message = "^docs|^chore|^ci|^style|^test", skip = true },
40+
]
41+
filter_commits = true
42+
protect_breaking_commits = true
43+
sort_commits = "oldest"
44+
topo_order = false
45+
skip_tags = "v*.*.*.post"

pyproject.toml

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
[build-system]
2-
requires = ["hatchling", "hatch-vcs"]
2+
requires = ["hatchling", "hatch-vcs", "hatch-fancy-pypi-readme"]
33
build-backend = "hatchling.build"
44

55
[project]
66
name = "dev-code"
7-
dynamic = ["version"]
7+
dynamic = ["version", "readme"]
88
description = "Project · editor · container — simplified"
9-
readme = "README.md"
109
requires-python = ">=3.10"
11-
dependencies = []
10+
authors = [
11+
{name = "Nasser Alansari", email = "dacrystal@users.noreply.github.com"},
12+
]
13+
dependencies = ["click>=8.0"]
1214
classifiers = [
1315
"Programming Language :: Python :: 3",
1416
"Programming Language :: Python :: 3.10",
@@ -24,7 +26,7 @@ classifiers = [
2426
"Issues" = "https://github.com/dacrystal/dev-code/issues"
2527

2628
[project.scripts]
27-
devcode = "devcode:main"
29+
devcode = "devcode:cli"
2830

2931
[tool.hatch.version]
3032
source = "vcs"
@@ -34,12 +36,18 @@ force-include = {"src/devcode.py" = "devcode.py", "src/templates" = "dev_code_te
3436

3537
[tool.pytest.ini_options]
3638
testpaths = ["tests"]
39+
pythonpath = ["src"]
3740

3841
[dependency-groups]
39-
dev = [
40-
"pytest>=7.0.1",
41-
"pyfiglet>=1.0.4",
42-
"pytest-cov>=4.0",
43-
"tox>=4.0",
44-
"pyyaml>=6.0",
45-
]
42+
test = ["pytest>=7", "pytest-cov>=4"]
43+
dev = ["tox>=4", "pyyaml>=6", "git-cliff>=2"]
44+
45+
[tool.hatch.metadata.hooks.fancy-pypi-readme]
46+
content-type = "text/markdown"
47+
48+
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
49+
path = "README.md"
50+
51+
[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
52+
pattern = '\(assets/'
53+
replacement = "(https://raw.githubusercontent.com/dacrystal/dev-code/main/assets/"

0 commit comments

Comments
 (0)