1import re
2import shutil
3import subprocess
4from pathlib import Path
5
6import click
7
8STARTER_REPOS = {
9 "app": "https://github.com/dropseed/plain-starter-app",
10 "bare": "https://github.com/dropseed/plain-starter-bare",
11}
12
13
14@click.command()
15@click.argument("project_name")
16@click.option(
17 "--type",
18 "starter_type",
19 type=click.Choice(["app", "bare"]),
20 default="app",
21 help="Type of starter template to use",
22)
23@click.option(
24 "--no-install",
25 is_flag=True,
26 help="Skip running ./scripts/install after setup",
27)
28def cli(project_name: str, starter_type: str, no_install: bool) -> None:
29 """Bootstrap a new Plain project from starter templates"""
30 project_path = Path.cwd() / project_name
31
32 if project_path.exists():
33 click.secho(
34 f"Error: Directory '{project_name}' already exists", fg="red", err=True
35 )
36 raise click.Abort()
37
38 # Clone the starter repository
39 repo_url = STARTER_REPOS[starter_type]
40 click.secho(f"Cloning {starter_type} starter template...", dim=True)
41
42 try:
43 subprocess.run(
44 ["git", "clone", "--depth", "1", repo_url, project_name],
45 check=True,
46 capture_output=True,
47 )
48 except subprocess.CalledProcessError as e:
49 click.secho(
50 f"Error cloning repository: {e.stderr.decode()}", fg="red", err=True
51 )
52 raise click.Abort()
53
54 # Remove .git directory and reinitialize
55 click.secho("Initializing new git repository...", dim=True)
56 git_dir = project_path / ".git"
57 if git_dir.exists():
58 shutil.rmtree(git_dir)
59
60 subprocess.run(
61 ["git", "init"],
62 cwd=project_path,
63 check=True,
64 capture_output=True,
65 )
66
67 # Replace project name in pyproject.toml
68 click.secho("Configuring project...", dim=True)
69 pyproject_path = project_path / "pyproject.toml"
70
71 if pyproject_path.exists():
72 content = pyproject_path.read_text()
73 # Replace the name field in pyproject.toml
74 # Matches: name = "anything" or name = 'anything'
75 content = re.sub(
76 r'^name\s*=\s*["\'].*?["\']',
77 f'name = "{project_name}"',
78 content,
79 count=1,
80 flags=re.MULTILINE,
81 )
82 pyproject_path.write_text(content)
83
84 # Run install script unless --no-install
85 if not no_install:
86 install_script = project_path / "scripts" / "install"
87 if install_script.exists():
88 click.echo(
89 click.style("Running installation:", bold=True)
90 + click.style(" ./scripts/install", dim=True)
91 )
92 try:
93 subprocess.run(
94 ["./scripts/install"],
95 cwd=project_path,
96 check=True,
97 )
98 except subprocess.CalledProcessError as e:
99 click.secho(
100 f"Warning: Installation script failed with exit code {e.returncode}",
101 fg="yellow",
102 err=True,
103 )
104 click.secho(
105 "You may need to run './scripts/install' manually.",
106 fg="yellow",
107 err=True,
108 )
109
110 # Success message
111 click.echo()
112 click.secho(
113 f"✓ Project '{project_name}' created successfully!", fg="green", bold=True
114 )
115 click.echo()
116 click.secho("Next steps:", bold=True)
117 click.secho(f" cd {project_name}")
118 if no_install:
119 click.secho(" ./scripts/install")
120 click.secho(" uv run plain dev")