deepctl-cmd-init 0.0.2__tar.gz → 0.0.3__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.
- {deepctl_cmd_init-0.0.2 → deepctl_cmd_init-0.0.3}/PKG-INFO +1 -1
- {deepctl_cmd_init-0.0.2 → deepctl_cmd_init-0.0.3}/pyproject.toml +1 -1
- deepctl_cmd_init-0.0.3/src/deepctl_cmd_init/command.py +415 -0
- deepctl_cmd_init-0.0.3/src/deepctl_cmd_init/lifecycle.py +204 -0
- {deepctl_cmd_init-0.0.2 → deepctl_cmd_init-0.0.3}/src/deepctl_cmd_init.egg-info/PKG-INFO +1 -1
- deepctl_cmd_init-0.0.2/src/deepctl_cmd_init/command.py +0 -321
- deepctl_cmd_init-0.0.2/src/deepctl_cmd_init/lifecycle.py +0 -104
- {deepctl_cmd_init-0.0.2 → deepctl_cmd_init-0.0.3}/README.md +0 -0
- {deepctl_cmd_init-0.0.2 → deepctl_cmd_init-0.0.3}/setup.cfg +0 -0
- {deepctl_cmd_init-0.0.2 → deepctl_cmd_init-0.0.3}/src/deepctl_cmd_init/__init__.py +0 -0
- {deepctl_cmd_init-0.0.2 → deepctl_cmd_init-0.0.3}/src/deepctl_cmd_init/models.py +0 -0
- {deepctl_cmd_init-0.0.2 → deepctl_cmd_init-0.0.3}/src/deepctl_cmd_init/templates_api.py +0 -0
- {deepctl_cmd_init-0.0.2 → deepctl_cmd_init-0.0.3}/src/deepctl_cmd_init.egg-info/SOURCES.txt +0 -0
- {deepctl_cmd_init-0.0.2 → deepctl_cmd_init-0.0.3}/src/deepctl_cmd_init.egg-info/dependency_links.txt +0 -0
- {deepctl_cmd_init-0.0.2 → deepctl_cmd_init-0.0.3}/src/deepctl_cmd_init.egg-info/entry_points.txt +0 -0
- {deepctl_cmd_init-0.0.2 → deepctl_cmd_init-0.0.3}/src/deepctl_cmd_init.egg-info/requires.txt +0 -0
- {deepctl_cmd_init-0.0.2 → deepctl_cmd_init-0.0.3}/src/deepctl_cmd_init.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "deepctl-cmd-init"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.3" # x-release-please-version
|
|
8
8
|
description = "Init command for deepctl — scaffold Deepgram starter apps"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -0,0 +1,415 @@
|
|
|
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
|
+
from deepctl_core import (
|
|
9
|
+
AuthManager,
|
|
10
|
+
BaseCommand,
|
|
11
|
+
Config,
|
|
12
|
+
DeepgramClient,
|
|
13
|
+
)
|
|
14
|
+
from deepctl_core.output import _agentic
|
|
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
|
+
status_console = Console(stderr=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InitCommand(BaseCommand):
|
|
26
|
+
"""Scaffold a Deepgram starter app from the templates gallery."""
|
|
27
|
+
|
|
28
|
+
name = "init"
|
|
29
|
+
help = (
|
|
30
|
+
"Browse, clone, and set up Deepgram starter apps.\n\n"
|
|
31
|
+
"Fetches templates from the Deepgram templates gallery. "
|
|
32
|
+
"Run with no arguments for an interactive search-and-select picker, "
|
|
33
|
+
"or pass a template name directly to clone immediately."
|
|
34
|
+
)
|
|
35
|
+
short_help = "Scaffold a Deepgram starter app (alpha)"
|
|
36
|
+
|
|
37
|
+
requires_auth = False
|
|
38
|
+
requires_project = False
|
|
39
|
+
ci_friendly = True
|
|
40
|
+
|
|
41
|
+
examples = [
|
|
42
|
+
"dg init",
|
|
43
|
+
"dg init --search python",
|
|
44
|
+
"dg init node-transcription",
|
|
45
|
+
"dg init node-transcription --dir ./my-app",
|
|
46
|
+
"dg init node-transcription --install",
|
|
47
|
+
"dg init node-transcription --no-install",
|
|
48
|
+
"dg init --list",
|
|
49
|
+
"dg init --list --search flask",
|
|
50
|
+
]
|
|
51
|
+
agent_help = (
|
|
52
|
+
"Scaffold a Deepgram starter app from the templates gallery. "
|
|
53
|
+
"Pass the template name/slug directly (e.g. 'node-transcription'). "
|
|
54
|
+
"Use --list to enumerate available templates. "
|
|
55
|
+
"After cloning, runs make check-prereqs then make init automatically. "
|
|
56
|
+
"Use --no-install to skip setup. "
|
|
57
|
+
"Requires git, node, npm, make, and curl to be installed."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def get_arguments(self) -> list[dict[str, Any]]:
|
|
61
|
+
"""Get command arguments and options."""
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
"name": "template",
|
|
65
|
+
"help": "Template name/slug to clone (e.g. node-transcription)",
|
|
66
|
+
"type": str,
|
|
67
|
+
"required": False,
|
|
68
|
+
"default": None,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"names": ["--dir", "-d"],
|
|
72
|
+
"help": "Target directory (default: ./{template-name})",
|
|
73
|
+
"type": str,
|
|
74
|
+
"is_option": True,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"names": ["--search", "-s"],
|
|
78
|
+
"help": "Filter templates by keyword",
|
|
79
|
+
"type": str,
|
|
80
|
+
"is_option": True,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"names": ["--list", "-l"],
|
|
84
|
+
"help": "List available templates and exit",
|
|
85
|
+
"is_flag": True,
|
|
86
|
+
"is_option": True,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"names": ["--install"],
|
|
90
|
+
"help": "Run setup (make check-prereqs && make init) without prompting",
|
|
91
|
+
"is_flag": True,
|
|
92
|
+
"is_option": True,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"names": ["--no-install"],
|
|
96
|
+
"help": "Skip setup step without prompting",
|
|
97
|
+
"is_flag": True,
|
|
98
|
+
"is_option": True,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"names": ["--start"],
|
|
102
|
+
"help": "Run start step after install without prompting",
|
|
103
|
+
"is_flag": True,
|
|
104
|
+
"is_option": True,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"names": ["--no-start"],
|
|
108
|
+
"help": "Skip start step without prompting",
|
|
109
|
+
"is_flag": True,
|
|
110
|
+
"is_option": True,
|
|
111
|
+
},
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
def handle(
|
|
115
|
+
self,
|
|
116
|
+
config: Config,
|
|
117
|
+
auth_manager: AuthManager,
|
|
118
|
+
client: DeepgramClient,
|
|
119
|
+
**kwargs: Any,
|
|
120
|
+
) -> Any:
|
|
121
|
+
"""Handle the init command."""
|
|
122
|
+
template_slug: str | None = kwargs.get("template")
|
|
123
|
+
target_dir: str | None = kwargs.get("dir")
|
|
124
|
+
search: str | None = kwargs.get("search")
|
|
125
|
+
list_mode: bool = kwargs.get("list", False)
|
|
126
|
+
do_install: bool = kwargs.get("install", False)
|
|
127
|
+
no_install: bool = kwargs.get("no_install", False)
|
|
128
|
+
do_start: bool = kwargs.get("start", False)
|
|
129
|
+
no_start: bool = kwargs.get("no_start", False)
|
|
130
|
+
|
|
131
|
+
# ── List mode ──────────────────────────────────────────────────────────
|
|
132
|
+
if list_mode:
|
|
133
|
+
return self._handle_list(search)
|
|
134
|
+
|
|
135
|
+
# ── Prerequisite check ─────────────────────────────────────────────────
|
|
136
|
+
# Check before any clone attempt so users get a clear error with install
|
|
137
|
+
# hints rather than a confusing git/make error mid-setup.
|
|
138
|
+
missing = lifecycle.check_prereqs()
|
|
139
|
+
if missing:
|
|
140
|
+
console.print("[red bold]Missing required tools:[/red bold]")
|
|
141
|
+
for _key, name, hint in missing:
|
|
142
|
+
console.print(f" [red]✗[/red] [bold]{name}[/bold]")
|
|
143
|
+
console.print(f" {hint}")
|
|
144
|
+
console.print()
|
|
145
|
+
console.print(
|
|
146
|
+
"[dim]Install the tools above and re-run [bold]dg init[/bold].[/dim]"
|
|
147
|
+
)
|
|
148
|
+
return InitResult(
|
|
149
|
+
status="error",
|
|
150
|
+
message=f"Missing tools: {', '.join(k for k, _, _ in missing)}",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# ── Template selection ─────────────────────────────────────────────────
|
|
154
|
+
if template_slug:
|
|
155
|
+
detail = self._fetch_template(template_slug)
|
|
156
|
+
if not detail:
|
|
157
|
+
return InitResult(
|
|
158
|
+
status="error", message=f"Template not found: {template_slug}"
|
|
159
|
+
)
|
|
160
|
+
elif _agentic:
|
|
161
|
+
# In agent mode, require an explicit template name
|
|
162
|
+
return InitResult(
|
|
163
|
+
status="error",
|
|
164
|
+
message=(
|
|
165
|
+
"In agent mode, pass the template slug directly, e.g.: "
|
|
166
|
+
"dg init node-transcription\n"
|
|
167
|
+
"Use dg init --list to see available templates."
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
detail = self._interactive_pick(search)
|
|
172
|
+
if not detail:
|
|
173
|
+
return InitResult(status="cancelled", message="No template selected")
|
|
174
|
+
|
|
175
|
+
# ── Resolve target directory ───────────────────────────────────────────
|
|
176
|
+
dest = Path(target_dir) if target_dir else Path(f"./{detail.name}")
|
|
177
|
+
dest = dest.resolve()
|
|
178
|
+
|
|
179
|
+
# ── Confirm ────────────────────────────────────────────────────────────
|
|
180
|
+
self._print_template_summary(detail)
|
|
181
|
+
console.print(f"[dim]Destination:[/dim] {dest}")
|
|
182
|
+
console.print()
|
|
183
|
+
|
|
184
|
+
if not _agentic and not self.confirm("Clone this template?", default=True):
|
|
185
|
+
return InitResult(status="cancelled", message="Clone cancelled")
|
|
186
|
+
|
|
187
|
+
# ── Clone ──────────────────────────────────────────────────────────────
|
|
188
|
+
github_url = detail.links.get("github")
|
|
189
|
+
if not github_url:
|
|
190
|
+
return InitResult(status="error", message="Template has no GitHub URL")
|
|
191
|
+
|
|
192
|
+
console.print()
|
|
193
|
+
with console.status("[blue]Cloning repository...[/blue]"):
|
|
194
|
+
try:
|
|
195
|
+
lifecycle.clone_template(github_url, dest)
|
|
196
|
+
except RuntimeError as e:
|
|
197
|
+
return InitResult(status="error", message=str(e))
|
|
198
|
+
|
|
199
|
+
console.print(f"[green]✓[/green] Cloned to [cyan]{dest}[/cyan]")
|
|
200
|
+
console.print(
|
|
201
|
+
"[dim]The cloned directory is a full git repository — "
|
|
202
|
+
"you can add your own remote and push.[/dim]"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# ── Inject API key ─────────────────────────────────────────────────────
|
|
206
|
+
try:
|
|
207
|
+
api_key = auth_manager.get_api_key()
|
|
208
|
+
if api_key:
|
|
209
|
+
lifecycle.inject_env(dest, api_key)
|
|
210
|
+
except Exception:
|
|
211
|
+
pass # Auth is optional
|
|
212
|
+
|
|
213
|
+
# ── Setup (make check-prereqs && make init) ────────────────────────────
|
|
214
|
+
installed = False
|
|
215
|
+
has_makefile = (dest / "Makefile").exists()
|
|
216
|
+
|
|
217
|
+
if not no_install:
|
|
218
|
+
if _agentic or do_install:
|
|
219
|
+
run_setup = True
|
|
220
|
+
elif has_makefile:
|
|
221
|
+
console.print()
|
|
222
|
+
run_setup = self.confirm(
|
|
223
|
+
"Run setup? (make check-prereqs && make init)", default=True
|
|
224
|
+
)
|
|
225
|
+
else:
|
|
226
|
+
run_setup = False
|
|
227
|
+
|
|
228
|
+
if run_setup:
|
|
229
|
+
console.print()
|
|
230
|
+
if has_makefile:
|
|
231
|
+
installed = lifecycle.run_make_init(dest)
|
|
232
|
+
elif detail.config and detail.config.install:
|
|
233
|
+
# Fallback: TOML lifecycle for repos without a Makefile
|
|
234
|
+
installed = lifecycle.run_lifecycle_step(
|
|
235
|
+
detail.config.install, dest
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# ── Start step (optional) ──────────────────────────────────────────────
|
|
239
|
+
started = False
|
|
240
|
+
if installed and detail.config and detail.config.start and not no_start:
|
|
241
|
+
if _agentic or do_start:
|
|
242
|
+
run_start = True
|
|
243
|
+
else:
|
|
244
|
+
console.print()
|
|
245
|
+
run_start = self.confirm("Run start step?", default=False)
|
|
246
|
+
|
|
247
|
+
if run_start:
|
|
248
|
+
console.print()
|
|
249
|
+
started = lifecycle.run_lifecycle_step(detail.config.start, dest)
|
|
250
|
+
|
|
251
|
+
# ── Summary ────────────────────────────────────────────────────────────
|
|
252
|
+
console.print()
|
|
253
|
+
console.print(
|
|
254
|
+
f"[green bold]Done![/green bold] "
|
|
255
|
+
f"[cyan]{detail.title}[/cyan] is ready at [cyan]{dest}[/cyan]"
|
|
256
|
+
)
|
|
257
|
+
console.print()
|
|
258
|
+
|
|
259
|
+
if not installed and has_makefile:
|
|
260
|
+
console.print(f"[dim]To set up: cd {dest.name} && make init[/dim]")
|
|
261
|
+
if not started and detail.config and detail.config.start:
|
|
262
|
+
console.print(f"[dim]To start: cd {dest.name} && make start[/dim]")
|
|
263
|
+
|
|
264
|
+
return InitResult(
|
|
265
|
+
status="success",
|
|
266
|
+
message=f"Cloned {detail.name}",
|
|
267
|
+
template=detail.name,
|
|
268
|
+
directory=str(dest),
|
|
269
|
+
installed=installed,
|
|
270
|
+
started=started,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def output_result(self, result: Any, config: Config) -> None:
|
|
274
|
+
"""Only emit structured output when an explicit --output format is set."""
|
|
275
|
+
from deepctl_core.output import get_output_format
|
|
276
|
+
|
|
277
|
+
if get_output_format() in ("default",):
|
|
278
|
+
return
|
|
279
|
+
super().output_result(result, config)
|
|
280
|
+
|
|
281
|
+
# ── List ──────────────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
def _handle_list(self, search: str | None) -> list[dict[str, Any]]:
|
|
284
|
+
"""List templates and return structured data."""
|
|
285
|
+
with console.status("[blue]Fetching templates...[/blue]"):
|
|
286
|
+
response = templates_api.list_templates(search=search)
|
|
287
|
+
|
|
288
|
+
templates = response.items
|
|
289
|
+
if not templates:
|
|
290
|
+
console.print("[yellow]No templates found[/yellow]")
|
|
291
|
+
return []
|
|
292
|
+
|
|
293
|
+
self._print_templates_table(templates)
|
|
294
|
+
console.print(f"\n[dim]{len(templates)} template(s)[/dim]")
|
|
295
|
+
return [t.model_dump() for t in templates]
|
|
296
|
+
|
|
297
|
+
# ── Interactive picker ────────────────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
def _interactive_pick(self, search: str | None) -> TemplateDetail | None:
|
|
300
|
+
"""Interactive search-and-select picker.
|
|
301
|
+
|
|
302
|
+
The user can type a number to select, or type a search term to filter
|
|
303
|
+
and re-display the list. Loops until a selection is made or cancelled.
|
|
304
|
+
"""
|
|
305
|
+
with console.status("[blue]Fetching templates...[/blue]"):
|
|
306
|
+
response = templates_api.list_templates()
|
|
307
|
+
|
|
308
|
+
all_templates = response.items
|
|
309
|
+
if not all_templates:
|
|
310
|
+
console.print("[yellow]No templates found[/yellow]")
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
# Apply initial search filter if provided
|
|
314
|
+
filtered = (
|
|
315
|
+
templates_api.filter_templates(all_templates, search)
|
|
316
|
+
if search
|
|
317
|
+
else all_templates
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
selected: TemplateInfo | None = None
|
|
321
|
+
|
|
322
|
+
while selected is None:
|
|
323
|
+
self._print_templates_table(filtered)
|
|
324
|
+
console.print()
|
|
325
|
+
|
|
326
|
+
if len(filtered) == 0:
|
|
327
|
+
console.print("[yellow]No templates match your search.[/yellow]")
|
|
328
|
+
filtered = all_templates
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
console.print(
|
|
332
|
+
"[dim]Type a [bold]number[/bold] to select, "
|
|
333
|
+
"or a [bold]search term[/bold] to filter. "
|
|
334
|
+
"Ctrl+C to cancel.[/dim]"
|
|
335
|
+
)
|
|
336
|
+
try:
|
|
337
|
+
raw = input("> ").strip()
|
|
338
|
+
except (EOFError, KeyboardInterrupt):
|
|
339
|
+
console.print()
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
if not raw:
|
|
343
|
+
if len(filtered) == 1:
|
|
344
|
+
selected = filtered[0]
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
# Try numeric selection
|
|
348
|
+
try:
|
|
349
|
+
idx = int(raw)
|
|
350
|
+
if 1 <= idx <= len(filtered):
|
|
351
|
+
selected = filtered[idx - 1]
|
|
352
|
+
else:
|
|
353
|
+
console.print(
|
|
354
|
+
f"[yellow]Enter a number between 1 and {len(filtered)}.[/yellow]"
|
|
355
|
+
)
|
|
356
|
+
except ValueError:
|
|
357
|
+
# Treat as search term
|
|
358
|
+
new_filtered = templates_api.filter_templates(all_templates, raw)
|
|
359
|
+
if not new_filtered:
|
|
360
|
+
console.print(
|
|
361
|
+
f"[yellow]No templates match '{raw}'. Showing all.[/yellow]"
|
|
362
|
+
)
|
|
363
|
+
filtered = all_templates
|
|
364
|
+
else:
|
|
365
|
+
filtered = new_filtered
|
|
366
|
+
console.print()
|
|
367
|
+
|
|
368
|
+
console.print()
|
|
369
|
+
with console.status(f"[blue]Fetching {selected.name}...[/blue]"):
|
|
370
|
+
return templates_api.get_template(selected.name)
|
|
371
|
+
|
|
372
|
+
def _fetch_template(self, slug: str) -> TemplateDetail | None:
|
|
373
|
+
"""Fetch template detail by slug."""
|
|
374
|
+
try:
|
|
375
|
+
with console.status(f"[blue]Fetching {slug}...[/blue]"):
|
|
376
|
+
return templates_api.get_template(slug)
|
|
377
|
+
except Exception as e:
|
|
378
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
# ── Rendering ─────────────────────────────────────────────────────────────
|
|
382
|
+
|
|
383
|
+
def _print_templates_table(self, templates: list[TemplateInfo]) -> None:
|
|
384
|
+
"""Print a numbered table of templates."""
|
|
385
|
+
table = Table(show_header=True, header_style="bold blue")
|
|
386
|
+
table.add_column("#", style="dim", width=4, justify="right")
|
|
387
|
+
table.add_column("Name")
|
|
388
|
+
table.add_column("Language")
|
|
389
|
+
table.add_column("Framework")
|
|
390
|
+
table.add_column("Use case")
|
|
391
|
+
table.add_column("Description", no_wrap=False, max_width=50)
|
|
392
|
+
|
|
393
|
+
for i, t in enumerate(templates, 1):
|
|
394
|
+
table.add_row(
|
|
395
|
+
str(i),
|
|
396
|
+
t.name,
|
|
397
|
+
t.language,
|
|
398
|
+
t.framework or "—",
|
|
399
|
+
t.category or "—",
|
|
400
|
+
t.description,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
console.print(table)
|
|
404
|
+
|
|
405
|
+
def _print_template_summary(self, detail: TemplateDetail) -> None:
|
|
406
|
+
"""Print a one-line summary of the selected template."""
|
|
407
|
+
console.print()
|
|
408
|
+
console.print(f"[bold]{detail.title}[/bold] [dim]{detail.name}[/dim]")
|
|
409
|
+
console.print(f"[dim]{detail.description}[/dim]")
|
|
410
|
+
lang_line = f"Language: [cyan]{detail.language}[/cyan]"
|
|
411
|
+
if detail.framework:
|
|
412
|
+
lang_line += f" Framework: [cyan]{detail.framework}[/cyan]"
|
|
413
|
+
if detail.stats.stars:
|
|
414
|
+
lang_line += f" ★ {detail.stats.stars}"
|
|
415
|
+
console.print(lang_line)
|
|
@@ -0,0 +1,204 @@
|
|
|
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
|
+
# ── Prerequisite tools ────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
#: Each entry: tool key → (display name, install hint)
|
|
21
|
+
PREREQS: dict[str, tuple[str, str]] = {
|
|
22
|
+
"git": (
|
|
23
|
+
"git",
|
|
24
|
+
"https://git-scm.com/downloads",
|
|
25
|
+
),
|
|
26
|
+
"node": (
|
|
27
|
+
"Node.js",
|
|
28
|
+
"https://nodejs.org",
|
|
29
|
+
),
|
|
30
|
+
"npm": (
|
|
31
|
+
"npm",
|
|
32
|
+
"bundled with Node.js — reinstall from https://nodejs.org",
|
|
33
|
+
),
|
|
34
|
+
"make": (
|
|
35
|
+
"make",
|
|
36
|
+
"macOS: xcode-select --install | Linux: sudo apt-get install make",
|
|
37
|
+
),
|
|
38
|
+
"curl": (
|
|
39
|
+
"curl",
|
|
40
|
+
"macOS: brew install curl | Linux: sudo apt-get install curl",
|
|
41
|
+
),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def check_prereqs() -> list[tuple[str, str, str]]:
|
|
46
|
+
"""Check for required tools.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
List of ``(key, display_name, install_hint)`` tuples for every missing tool.
|
|
50
|
+
Empty list means all tools are present.
|
|
51
|
+
"""
|
|
52
|
+
missing: list[tuple[str, str, str]] = []
|
|
53
|
+
for key, (name, hint) in PREREQS.items():
|
|
54
|
+
if shutil.which(key) is None:
|
|
55
|
+
missing.append((key, name, hint))
|
|
56
|
+
return missing
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ── Git clone ─────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def clone_template(github_url: str, target_dir: Path) -> None:
|
|
63
|
+
"""Clone a template repository, keeping the .git directory intact.
|
|
64
|
+
|
|
65
|
+
The cloned directory is a fully functional git repository so users can
|
|
66
|
+
commit changes, add their own remote, and push to their own repo.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
github_url: Full GitHub URL to clone.
|
|
70
|
+
target_dir: Local directory to clone into (must not exist).
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
RuntimeError: If the directory already exists or git clone fails.
|
|
74
|
+
"""
|
|
75
|
+
if target_dir.exists():
|
|
76
|
+
raise RuntimeError(f"Directory already exists: {target_dir}")
|
|
77
|
+
|
|
78
|
+
result = subprocess.run(
|
|
79
|
+
["git", "clone", github_url, str(target_dir)],
|
|
80
|
+
capture_output=True,
|
|
81
|
+
text=True,
|
|
82
|
+
)
|
|
83
|
+
if result.returncode != 0:
|
|
84
|
+
raise RuntimeError(f"git clone failed:\n{result.stderr.strip()}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ── .env injection ────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def inject_env(target_dir: Path, api_key: str) -> None:
|
|
91
|
+
"""Create or update DEEPGRAM_API_KEY in .env inside target_dir.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
target_dir: Directory containing the cloned template.
|
|
95
|
+
api_key: Deepgram API key to inject.
|
|
96
|
+
"""
|
|
97
|
+
env_path = target_dir / ".env"
|
|
98
|
+
|
|
99
|
+
if env_path.exists():
|
|
100
|
+
content = env_path.read_text()
|
|
101
|
+
if "DEEPGRAM_API_KEY=" in content and "DEEPGRAM_API_KEY=$" not in content:
|
|
102
|
+
console.print("[dim]DEEPGRAM_API_KEY already set in .env[/dim]")
|
|
103
|
+
return
|
|
104
|
+
# Replace placeholder or append
|
|
105
|
+
if "DEEPGRAM_API_KEY=" in content:
|
|
106
|
+
lines = content.splitlines()
|
|
107
|
+
new_lines = [
|
|
108
|
+
f"DEEPGRAM_API_KEY={api_key}"
|
|
109
|
+
if ln.startswith("DEEPGRAM_API_KEY=")
|
|
110
|
+
else ln
|
|
111
|
+
for ln in lines
|
|
112
|
+
]
|
|
113
|
+
env_path.write_text("\n".join(new_lines) + "\n")
|
|
114
|
+
else:
|
|
115
|
+
with open(env_path, "a") as f:
|
|
116
|
+
if not content.endswith("\n"):
|
|
117
|
+
f.write("\n")
|
|
118
|
+
f.write(f"DEEPGRAM_API_KEY={api_key}\n")
|
|
119
|
+
else:
|
|
120
|
+
env_path.write_text(f"DEEPGRAM_API_KEY={api_key}\n")
|
|
121
|
+
|
|
122
|
+
console.print("[green]✓[/green] Injected DEEPGRAM_API_KEY into .env")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ── make init ─────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def run_make_init(dest: Path) -> bool:
|
|
129
|
+
"""Run ``make check-prereqs && make init`` in the cloned directory.
|
|
130
|
+
|
|
131
|
+
Used for starter repos that ship a Makefile with these standard targets.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
dest: The cloned project directory.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
True if both commands succeeded, False otherwise.
|
|
138
|
+
"""
|
|
139
|
+
makefile = dest / "Makefile"
|
|
140
|
+
if not makefile.exists():
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
for target in ("check-prereqs", "init"):
|
|
144
|
+
result = subprocess.run(
|
|
145
|
+
["make", target],
|
|
146
|
+
cwd=str(dest),
|
|
147
|
+
)
|
|
148
|
+
if result.returncode != 0:
|
|
149
|
+
console.print(
|
|
150
|
+
f"\n[red]✗[/red] [bold]make {target}[/bold] failed "
|
|
151
|
+
f"(exit {result.returncode})"
|
|
152
|
+
)
|
|
153
|
+
if target == "check-prereqs":
|
|
154
|
+
console.print(
|
|
155
|
+
"[dim]Fix the missing prerequisites above and run "
|
|
156
|
+
f"[bold]make init[/bold] inside [cyan]{dest.name}[/cyan][/dim]"
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
console.print(
|
|
160
|
+
f"[dim]Re-run manually: cd {dest.name} && make init[/dim]"
|
|
161
|
+
)
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ── TOML lifecycle (fallback) ─────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def run_lifecycle_step(step: LifecycleStep, cwd: Path) -> bool:
|
|
171
|
+
"""Execute a lifecycle step from deepgram.toml.
|
|
172
|
+
|
|
173
|
+
Used as a fallback for repos that don't have a Makefile.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
step: The lifecycle step definition.
|
|
177
|
+
cwd: Working directory for command execution.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
True if the step succeeded, False otherwise.
|
|
181
|
+
"""
|
|
182
|
+
if step.command is None:
|
|
183
|
+
return True
|
|
184
|
+
|
|
185
|
+
commands = step.command if isinstance(step.command, list) else [step.command]
|
|
186
|
+
|
|
187
|
+
for cmd in commands:
|
|
188
|
+
if step.message:
|
|
189
|
+
console.print(f"[blue]{step.message}[/blue]")
|
|
190
|
+
else:
|
|
191
|
+
console.print(f"[dim]Running: {cmd}[/dim]")
|
|
192
|
+
|
|
193
|
+
result = subprocess.run(
|
|
194
|
+
cmd,
|
|
195
|
+
cwd=str(cwd),
|
|
196
|
+
shell=True,
|
|
197
|
+
)
|
|
198
|
+
if result.returncode != 0:
|
|
199
|
+
console.print(
|
|
200
|
+
f"[red]✗[/red] Command failed (exit {result.returncode}): {cmd}"
|
|
201
|
+
)
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
return True
|
|
@@ -1,321 +0,0 @@
|
|
|
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
|
-
)
|
|
@@ -1,104 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deepctl_cmd_init-0.0.2 → deepctl_cmd_init-0.0.3}/src/deepctl_cmd_init.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{deepctl_cmd_init-0.0.2 → deepctl_cmd_init-0.0.3}/src/deepctl_cmd_init.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{deepctl_cmd_init-0.0.2 → deepctl_cmd_init-0.0.3}/src/deepctl_cmd_init.egg-info/requires.txt
RENAMED
|
File without changes
|
{deepctl_cmd_init-0.0.2 → deepctl_cmd_init-0.0.3}/src/deepctl_cmd_init.egg-info/top_level.txt
RENAMED
|
File without changes
|