devskills-cli 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ PYPI_API_TOKEN=pypi-AgEIcHlwaS5vcmcCJDQzZWNhYjM2LTEzZWYtNGUxZS04NjhhLWRiM2U5MDYwN2IxOQACKlszLCJhYzVkYjJjNC1kNmQ0LTQ2MjItYjg3NS01Y2IyMWM4NjNmOTQiXQAABiB23VvfGF8pUltnAPMPQ7zqgT3wZwGRr0j8BQdfjg02VA
@@ -0,0 +1,7 @@
1
+ .claude/
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+ *.pyo
6
+ *.pyd
7
+ CLAUDE.md
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: devskills-cli
3
+ Version: 0.1.1
4
+ Summary: Interactive CLI to scaffold AI production projects using uv
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx>=0.27.0
7
+ Requires-Dist: questionary>=2.0.0
8
+ Requires-Dist: rich>=13.0.0
9
+ Requires-Dist: typer>=0.12.0
File without changes
File without changes
File without changes
@@ -0,0 +1,64 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+ from devskills.templates.base import make_dir, make_file
4
+
5
+
6
+ def run_uv_init(project_path: Path) -> None:
7
+ subprocess.run(
8
+ ["uv", "init", project_path.name],
9
+ cwd=project_path.parent,
10
+ check=True,
11
+ )
12
+
13
+
14
+ def create_base_structure(project_path: Path) -> None:
15
+ dirs = [
16
+ "app/api",
17
+ "app/dependencies",
18
+ "src/services",
19
+ "src/database",
20
+ "utils",
21
+ "models",
22
+ "config",
23
+ "tests",
24
+ "docs",
25
+ "scripts",
26
+ ]
27
+ for d in dirs:
28
+ make_dir(project_path / d)
29
+
30
+ make_file(project_path / "app" / "main.py", 'def main():\n pass\n')
31
+ make_file(project_path / "config" / "settings.py", '# Application settings\n')
32
+ make_file(project_path / "config" / "logging.py", '# Logging configuration\n')
33
+ make_file(project_path / ".env", '')
34
+ make_file(project_path / ".env.example", '# Copy this to .env and fill in values\n')
35
+ make_file(project_path / ".gitignore", _gitignore_content())
36
+ make_file(project_path / ".dockerignore", _dockerignore_content())
37
+ make_file(project_path / ".gitattributes", '* text=auto\n')
38
+ make_file(project_path / ".python-version", '3.11\n')
39
+ make_file(project_path / "requirements.txt", '')
40
+ make_file(project_path / "ISSUES.md", '# Issues\n\nTrack known issues and bugs here.\n')
41
+
42
+
43
+ def _gitignore_content() -> str:
44
+ return """\
45
+ __pycache__/
46
+ *.py[cod]
47
+ *.egg-info/
48
+ dist/
49
+ .venv/
50
+ .env
51
+ .mypy_cache/
52
+ .ruff_cache/
53
+ .pytest_cache/
54
+ """
55
+
56
+
57
+ def _dockerignore_content() -> str:
58
+ return """\
59
+ __pycache__/
60
+ *.py[cod]
61
+ .venv/
62
+ .env
63
+ .git/
64
+ """
@@ -0,0 +1,71 @@
1
+ from pathlib import Path
2
+ from rich.console import Console
3
+ from rich.panel import Panel
4
+ from devskills.core import scaffold
5
+ from devskills.templates import docker, claude
6
+
7
+ console = Console()
8
+
9
+ _TEMPLATE_MAP = {
10
+ "docker": docker.create,
11
+ "claude": claude.create,
12
+ }
13
+
14
+
15
+ def generate(location: str, structure: str, selected_features: list[str], run_venv: bool = False) -> None:
16
+ if location == ".":
17
+ project_path = Path.cwd()
18
+ project_name = project_path.name
19
+ else:
20
+ project_path = Path.cwd() / location
21
+ project_name = location
22
+
23
+ with console.status("[bold red]Initializing project with uv...", spinner="dots"):
24
+ scaffold.run_uv_init(project_path, location)
25
+
26
+ with console.status("[bold red]Creating base project structure...", spinner="dots"):
27
+ scaffold.create_base(project_path, structure)
28
+
29
+ for feature in selected_features:
30
+ handler = _TEMPLATE_MAP.get(feature)
31
+ if handler:
32
+ with console.status(f"[bold red]Adding {feature}...", spinner="dots"):
33
+ handler(project_path)
34
+
35
+ if run_venv:
36
+ with console.status("[bold red]Creating .venv (uv venv)...", spinner="dots"):
37
+ scaffold.run_uv_venv(project_path)
38
+
39
+ _print_success(project_name, structure, selected_features, run_venv)
40
+
41
+
42
+ def _print_success(project_name: str, structure: str, features: list[str], run_venv: bool) -> None:
43
+ lines = [f"[bold red]{project_name}[/bold red] created successfully\n"]
44
+
45
+ lines.append(f" [dim]Base structure [/dim] [bold red]✓[/bold red] ({structure})")
46
+ for f in features:
47
+ lines.append(f" [dim]{f.capitalize():<16}[/dim] [bold red]✓[/bold red]")
48
+ if run_venv:
49
+ lines.append(" [dim]Virtualenv [/dim] [bold red]✓[/bold red] .venv/")
50
+
51
+ lines.append(f"\n [dim]Next steps:[/dim]")
52
+ lines.append(f" cd {project_name}")
53
+ if not run_venv:
54
+ lines.append(" uv venv")
55
+ lines.append(" uv sync")
56
+ lines.append(" cp .env.example .env")
57
+
58
+ if "claude" in features:
59
+ lines.append(
60
+ "\n [dim]Add skills anytime:[/dim]\n"
61
+ " dev skills find <keyword>\n"
62
+ " Browse: [cyan]https://skills.sh[/cyan]"
63
+ )
64
+
65
+ console.print(
66
+ Panel(
67
+ "\n".join(lines),
68
+ border_style="red",
69
+ padding=(1, 2),
70
+ )
71
+ )
@@ -0,0 +1,113 @@
1
+ import questionary
2
+ from questionary import Style
3
+ from rich.console import Console
4
+
5
+ console = Console()
6
+
7
+ _style = Style([
8
+ ("qmark", "fg:#ff4444 bold"),
9
+ ("question", "fg:#ffffff bold"),
10
+ ("instruction", "fg:#555555 italic"),
11
+ ("pointer", "fg:#ff4444 bold"),
12
+ ("highlighted", "fg:#ff4444 bold"),
13
+ ("selected", "fg:#ff6666 bold"),
14
+ ("separator", "fg:#444444"),
15
+ ("answer", "fg:#ff6666 bold"),
16
+ ])
17
+
18
+
19
+ def ask_location() -> str:
20
+ name = questionary.text(
21
+ "Project name (or . to scaffold in current folder):",
22
+ style=_style,
23
+ ).ask()
24
+ if name is None:
25
+ raise KeyboardInterrupt
26
+ return name.strip()
27
+
28
+
29
+ def ask_structure() -> str:
30
+ return questionary.select(
31
+ "Project structure:",
32
+ choices=[
33
+ questionary.Choice(
34
+ " AI / ML app/ src/inference src/services src/database models/ tests/ docs/ .github/",
35
+ value="aiml",
36
+ ),
37
+ questionary.Choice(
38
+ " API app/ src/services src/database tests/ docs/ config/",
39
+ value="api",
40
+ ),
41
+ questionary.Choice(
42
+ " Minimal app/ tests/ config/",
43
+ value="minimal",
44
+ ),
45
+ questionary.Choice(
46
+ " None empty project (just pyproject.toml + .gitignore)",
47
+ value="none",
48
+ ),
49
+ ],
50
+ style=_style,
51
+ ).ask()
52
+
53
+
54
+ def ask_features() -> list[str]:
55
+ selected = questionary.checkbox(
56
+ "Select features to scaffold (space = toggle, enter = confirm):",
57
+ choices=[
58
+ questionary.Choice(title="[Docker] Dockerfile + .dockerignore", value="docker"),
59
+ questionary.Choice(title="[Claude] .claude/ folder for Claude Code integration", value="claude"),
60
+ ],
61
+ style=_style,
62
+ instruction=" ",
63
+ ).ask()
64
+ return selected if selected is not None else []
65
+
66
+
67
+ def ask_venv() -> bool:
68
+ choice = questionary.select(
69
+ "Create virtual environment (.venv)?",
70
+ choices=[
71
+ questionary.Choice(" Yes — create .venv now (runs uv venv)", value=True),
72
+ questionary.Choice(" No — I'll do it manually", value=False),
73
+ ],
74
+ style=_style,
75
+ ).ask()
76
+ return bool(choice)
77
+
78
+
79
+ def ask_skills_select(refs: list[str]) -> list[str]:
80
+ _reset_windows_console()
81
+ print()
82
+ selected = questionary.select(
83
+ "Select a skill to install (arrow keys + enter):",
84
+ choices=[questionary.Choice(title=ref, value=ref) for ref in refs],
85
+ style=_style,
86
+ ).ask()
87
+ return [selected] if selected is not None else []
88
+
89
+
90
+ def _reset_windows_console() -> None:
91
+ """Re-enable Virtual Terminal Processing so questionary renders correctly
92
+ after subprocess output has potentially changed the console mode."""
93
+ import os
94
+ if os.name != "nt":
95
+ return
96
+ try:
97
+ import ctypes
98
+ kernel32 = ctypes.windll.kernel32
99
+ handle = kernel32.GetStdHandle(-11) # STD_OUTPUT_HANDLE
100
+ kernel32.SetConsoleMode(
101
+ handle,
102
+ 0x0001 | 0x0002 | 0x0004, # PROCESSED | WRAP_AT_EOL | VIRTUAL_TERMINAL_PROCESSING
103
+ )
104
+ except Exception:
105
+ pass
106
+
107
+
108
+ def ask_confirm(location: str, structure: str, features: list[str]) -> bool:
109
+ console.print(f"\n [dim]Location :[/dim] {location}")
110
+ console.print(f" [dim]Structure :[/dim] {structure}")
111
+ console.print(f" [dim]Features :[/dim] {', '.join(features) if features else '(none)'}")
112
+ console.print()
113
+ return questionary.confirm("Looks good?", default=True, style=_style).ask()
@@ -0,0 +1,72 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+ from devskills.templates.helpers import make_dir, make_file
4
+
5
+
6
+ def run_uv_init(project_path: Path, location: str) -> None:
7
+ if location == ".":
8
+ subprocess.run(["uv", "init", "."], cwd=project_path, check=True)
9
+ else:
10
+ subprocess.run(["uv", "init", project_path.name], cwd=project_path.parent, check=True)
11
+
12
+
13
+ def run_uv_venv(project_path: Path) -> None:
14
+ subprocess.run(["uv", "venv"], cwd=project_path, check=True)
15
+
16
+
17
+ _STRUCTURE_DIRS = {
18
+ "none": [],
19
+ "aiml": [
20
+ "app",
21
+ "src/inference",
22
+ "src/services",
23
+ "src/database",
24
+ "models",
25
+ "tests",
26
+ "docs",
27
+ "config",
28
+ ".github/workflows",
29
+ ],
30
+ "api": [
31
+ "app",
32
+ "src/services",
33
+ "src/database",
34
+ "tests",
35
+ "docs",
36
+ "config",
37
+ ],
38
+ "minimal": [
39
+ "app",
40
+ "tests",
41
+ "config",
42
+ ],
43
+ }
44
+
45
+
46
+ def create_base(project_path: Path, structure: str = "aiml") -> None:
47
+ dirs = _STRUCTURE_DIRS.get(structure, _STRUCTURE_DIRS["aiml"])
48
+ for d in dirs:
49
+ make_dir(project_path / d)
50
+
51
+ make_file(project_path / ".gitignore", _gitignore_content())
52
+ make_file(project_path / ".python-version", "3.11\n")
53
+
54
+ if structure == "none":
55
+ return
56
+
57
+ make_file(project_path / "app" / "main.py", "def main():\n pass\n")
58
+ make_file(project_path / "config" / "settings.py", "# Application settings\n")
59
+ make_file(project_path / ".env", "")
60
+ make_file(project_path / ".env.example", "# Copy this to .env and fill in values\n")
61
+
62
+
63
+ def _gitignore_content() -> str:
64
+ return """\
65
+ models/
66
+ .env
67
+ .venv/
68
+ __pycache__/
69
+ *.pyc
70
+ dist/
71
+ *.egg-info/
72
+ """
@@ -0,0 +1,101 @@
1
+ import re
2
+ import subprocess
3
+ import sys
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ import httpx
8
+ from rich.console import Console
9
+
10
+ console = Console()
11
+
12
+ _SKILLS_SH_API = "https://skills.sh/api"
13
+
14
+
15
+ @dataclass
16
+ class Skill:
17
+ ref: str # owner/repo format e.g. "anthropics/skills/pdf"
18
+ name: str # short name e.g. "pdf"
19
+ description: str
20
+ installs: str # display string e.g. "44.9K"
21
+
22
+
23
+ def search_skills_sh(query: str) -> list[str]:
24
+ try:
25
+ # Tee approach: pipe stdout+stderr through Python so we can both
26
+ # display and capture in one run, without letting npx touch the
27
+ # Windows console mode (which would break questionary afterwards)
28
+ proc = subprocess.Popen(
29
+ ["npx", "skills", "find", query],
30
+ shell=True,
31
+ stdout=subprocess.PIPE,
32
+ stderr=subprocess.STDOUT,
33
+ encoding="utf-8",
34
+ errors="replace",
35
+ )
36
+ lines = []
37
+ for line in proc.stdout:
38
+ sys.stdout.write(line)
39
+ sys.stdout.flush()
40
+ lines.append(line)
41
+ proc.wait()
42
+
43
+ combined = "".join(lines)
44
+ # strip ANSI codes, normalise line endings
45
+ clean = re.sub(r"\x1b(?:[@-Z\\-_]|\[[0-9;?]*[ -/]*[@-~])", "", combined)
46
+ clean = clean.replace("\r\n", "\n").replace("\r", "\n")
47
+
48
+ refs = re.findall(
49
+ r"^\s*(\S+)\s+[\d.,]+[KMBkmb]?\s+installs",
50
+ clean,
51
+ re.MULTILINE,
52
+ )
53
+ return refs
54
+ except FileNotFoundError:
55
+ console.print("npx is required. Install Node.js from https://nodejs.org")
56
+ return []
57
+
58
+
59
+ def list_top_skills() -> list[Skill]:
60
+ try:
61
+ r = httpx.get(f"{_SKILLS_SH_API}/top", timeout=8)
62
+ r.raise_for_status()
63
+ return [_parse_skill(s) for s in r.json()]
64
+ except Exception:
65
+ return []
66
+
67
+
68
+ def install_skill(ref: str, target: Path) -> None:
69
+ # ref is "owner/repo@skill-name" — split into repo + skill
70
+ if "@" in ref:
71
+ repo, skill_name = ref.split("@", 1)
72
+ cmd = ["npx", "skills", "add", repo, "--skill", skill_name, "-a", "claude-code", "-y"]
73
+ else:
74
+ repo = ref
75
+ skill_name = ref.rstrip("/").split("/")[-1]
76
+ cmd = ["npx", "skills", "add", repo, "-a", "claude-code", "-y"]
77
+
78
+ result = subprocess.run(cmd, shell=True, cwd=target)
79
+ if result.returncode != 0:
80
+ console.print(f" [red]✗[/red] Failed to install {ref} (exit {result.returncode})")
81
+
82
+
83
+ def assert_claude_dir(cwd: Path) -> Path:
84
+ claude_dir = cwd / ".claude"
85
+ if not claude_dir.exists():
86
+ console.print("\n [red]No .claude/ folder found in current directory.[/red]")
87
+ console.print(" Run [bold]dev start[/bold] and select Claude to initialise it,")
88
+ console.print(" or create .claude/ manually.\n")
89
+ sys.exit(1)
90
+ return claude_dir
91
+
92
+
93
+ def _parse_skill(data: dict) -> Skill:
94
+ ref = data.get("ref") or data.get("name", "")
95
+ name = ref.rstrip("/").split("/")[-1] if ref else ""
96
+ return Skill(
97
+ ref=ref,
98
+ name=name,
99
+ description=data.get("description", ""),
100
+ installs=data.get("installs", ""),
101
+ )
@@ -0,0 +1,115 @@
1
+ import typer
2
+ from pathlib import Path
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+ from devskills.core import prompts, generator, skills as skills_mod
6
+
7
+ app = typer.Typer(help="Scaffold AI production projects interactively.")
8
+ skills_app = typer.Typer(help="Search and install Claude Code skills from skills.sh.")
9
+ app.add_typer(skills_app, name="skills")
10
+
11
+ console = Console()
12
+
13
+ BANNER = """
14
+ ██████╗ ███████╗██╗ ██╗.
15
+ ██╔══██╗██╔════╝██║ ██║
16
+ ██║ ██║█████╗ ██║ ██║
17
+ ██║ ██║██╔══╝ ╚██╗ ██╔╝
18
+ ██████╔╝███████╗ ╚████╔╝
19
+ ╚═════╝ ╚══════╝ ╚═══╝
20
+
21
+ THE OPEN DEV KITS ECOSYSTEM
22
+ """
23
+
24
+
25
+ def show_banner() -> None:
26
+ console.print(f"[bold red]{BANNER}[/bold red]")
27
+
28
+
29
+ @app.callback()
30
+ def main():
31
+ pass
32
+
33
+
34
+ # ── dev start ────────────────────────────────────────────────────────────────
35
+
36
+ @app.command("start")
37
+ def start():
38
+ """Scaffold a new AI/ML production project interactively."""
39
+ show_banner()
40
+
41
+ try:
42
+ location = prompts.ask_location()
43
+ structure = prompts.ask_structure()
44
+ features = prompts.ask_features()
45
+ run_venv = prompts.ask_venv()
46
+
47
+ if prompts.ask_confirm(location, structure, features):
48
+ generator.generate(location, structure, features, run_venv)
49
+ except KeyboardInterrupt:
50
+ console.print("\n [dim]Cancelled.[/dim]\n")
51
+ raise typer.Exit()
52
+
53
+
54
+ # ── dev skills ────────────────────────────────────────────────────────────────
55
+
56
+ @skills_app.command("find")
57
+ def skills_find(query: str = typer.Argument(..., help="Keyword to search on skills.sh")):
58
+ """Search skills.sh by keyword and optionally install."""
59
+ refs = skills_mod.search_skills_sh(query)
60
+ if not refs:
61
+ return
62
+
63
+ selected = prompts.ask_skills_select(refs)
64
+ if not selected:
65
+ return
66
+
67
+ cwd = Path.cwd()
68
+ skills_mod.assert_claude_dir(cwd)
69
+
70
+ for ref in selected:
71
+ console.print(f" Installing [cyan]{ref}[/cyan] via npx skills add...")
72
+ skills_mod.install_skill(ref, cwd)
73
+
74
+ if len(selected) > 1:
75
+ console.print(f"\n [bold red]✓[/bold red] {len(selected)} skills installed into .claude/skills/\n")
76
+
77
+
78
+ @skills_app.command("install")
79
+ def skills_install(
80
+ refs: list[str] = typer.Argument(..., help="One or more owner/repo refs to install"),
81
+ ):
82
+ """Install one or more skills from skills.sh into .claude/skills/."""
83
+ cwd = Path.cwd()
84
+ skills_mod.assert_claude_dir(cwd)
85
+
86
+ for ref in refs:
87
+ console.print(f" Installing [cyan]{ref}[/cyan] via npx skills add...")
88
+ skills_mod.install_skill(ref, cwd)
89
+
90
+ if len(refs) > 1:
91
+ console.print(f"\n [bold red]✓[/bold red] {len(refs)} skills installed into .claude/skills/\n")
92
+
93
+
94
+ @skills_app.command("list")
95
+ def skills_list():
96
+ """Browse the top skills from the skills.sh leaderboard."""
97
+ console.print("\n [bold]Top skills on skills.sh[/bold] [dim](live from https://skills.sh)[/dim]\n")
98
+
99
+ top = skills_mod.list_top_skills()
100
+
101
+ if not top:
102
+ console.print(" [dim]Could not reach skills.sh. Check your connection.[/dim]\n")
103
+ return
104
+
105
+ table = Table(border_style="dim", show_header=True, header_style="bold red")
106
+ table.add_column("#", justify="right", style="dim")
107
+ table.add_column("Skill", style="cyan")
108
+ table.add_column("Installs", justify="right", style="dim")
109
+
110
+ for i, s in enumerate(top, 1):
111
+ table.add_row(str(i), s.ref, s.installs or "—")
112
+
113
+ console.print(table)
114
+ console.print("\n Browse all: [cyan]https://skills.sh[/cyan]")
115
+ console.print(" Search: [dim]dev skills find <query>[/dim]\n")
File without changes
@@ -0,0 +1,9 @@
1
+ from pathlib import Path
2
+ from devskills.templates.base import make_dir, make_file
3
+
4
+
5
+ def create(project_path: Path) -> None:
6
+ for folder in ["agents", "prompts", "tools", "memory"]:
7
+ dir_path = project_path / folder
8
+ make_dir(dir_path)
9
+ make_file(dir_path / ".gitkeep")
@@ -0,0 +1,11 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def make_dir(path: Path) -> None:
5
+ path.mkdir(parents=True, exist_ok=True)
6
+
7
+
8
+ def make_file(path: Path, content: str = "") -> None:
9
+ if not path.exists():
10
+ path.parent.mkdir(parents=True, exist_ok=True)
11
+ path.write_text(content, encoding="utf-8")
@@ -0,0 +1,17 @@
1
+ from pathlib import Path
2
+ from devskills.templates.helpers import make_dir, make_file
3
+
4
+
5
+ def create(project_path: Path) -> None:
6
+ claude_dir = project_path / ".claude"
7
+ make_dir(claude_dir)
8
+ make_dir(claude_dir / "skills")
9
+
10
+ make_file(
11
+ claude_dir / "CLAUDE.md",
12
+ "# Project Context\n\nDescribe this project for Claude Code here.\n",
13
+ )
14
+ make_file(
15
+ claude_dir / "AGENTS.md",
16
+ "# Agents\n\nDescribe agent roles and responsibilities here.\n",
17
+ )
@@ -0,0 +1,33 @@
1
+ from pathlib import Path
2
+ from devskills.templates.helpers import make_file
3
+
4
+
5
+ def create(project_path: Path) -> None:
6
+ make_file(project_path / "Dockerfile", _dockerfile_content())
7
+ make_file(project_path / ".dockerignore", _dockerignore_content())
8
+
9
+
10
+ def _dockerfile_content() -> str:
11
+ return """\
12
+ FROM python:3.11-slim
13
+
14
+ WORKDIR /app
15
+
16
+ COPY requirements.txt .
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ COPY . .
20
+
21
+ CMD ["python", "app/main.py"]
22
+ """
23
+
24
+
25
+ def _dockerignore_content() -> str:
26
+ return """\
27
+ models/
28
+ .env
29
+ __pycache__/
30
+ *.pyc
31
+ .venv/
32
+ .git/
33
+ """
@@ -0,0 +1,8 @@
1
+ from pathlib import Path
2
+ from devskills.templates.base import make_dir, make_file
3
+
4
+
5
+ def create(project_path: Path) -> None:
6
+ feature_dir = project_path / "features" / "example_feature"
7
+ make_dir(feature_dir)
8
+ make_file(feature_dir / "__init__.py", "# Example feature module\n")
@@ -0,0 +1,11 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def make_dir(path: Path) -> None:
5
+ path.mkdir(parents=True, exist_ok=True)
6
+
7
+
8
+ def make_file(path: Path, content: str = "") -> None:
9
+ if not path.exists():
10
+ path.parent.mkdir(parents=True, exist_ok=True)
11
+ path.write_text(content, encoding="utf-8")
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "devskills-cli"
7
+ version = "0.1.1"
8
+ description = "Interactive CLI to scaffold AI production projects using uv"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "typer>=0.12.0",
12
+ "questionary>=2.0.0",
13
+ "rich>=13.0.0",
14
+ "httpx>=0.27.0",
15
+ ]
16
+
17
+ [project.scripts]
18
+ dev = "devskills.main:app"
19
+
20
+ [tool.hatch.build.targets.wheel]
21
+ packages = ["devskills"]
@@ -0,0 +1,4 @@
1
+ typer>=0.12.0
2
+ questionary>=2.0.0
3
+ rich>=13.0.0
4
+ httpx>=0.27.0
@@ -0,0 +1,242 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.10"
4
+
5
+ [[package]]
6
+ name = "annotated-doc"
7
+ version = "0.0.4"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "anyio"
16
+ version = "4.12.1"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ dependencies = [
19
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
20
+ { name = "idna" },
21
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
22
+ ]
23
+ sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
24
+ wheels = [
25
+ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
26
+ ]
27
+
28
+ [[package]]
29
+ name = "certifi"
30
+ version = "2026.2.25"
31
+ source = { registry = "https://pypi.org/simple" }
32
+ sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
33
+ wheels = [
34
+ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
35
+ ]
36
+
37
+ [[package]]
38
+ name = "click"
39
+ version = "8.3.1"
40
+ source = { registry = "https://pypi.org/simple" }
41
+ dependencies = [
42
+ { name = "colorama", marker = "sys_platform == 'win32'" },
43
+ ]
44
+ sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
45
+ wheels = [
46
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
47
+ ]
48
+
49
+ [[package]]
50
+ name = "colorama"
51
+ version = "0.4.6"
52
+ source = { registry = "https://pypi.org/simple" }
53
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
54
+ wheels = [
55
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
56
+ ]
57
+
58
+ [[package]]
59
+ name = "devcli"
60
+ version = "0.1.0"
61
+ source = { editable = "." }
62
+ dependencies = [
63
+ { name = "httpx" },
64
+ { name = "questionary" },
65
+ { name = "rich" },
66
+ { name = "typer" },
67
+ ]
68
+
69
+ [package.metadata]
70
+ requires-dist = [
71
+ { name = "httpx", specifier = ">=0.27.0" },
72
+ { name = "questionary", specifier = ">=2.0.0" },
73
+ { name = "rich", specifier = ">=13.0.0" },
74
+ { name = "typer", specifier = ">=0.12.0" },
75
+ ]
76
+
77
+ [[package]]
78
+ name = "exceptiongroup"
79
+ version = "1.3.1"
80
+ source = { registry = "https://pypi.org/simple" }
81
+ dependencies = [
82
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
83
+ ]
84
+ sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
85
+ wheels = [
86
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
87
+ ]
88
+
89
+ [[package]]
90
+ name = "h11"
91
+ version = "0.16.0"
92
+ source = { registry = "https://pypi.org/simple" }
93
+ sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
94
+ wheels = [
95
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
96
+ ]
97
+
98
+ [[package]]
99
+ name = "httpcore"
100
+ version = "1.0.9"
101
+ source = { registry = "https://pypi.org/simple" }
102
+ dependencies = [
103
+ { name = "certifi" },
104
+ { name = "h11" },
105
+ ]
106
+ sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
107
+ wheels = [
108
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
109
+ ]
110
+
111
+ [[package]]
112
+ name = "httpx"
113
+ version = "0.28.1"
114
+ source = { registry = "https://pypi.org/simple" }
115
+ dependencies = [
116
+ { name = "anyio" },
117
+ { name = "certifi" },
118
+ { name = "httpcore" },
119
+ { name = "idna" },
120
+ ]
121
+ sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
122
+ wheels = [
123
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
124
+ ]
125
+
126
+ [[package]]
127
+ name = "idna"
128
+ version = "3.11"
129
+ source = { registry = "https://pypi.org/simple" }
130
+ sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
131
+ wheels = [
132
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
133
+ ]
134
+
135
+ [[package]]
136
+ name = "markdown-it-py"
137
+ version = "4.0.0"
138
+ source = { registry = "https://pypi.org/simple" }
139
+ dependencies = [
140
+ { name = "mdurl" },
141
+ ]
142
+ sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
143
+ wheels = [
144
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
145
+ ]
146
+
147
+ [[package]]
148
+ name = "mdurl"
149
+ version = "0.1.2"
150
+ source = { registry = "https://pypi.org/simple" }
151
+ sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
152
+ wheels = [
153
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
154
+ ]
155
+
156
+ [[package]]
157
+ name = "prompt-toolkit"
158
+ version = "3.0.52"
159
+ source = { registry = "https://pypi.org/simple" }
160
+ dependencies = [
161
+ { name = "wcwidth" },
162
+ ]
163
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
164
+ wheels = [
165
+ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
166
+ ]
167
+
168
+ [[package]]
169
+ name = "pygments"
170
+ version = "2.19.2"
171
+ source = { registry = "https://pypi.org/simple" }
172
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
173
+ wheels = [
174
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
175
+ ]
176
+
177
+ [[package]]
178
+ name = "questionary"
179
+ version = "2.1.1"
180
+ source = { registry = "https://pypi.org/simple" }
181
+ dependencies = [
182
+ { name = "prompt-toolkit" },
183
+ ]
184
+ sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" }
185
+ wheels = [
186
+ { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" },
187
+ ]
188
+
189
+ [[package]]
190
+ name = "rich"
191
+ version = "14.3.3"
192
+ source = { registry = "https://pypi.org/simple" }
193
+ dependencies = [
194
+ { name = "markdown-it-py" },
195
+ { name = "pygments" },
196
+ ]
197
+ sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
198
+ wheels = [
199
+ { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
200
+ ]
201
+
202
+ [[package]]
203
+ name = "shellingham"
204
+ version = "1.5.4"
205
+ source = { registry = "https://pypi.org/simple" }
206
+ sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
207
+ wheels = [
208
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
209
+ ]
210
+
211
+ [[package]]
212
+ name = "typer"
213
+ version = "0.24.1"
214
+ source = { registry = "https://pypi.org/simple" }
215
+ dependencies = [
216
+ { name = "annotated-doc" },
217
+ { name = "click" },
218
+ { name = "rich" },
219
+ { name = "shellingham" },
220
+ ]
221
+ sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" }
222
+ wheels = [
223
+ { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" },
224
+ ]
225
+
226
+ [[package]]
227
+ name = "typing-extensions"
228
+ version = "4.15.0"
229
+ source = { registry = "https://pypi.org/simple" }
230
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
231
+ wheels = [
232
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
233
+ ]
234
+
235
+ [[package]]
236
+ name = "wcwidth"
237
+ version = "0.6.0"
238
+ source = { registry = "https://pypi.org/simple" }
239
+ sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" }
240
+ wheels = [
241
+ { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" },
242
+ ]