claude-code-tools 0.1.8__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.

Potentially problematic release.


This version of claude-code-tools might be problematic. Click here for more details.

@@ -0,0 +1,3 @@
1
+ """Claude Code Tools - Collection of utilities for Claude Code."""
2
+
3
+ __version__ = "0.1.8"
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env python3
2
+ """Centralized encrypted backup for .env files using SOPS."""
3
+
4
+ import os
5
+ import sys
6
+ import subprocess
7
+ import datetime
8
+ from pathlib import Path
9
+
10
+ import click
11
+
12
+
13
+ class DotenvVault:
14
+ def __init__(self):
15
+ self.vault_dir = Path.home() / "Git" / "dotenvs"
16
+ self.vault_dir.mkdir(parents=True, exist_ok=True)
17
+ self._ensure_gpg_key()
18
+
19
+ def _ensure_gpg_key(self):
20
+ """Detect GPG key or fail early."""
21
+ try:
22
+ result = subprocess.run(
23
+ ["gpg", "--list-secret-keys", "--keyid-format", "LONG"],
24
+ capture_output=True,
25
+ text=True,
26
+ check=True
27
+ )
28
+ lines = result.stdout.strip().split('\n')
29
+ for line in lines:
30
+ if 'sec' in line:
31
+ key = line.split()[1].split('/')[1]
32
+ self.gpg_key = key
33
+ return
34
+ click.echo("❌ No GPG key found. Run: gpg --gen-key", err=True)
35
+ sys.exit(1)
36
+ except subprocess.CalledProcessError:
37
+ click.echo("❌ GPG not found or error listing keys", err=True)
38
+ sys.exit(1)
39
+
40
+ def _project_name(self):
41
+ """Get current project name from directory."""
42
+ return Path.cwd().name
43
+
44
+ def _backup_path(self, project=None):
45
+ """Get encrypted file path for project."""
46
+ project = project or self._project_name()
47
+ return self.vault_dir / f"{project}.env.encrypt"
48
+
49
+ def encrypt(self, force=False):
50
+ """Encrypt current .env to centralized vault with protection."""
51
+ env_file = Path.cwd() / ".env"
52
+ if not env_file.exists():
53
+ click.echo("❌ .env not found in current directory")
54
+ return False
55
+
56
+ backup_path = self._backup_path()
57
+
58
+ # Protect encrypted backup
59
+ if backup_path.exists():
60
+ if not force:
61
+ click.echo("⚠️ Encrypted backup already exists!")
62
+ if not click.confirm("Overwrite encrypted backup?", default=False):
63
+ return False
64
+
65
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
66
+ backup_save = f"{backup_path}.backup-{timestamp}"
67
+ backup_path.rename(backup_save)
68
+ click.echo(f"💾 Backed up encrypted file → {backup_save}")
69
+
70
+ cmd = [
71
+ "sops", "--encrypt",
72
+ "--pgp", self.gpg_key,
73
+ "--input-type", "dotenv",
74
+ "--output-type", "dotenv",
75
+ str(env_file)
76
+ ]
77
+
78
+ try:
79
+ with open(backup_path, 'w') as f:
80
+ subprocess.run(cmd, stdout=f, check=True)
81
+ click.echo(f"✅ Encrypted .env → {backup_path}")
82
+ return True
83
+ except subprocess.CalledProcessError as e:
84
+ click.echo(f"❌ Encryption failed: {e}")
85
+ return False
86
+
87
+ def decrypt(self, force=False):
88
+ """Decrypt from centralized vault to .env with safety checks."""
89
+ backup_path = self._backup_path()
90
+ if not backup_path.exists():
91
+ click.echo(f"❌ {backup_path} not found")
92
+ return False
93
+
94
+ env_file = Path.cwd() / ".env"
95
+
96
+ # Check if current .env is newer than backup
97
+ if env_file.exists():
98
+ env_mtime = env_file.stat().st_mtime
99
+ backup_mtime = backup_path.stat().st_mtime
100
+
101
+ if env_mtime > backup_mtime:
102
+ click.echo("⚠️ Current .env is newer than backup!")
103
+ if not click.confirm("Proceed with decryption?", default=False):
104
+ return False
105
+
106
+ # Always backup current .env if it exists
107
+ if env_file.exists():
108
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
109
+ backup_name = f".env.backup-{timestamp}"
110
+ env_file.rename(backup_name)
111
+ click.echo(f"💾 Backed up current .env → {backup_name}")
112
+
113
+ cmd = [
114
+ "sops", "--decrypt",
115
+ "--input-type", "dotenv",
116
+ "--output-type", "dotenv",
117
+ str(backup_path)
118
+ ]
119
+
120
+ try:
121
+ with open(env_file, 'w') as f:
122
+ subprocess.run(cmd, stdout=f, check=True)
123
+ click.echo(f"✅ Decrypted {backup_path} → {env_file}")
124
+ return True
125
+ except subprocess.CalledProcessError as e:
126
+ click.echo(f"❌ Decryption failed: {e}")
127
+ return False
128
+
129
+ def list_backups(self):
130
+ """List all encrypted backups."""
131
+ backups = list(self.vault_dir.glob("*.env.encrypt"))
132
+ if not backups:
133
+ click.echo("❌ No encrypted files found")
134
+ return
135
+
136
+ click.echo(f"📋 Encrypted files in {self.vault_dir}:")
137
+ for backup in sorted(backups):
138
+ size = backup.stat().st_size
139
+ click.echo(f" {backup.name} ({size} bytes)")
140
+
141
+ def status(self):
142
+ """Show status for current project."""
143
+ project = self._project_name()
144
+ backup_path = self._backup_path()
145
+
146
+ env_exists = Path(".env").exists()
147
+ backup_exists = backup_path.exists()
148
+
149
+ if env_exists and backup_exists:
150
+ env_mtime = Path(".env").stat().st_mtime
151
+ backup_mtime = backup_path.stat().st_mtime
152
+
153
+ if env_mtime > backup_mtime:
154
+ click.echo("📊 Local .env is NEWER than backup")
155
+ click.echo(f" .env: {datetime.datetime.fromtimestamp(env_mtime).strftime('%Y-%m-%d %H:%M:%S')}")
156
+ click.echo(f" backup: {datetime.datetime.fromtimestamp(backup_mtime).strftime('%Y-%m-%d %H:%M:%S')}")
157
+ return "local_newer"
158
+ elif backup_mtime > env_mtime:
159
+ click.echo("📊 Backup is NEWER than local .env")
160
+ click.echo(f" .env: {datetime.datetime.fromtimestamp(env_mtime).strftime('%Y-%m-%d %H:%M:%S')}")
161
+ click.echo(f" backup: {datetime.datetime.fromtimestamp(backup_mtime).strftime('%Y-%m-%d %H:%M:%S')}")
162
+ return "backup_newer"
163
+ else:
164
+ click.echo("✅ .env and backup are identical")
165
+ return "identical"
166
+ elif env_exists:
167
+ click.echo("📊 .env exists, no backup")
168
+ return "local_only"
169
+ elif backup_exists:
170
+ click.echo("📊 Backup exists, no local .env")
171
+ return "backup_only"
172
+ else:
173
+ click.echo("❌ Neither .env nor backup exists")
174
+ return "neither"
175
+
176
+ def sync(self, direction=None):
177
+ """Smart sync between local .env and centralized vault."""
178
+ project = self._project_name()
179
+ backup_path = self._backup_path()
180
+ env_file = Path.cwd() / ".env"
181
+
182
+ status = self.status()
183
+
184
+ if status == "identical":
185
+ click.echo("✅ Already in sync")
186
+ return True
187
+
188
+ elif status == "local_only":
189
+ click.echo("🔄 Encrypting local .env to centralized vault")
190
+ return self.encrypt()
191
+
192
+ elif status == "backup_only":
193
+ click.echo("🔄 Decrypting centralized vault to local .env")
194
+ return self.decrypt()
195
+
196
+ elif status == "local_newer":
197
+ if direction == "pull":
198
+ click.echo("⚠️ Local .env is newer but --pull requested")
199
+ if click.confirm("Overwrite local .env with backup?", default=False):
200
+ return self.decrypt()
201
+ else:
202
+ click.echo("🔄 Encrypting local .env to centralized vault")
203
+ return self.encrypt()
204
+
205
+ elif status == "backup_newer":
206
+ if direction == "push":
207
+ click.echo("⚠️ Backup is newer but --push requested")
208
+ if click.confirm("Overwrite backup with local .env?", default=False):
209
+ return self.encrypt()
210
+ else:
211
+ click.echo("🔄 Decrypting centralized vault to local .env")
212
+ return self.decrypt()
213
+
214
+
215
+ def main():
216
+ """Centralized encrypted backup for .env files."""
217
+ import sys
218
+
219
+ if len(sys.argv) == 1:
220
+ # Default to sync if no command provided
221
+ vault = DotenvVault()
222
+ vault.sync()
223
+ else:
224
+ # Normal CLI handling
225
+ import click
226
+
227
+ @click.group()
228
+ def cli():
229
+ """Centralized encrypted backup for .env files."""
230
+ pass
231
+
232
+ @cli.command()
233
+ def encrypt():
234
+ """Encrypt current .env to centralized vault."""
235
+ vault = DotenvVault()
236
+ vault.encrypt()
237
+
238
+ @cli.command()
239
+ def decrypt():
240
+ """Decrypt from centralized vault to .env."""
241
+ vault = DotenvVault()
242
+ vault.decrypt()
243
+
244
+ @cli.command()
245
+ def list():
246
+ """List all encrypted backups."""
247
+ vault = DotenvVault()
248
+ vault.list_backups()
249
+
250
+ @cli.command()
251
+ def status():
252
+ """Show detailed status for current project."""
253
+ vault = DotenvVault()
254
+ vault.status()
255
+
256
+ @cli.command()
257
+ @click.option('--push', 'direction', flag_value='push', help='Force push (encrypt)')
258
+ @click.option('--pull', 'direction', flag_value='pull', help='Force pull (decrypt)')
259
+ def sync(direction):
260
+ """Smart sync between local .env and centralized vault."""
261
+ vault = DotenvVault()
262
+ vault.sync(direction)
263
+
264
+ cli()
265
+
266
+
267
+ if __name__ == "__main__":
268
+ main()