Skip to content

Commit f1ec137

Browse files
Improve project initialization UX (#98)
1 parent 0d7e3ec commit f1ec137

4 files changed

Lines changed: 106 additions & 22 deletions

File tree

docs/README.md

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,23 @@ Click this button to create a new repository for your project, then clone the ne
4242
### Rename the project
4343
After cloning the repository, rename the project by running:
4444
```bash
45-
mise run project --name "" --description "" --author "" --email "" --github ""
45+
mise run project
4646
```
47-
Pass the following parameters:
47+
The script will interactively prompt you for the following details, with smart defaults based on your environment:
4848

4949
Parameter | Description
5050
--- | ---
51-
`name` | Project new name
51+
`name` | Project new name (defaults to current directory name)
5252
`description` | Project short description
53-
`author` | Author name
54-
`email`| Author email
53+
`author` | Author name (defaults to `git config user.name`)
54+
`email`| Author email (defaults to `git config user.email`)
5555
`github`| GitHub username (for GitHub funding)
5656

57+
You can also pass these as flags:
58+
```bash
59+
mise run project --name "my-project" --description "A cool project" --author "Alice" --email "alice@example.com" --github "alice"
60+
```
61+
5762

5863
## Prerequisites
5964
### Dev container
@@ -104,11 +109,14 @@ Try running `mise run app -- -h` or `mise run app -- --help` to get the help mes
104109
```bash
105110
Usage: app [OPTIONS]
106111

107-
Say hello
112+
Say hello to a user.
108113

109114
Options:
110-
-n, --name TEXT Name [default: World]
111-
-h, --help Show this message and exit.
115+
-n, --name <name> The name of the person to greet. [default: World]
116+
-V, --version Show the version and exit.
117+
-h, --help Show this message and exit.
118+
119+
Example: app --name Alice
112120
```
113121

114122
### Docker

project/app.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33

44
@command(
5+
name="app",
56
context_settings={"help_option_names": ["-h", "--help"]},
67
help="Say hello to a user.",
78
epilog="Example: app --name Alice",
@@ -14,7 +15,7 @@
1415
show_default=True,
1516
metavar="<name>",
1617
)
17-
@version_option()
18+
@version_option(None, "-V", "--version")
1819
def main(name: str = "World"):
1920
"""
2021
Say hello to the given name.

scripts/rename.py

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,72 @@
11
import os
22
import re
33
import shutil
4+
import subprocess
45
from pathlib import Path
56

6-
from click import ClickException, UsageError, command, echo, option
7-
8-
9-
@command()
10-
@option("--name", required=True, help="Project new name")
11-
@option("--description", required=True, help="Project short description")
12-
@option("--author", required=True, help="Author name")
13-
@option("--email", required=True, help="Author email")
14-
@option("--github", required=True, help="GitHub username")
7+
from click import ClickException, UsageError, command, confirm, echo, option, secho
8+
9+
10+
def _get_git_config(key: str) -> str:
11+
try:
12+
return subprocess.check_output(["/usr/bin/git", "config", key], text=True).strip() # noqa: S603
13+
except (subprocess.CalledProcessError, FileNotFoundError):
14+
return ""
15+
16+
17+
def _get_default_github() -> str:
18+
# Try git config first
19+
username = _get_git_config("github.user") or _get_git_config("user.name")
20+
if username and re.match(r"^[a-zA-Z0-9-]+$", username):
21+
return username
22+
23+
# Try to extract from remote URL
24+
try:
25+
url = subprocess.check_output( # noqa: S603
26+
["/usr/bin/git", "remote", "get-url", "origin"], text=True
27+
).strip()
28+
if "github.com" in url:
29+
if url.startswith("https"):
30+
return url.split("/")[-2]
31+
if url.startswith("git@"):
32+
return url.split(":")[-1].split("/")[0]
33+
except (subprocess.CalledProcessError, FileNotFoundError):
34+
pass
35+
36+
return ""
37+
38+
39+
@command(context_settings={"help_option_names": ["-h", "--help"]})
40+
@option(
41+
"--name",
42+
prompt="Project name",
43+
default=lambda: Path.cwd().name,
44+
help="Project new name",
45+
)
46+
@option(
47+
"--description",
48+
prompt="Project description",
49+
default="A Python project",
50+
help="Project short description",
51+
)
52+
@option(
53+
"--author",
54+
prompt="Author name",
55+
default=lambda: _get_git_config("user.name"),
56+
help="Author name",
57+
)
58+
@option(
59+
"--email",
60+
prompt="Author email",
61+
default=lambda: _get_git_config("user.email"),
62+
help="Author email",
63+
)
64+
@option(
65+
"--github",
66+
prompt="GitHub username",
67+
default=_get_default_github,
68+
help="GitHub username",
69+
)
1570
def main(name: str, description: str, author: str, email: str, github: str):
1671
# Validate inputs to prevent configuration injection
1772
for label, value in [
@@ -49,11 +104,23 @@ def toml_escape(s: str) -> str:
49104

50105
source = name.replace("-", "_").lower()
51106

52-
echo(f"Initializing project '{name}' (source: '{source}')...")
107+
echo(f"\nProject Configuration:")
108+
echo(f" Name: {name}")
109+
echo(f" Source: {source}")
110+
echo(f" Description: {description}")
111+
echo(f" Author: {author} <{email}>")
112+
echo(f" GitHub: {github}\n")
113+
114+
if not confirm("Do you want to proceed with these settings?", default=True):
115+
secho("Aborted! ❌", fg="red")
116+
return
117+
118+
secho(f"\nInitializing project '{name}'... 🚀", fg="green", bold=True)
53119

54120
# 1. Rename project directory
55121
if os.path.isdir("project"):
56122
shutil.move("project", source)
123+
secho(f"Renamed 'project' directory to '{source}'", fg="blue")
57124
elif not os.path.isdir(source):
58125
raise ClickException(f"Error: Neither 'project' nor '{source}' directory found.")
59126

@@ -75,16 +142,16 @@ def toml_escape(s: str) -> str:
75142
for filepath, pattern, replacement in replacements:
76143
path = Path(filepath)
77144
if not path.exists():
78-
echo(f"Warning: File {filepath} not found, skipping.")
145+
secho(f" Warning: File {filepath} not found, skipping. ⚠️", fg="yellow")
79146
continue
80147

81148
content = path.read_text()
82149
# Use a lambda for replacement to avoid regex backreference injection
83150
new_content = re.sub(pattern, lambda _: replacement, content, flags=re.MULTILINE)
84151
path.write_text(new_content)
85-
echo(f"Updated {filepath}")
152+
secho(f" Updated {filepath} ✅", fg="blue")
86153

87-
echo("Project initialization complete.")
154+
secho("\nProject initialization complete! ✨", fg="green", bold=True)
88155

89156

90157
if __name__ == "__main__":

tests/test_app.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,17 @@ def test_help():
88
result = runner.invoke(main, ["--help"])
99
assert result.exit_code == 0
1010
assert "--name <name>" in result.output
11+
assert "-V, --version" in result.output
1112
assert "Example: app --name Alice" in result.output
1213

1314

15+
def test_version():
16+
runner = CliRunner()
17+
result = runner.invoke(main, ["-V"])
18+
assert result.exit_code == 0
19+
assert "app, version 0.1.0" in result.output
20+
21+
1422
def test_greet():
1523
runner = CliRunner()
1624
result = runner.invoke(main, ["--name", "Jules"])

0 commit comments

Comments
 (0)