deepctl-cmd-init 0.0.4__tar.gz → 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepctl-cmd-init
3
- Version: 0.0.4
3
+ Version: 0.1.1
4
4
  Summary: Init command for deepctl — scaffold Deepgram starter apps
5
5
  Author-email: Deepgram <devrel@deepgram.com>
6
6
  Maintainer-email: Deepgram <devrel@deepgram.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepctl-cmd-init"
7
- version = "0.0.4" # x-release-please-version
7
+ version = "0.1.1" # x-release-please-version
8
8
  description = "Init command for deepctl — scaffold Deepgram starter apps"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  from typing import Any
7
7
 
8
8
  from deepctl_core import (
9
+ AuthenticationError,
9
10
  AuthManager,
10
11
  BaseCommand,
11
12
  Config,
@@ -207,8 +208,10 @@ class InitCommand(BaseCommand):
207
208
  api_key = auth_manager.get_api_key()
208
209
  if api_key:
209
210
  lifecycle.inject_env(dest, api_key)
210
- except Exception:
211
- pass # Auth is optional
211
+ except (AuthenticationError, OSError) as exc:
212
+ console.print(
213
+ f"[yellow]Could not inject Deepgram credentials:[/yellow] {exc}"
214
+ )
212
215
 
213
216
  # ── Setup (make check-prereqs && make init) ────────────────────────────
214
217
  installed = False
@@ -2,10 +2,12 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
5
6
  import shutil
6
7
  import subprocess
7
8
  from typing import TYPE_CHECKING
8
9
 
10
+ from deepctl_core.output import print_hint, print_info, print_warning
9
11
  from rich.console import Console
10
12
 
11
13
  if TYPE_CHECKING:
@@ -76,7 +78,9 @@ def clone_template(github_url: str, target_dir: Path) -> None:
76
78
  raise RuntimeError(f"Directory already exists: {target_dir}")
77
79
 
78
80
  result = subprocess.run(
79
- ["git", "clone", github_url, str(target_dir)],
81
+ # "--" ends git's options: a server-supplied URL that starts with "-"
82
+ # is taken as the repository, never as a git option.
83
+ ["git", "clone", "--", github_url, str(target_dir)],
80
84
  capture_output=True,
81
85
  text=True,
82
86
  )
@@ -87,19 +91,148 @@ def clone_template(github_url: str, target_dir: Path) -> None:
87
91
  # ── .env injection ────────────────────────────────────────────────────────────
88
92
 
89
93
 
94
+ #: Lines in a .gitignore that already keep ``.env`` out of git. Used only
95
+ #: when git itself isn't available to ask.
96
+ _GITIGNORE_ENV_PATTERNS = {".env", "/.env", ".env*", "/.env*", "*.env"}
97
+
98
+ _NOFOLLOW = getattr(os, "O_NOFOLLOW", 0)
99
+
100
+
101
+ def _write_private(path: Path, text: str) -> None:
102
+ """Write ``text`` to ``path`` readable by the owner only (0600).
103
+
104
+ The file is created 0600 (no window where the umask applies), and an
105
+ existing file is narrowed to 0600 before its new content is written.
106
+ """
107
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC | _NOFOLLOW, 0o600)
108
+ try:
109
+ if os.name == "posix":
110
+ os.fchmod(fd, 0o600)
111
+ os.write(fd, text.encode())
112
+ finally:
113
+ os.close(fd)
114
+
115
+
116
+ def _restrict_to_owner(path: Path) -> None:
117
+ """Narrow an existing file that holds a key to owner-only (POSIX)."""
118
+ if os.name != "posix":
119
+ return
120
+ fd = os.open(path, os.O_RDONLY | _NOFOLLOW)
121
+ try:
122
+ os.fchmod(fd, 0o600)
123
+ finally:
124
+ os.close(fd)
125
+
126
+
127
+ def _git(target_dir: Path, *args: str) -> int | None:
128
+ """Exit code of ``git -C target_dir args``, or None if git can't run."""
129
+ if shutil.which("git") is None:
130
+ return None
131
+ try:
132
+ # Only this project's repository: an inherited GIT_DIR, GIT_WORK_TREE
133
+ # or GIT_INDEX_FILE would point git at another one.
134
+ env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
135
+ result = subprocess.run(
136
+ ["git", "-C", str(target_dir), *args],
137
+ capture_output=True,
138
+ text=True,
139
+ env=env,
140
+ )
141
+ except OSError:
142
+ return None
143
+ return result.returncode
144
+
145
+
146
+ def _env_ignored(target_dir: Path) -> bool | None:
147
+ """git's answer to "is .env ignored?", or None when git can't say."""
148
+ rc = _git(target_dir, "check-ignore", "-q", ".env")
149
+ if rc in (0, 1):
150
+ return rc == 0
151
+ return None
152
+
153
+
154
+ def _append_env_line(gitignore: Path) -> None:
155
+ """Append ``.env`` as the last line: the last matching pattern wins,
156
+ so it also overrides an earlier ``!.env``."""
157
+ content = gitignore.read_bytes() if gitignore.exists() else b""
158
+ newline = b"\r\n" if b"\r\n" in content else b"\n"
159
+ sep = newline if content and not content.endswith(b"\n") else b""
160
+ with open(gitignore, "ab") as f:
161
+ f.write(sep + b".env" + newline)
162
+
163
+
164
+ def ensure_env_gitignored(target_dir: Path) -> None:
165
+ """Keep ``.env`` out of git in ``target_dir`` and say what happened.
166
+
167
+ Asks git (``check-ignore``) when it can, so negations and order count,
168
+ and warns when the template already tracks ``.env``: ignoring a
169
+ tracked file doesn't stop a commit from including it.
170
+ """
171
+ gitignore = target_dir / ".gitignore"
172
+ tracked = _git(target_dir, "ls-files", "--error-unmatch", ".env") == 0
173
+ if tracked:
174
+ # Ignoring a tracked file changes nothing; untracking it does.
175
+ print_warning(
176
+ "This template tracks .env in git, so a commit would include your API key.",
177
+ stderr=True,
178
+ )
179
+ print_hint("Before you commit, run: git rm --cached .env")
180
+ return
181
+ if gitignore.is_symlink():
182
+ # A cloned repo controls its links: never write through one.
183
+ print_warning(
184
+ "Did not add .env to .gitignore: it is a symbolic link. "
185
+ "Make sure git ignores .env before you commit.",
186
+ stderr=True,
187
+ )
188
+ return
189
+ ignored = _env_ignored(target_dir)
190
+ if ignored is None:
191
+ lines = (
192
+ {ln.strip() for ln in gitignore.read_text().splitlines()}
193
+ if gitignore.exists()
194
+ else set()
195
+ )
196
+ if not lines & _GITIGNORE_ENV_PATTERNS:
197
+ _append_env_line(gitignore)
198
+ print_info(_ADDED, stderr=True)
199
+ elif not ignored:
200
+ _append_env_line(gitignore)
201
+ if _env_ignored(target_dir):
202
+ print_info(_ADDED, stderr=True)
203
+
204
+
205
+ _ADDED = "Added .env to .gitignore to keep your API key out of git."
206
+
207
+
90
208
  def inject_env(target_dir: Path, api_key: str) -> None:
91
209
  """Create or update DEEPGRAM_API_KEY in .env inside target_dir.
92
210
 
211
+ The .env file is owner-only (0600) and kept out of git, since it holds
212
+ the API key.
213
+
93
214
  Args:
94
215
  target_dir: Directory containing the cloned template.
95
216
  api_key: Deepgram API key to inject.
96
217
  """
97
218
  env_path = target_dir / ".env"
98
219
 
220
+ if env_path.is_symlink():
221
+ # A cloned repo controls its links: never write the key through one.
222
+ print_warning(
223
+ "Not writing your API key to .env: it is a symbolic link.", stderr=True
224
+ )
225
+ print_hint(
226
+ "If you trust the file it links to, add DEEPGRAM_API_KEY there yourself."
227
+ )
228
+ return
229
+
99
230
  if env_path.exists():
100
231
  content = env_path.read_text()
101
232
  if "DEEPGRAM_API_KEY=" in content and "DEEPGRAM_API_KEY=$" not in content:
233
+ _restrict_to_owner(env_path)
102
234
  console.print("[dim]DEEPGRAM_API_KEY already set in .env[/dim]")
235
+ ensure_env_gitignored(target_dir)
103
236
  return
104
237
  # Replace placeholder or append
105
238
  if "DEEPGRAM_API_KEY=" in content:
@@ -110,16 +243,15 @@ def inject_env(target_dir: Path, api_key: str) -> None:
110
243
  else ln
111
244
  for ln in lines
112
245
  ]
113
- env_path.write_text("\n".join(new_lines) + "\n")
246
+ _write_private(env_path, "\n".join(new_lines) + "\n")
114
247
  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")
248
+ sep = "" if content.endswith("\n") or not content else "\n"
249
+ _write_private(env_path, f"{content}{sep}DEEPGRAM_API_KEY={api_key}\n")
119
250
  else:
120
- env_path.write_text(f"DEEPGRAM_API_KEY={api_key}\n")
251
+ _write_private(env_path, f"DEEPGRAM_API_KEY={api_key}\n")
121
252
 
122
253
  console.print("[green]✓[/green] Injected DEEPGRAM_API_KEY into .env")
254
+ ensure_env_gitignored(target_dir)
123
255
 
124
256
 
125
257
  # ── make init ─────────────────────────────────────────────────────────────────
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepctl-cmd-init
3
- Version: 0.0.4
3
+ Version: 0.1.1
4
4
  Summary: Init command for deepctl — scaffold Deepgram starter apps
5
5
  Author-email: Deepgram <devrel@deepgram.com>
6
6
  Maintainer-email: Deepgram <devrel@deepgram.com>