create-ailab 0.1.0__py3-none-any.whl

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.
Files changed (178) hide show
  1. create_ai/__init__.py +13 -0
  2. create_ai/__main__.py +8 -0
  3. create_ai/cli/__init__.py +1 -0
  4. create_ai/cli/app.py +82 -0
  5. create_ai/cli/commands/__init__.py +1 -0
  6. create_ai/cli/commands/add.py +28 -0
  7. create_ai/cli/commands/doctor.py +62 -0
  8. create_ai/cli/commands/list_types.py +28 -0
  9. create_ai/cli/commands/new.py +346 -0
  10. create_ai/cli/console.py +36 -0
  11. create_ai/cli/prompts.py +184 -0
  12. create_ai/cli/reporters.py +31 -0
  13. create_ai/core/__init__.py +6 -0
  14. create_ai/core/configuration.py +166 -0
  15. create_ai/core/dependencies.py +128 -0
  16. create_ai/core/generator.py +195 -0
  17. create_ai/core/project.py +100 -0
  18. create_ai/core/registry.py +98 -0
  19. create_ai/core/templating.py +130 -0
  20. create_ai/core/validation.py +22 -0
  21. create_ai/errors.py +46 -0
  22. create_ai/generators/__init__.py +18 -0
  23. create_ai/generators/base.py +94 -0
  24. create_ai/generators/computer_vision.py +34 -0
  25. create_ai/generators/deep_learning.py +31 -0
  26. create_ai/generators/llm.py +37 -0
  27. create_ai/generators/ml.py +33 -0
  28. create_ai/integrations/__init__.py +20 -0
  29. create_ai/integrations/base.py +58 -0
  30. create_ai/integrations/docker.py +18 -0
  31. create_ai/integrations/git.py +55 -0
  32. create_ai/integrations/jupyter.py +11 -0
  33. create_ai/integrations/package_manager.py +88 -0
  34. create_ai/integrations/python.py +33 -0
  35. create_ai/integrations/uv.py +44 -0
  36. create_ai/py.typed +0 -0
  37. create_ai/templates/common/.dockerignore.j2 +22 -0
  38. create_ai/templates/common/.env.example.j2 +14 -0
  39. create_ai/templates/common/.github/workflows/ci.yml.j2 +38 -0
  40. create_ai/templates/common/.gitignore.j2 +47 -0
  41. create_ai/templates/common/.pre-commit-config.yaml.j2 +15 -0
  42. create_ai/templates/common/Dockerfile.j2 +27 -0
  43. create_ai/templates/common/LICENSE.j2 +21 -0
  44. create_ai/templates/common/Makefile.j2 +29 -0
  45. create_ai/templates/common/README.md.j2 +84 -0
  46. create_ai/templates/common/configs/config.yaml.j2 +9 -0
  47. create_ai/templates/common/notebooks/01-exploration.ipynb.j2 +29 -0
  48. create_ai/templates/common/pyproject.toml.j2 +58 -0
  49. create_ai/templates/common/src/{{package_name}}/__init__.py.j2 +5 -0
  50. create_ai/templates/common/src/{{package_name}}/config.py.j2 +35 -0
  51. create_ai/templates/common/src/{{package_name}}/tracking.py.j2 +63 -0
  52. create_ai/templates/common/tests/conftest.py.j2 +13 -0
  53. create_ai/templates/computer_vision/pytorch/configs/model.yaml.j2 +5 -0
  54. create_ai/templates/computer_vision/pytorch/configs/training.yaml.j2 +10 -0
  55. create_ai/templates/computer_vision/pytorch/evaluate.py.j2 +31 -0
  56. create_ai/templates/computer_vision/pytorch/inference.py.j2 +32 -0
  57. create_ai/templates/computer_vision/pytorch/src/{{package_name}}/datasets/__init__.py.j2 +1 -0
  58. create_ai/templates/computer_vision/pytorch/src/{{package_name}}/datasets/folder.py.j2 +27 -0
  59. create_ai/templates/computer_vision/pytorch/src/{{package_name}}/datasets/synthetic.py.j2 +58 -0
  60. create_ai/templates/computer_vision/pytorch/src/{{package_name}}/evaluation/__init__.py.j2 +1 -0
  61. create_ai/templates/computer_vision/pytorch/src/{{package_name}}/evaluation/evaluate.py.j2 +22 -0
  62. create_ai/templates/computer_vision/pytorch/src/{{package_name}}/inference/__init__.py.j2 +1 -0
  63. create_ai/templates/computer_vision/pytorch/src/{{package_name}}/inference/predict.py.j2 +27 -0
  64. create_ai/templates/computer_vision/pytorch/src/{{package_name}}/models/__init__.py.j2 +1 -0
  65. create_ai/templates/computer_vision/pytorch/src/{{package_name}}/models/cnn.py.j2 +49 -0
  66. create_ai/templates/computer_vision/pytorch/src/{{package_name}}/training/__init__.py.j2 +1 -0
  67. create_ai/templates/computer_vision/pytorch/src/{{package_name}}/training/trainer.py.j2 +81 -0
  68. create_ai/templates/computer_vision/pytorch/src/{{package_name}}/transforms/__init__.py.j2 +5 -0
  69. create_ai/templates/computer_vision/pytorch/src/{{package_name}}/transforms/pipelines.py.j2 +31 -0
  70. create_ai/templates/computer_vision/pytorch/tests/test_model.py.j2 +15 -0
  71. create_ai/templates/computer_vision/pytorch/tests/test_trainer.py.j2 +23 -0
  72. create_ai/templates/computer_vision/pytorch/tests/test_transforms.py.j2 +14 -0
  73. create_ai/templates/computer_vision/pytorch/train.py.j2 +53 -0
  74. create_ai/templates/computer_vision/tensorflow/configs/model.yaml.j2 +5 -0
  75. create_ai/templates/computer_vision/tensorflow/configs/training.yaml.j2 +9 -0
  76. create_ai/templates/computer_vision/tensorflow/evaluate.py.j2 +30 -0
  77. create_ai/templates/computer_vision/tensorflow/inference.py.j2 +33 -0
  78. create_ai/templates/computer_vision/tensorflow/src/{{package_name}}/datasets/__init__.py.j2 +1 -0
  79. create_ai/templates/computer_vision/tensorflow/src/{{package_name}}/datasets/synthetic.py.j2 +43 -0
  80. create_ai/templates/computer_vision/tensorflow/src/{{package_name}}/evaluation/__init__.py.j2 +1 -0
  81. create_ai/templates/computer_vision/tensorflow/src/{{package_name}}/evaluation/evaluate.py.j2 +11 -0
  82. create_ai/templates/computer_vision/tensorflow/src/{{package_name}}/inference/__init__.py.j2 +1 -0
  83. create_ai/templates/computer_vision/tensorflow/src/{{package_name}}/inference/predict.py.j2 +12 -0
  84. create_ai/templates/computer_vision/tensorflow/src/{{package_name}}/models/__init__.py.j2 +1 -0
  85. create_ai/templates/computer_vision/tensorflow/src/{{package_name}}/models/cnn.py.j2 +36 -0
  86. create_ai/templates/computer_vision/tensorflow/src/{{package_name}}/training/__init__.py.j2 +1 -0
  87. create_ai/templates/computer_vision/tensorflow/src/{{package_name}}/training/trainer.py.j2 +28 -0
  88. create_ai/templates/computer_vision/tensorflow/src/{{package_name}}/transforms/__init__.py.j2 +18 -0
  89. create_ai/templates/computer_vision/tensorflow/tests/test_model.py.j2 +16 -0
  90. create_ai/templates/computer_vision/tensorflow/tests/test_trainer.py.j2 +24 -0
  91. create_ai/templates/computer_vision/tensorflow/train.py.j2 +48 -0
  92. create_ai/templates/deep_learning/pytorch/configs/model.yaml.j2 +6 -0
  93. create_ai/templates/deep_learning/pytorch/configs/training.yaml.j2 +10 -0
  94. create_ai/templates/deep_learning/pytorch/evaluate.py.j2 +31 -0
  95. create_ai/templates/deep_learning/pytorch/inference.py.j2 +30 -0
  96. create_ai/templates/deep_learning/pytorch/src/{{package_name}}/datasets/__init__.py.j2 +1 -0
  97. create_ai/templates/deep_learning/pytorch/src/{{package_name}}/datasets/synthetic.py.j2 +58 -0
  98. create_ai/templates/deep_learning/pytorch/src/{{package_name}}/evaluation/__init__.py.j2 +1 -0
  99. create_ai/templates/deep_learning/pytorch/src/{{package_name}}/evaluation/evaluate.py.j2 +22 -0
  100. create_ai/templates/deep_learning/pytorch/src/{{package_name}}/inference/__init__.py.j2 +1 -0
  101. create_ai/templates/deep_learning/pytorch/src/{{package_name}}/inference/predict.py.j2 +27 -0
  102. create_ai/templates/deep_learning/pytorch/src/{{package_name}}/losses/__init__.py.j2 +17 -0
  103. create_ai/templates/deep_learning/pytorch/src/{{package_name}}/models/__init__.py.j2 +1 -0
  104. create_ai/templates/deep_learning/pytorch/src/{{package_name}}/models/mlp.py.j2 +39 -0
  105. create_ai/templates/deep_learning/pytorch/src/{{package_name}}/training/__init__.py.j2 +1 -0
  106. create_ai/templates/deep_learning/pytorch/src/{{package_name}}/training/trainer.py.j2 +84 -0
  107. create_ai/templates/deep_learning/pytorch/tests/test_inference.py.j2 +16 -0
  108. create_ai/templates/deep_learning/pytorch/tests/test_model.py.j2 +15 -0
  109. create_ai/templates/deep_learning/pytorch/tests/test_trainer.py.j2 +27 -0
  110. create_ai/templates/deep_learning/pytorch/train.py.j2 +55 -0
  111. create_ai/templates/deep_learning/tensorflow/configs/model.yaml.j2 +6 -0
  112. create_ai/templates/deep_learning/tensorflow/configs/training.yaml.j2 +9 -0
  113. create_ai/templates/deep_learning/tensorflow/evaluate.py.j2 +30 -0
  114. create_ai/templates/deep_learning/tensorflow/inference.py.j2 +31 -0
  115. create_ai/templates/deep_learning/tensorflow/src/{{package_name}}/datasets/__init__.py.j2 +1 -0
  116. create_ai/templates/deep_learning/tensorflow/src/{{package_name}}/datasets/synthetic.py.j2 +41 -0
  117. create_ai/templates/deep_learning/tensorflow/src/{{package_name}}/evaluation/__init__.py.j2 +1 -0
  118. create_ai/templates/deep_learning/tensorflow/src/{{package_name}}/evaluation/evaluate.py.j2 +11 -0
  119. create_ai/templates/deep_learning/tensorflow/src/{{package_name}}/inference/__init__.py.j2 +1 -0
  120. create_ai/templates/deep_learning/tensorflow/src/{{package_name}}/inference/predict.py.j2 +12 -0
  121. create_ai/templates/deep_learning/tensorflow/src/{{package_name}}/models/__init__.py.j2 +1 -0
  122. create_ai/templates/deep_learning/tensorflow/src/{{package_name}}/models/mlp.py.j2 +31 -0
  123. create_ai/templates/deep_learning/tensorflow/src/{{package_name}}/training/__init__.py.j2 +1 -0
  124. create_ai/templates/deep_learning/tensorflow/src/{{package_name}}/training/trainer.py.j2 +28 -0
  125. create_ai/templates/deep_learning/tensorflow/tests/test_model.py.j2 +16 -0
  126. create_ai/templates/deep_learning/tensorflow/tests/test_trainer.py.j2 +24 -0
  127. create_ai/templates/deep_learning/tensorflow/train.py.j2 +48 -0
  128. create_ai/templates/llm/configs/evaluation.yaml.j2 +5 -0
  129. create_ai/templates/llm/configs/model.yaml.j2 +4 -0
  130. create_ai/templates/llm/configs/training.yaml.j2 +8 -0
  131. create_ai/templates/llm/data/sample.jsonl +8 -0
  132. create_ai/templates/llm/evaluate.py.j2 +40 -0
  133. create_ai/templates/llm/inference.py.j2 +34 -0
  134. create_ai/templates/llm/src/{{package_name}}/datasets/__init__.py.j2 +1 -0
  135. create_ai/templates/llm/src/{{package_name}}/datasets/jsonl.py.j2 +34 -0
  136. create_ai/templates/llm/src/{{package_name}}/evaluation/__init__.py.j2 +1 -0
  137. create_ai/templates/llm/src/{{package_name}}/evaluation/perplexity.py.j2 +26 -0
  138. create_ai/templates/llm/src/{{package_name}}/inference/__init__.py.j2 +1 -0
  139. create_ai/templates/llm/src/{{package_name}}/inference/generate.py.j2 +27 -0
  140. create_ai/templates/llm/src/{{package_name}}/models/__init__.py.j2 +1 -0
  141. create_ai/templates/llm/src/{{package_name}}/models/loader.py.j2 +22 -0
  142. create_ai/templates/llm/src/{{package_name}}/prompts/__init__.py.j2 +1 -0
  143. create_ai/templates/llm/src/{{package_name}}/prompts/templates.py.j2 +29 -0
  144. create_ai/templates/llm/src/{{package_name}}/training/__init__.py.j2 +1 -0
  145. create_ai/templates/llm/src/{{package_name}}/training/finetune.py.j2 +42 -0
  146. create_ai/templates/llm/src/{{package_name}}/utils/__init__.py.j2 +1 -0
  147. create_ai/templates/llm/src/{{package_name}}/utils/seeding.py.j2 +25 -0
  148. create_ai/templates/llm/tests/test_config.py.j2 +10 -0
  149. create_ai/templates/llm/tests/test_datasets.py.j2 +20 -0
  150. create_ai/templates/llm/tests/test_prompts.py.j2 +17 -0
  151. create_ai/templates/llm/train.py.j2 +60 -0
  152. create_ai/templates/ml/configs/config.yaml.j2 +18 -0
  153. create_ai/templates/ml/evaluate.py.j2 +33 -0
  154. create_ai/templates/ml/predict.py.j2 +33 -0
  155. create_ai/templates/ml/src/{{package_name}}/data/__init__.py.j2 +1 -0
  156. create_ai/templates/ml/src/{{package_name}}/data/dataset.py.j2 +56 -0
  157. create_ai/templates/ml/src/{{package_name}}/evaluation/__init__.py.j2 +1 -0
  158. create_ai/templates/ml/src/{{package_name}}/evaluation/metrics.py.j2 +22 -0
  159. create_ai/templates/ml/src/{{package_name}}/features/__init__.py.j2 +1 -0
  160. create_ai/templates/ml/src/{{package_name}}/features/preprocessing.py.j2 +12 -0
  161. create_ai/templates/ml/src/{{package_name}}/inference/__init__.py.j2 +1 -0
  162. create_ai/templates/ml/src/{{package_name}}/inference/predict.py.j2 +21 -0
  163. create_ai/templates/ml/src/{{package_name}}/models/__init__.py.j2 +1 -0
  164. create_ai/templates/ml/src/{{package_name}}/models/model.py.j2 +36 -0
  165. create_ai/templates/ml/src/{{package_name}}/pipeline.py.j2 +56 -0
  166. create_ai/templates/ml/tests/test_data.py.j2 +21 -0
  167. create_ai/templates/ml/tests/test_model.py.j2 +24 -0
  168. create_ai/templates/ml/tests/test_pipeline.py.j2 +19 -0
  169. create_ai/templates/ml/train.py.j2 +23 -0
  170. create_ai/utils/__init__.py +1 -0
  171. create_ai/utils/filesystem.py +68 -0
  172. create_ai/utils/shell.py +111 -0
  173. create_ai/utils/validation.py +95 -0
  174. create_ailab-0.1.0.dist-info/METADATA +299 -0
  175. create_ailab-0.1.0.dist-info/RECORD +178 -0
  176. create_ailab-0.1.0.dist-info/WHEEL +4 -0
  177. create_ailab-0.1.0.dist-info/entry_points.txt +2 -0
  178. create_ailab-0.1.0.dist-info/licenses/LICENSE +21 -0
create_ai/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """create-ai: the Vite-style project scaffolder for AI/ML engineering.
2
+
3
+ The public surface is intentionally small. Most consumers only need
4
+ :data:`__version__` and the CLI entrypoint in :mod:`create_ai.cli.app`.
5
+ Programmatic users can build a :class:`~create_ai.core.configuration.ProjectConfig`
6
+ and hand it to :class:`~create_ai.core.generator.GenerationEngine`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ __version__ = "0.1.0"
12
+
13
+ __all__ = ["__version__"]
create_ai/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Enable ``python -m create_ai`` as an alias for the ``create-ai`` command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from create_ai.cli.app import app
6
+
7
+ if __name__ == "__main__": # pragma: no cover - thin shim
8
+ app()
@@ -0,0 +1 @@
1
+ """Command-line interface - a thin adapter over :mod:`create_ai.core`."""
create_ai/cli/app.py ADDED
@@ -0,0 +1,82 @@
1
+ """Typer application wiring.
2
+
3
+ ``create-ai`` behaves like ``npm create vite``:
4
+
5
+ * ``create-ai`` -> interactive project creation
6
+ * ``create-ai my-project`` -> ``create-ai new my-project``
7
+ * ``create-ai new my-project`` -> same, explicit
8
+ * ``create-ai doctor`` / ``list`` / ``add``
9
+
10
+ The "bare name is really ``new``" behaviour is a tiny ``TyperGroup`` subclass
11
+ that rewrites the argument list *before* command resolution, so there is exactly
12
+ one implementation of project creation and no dependency on click internals.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Any
18
+
19
+ import typer
20
+ from typer.core import TyperGroup
21
+
22
+ from create_ai import __version__
23
+ from create_ai.cli.commands.add import add
24
+ from create_ai.cli.commands.doctor import doctor
25
+ from create_ai.cli.commands.list_types import list_types
26
+ from create_ai.cli.commands.new import new, run_default
27
+
28
+ _DEFAULT_COMMAND = "new"
29
+
30
+
31
+ class DefaultCommandGroup(TyperGroup):
32
+ """Route an unknown first token to ``new`` (so ``create-ai <name>`` works)."""
33
+
34
+ def resolve_command(self, ctx: Any, args: list[str]) -> Any:
35
+ if args and not args[0].startswith("-") and args[0] not in self.commands:
36
+ args = [_DEFAULT_COMMAND, *args]
37
+ return super().resolve_command(ctx, args)
38
+
39
+
40
+ app = typer.Typer(
41
+ cls=DefaultCommandGroup,
42
+ add_completion=False,
43
+ no_args_is_help=False,
44
+ rich_markup_mode="rich",
45
+ help="Scaffold production-ready AI/ML projects - the Vite experience for AI.",
46
+ )
47
+
48
+ app.command("new")(new)
49
+ app.command("doctor")(doctor)
50
+ app.command("list")(list_types)
51
+ app.command("add")(add)
52
+
53
+
54
+ def _version_callback(value: bool) -> None:
55
+ if value:
56
+ typer.echo(f"create-ai {__version__}")
57
+ raise typer.Exit
58
+
59
+
60
+ @app.callback(invoke_without_command=True)
61
+ def _root(
62
+ ctx: typer.Context,
63
+ version: bool = typer.Option(
64
+ False,
65
+ "--version",
66
+ "-V",
67
+ help="Show the create-ai version and exit.",
68
+ callback=_version_callback,
69
+ is_eager=True,
70
+ ),
71
+ ) -> None:
72
+ if ctx.invoked_subcommand is None:
73
+ # Bare `create-ai` -> interactive `new` (with all-default arguments).
74
+ run_default()
75
+
76
+
77
+ def main() -> None: # pragma: no cover - console-script shim
78
+ app()
79
+
80
+
81
+ if __name__ == "__main__": # pragma: no cover
82
+ app()
@@ -0,0 +1 @@
1
+ """Sub-command implementations for the ``create-ai`` CLI."""
@@ -0,0 +1,28 @@
1
+ """``create-ai add`` - placeholder for the v0.2 "extend an existing project" flow.
2
+
3
+ The command exists now so the CLI surface is stable and discoverable; the
4
+ implementation lands with the plugin architecture in v0.2.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import typer
10
+
11
+ from create_ai.cli.console import console
12
+
13
+
14
+ def add(
15
+ component: str | None = typer.Argument(
16
+ None, help="Component to add (e.g. mlflow, docker, robotics)."
17
+ ),
18
+ ) -> None:
19
+ """Add a component to an existing project (coming in v0.2)."""
20
+
21
+ console.print(
22
+ "[yellow]`create-ai add` is planned for v0.2.[/yellow]\n"
23
+ "It will layer integrations (MLflow, Docker, W&B) and new domains "
24
+ "(Robotics, ROS 2, RL) onto an existing project."
25
+ )
26
+ if component:
27
+ console.print(f"[dim]Requested component: {component}[/dim]")
28
+ raise typer.Exit(code=0)
@@ -0,0 +1,62 @@
1
+ """``create-ai doctor`` - report on the external tools create-ai can use."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass
7
+
8
+ from create_ai.cli.console import console
9
+ from create_ai.integrations import docker as docker_integration
10
+ from create_ai.integrations import git as git_integration
11
+ from create_ai.integrations import python as python_integration
12
+ from create_ai.utils.shell import tool_version
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Check:
17
+ """One environment probe. ``probe`` returns a version string or ``None``."""
18
+
19
+ label: str
20
+ probe: Callable[[], str | None]
21
+ required: bool = False
22
+
23
+
24
+ # Ordered; add new probes here - the command body never changes.
25
+ CHECKS: tuple[Check, ...] = (
26
+ Check("Python", lambda: python_integration.current_version(), required=True),
27
+ Check("Git", git_integration.version),
28
+ Check("uv", lambda: tool_version("uv")),
29
+ Check("Docker", docker_integration.version),
30
+ )
31
+
32
+
33
+ def _clean(value: str, label: str) -> str:
34
+ """Trim a redundant leading ``"<tool> version"`` from a version string."""
35
+
36
+ text = value.strip()
37
+ for prefix in (f"{label.lower()} version", label.lower(), "version"):
38
+ if text.lower().startswith(prefix):
39
+ text = text[len(prefix) :].strip()
40
+ return text or "(version unknown)"
41
+
42
+
43
+ def doctor() -> None:
44
+ """Check for Python, Git, uv and Docker and print a short report."""
45
+
46
+ console.print("[bold]create-ai doctor[/bold]\n")
47
+ missing_required = False
48
+ for check in CHECKS:
49
+ version = check.probe()
50
+ if version:
51
+ console.print(f"[green]✔[/green] {check.label} {_clean(version, check.label)}")
52
+ elif check.required:
53
+ missing_required = True
54
+ console.print(f"[red]✗[/red] {check.label} not found (required)")
55
+ else:
56
+ console.print(f"[dim]○ {check.label} not installed[/dim]")
57
+
58
+ console.print()
59
+ if missing_required:
60
+ console.print("[red]Some required tools are missing.[/red]")
61
+ raise SystemExit(1)
62
+ console.print("[green]All required tools are available.[/green]")
@@ -0,0 +1,28 @@
1
+ """``create-ai list`` - show the registered project types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.table import Table
6
+
7
+ from create_ai.cli.console import console
8
+ from create_ai.generators import generator_registry
9
+
10
+
11
+ def list_types() -> None:
12
+ """List every project type create-ai can scaffold."""
13
+
14
+ table = Table(title="Available project types", title_justify="left")
15
+ table.add_column("key", style="bold cyan")
16
+ table.add_column("name")
17
+ table.add_column("frameworks", style="dim")
18
+ table.add_column("description")
19
+
20
+ for key, generator_cls in generator_registry.items():
21
+ frameworks = ", ".join(sorted(f.value for f in generator_cls.supported_frameworks))
22
+ table.add_row(
23
+ key,
24
+ generator_cls.project_type.label,
25
+ frameworks,
26
+ generator_cls.summary,
27
+ )
28
+ console.print(table)
@@ -0,0 +1,346 @@
1
+ """``create-ai new`` - create a project (also the default, bare ``create-ai``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import TypeVar
8
+
9
+ import typer
10
+ from rich.panel import Panel
11
+
12
+ from create_ai.cli.console import console, err_console, print_error, summary_table
13
+ from create_ai.cli.prompts import run_questionnaire, stdin_is_interactive
14
+ from create_ai.cli.reporters import RichReporter
15
+ from create_ai.core.configuration import (
16
+ DEFAULT_PYTHON_VERSION,
17
+ EnvironmentManager,
18
+ ExperimentTracking,
19
+ Framework,
20
+ Linting,
21
+ ProjectConfig,
22
+ ProjectType,
23
+ Testing,
24
+ )
25
+ from create_ai.core.generator import GenerationEngine, GenerationResult
26
+ from create_ai.core.project import Project
27
+ from create_ai.core.validation import validate_config
28
+ from create_ai.errors import CreateAIError
29
+ from create_ai.generators import generator_registry
30
+ from create_ai.integrations import resolve_environment_manager
31
+ from create_ai.utils.filesystem import is_empty_dir
32
+ from create_ai.utils.validation import resolve_destination
33
+
34
+ _E = TypeVar("_E", bound=Enum)
35
+
36
+
37
+ def _coerce(enum_cls: type[_E], value: str, flag: str) -> _E:
38
+ try:
39
+ return enum_cls(value)
40
+ except ValueError:
41
+ allowed = ", ".join(m.value for m in enum_cls)
42
+ raise CreateAIError(
43
+ f"Invalid value {value!r} for {flag}.", hint=f"Choose one of: {allowed}."
44
+ ) from None
45
+
46
+
47
+ def _config_from_flags(
48
+ *,
49
+ name: str,
50
+ project_type: str | None,
51
+ python_version: str | None,
52
+ framework: str | None,
53
+ manager: str | None,
54
+ testing: str | None,
55
+ linting: str | None,
56
+ tracking: str | None,
57
+ docker: bool | None,
58
+ jupyter: bool | None,
59
+ git: bool | None,
60
+ ) -> ProjectConfig:
61
+ ptype = _coerce(ProjectType, project_type or ProjectType.MACHINE_LEARNING.value, "--type")
62
+ if project_type is None:
63
+ console.print("[dim]No --type given; defaulting to Machine Learning.[/dim]")
64
+ fw_value = framework or generator_registry.get(ptype.value).default_framework.value
65
+ return ProjectConfig(
66
+ name=name,
67
+ project_type=ptype,
68
+ python_version=python_version or DEFAULT_PYTHON_VERSION,
69
+ framework=_coerce(Framework, fw_value, "--framework"),
70
+ environment_manager=_coerce(EnvironmentManager, manager or "uv", "--manager"),
71
+ testing=_coerce(Testing, testing or "pytest", "--testing"),
72
+ linting=_coerce(Linting, linting or "ruff", "--linting"),
73
+ experiment_tracking=_coerce(ExperimentTracking, tracking or "none", "--tracking"),
74
+ docker=False if docker is None else docker,
75
+ jupyter=False if jupyter is None else jupyter,
76
+ git=True if git is None else git,
77
+ )
78
+
79
+
80
+ def _resolve_existing_directory(config: ProjectConfig, parent: Path) -> tuple[Path, ProjectConfig]:
81
+ """Interactively deal with a non-empty target directory (never deletes)."""
82
+
83
+ import questionary
84
+
85
+ while True:
86
+ destination, _ = resolve_destination(config.name, parent=parent)
87
+ if is_empty_dir(destination):
88
+ return destination, config
89
+ choice = questionary.select(
90
+ f'Directory "{destination}" already exists and is not empty.',
91
+ choices=["Cancel", "Choose another name"],
92
+ qmark="?",
93
+ ).ask()
94
+ if choice != "Choose another name":
95
+ raise CreateAIError("Cancelled - existing directory left untouched.")
96
+ new_name = questionary.text("New project name:", qmark="?").ask()
97
+ if not new_name:
98
+ raise CreateAIError("Cancelled.")
99
+ config = config.model_copy(update={"name": new_name.strip()})
100
+
101
+
102
+ def _print_next_steps(result: GenerationResult, *, installed: bool, dry_run: bool) -> None:
103
+ project = result.project
104
+ lines: list[str] = []
105
+ rel = project.destination.name
106
+ lines.append(f" cd {rel}")
107
+ if installed:
108
+ manager = resolve_environment_manager(project.destination, project.config)
109
+ lines.append(f" {manager.activation_hint()}")
110
+ else:
111
+ setup = (
112
+ "uv sync"
113
+ if project.config.environment_manager is EnvironmentManager.UV
114
+ else ("python -m venv .venv && source .venv/bin/activate && pip install -e .")
115
+ )
116
+ lines.append(f" {setup}")
117
+ entry = project.generator.entrypoints[0]
118
+ lines.append(f" {project.config.run_prefix}python {entry}")
119
+
120
+ if result.warnings:
121
+ for warning in result.warnings:
122
+ console.print(f"[yellow]![/yellow] {warning}")
123
+
124
+ header = (
125
+ "[bold]Dry run complete[/bold] - no files were written."
126
+ if dry_run
127
+ else "[bold green]🎉 Project created successfully![/bold green]"
128
+ )
129
+ body = "\n".join([header, "", "Next steps:", *lines])
130
+ console.print()
131
+ console.print(Panel(body, border_style="green", expand=False))
132
+
133
+
134
+ def _run(
135
+ name: str | None,
136
+ *,
137
+ project_type: str | None,
138
+ python_version: str | None,
139
+ framework: str | None,
140
+ manager: str | None,
141
+ testing: str | None,
142
+ linting: str | None,
143
+ tracking: str | None,
144
+ docker: bool | None,
145
+ jupyter: bool | None,
146
+ git: bool | None,
147
+ yes: bool,
148
+ install: bool,
149
+ force: bool,
150
+ dry_run: bool,
151
+ output_dir: Path | None,
152
+ ) -> None:
153
+ parent = (output_dir or Path.cwd()).resolve()
154
+ interactive = not yes and stdin_is_interactive()
155
+
156
+ if interactive and _needs_prompt(name, project_type):
157
+ config = run_questionnaire(
158
+ name=name,
159
+ project_type=project_type,
160
+ python_version=python_version,
161
+ framework=framework,
162
+ environment_manager=manager,
163
+ testing=testing,
164
+ linting=linting,
165
+ experiment_tracking=tracking,
166
+ docker=docker,
167
+ jupyter=jupyter,
168
+ git=git,
169
+ )
170
+ else:
171
+ if name is None:
172
+ raise CreateAIError(
173
+ "A project name is required.",
174
+ hint="Run `create-ai <name> --type <type>` or use interactive mode.",
175
+ )
176
+ config = _config_from_flags(
177
+ name=name,
178
+ project_type=project_type,
179
+ python_version=python_version,
180
+ framework=framework,
181
+ manager=manager,
182
+ testing=testing,
183
+ linting=linting,
184
+ tracking=tracking,
185
+ docker=docker,
186
+ jupyter=jupyter,
187
+ git=git,
188
+ )
189
+
190
+ validate_config(config)
191
+
192
+ if interactive:
193
+ destination, config = _resolve_existing_directory(config, parent)
194
+ else:
195
+ destination, _ = resolve_destination(config.name, parent=parent)
196
+
197
+ project = Project.build(config, destination=destination)
198
+
199
+ if interactive and not yes:
200
+ console.print()
201
+ plan = Panel(summary_table(config.summary_rows()), title="Project plan", expand=False)
202
+ console.print(plan)
203
+ import questionary
204
+
205
+ if not questionary.confirm("Create this project?", default=True, qmark="?").ask():
206
+ raise CreateAIError("Cancelled.")
207
+ console.print()
208
+
209
+ engine = GenerationEngine(
210
+ project,
211
+ reporter=RichReporter(console),
212
+ install=install and not dry_run,
213
+ init_git=config.git,
214
+ force=force,
215
+ dry_run=dry_run,
216
+ )
217
+ result = engine.run()
218
+ _print_next_steps(
219
+ result,
220
+ installed=install and not dry_run and _env_created(result),
221
+ dry_run=dry_run,
222
+ )
223
+
224
+
225
+ def _env_created(result: GenerationResult) -> bool:
226
+ return any(
227
+ title == "Creating environment" and outcome.ok and not outcome.skipped
228
+ for title, outcome in result.steps
229
+ )
230
+
231
+
232
+ def _needs_prompt(name: str | None, project_type: str | None) -> bool:
233
+ return name is None or project_type is None
234
+
235
+
236
+ def _guarded(
237
+ name: str | None,
238
+ *,
239
+ project_type: str | None = None,
240
+ python_version: str | None = None,
241
+ framework: str | None = None,
242
+ manager: str | None = None,
243
+ testing: str | None = None,
244
+ linting: str | None = None,
245
+ tracking: str | None = None,
246
+ docker: bool | None = None,
247
+ jupyter: bool | None = None,
248
+ git: bool | None = None,
249
+ yes: bool = False,
250
+ install: bool = True,
251
+ force: bool = False,
252
+ dry_run: bool = False,
253
+ output_dir: Path | None = None,
254
+ debug: bool = False,
255
+ ) -> None:
256
+ """Run the create flow, converting expected errors into a clean exit."""
257
+
258
+ try:
259
+ _run(
260
+ name,
261
+ project_type=project_type,
262
+ python_version=python_version,
263
+ framework=framework,
264
+ manager=manager,
265
+ testing=testing,
266
+ linting=linting,
267
+ tracking=tracking,
268
+ docker=docker,
269
+ jupyter=jupyter,
270
+ git=git,
271
+ yes=yes,
272
+ install=install,
273
+ force=force,
274
+ dry_run=dry_run,
275
+ output_dir=output_dir,
276
+ )
277
+ except CreateAIError as exc:
278
+ print_error(exc)
279
+ raise typer.Exit(code=1) from None
280
+ except typer.Exit:
281
+ raise
282
+ except Exception as exc: # pragma: no cover - safety net
283
+ if debug:
284
+ raise
285
+ err_console.print(
286
+ f"[red]✗ unexpected error:[/red] {exc}\n[dim]Re-run with --debug for a traceback.[/dim]"
287
+ )
288
+ raise typer.Exit(code=1) from None
289
+
290
+
291
+ def run_default() -> None:
292
+ """Entry point for bare ``create-ai`` (no subcommand): interactive creation."""
293
+
294
+ _guarded(None)
295
+
296
+
297
+ def new(
298
+ name: str | None = typer.Argument(None, help="Project name / directory to create."),
299
+ project_type: str | None = typer.Option(
300
+ None, "--type", "-t", help="ml | deep-learning | llm | computer-vision"
301
+ ),
302
+ python_version: str | None = typer.Option(None, "--python", help="CPython version, e.g. 3.12"),
303
+ framework: str | None = typer.Option(
304
+ None, "--framework", "-f", help="pytorch | tensorflow | sklearn | none"
305
+ ),
306
+ manager: str | None = typer.Option(None, "--manager", "-m", help="uv | venv"),
307
+ testing: str | None = typer.Option(None, "--testing", help="pytest | none"),
308
+ linting: str | None = typer.Option(None, "--linting", help="ruff | ruff+black | none"),
309
+ tracking: str | None = typer.Option(None, "--tracking", help="mlflow | wandb | none"),
310
+ docker: bool | None = typer.Option(None, "--docker/--no-docker", help="Generate Docker files."),
311
+ jupyter: bool | None = typer.Option(
312
+ None, "--jupyter/--no-jupyter", help="Add notebooks/ and Jupyter deps."
313
+ ),
314
+ git: bool | None = typer.Option(None, "--git/--no-git", help="Run git init."),
315
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip prompts; accept defaults."),
316
+ install: bool = typer.Option(
317
+ True, "--install/--no-install", help="Create the environment and install deps."
318
+ ),
319
+ force: bool = typer.Option(False, "--force", help="Allow writing into a non-empty directory."),
320
+ dry_run: bool = typer.Option(False, "--dry-run", help="Report actions without writing files."),
321
+ output_dir: Path | None = typer.Option(
322
+ None, "--output-dir", "-o", help="Parent directory for the new project."
323
+ ),
324
+ debug: bool = typer.Option(False, "--debug", help="Show full tracebacks on error."),
325
+ ) -> None:
326
+ """Scaffold a new AI/ML project."""
327
+
328
+ _guarded(
329
+ name,
330
+ project_type=project_type,
331
+ python_version=python_version,
332
+ framework=framework,
333
+ manager=manager,
334
+ testing=testing,
335
+ linting=linting,
336
+ tracking=tracking,
337
+ docker=docker,
338
+ jupyter=jupyter,
339
+ git=git,
340
+ yes=yes,
341
+ install=install,
342
+ force=force,
343
+ dry_run=dry_run,
344
+ output_dir=output_dir,
345
+ debug=debug,
346
+ )
@@ -0,0 +1,36 @@
1
+ """Shared Rich consoles and small rendering helpers.
2
+
3
+ Kept deliberately spare: a couple of consoles, an error panel, a key/value
4
+ table. No animations, no banners bigger than a single line.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+ from rich.table import Table
12
+ from rich.text import Text
13
+
14
+ from create_ai.errors import CreateAIError
15
+
16
+ console = Console()
17
+ err_console = Console(stderr=True)
18
+
19
+
20
+ def print_error(exc: CreateAIError) -> None:
21
+ """Render a typed error as a compact red panel with an optional hint."""
22
+
23
+ body = Text(exc.message)
24
+ if exc.hint:
25
+ body.append("\n\n")
26
+ body.append(exc.hint, style="dim")
27
+ err_console.print(Panel(body, title="[bold red]✗ error", border_style="red", expand=False))
28
+
29
+
30
+ def summary_table(rows: list[tuple[str, str]], *, title: str | None = None) -> Table:
31
+ table = Table(show_header=False, box=None, pad_edge=False, title=title)
32
+ table.add_column(style="dim")
33
+ table.add_column(style="bold")
34
+ for label, value in rows:
35
+ table.add_row(label, value)
36
+ return table