Skip to content

Commit 8879ab6

Browse files
committed
Merge branch 'dev'
2 parents 45ba5cb + aa37437 commit 8879ab6

File tree

3 files changed

+481
-261
lines changed

3 files changed

+481
-261
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,10 @@ Default location:
111111
~/.local/share/dev-code/templates/
112112
```
113113

114-
Override with:
114+
Override with a colon-separated search path (first entry is the write directory):
115115

116116
```
117-
$DEVCODE_TEMPLATE_DIR
117+
$DEVCODE_TEMPLATE_PATH=/my/templates:/shared/team/templates
118118
```
119119

120120
---
@@ -153,8 +153,8 @@ Pass `-v` / `--verbose` before the subcommand to enable debug output (e.g. `devc
153153
| `devcode open <template> <path>` | Open a project using a template name or path to a devcontainer |
154154
| `devcode init` | Seed the default template |
155155
| `devcode new <name> [base]` | Create a new template |
156-
| `devcode edit [template]` | Edit a template |
157-
| `devcode list [--long]` | List templates |
156+
| `devcode edit [template]` | Open a template directory in VS Code |
157+
| `devcode list [--long]` | List templates (`--long` shows one section per search dir) |
158158
| `devcode ps [-a] [-i]` | List containers (`-a` includes stopped, `-i` interactive mode) |
159159
| `devcode completion <shell>` | Generate shell completion |
160160

src/devcode.py

Lines changed: 106 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -89,36 +89,43 @@ def wsl_to_windows(path: str) -> str:
8989
raise RuntimeError(f"Failed to convert path with wslpath: {path}") from e
9090

9191

92-
def resolve_template_dir() -> str:
93-
"""Return the user template directory (DEVCODE_TEMPLATE_DIR or XDG default)."""
94-
override = os.environ.get("DEVCODE_TEMPLATE_DIR")
95-
if override:
96-
return override
92+
def resolve_template_search_path() -> list[str]:
93+
"""Return ordered list of template search directories from DEVCODE_TEMPLATE_PATH."""
94+
new_var = os.environ.get("DEVCODE_TEMPLATE_PATH")
9795
xdg = os.environ.get("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
98-
return os.path.join(xdg, "dev-code", "templates")
96+
default = os.path.join(xdg, "dev-code", "templates")
97+
if not new_var:
98+
return [default]
99+
dirs = [d for d in new_var.split(":") if d]
100+
return dirs if dirs else [default]
101+
102+
103+
def _write_template_dir() -> str:
104+
"""Return the first (canonical write) directory from the template search path."""
105+
return resolve_template_search_path()[0]
99106

100107

101108
def _list_template_names() -> list:
102-
"""Return sorted deduplicated list of all template names (builtins + user)."""
103-
names = set()
104-
try:
105-
module_dir = os.path.dirname(os.path.abspath(__file__))
106-
builtin_base = os.path.join(module_dir, "dev_code_templates")
107-
if os.path.isdir(builtin_base):
108-
for name in os.listdir(builtin_base):
109-
if os.path.isdir(os.path.join(builtin_base, name)):
110-
names.add(name)
111-
except Exception:
112-
pass
113-
try:
114-
user_dir = resolve_template_dir()
115-
if os.path.isdir(user_dir):
116-
for name in os.listdir(user_dir):
117-
if os.path.isdir(os.path.join(user_dir, name)):
118-
names.add(name)
119-
except Exception:
120-
pass
121-
return sorted(names)
109+
"""Return sorted deduplicated list of valid user template names across all search dirs."""
110+
seen = []
111+
seen_set = set()
112+
for search_dir in resolve_template_search_path():
113+
if not os.path.isdir(search_dir):
114+
logger.debug("template search dir not found, skipping: %s", search_dir)
115+
continue
116+
try:
117+
for name in sorted(os.listdir(search_dir)):
118+
if name in seen_set:
119+
continue
120+
candidate = os.path.join(search_dir, name)
121+
if _is_valid_template(candidate):
122+
seen.append(name)
123+
seen_set.add(name)
124+
else:
125+
logger.debug("skipping invalid template: %s", candidate)
126+
except Exception:
127+
pass
128+
return sorted(seen)
122129

123130

124131
def get_builtin_template_path(name: str) -> str | None:
@@ -137,6 +144,22 @@ def get_builtin_template_path(name: str) -> str | None:
137144
return None
138145

139146

147+
def _is_valid_template(template_root: str) -> bool:
148+
"""Return True if template_root contains .devcontainer/devcontainer.json."""
149+
return os.path.isfile(
150+
os.path.join(template_root, ".devcontainer", "devcontainer.json")
151+
)
152+
153+
154+
def _find_template_in_search_path(name: str) -> str | None:
155+
"""Search all template dirs for name; return template root path or None."""
156+
for search_dir in resolve_template_search_path():
157+
candidate = os.path.join(search_dir, name)
158+
if _is_valid_template(candidate):
159+
return candidate
160+
return None
161+
162+
140163
def resolve_template(name: str) -> str:
141164
"""Return absolute path to template's devcontainer.json. Exits on failure."""
142165
# 1. Explicit path prefix — skip template lookup entirely
@@ -153,27 +176,17 @@ def resolve_template(name: str) -> str:
153176
sys.exit(1)
154177
logger.error("path not found: %s", name)
155178
sys.exit(1)
156-
# 2. Try template lookup (user templates, then builtins)
157-
user_path = os.path.join(resolve_template_dir(), name, ".devcontainer", "devcontainer.json")
158-
if os.path.exists(user_path):
179+
# 2. Try template lookup across all search dirs
180+
template_root = _find_template_in_search_path(name)
181+
if template_root:
182+
config = os.path.join(template_root, ".devcontainer", "devcontainer.json")
159183
if _resolve_as_path(name):
160184
logger.warning(
161185
"'%s' matches both a template and a local path — using template. "
162186
"Use './%s' to open as path instead.",
163187
name, name,
164188
)
165-
return user_path
166-
builtin = get_builtin_template_path(name)
167-
if builtin:
168-
path = os.path.join(builtin, ".devcontainer", "devcontainer.json")
169-
if os.path.exists(path):
170-
if _resolve_as_path(name):
171-
logger.warning(
172-
"'%s' matches both a template and a local path — using template. "
173-
"Use './%s' to open as path instead.",
174-
name, name,
175-
)
176-
return path
189+
return config
177190
# 3. No template — try path fallback
178191
path_result = _resolve_as_path(name)
179192
if path_result:
@@ -548,19 +561,19 @@ def _cmd_open_dry_run(config_file: str, project_path: str, uri: str) -> None:
548561

549562
def cmd_new(args) -> None:
550563
"""Create a new template by copying a base template."""
551-
template_dir = resolve_template_dir()
552-
dest = os.path.join(template_dir, args.name)
564+
write_dir = _write_template_dir()
565+
dest = os.path.join(write_dir, args.name)
553566

554567
# Step 1: fail if name already exists
555568
if os.path.exists(dest):
556569
logger.error("template '%s' already exists: %s", args.name, dest)
557570
sys.exit(1)
558571

559-
# Step 2: resolve base (check before creating dirs)
572+
# Step 2: resolve base — search all dirs, then builtins
560573
base_name = args.base or "dev-code"
561-
base_user = os.path.join(template_dir, base_name)
562-
if os.path.isdir(base_user):
563-
base_src = base_user
574+
base_root = _find_template_in_search_path(base_name)
575+
if base_root:
576+
base_src = base_root
564577
else:
565578
builtin = get_builtin_template_path(base_name)
566579
if builtin:
@@ -569,11 +582,11 @@ def cmd_new(args) -> None:
569582
logger.error("base template not found: %s", base_name)
570583
sys.exit(1)
571584

572-
# Step 3-4: create template dir
585+
# Step 3-4: create write dir
573586
try:
574-
os.makedirs(template_dir, exist_ok=True)
587+
os.makedirs(write_dir, exist_ok=True)
575588
except OSError as e:
576-
logger.error("cannot create template dir %s: %s", template_dir, e)
589+
logger.error("cannot create template dir %s: %s", write_dir, e)
577590
sys.exit(1)
578591

579592
# Step 5: copy
@@ -593,28 +606,22 @@ def cmd_new(args) -> None:
593606

594607

595608
def cmd_edit(args) -> None:
596-
"""Open a template directory for editing using the built-in dev-code devcontainer."""
597-
template_dir = resolve_template_dir()
598-
599-
if args.template is None:
600-
if not os.path.isdir(template_dir):
601-
logger.error("template dir not found: %s — run 'devcode init' first", template_dir)
602-
sys.exit(1)
603-
project_path = template_dir
604-
else:
605-
project_path = os.path.join(template_dir, args.template)
606-
if not os.path.isdir(project_path):
609+
"""Open a template directory directly in VS Code for editing."""
610+
if args.template is not None:
611+
template_root = _find_template_in_search_path(args.template)
612+
if template_root is None:
607613
logger.error("template not found: %s", args.template)
608614
sys.exit(1)
609-
610-
open_args = argparse.Namespace(
611-
template="dev-code",
612-
projectpath=project_path,
613-
container_folder=None,
614-
timeout=300,
615-
dry_run=False,
616-
)
617-
cmd_open(open_args)
615+
subprocess.run(["code", template_root])
616+
else:
617+
for search_dir in resolve_template_search_path():
618+
if os.path.isdir(search_dir):
619+
subprocess.run(["code", search_dir])
620+
return
621+
logger.error(
622+
"no template directory found — run 'devcode init' or 'devcode new <name>' to get started"
623+
)
624+
sys.exit(1)
618625

619626

620627
def cmd_init(args) -> None:
@@ -624,17 +631,17 @@ def cmd_init(args) -> None:
624631
logger.error("built-in template 'dev-code' not found — packaging error")
625632
sys.exit(1)
626633

627-
template_dir = resolve_template_dir()
628-
dest = os.path.join(template_dir, "dev-code")
634+
write_dir = _write_template_dir()
635+
dest = os.path.join(write_dir, "dev-code")
629636

630637
if os.path.exists(dest):
631638
print(f"Skipped 'dev-code': already exists at {dest}")
632639
return
633640

634641
try:
635-
os.makedirs(template_dir, exist_ok=True)
642+
os.makedirs(write_dir, exist_ok=True)
636643
except OSError as e:
637-
logger.error("cannot create template dir %s: %s", template_dir, e)
644+
logger.error("cannot create template dir %s: %s", write_dir, e)
638645
sys.exit(1)
639646

640647
try:
@@ -648,55 +655,38 @@ def cmd_init(args) -> None:
648655

649656
def cmd_list(args) -> None:
650657
"""List available templates."""
651-
# Collect built-ins
652-
builtins = []
653-
module_dir = os.path.dirname(os.path.abspath(__file__))
654-
builtin_base = os.path.join(module_dir, "dev_code_templates")
655-
if os.path.isdir(builtin_base):
656-
for name in sorted(os.listdir(builtin_base)):
657-
p = os.path.join(builtin_base, name)
658-
if os.path.isdir(p):
659-
builtins.append((name, p))
660-
661-
# Collect user templates
662-
template_dir = resolve_template_dir()
663-
user = []
664-
if os.path.isdir(template_dir):
665-
for name in sorted(os.listdir(template_dir)):
666-
p = os.path.join(template_dir, name)
667-
if os.path.isdir(p):
668-
user.append((name, p))
658+
search_dirs = resolve_template_search_path()
669659

670660
if not args.long:
671-
for name, _ in builtins:
672-
print(name)
673-
for name, _ in user:
661+
names = _list_template_names()
662+
for name in names:
674663
print(name)
675-
if not user and not builtins:
676-
print("(no templates — run 'devcode init' to get started)")
677-
elif not user:
678-
print("(no user templates — run 'devcode init' to get started)")
664+
if not names:
665+
print("(no templates — run 'devcode init' or 'devcode new <name>' to get started)")
679666
return
680667

681-
# --long output
682-
print(f"Template dir: {template_dir}")
683-
print()
684-
685-
all_names = [n for n, _ in builtins] + [n for n, _ in user]
686-
col_w = max((len(n) for n in all_names), default=8) + 2
687-
688-
if builtins:
689-
print("BUILT-IN")
690-
for name, path in builtins:
691-
print(f" {name:<{col_w}}{path}")
668+
# --long output: one section per search dir
669+
any_printed = False
670+
for search_dir in search_dirs:
671+
if not os.path.isdir(search_dir):
672+
logger.debug("template search dir not found, skipping: %s", search_dir)
673+
continue
674+
templates = [
675+
name for name in sorted(os.listdir(search_dir))
676+
if _is_valid_template(os.path.join(search_dir, name))
677+
]
678+
print(search_dir)
679+
if templates:
680+
col_w = max(len(n) for n in templates) + 2
681+
for name in templates:
682+
print(f" {name:<{col_w}}{os.path.join(search_dir, name)}")
683+
else:
684+
print(" (no templates)")
692685
print()
686+
any_printed = True
693687

694-
if user:
695-
print("USER")
696-
for name, path in user:
697-
print(f" {name:<{col_w}}{path}")
698-
else:
699-
print(" (no user templates — run 'devcode init' to get started)")
688+
if not any_printed:
689+
print("(no template directories found — run 'devcode init' or 'devcode new <name>' to get started)")
700690

701691

702692
def _template_name_from_config(config_path: str) -> str:

0 commit comments

Comments
 (0)