deepctl-cmd-init 0.0.2__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.
@@ -0,0 +1,6 @@
1
+ """Init command package for deepctl."""
2
+
3
+ from .command import InitCommand
4
+ from .models import InitResult
5
+
6
+ __all__ = ["InitCommand", "InitResult"]
@@ -0,0 +1,321 @@
1
+ """Init command for deepctl — scaffold Deepgram starter apps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import click
9
+ from deepctl_core import (
10
+ AuthManager,
11
+ BaseCommand,
12
+ Config,
13
+ DeepgramClient,
14
+ )
15
+ from rich.console import Console
16
+ from rich.table import Table
17
+
18
+ from . import lifecycle, templates_api
19
+ from .models import InitResult, TemplateDetail, TemplateInfo
20
+
21
+ console = Console()
22
+
23
+
24
+ class InitCommand(BaseCommand):
25
+ """Scaffold a Deepgram starter app from the templates gallery."""
26
+
27
+ name = "init"
28
+ help = (
29
+ "Browse, clone, and set up Deepgram starter apps. "
30
+ "Fetches templates from the Deepgram templates gallery and "
31
+ "optionally runs install/start lifecycle steps."
32
+ )
33
+ short_help = "Scaffold a Deepgram starter app"
34
+
35
+ requires_auth = False
36
+ requires_project = False
37
+ ci_friendly = True
38
+
39
+ examples = [
40
+ "dg init",
41
+ "dg init --search python",
42
+ "dg init node-transcription",
43
+ "dg init node-transcription --dir ./my-app",
44
+ "dg init node-transcription --install --start",
45
+ "dg init --list",
46
+ "dg init --list --search flask",
47
+ ]
48
+ agent_help = (
49
+ "Scaffold a Deepgram starter app from the templates gallery. "
50
+ "Can list available templates, search/filter them, or clone a "
51
+ "specific template by name. After cloning, optionally runs "
52
+ "install and start lifecycle steps defined in deepgram.toml. "
53
+ "If authenticated, injects DEEPGRAM_API_KEY into .env automatically."
54
+ )
55
+
56
+ def get_arguments(self) -> list[dict[str, Any]]:
57
+ """Get command arguments and options."""
58
+ return [
59
+ {
60
+ "name": "template",
61
+ "help": "Template name/slug to clone directly",
62
+ "type": str,
63
+ "required": False,
64
+ "default": None,
65
+ },
66
+ {
67
+ "names": ["--dir", "-d"],
68
+ "help": "Target directory (default: ./{template-name})",
69
+ "type": str,
70
+ "is_option": True,
71
+ },
72
+ {
73
+ "names": ["--search", "-s"],
74
+ "help": "Filter templates by search term",
75
+ "type": str,
76
+ "is_option": True,
77
+ },
78
+ {
79
+ "names": ["--list", "-l"],
80
+ "help": "List matching templates and exit",
81
+ "is_flag": True,
82
+ "is_option": True,
83
+ },
84
+ {
85
+ "names": ["--install"],
86
+ "help": "Run install step without prompting",
87
+ "is_flag": True,
88
+ "is_option": True,
89
+ },
90
+ {
91
+ "names": ["--start"],
92
+ "help": "Run start step without prompting",
93
+ "is_flag": True,
94
+ "is_option": True,
95
+ },
96
+ {
97
+ "names": ["--no-install"],
98
+ "help": "Skip install step without prompting",
99
+ "is_flag": True,
100
+ "is_option": True,
101
+ },
102
+ {
103
+ "names": ["--no-start"],
104
+ "help": "Skip start step without prompting",
105
+ "is_flag": True,
106
+ "is_option": True,
107
+ },
108
+ ]
109
+
110
+ def handle(
111
+ self,
112
+ config: Config,
113
+ auth_manager: AuthManager,
114
+ client: DeepgramClient,
115
+ **kwargs: Any,
116
+ ) -> Any:
117
+ """Handle the init command."""
118
+ template_slug: str | None = kwargs.get("template")
119
+ target_dir: str | None = kwargs.get("dir")
120
+ search: str | None = kwargs.get("search")
121
+ list_mode: bool = kwargs.get("list", False)
122
+ do_install: bool = kwargs.get("install", False)
123
+ do_start: bool = kwargs.get("start", False)
124
+ no_install: bool = kwargs.get("no_install", False)
125
+ no_start: bool = kwargs.get("no_start", False)
126
+
127
+ # --- List mode ---
128
+ if list_mode:
129
+ return self._handle_list(search)
130
+
131
+ # --- Direct template selection ---
132
+ if template_slug:
133
+ detail = self._fetch_template(template_slug)
134
+ if not detail:
135
+ return InitResult(
136
+ status="error", message=f"Template not found: {template_slug}"
137
+ )
138
+ else:
139
+ # --- Interactive picker ---
140
+ detail = self._interactive_pick(search)
141
+ if not detail:
142
+ return InitResult(status="cancelled", message="No template selected")
143
+
144
+ # Resolve target directory
145
+ dest = Path(target_dir) if target_dir else Path(f"./{detail.name}")
146
+ dest = dest.resolve()
147
+
148
+ # Confirm selection
149
+ self._print_template_summary(detail)
150
+ console.print(f"[dim]Directory:[/dim] {dest}")
151
+ console.print()
152
+
153
+ if not self.confirm("Clone this template?", default=True):
154
+ return InitResult(status="cancelled", message="Clone cancelled")
155
+
156
+ # Clone
157
+ github_url = detail.links.get("github")
158
+ if not github_url:
159
+ return InitResult(status="error", message="Template has no GitHub URL")
160
+
161
+ console.print()
162
+ with console.status("[blue]Cloning template...[/blue]"):
163
+ lifecycle.clone_template(github_url, dest)
164
+ console.print(f"[green]Cloned to {dest}[/green]")
165
+
166
+ # Inject API key if authenticated
167
+ try:
168
+ api_key = auth_manager.get_api_key()
169
+ if api_key:
170
+ lifecycle.inject_env(dest, api_key)
171
+ except Exception:
172
+ pass # Auth is optional
173
+
174
+ installed = False
175
+ started = False
176
+
177
+ # Run lifecycle steps if config exists
178
+ if detail.config:
179
+ # Install step
180
+ if detail.config.install:
181
+ should_install = do_install or (
182
+ not no_install and self.confirm("Run install step?", default=True)
183
+ )
184
+ if should_install:
185
+ console.print()
186
+ installed = lifecycle.run_lifecycle_step(
187
+ detail.config.install, dest
188
+ )
189
+
190
+ # Start step
191
+ if detail.config.start:
192
+ should_start = do_start or (
193
+ not no_start and self.confirm("Run start step?", default=True)
194
+ )
195
+ if should_start:
196
+ console.print()
197
+ started = lifecycle.run_lifecycle_step(detail.config.start, dest)
198
+
199
+ # Summary
200
+ console.print()
201
+ console.print(
202
+ f"[green bold]Done![/green bold] Your app is ready at [cyan]{dest}[/cyan]"
203
+ )
204
+ if not installed and detail.config and detail.config.install:
205
+ console.print(
206
+ f"[dim]To install: cd {dest.name} && run the install command[/dim]"
207
+ )
208
+ if not started and detail.config and detail.config.start:
209
+ console.print(
210
+ f"[dim]To start: cd {dest.name} && run the start command[/dim]"
211
+ )
212
+
213
+ return InitResult(
214
+ status="success",
215
+ message=f"Cloned {detail.name}",
216
+ template=detail.name,
217
+ directory=str(dest),
218
+ installed=installed,
219
+ started=started,
220
+ )
221
+
222
+ def output_result(self, result: Any, config: Config) -> None:
223
+ """Only emit structured output when an explicit --output format is set."""
224
+ from deepctl_core.output import get_output_format
225
+
226
+ output_format = get_output_format()
227
+
228
+ # Default format: the Rich table / console messages are already printed,
229
+ # so skip the automatic JSON dump.
230
+ if output_format in ("default",):
231
+ return
232
+
233
+ # Explicit --output json/yaml/table/csv: delegate to base
234
+ super().output_result(result, config)
235
+
236
+ def _handle_list(self, search: str | None) -> list[dict[str, Any]]:
237
+ """List templates and return structured data."""
238
+ with console.status("[blue]Fetching templates...[/blue]"):
239
+ response = templates_api.list_templates(search=search)
240
+
241
+ templates = response.items
242
+ if not templates:
243
+ console.print("[yellow]No templates found[/yellow]")
244
+ return []
245
+
246
+ self._print_templates_table(templates)
247
+ console.print(f"\n[dim]{len(templates)} template(s) found[/dim]")
248
+
249
+ return [t.model_dump() for t in templates]
250
+
251
+ def _interactive_pick(self, search: str | None) -> TemplateDetail | None:
252
+ """Show interactive picker and return selected template detail."""
253
+ with console.status("[blue]Fetching templates...[/blue]"):
254
+ response = templates_api.list_templates(search=search)
255
+
256
+ templates = response.items
257
+ if not templates:
258
+ console.print("[yellow]No templates found[/yellow]")
259
+ return None
260
+
261
+ self._print_templates_table(templates)
262
+ console.print()
263
+
264
+ try:
265
+ choice = click.prompt(
266
+ "Select a template",
267
+ type=click.IntRange(1, len(templates)),
268
+ )
269
+ except (click.Abort, EOFError):
270
+ return None
271
+
272
+ selected = templates[choice - 1]
273
+ console.print()
274
+
275
+ # Fetch full detail
276
+ with console.status(f"[blue]Fetching {selected.name}...[/blue]"):
277
+ return templates_api.get_template(selected.name)
278
+
279
+ def _fetch_template(self, slug: str) -> TemplateDetail | None:
280
+ """Fetch template detail by slug, handling errors."""
281
+ try:
282
+ with console.status(f"[blue]Fetching {slug}...[/blue]"):
283
+ return templates_api.get_template(slug)
284
+ except Exception as e:
285
+ console.print(f"[red]Error:[/red] {e}")
286
+ return None
287
+
288
+ def _print_templates_table(self, templates: list[TemplateInfo]) -> None:
289
+ """Print a numbered table of templates."""
290
+ table = Table(show_header=True, header_style="bold blue")
291
+ table.add_column("#", style="dim", width=4, justify="right")
292
+ table.add_column("Name")
293
+ table.add_column("Language")
294
+ table.add_column("Framework")
295
+ table.add_column("Category")
296
+
297
+ for i, t in enumerate(templates, 1):
298
+ table.add_row(
299
+ str(i),
300
+ t.name,
301
+ t.language,
302
+ t.framework or "—",
303
+ t.category or "—",
304
+ )
305
+
306
+ console.print(table)
307
+
308
+ def _print_template_summary(self, detail: TemplateDetail) -> None:
309
+ """Print a summary of the selected template."""
310
+ console.print()
311
+ console.print(f"[bold]{detail.title}[/bold]")
312
+ console.print(f"[dim]{detail.description}[/dim]")
313
+ console.print(
314
+ f"Language: [cyan]{detail.language}[/cyan]"
315
+ + (
316
+ f" Framework: [cyan]{detail.framework}[/cyan]"
317
+ if detail.framework
318
+ else ""
319
+ )
320
+ + (f" Stars: {detail.stats.stars}" if detail.stats.stars else "")
321
+ )
@@ -0,0 +1,104 @@
1
+ """Clone template repos and run lifecycle steps from deepgram.toml."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ import subprocess
7
+ from typing import TYPE_CHECKING
8
+
9
+ from rich.console import Console
10
+
11
+ if TYPE_CHECKING:
12
+ from pathlib import Path
13
+
14
+ from .models import LifecycleStep
15
+
16
+ console = Console()
17
+
18
+
19
+ def clone_template(github_url: str, target_dir: Path) -> None:
20
+ """Clone a template repo with depth 1, then remove .git.
21
+
22
+ Args:
23
+ github_url: Full GitHub URL to clone.
24
+ target_dir: Local directory to clone into.
25
+
26
+ Raises:
27
+ RuntimeError: If git clone fails.
28
+ """
29
+ if target_dir.exists():
30
+ raise RuntimeError(f"Directory already exists: {target_dir}")
31
+
32
+ result = subprocess.run(
33
+ ["git", "clone", "--depth", "1", github_url, str(target_dir)],
34
+ capture_output=True,
35
+ text=True,
36
+ )
37
+ if result.returncode != 0:
38
+ raise RuntimeError(f"git clone failed: {result.stderr.strip()}")
39
+
40
+ # Remove .git so the user starts fresh
41
+ git_dir = target_dir / ".git"
42
+ if git_dir.exists():
43
+ shutil.rmtree(git_dir)
44
+
45
+
46
+ def inject_env(target_dir: Path, api_key: str) -> None:
47
+ """Create or append DEEPGRAM_API_KEY to .env in target_dir.
48
+
49
+ Args:
50
+ target_dir: Directory containing the cloned template.
51
+ api_key: Deepgram API key to inject.
52
+ """
53
+ env_path = target_dir / ".env"
54
+
55
+ # Check if key already present
56
+ if env_path.exists():
57
+ content = env_path.read_text()
58
+ if "DEEPGRAM_API_KEY=" in content:
59
+ console.print("[dim]DEEPGRAM_API_KEY already set in .env, skipping[/dim]")
60
+ return
61
+ # Append with newline separator
62
+ with open(env_path, "a") as f:
63
+ if not content.endswith("\n"):
64
+ f.write("\n")
65
+ f.write(f"DEEPGRAM_API_KEY={api_key}\n")
66
+ else:
67
+ env_path.write_text(f"DEEPGRAM_API_KEY={api_key}\n")
68
+
69
+ console.print("[green]Injected DEEPGRAM_API_KEY into .env[/green]")
70
+
71
+
72
+ def run_lifecycle_step(step: LifecycleStep, cwd: Path) -> bool:
73
+ """Execute a lifecycle step from deepgram.toml.
74
+
75
+ Args:
76
+ step: The lifecycle step definition.
77
+ cwd: Working directory for command execution.
78
+
79
+ Returns:
80
+ True if the step succeeded, False otherwise.
81
+ """
82
+ if step.command is None:
83
+ return True
84
+
85
+ commands = step.command if isinstance(step.command, list) else [step.command]
86
+
87
+ for cmd in commands:
88
+ if step.message:
89
+ console.print(f"[blue]{step.message}[/blue]")
90
+ else:
91
+ console.print(f"[dim]Running: {cmd}[/dim]")
92
+
93
+ result = subprocess.run(
94
+ cmd,
95
+ cwd=str(cwd),
96
+ shell=True,
97
+ )
98
+ if result.returncode != 0:
99
+ console.print(
100
+ f"[red]Command failed (exit {result.returncode}):[/red] {cmd}"
101
+ )
102
+ return False
103
+
104
+ return True
@@ -0,0 +1,79 @@
1
+ """Models for init command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from deepctl_core import BaseResult
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class TemplateInfo(BaseModel):
12
+ """Summary info for a template (from list endpoint)."""
13
+
14
+ name: str
15
+ title: str
16
+ description: str
17
+ language: str
18
+ framework: str | None = None
19
+ category: str | None = None
20
+
21
+
22
+ class TemplateStats(BaseModel):
23
+ """Repository statistics for a template."""
24
+
25
+ stars: int = 0
26
+ forks: int = 0
27
+ last_updated: str | None = Field(default=None, alias="lastUpdated")
28
+
29
+ model_config = {"populate_by_name": True}
30
+
31
+
32
+ class LifecycleStep(BaseModel):
33
+ """A single lifecycle step from deepgram.toml."""
34
+
35
+ command: str | list[str] | None = None
36
+ message: str | None = None
37
+ parallel: bool | None = None
38
+ pre: dict[str, Any] | None = None
39
+ post: dict[str, Any] | None = None
40
+
41
+
42
+ class TomlConfig(BaseModel):
43
+ """Parsed deepgram.toml configuration."""
44
+
45
+ check: LifecycleStep | None = None
46
+ install: LifecycleStep | None = None
47
+ start: LifecycleStep | None = None
48
+
49
+
50
+ class TemplateDetail(TemplateInfo):
51
+ """Full template detail (from detail endpoint)."""
52
+
53
+ sdk: str | None = None
54
+ tags: list[str] = Field(default_factory=list)
55
+ links: dict[str, str | None] = Field(default_factory=dict)
56
+ stats: TemplateStats = Field(default_factory=TemplateStats)
57
+ readme: str | None = None
58
+ config: TomlConfig | None = None
59
+
60
+
61
+ class TemplateListResponse(BaseModel):
62
+ """Response from the templates list API."""
63
+
64
+ total: int = 0
65
+ page: int = 1
66
+ limit: int = 100
67
+ total_pages: int = Field(default=1, alias="totalPages")
68
+ items: list[TemplateInfo] = Field(default_factory=list)
69
+
70
+ model_config = {"populate_by_name": True}
71
+
72
+
73
+ class InitResult(BaseResult):
74
+ """Result from init command."""
75
+
76
+ template: str | None = None
77
+ directory: str | None = None
78
+ installed: bool = False
79
+ started: bool = False
@@ -0,0 +1,80 @@
1
+ """HTTP client for the Deepgram templates API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+
7
+ from .models import TemplateDetail, TemplateInfo, TemplateListResponse
8
+
9
+ BASE_URL = "https://templates.dx.deepgram.com"
10
+ DEFAULT_TIMEOUT = 15.0
11
+
12
+
13
+ def list_templates(
14
+ search: str | None = None,
15
+ page: int = 1,
16
+ limit: int = 100,
17
+ ) -> TemplateListResponse:
18
+ """Fetch templates from the API.
19
+
20
+ Args:
21
+ search: Optional search/filter term.
22
+ page: Page number (1-based).
23
+ limit: Results per page.
24
+
25
+ Returns:
26
+ Parsed template list response.
27
+ """
28
+ params: dict[str, str | int] = {"page": page, "limit": limit}
29
+ if search:
30
+ params["search"] = search
31
+
32
+ resp = httpx.get(
33
+ f"{BASE_URL}/api/templates",
34
+ params=params,
35
+ timeout=DEFAULT_TIMEOUT,
36
+ )
37
+ resp.raise_for_status()
38
+ return TemplateListResponse.model_validate(resp.json())
39
+
40
+
41
+ def get_template(slug: str) -> TemplateDetail:
42
+ """Fetch a single template by slug.
43
+
44
+ Args:
45
+ slug: Template name/slug (e.g. "node-transcription").
46
+
47
+ Returns:
48
+ Parsed template detail.
49
+ """
50
+ resp = httpx.get(
51
+ f"{BASE_URL}/api/templates/{slug}",
52
+ timeout=DEFAULT_TIMEOUT,
53
+ )
54
+ resp.raise_for_status()
55
+ return TemplateDetail.model_validate(resp.json())
56
+
57
+
58
+ def filter_templates(templates: list[TemplateInfo], search: str) -> list[TemplateInfo]:
59
+ """Client-side filter of templates by search term.
60
+
61
+ Matches against name, title, description, language, framework, and category.
62
+
63
+ Args:
64
+ templates: List of templates to filter.
65
+ search: Search term (case-insensitive).
66
+
67
+ Returns:
68
+ Filtered list.
69
+ """
70
+ term = search.lower()
71
+ results: list[TemplateInfo] = []
72
+ for t in templates:
73
+ fields = [t.name, t.title, t.description, t.language]
74
+ if t.framework:
75
+ fields.append(t.framework)
76
+ if t.category:
77
+ fields.append(t.category)
78
+ if any(term in f.lower() for f in fields):
79
+ results.append(t)
80
+ return results
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepctl-cmd-init
3
+ Version: 0.0.2
4
+ Summary: Init command for deepctl — scaffold Deepgram starter apps
5
+ Author-email: Deepgram <devrel@deepgram.com>
6
+ Maintainer-email: Deepgram <devrel@deepgram.com>
7
+ License-Expression: MIT
8
+ Keywords: deepgram,cli,init,starter,templates
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: deepctl-core>=0.1.10
18
+ Requires-Dist: click>=8.0.0
19
+ Requires-Dist: rich>=13.0.0
20
+ Requires-Dist: httpx>=0.24.0
21
+ Requires-Dist: pydantic>=2.0.0
22
+
23
+ # deepctl-cmd-init
24
+
25
+ > Part of [deepctl](https://github.com/deepgram/cli) — Official Deepgram CLI
26
+
27
+ Init command for deepctl — scaffold Deepgram starter apps
28
+
29
+ ## Installation
30
+
31
+ This package is included with deepctl and does not need to be installed separately.
32
+
33
+ ### Install deepctl
34
+
35
+ ```bash
36
+ # Install with pip
37
+ pip install deepctl
38
+
39
+ # Or install with uv
40
+ uv tool install deepctl
41
+
42
+ # Or install with pipx
43
+ pipx install deepctl
44
+
45
+ # Or run without installing
46
+ uvx deepctl --help
47
+ pipx run deepctl --help
48
+ ```
49
+
50
+ ## Commands
51
+
52
+ | Command | Entry Point |
53
+ |---------|-------------|
54
+ | `deepctl init` | `deepctl_cmd_init.command:InitCommand` |
55
+
56
+ ## Dependencies
57
+
58
+ - `click>=8.0.0`
59
+ - `rich>=13.0.0`
60
+ - `httpx>=0.24.0`
61
+ - `pydantic>=2.0.0`
62
+
63
+ ## License
64
+
65
+ MIT — see [LICENSE](../../LICENSE)
@@ -0,0 +1,10 @@
1
+ deepctl_cmd_init/__init__.py,sha256=mJ3X78F-lDDsIxAdV8I8O_cG0w--U641JCzrLZ-pftA,146
2
+ deepctl_cmd_init/command.py,sha256=IP_QZJ0aEQFMhPA_ySDbCvbz08a-W2IxUC0wVJY1kbc,11079
3
+ deepctl_cmd_init/lifecycle.py,sha256=5-ICKF3RTI-erUoJmMBHKKfF-8onSXXsrsBZLdTpMV0,2907
4
+ deepctl_cmd_init/models.py,sha256=uhBYvCYrOSbZ-mdrEwbCorApB89RemIAUtvBbNlxO4A,1971
5
+ deepctl_cmd_init/templates_api.py,sha256=0BDO8lFlmAEcaro1rDf213T2qv01d22zaL6cy0KCdVg,2068
6
+ deepctl_cmd_init-0.0.2.dist-info/METADATA,sha256=EhFy6qYfHtXc6P9Hgaw0e5KXZPLI0Ap59EHoHDYofuA,1563
7
+ deepctl_cmd_init-0.0.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ deepctl_cmd_init-0.0.2.dist-info/entry_points.txt,sha256=lAaUfHpN1Zj3TwaOqgFfCu-WcK9BJgDkt0jD9v1nCq0,63
9
+ deepctl_cmd_init-0.0.2.dist-info/top_level.txt,sha256=2eTFp6AsUibDM5tRczLTEtdiznBPlMg1I14QCR38qoM,17
10
+ deepctl_cmd_init-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [deepctl.commands]
2
+ init = deepctl_cmd_init.command:InitCommand
@@ -0,0 +1 @@
1
+ deepctl_cmd_init