-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall.py
More file actions
407 lines (349 loc) · 18.4 KB
/
Copy pathinstall.py
File metadata and controls
407 lines (349 loc) · 18.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
#!/usr/bin/env python3
# konsole-claude-counter — install.py
#
# A platform-agnostic, pure-Python installer for the Claude Code statusline
# and Gemini CLI custom Corporate theme and hooks. This script works natively
# on Linux (Debian, Arch), macOS, and Windows without external dependencies (like bash or jq).
import os
import sys
import json
import shutil
import time
from pathlib import Path
def print_step(msg):
print(f"\033[38;5;178m•\033[0m {msg}")
def print_success(msg):
print(f"\033[32m✔\033[0m {msg}")
def print_error(msg):
print(f"\033[31m✘\033[0m {msg}", file=sys.stderr)
def backup_file(file_path):
if file_path.exists():
backup_path = file_path.with_suffix(f".bak.{int(time.time())}")
try:
shutil.copy2(file_path, backup_path)
print_step(f"Created backup of existing configuration: {backup_path.name}")
except Exception as e:
print_error(f"Failed to create backup of {file_path.name}: {e}")
def merge_yaml_keys(file_path, keys):
"""Write or merge flat key: value entries into a YAML config file without external deps."""
lines = []
if file_path.exists():
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
except Exception:
lines = []
updated = set()
new_lines = []
for line in lines:
stripped = line.strip()
if not stripped or stripped.startswith('#'):
new_lines.append(line)
continue
if ':' in stripped:
key = stripped.split(':', 1)[0].strip()
if key in keys:
new_lines.append(f"{key}: {keys[key]}\n")
updated.add(key)
continue
new_lines.append(line)
for key, value in keys.items():
if key not in updated:
new_lines.append(f"{key}: {value}\n")
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
def main():
print("\n\033[1;38;5;178m┌────────────────────────────────────────────────────────┐\033[0m")
print("\033[1;38;5;178m│ KONSOLE CLAUDE COUNTER — UNIVERSAL INSTALLER │\033[0m")
print("\033[1;38;5;178m└────────────────────────────────────────────────────────┘\033[0m\n")
# ---- Establish standard directory paths ------------------------
home = Path.home()
claude_dir = Path(os.environ.get("CLAUDE_DIR", home / ".claude"))
gemini_dir = Path(os.environ.get("GEMINI_CLI_HOME", home / ".gemini"))
aider_dir = home / ".aider"
aider_conf = home / ".aider.conf.yml"
local_bin_dir = home / ".local" / "bin"
# Source files in current working directory
current_dir = Path(__file__).parent.resolve()
python_src = current_dir / "statusline.py"
if not python_src.exists():
print_error(f"Error: Could not find 'statusline.py' in current directory ({current_dir})")
sys.exit(1)
# Detect execution environment details
is_windows = os.name == "nt"
python_cmd = "python" if is_windows else "python3"
print_step(f"Detected Operating System: {'Windows' if is_windows else 'POSIX (' + sys.platform + ')'}")
print_step(f"User Home Directory: {home}")
# Determine if targets are present on machine
has_claude = shutil.which("claude") is not None or claude_dir.exists()
has_gemini = shutil.which("gemini") is not None or gemini_dir.exists()
has_aider = shutil.which("aider") is not None or aider_dir.exists() or aider_conf.exists()
if not has_claude and not has_gemini and not has_aider:
print_step("Neither 'claude', 'gemini', nor 'aider' CLI binaries were detected.")
print_step("Configuring all environments as standard targets.")
has_claude = True
has_gemini = True
has_aider = True
# ---- 1. Install Claude Code Integration ------------------------
if has_claude:
print("\n\033[1;36m[Claude Code Configuration]\033[0m")
try:
claude_dir.mkdir(parents=True, exist_ok=True)
# Copy backend statusline.py
claude_python_dest = claude_dir / "statusline.py"
shutil.copy2(python_src, claude_python_dest)
# Set executable permissions on Unix systems
if not is_windows:
try:
claude_python_dest.chmod(0o755)
except Exception:
pass
print_success(f"Copied statusline.py backend -> {claude_python_dest}")
# Generate and write OS-appropriate wrapper
if is_windows:
claude_wrapper_dest = claude_dir / "konsole-claude-counter.bat"
with open(claude_wrapper_dest, "w", encoding="utf-8") as f:
f.write(f'@echo off\npython "%~dp0statusline.py" %*\n')
print_success(f"Generated Windows CMD wrapper -> {claude_wrapper_dest}")
statusline_cmd_val = str(claude_wrapper_dest)
else:
claude_wrapper_dest = claude_dir / "konsole-claude-counter.sh"
with open(claude_wrapper_dest, "w", encoding="utf-8") as f:
f.write(f'#!/bin/sh\nexec python3 "$(dirname "$0")/statusline.py" "$@"\n')
try:
claude_wrapper_dest.chmod(0o755)
except Exception:
pass
print_success(f"Generated POSIX shell wrapper -> {claude_wrapper_dest}")
statusline_cmd_val = str(claude_wrapper_dest)
# Update settings.json safely using standard json module
claude_settings = claude_dir / "settings.json"
backup_file(claude_settings)
settings_data = {}
if claude_settings.exists():
try:
with open(claude_settings, "r", encoding="utf-8") as f:
settings_data = json.load(f)
except Exception as e:
print_step(f"Warning: Failed to parse existing Claude settings.json ({e}). Creating fresh.")
settings_data["statusLine"] = {
"type": "command",
"command": statusline_cmd_val,
"padding": 0
}
with open(claude_settings, "w", encoding="utf-8") as f:
json.dump(settings_data, f, indent=4)
print_success(f"Successfully configured Claude settings -> {claude_settings}")
except Exception as e:
print_error(f"Failed to configure Claude Code integration: {e}")
# ---- 2. Install Gemini CLI Integration -------------------------
if has_gemini:
print("\n\033[1;35m[Gemini CLI Configuration]\033[0m")
try:
gemini_dir.mkdir(parents=True, exist_ok=True)
# Copy backend statusline.py
gemini_python_dest = gemini_dir / "statusline.py"
shutil.copy2(python_src, gemini_python_dest)
if not is_windows:
try:
gemini_python_dest.chmod(0o755)
except Exception:
pass
print_success(f"Copied statusline.py backend -> {gemini_python_dest}")
# Update settings.json safely
gemini_settings = gemini_dir / "settings.json"
backup_file(gemini_settings)
settings_data = {}
if gemini_settings.exists():
try:
with open(gemini_settings, "r", encoding="utf-8") as f:
settings_data = json.load(f)
except Exception as e:
print_step(f"Warning: Failed to parse existing Gemini settings.json ({e}). Creating fresh.")
# Inject Hooks for live statusline integration in Gemini
if "hooks" not in settings_data:
settings_data["hooks"] = {}
# Ensure hooksConfig is enabled
if "hooksConfig" not in settings_data:
settings_data["hooksConfig"] = {}
settings_data["hooksConfig"]["enabled"] = True
if "notifications" not in settings_data["hooksConfig"]:
settings_data["hooksConfig"]["notifications"] = False
# Build command to execute Python statusline script
# Path should use forward slashes even on Windows for settings.json standard compatibility
statusline_py_path = str(gemini_python_dest).replace("\\", "/")
hook_cmd = f'{python_cmd} "{statusline_py_path}"'
hook_item = {
"name": "statusline",
"type": "command",
"command": hook_cmd,
"description": "Displays the compact AI usage statusline"
}
for event in ["SessionStart", "AfterAgent"]:
existing_groups = settings_data["hooks"].get(event, [])
if not isinstance(existing_groups, list):
existing_groups = []
# Clean up legacy flat array items (e.g. dicts without a "hooks" key)
existing_groups = [
g for g in existing_groups
if isinstance(g, dict) and "hooks" in g
]
# Find or create a group with matcher "*"
target_group = None
for group in existing_groups:
if isinstance(group, dict) and "hooks" in group and isinstance(group["hooks"], list):
target_group = group
break
if target_group is None:
target_group = {
"matcher": "*",
"sequential": False,
"hooks": []
}
existing_groups.append(target_group)
# Check if hook with this command already exists inside this group
already_exists = any(
isinstance(h, dict) and h.get("command") == hook_cmd
for h in target_group["hooks"]
)
if not already_exists:
target_group["hooks"].append(hook_item)
settings_data["hooks"][event] = existing_groups
with open(gemini_settings, "w", encoding="utf-8") as f:
json.dump(settings_data, f, indent=4)
print_success(f"Successfully configured Gemini theme, footer options, and hooks -> {gemini_settings}")
except Exception as e:
print_error(f"Failed to configure Gemini CLI integration: {e}")
# ---- 3. Install Aider Configuration ----------------------------
if has_aider:
print("\n\033[1;32m[Aider Configuration]\033[0m")
try:
aider_dir.mkdir(parents=True, exist_ok=True)
print_success(f"Ensured ~/.aider directory exists -> {aider_dir}")
backup_file(aider_conf)
merge_yaml_keys(aider_conf, {
"analytics-log": "~/.aider/analytics.jsonl",
"attribute-co-authored-by": "false"
})
print_success(f"Configured Aider analytics log and co-author attribution -> {aider_conf}")
except Exception as e:
print_error(f"Failed to configure Aider integration: {e}")
# ---- 4. Install VS Code Extension (optional) -------------------
print("\n\033[1;34m[VS Code Extension Installation]\033[0m")
try:
# Locate the VSIX file relative to this script
vsix_dir = current_dir / "vscode-extension"
vsix_candidates = list(vsix_dir.glob("*.vsix")) if vsix_dir.is_dir() else []
vsix_path = vsix_candidates[0] if vsix_candidates else None
# Resolve the 'code' CLI binary — VS Code ships it on all platforms;
# Windows also registers 'code.cmd' in PATH.
code_cmd = shutil.which("code") or shutil.which("code.cmd")
if not vsix_path:
print_step("No .vsix package found in vscode-extension/ — skipping VS Code installation.")
print_step("Run 'npm install && npx @vscode/vsce package' inside vscode-extension/ to build it.")
elif not code_cmd:
print_step("VS Code 'code' CLI not found in PATH — skipping VS Code extension installation.")
if is_windows:
print_step("On Windows: ensure VS Code is installed and 'Add to PATH' was checked during setup,")
print_step(" or launch VS Code and run: Extensions → Install from VSIX…")
else:
print_step("On macOS/Linux: open VS Code → Command Palette → 'Install code command in PATH',")
print_step(" then re-run this installer, or install manually:")
print_step(f" code --install-extension \"{vsix_path}\"")
else:
import subprocess
print_step(f"Found VSIX: {vsix_path.name}")
print_step(f"Installing with: {code_cmd} --install-extension ...")
result = subprocess.run(
[code_cmd, "--install-extension", str(vsix_path), "--force"],
capture_output=True, text=True
)
if result.returncode == 0:
print_success(f"VS Code extension installed successfully.")
print_success("Reload VS Code to activate: the AI status bar and sidebar will appear automatically.")
else:
err = (result.stderr or result.stdout or "").strip()
print_error(f"VS Code extension installation failed (exit {result.returncode}): {err}")
print_step(f"Manual install: code --install-extension \"{vsix_path}\"")
except Exception as e:
print_error(f"VS Code extension step failed: {e}")
# ---- 5. Create Local Command Line Utility ----------------------
print("\n\033[1;33m[Unified CLI Utility Installation]\033[0m")
try:
local_bin_dir.mkdir(parents=True, exist_ok=True)
if is_windows:
report_script_dest = local_bin_dir / "cognitive-report.bat"
with open(report_script_dest, "w", encoding="utf-8") as f:
f.write(
f'@echo off\n'
f'if exist "%USERPROFILE%\\.gemini\\statusline.py" (\n'
f' python "%USERPROFILE%\\.gemini\\statusline.py" %*\n'
f') else if exist "%USERPROFILE%\\.claude\\statusline.py" (\n'
f' python "%USERPROFILE%\\.claude\\statusline.py" %*\n'
f') else (\n'
f' echo Error: statusline.py is not installed in .gemini or .claude.\n'
f' exit /b 1\n'
f')\n'
)
print_success(f"Installed Windows batch CLI shortcut -> {report_script_dest}")
else:
report_script_dest = local_bin_dir / "cognitive-report"
with open(report_script_dest, "w", encoding="utf-8") as f:
f.write(
f'#!/bin/sh\n'
f'# cognitive-report — A unified AI usage and cost reporter.\n\n'
f'if [ -f "$HOME/.gemini/statusline.py" ]; then\n'
f' exec {python_cmd} "$HOME/.gemini/statusline.py" "$@"\n'
f'elif [ -f "$HOME/.claude/statusline.py" ]; then\n'
f' exec {python_cmd} "$HOME/.claude/statusline.py" "$@"\n'
f'else\n'
f' echo "Error: statusline.py is not installed in ~/.gemini/ or ~/.claude/" >&2\n'
f' exit 1\n'
f'fi\n'
)
try:
report_script_dest.chmod(0o755)
except Exception:
pass
print_success(f"Installed POSIX CLI shortcut -> {report_script_dest}")
except Exception as e:
print_error(f"Failed to set up unified CLI report utility: {e}")
# ---- Installation Wrap-up --------------------------------------
print("\n\033[1;32m┌────────────────────────────────────────────────────────┐\033[0m")
print("\033[1;32m│ INSTALLATION COMPLETED SUCCESS! │\033[0m")
print("\033[1;32m└────────────────────────────────────────────────────────┘\033[0m\n")
if has_claude:
print("• \033[1mClaude Code\033[0m: Launch or restart a session ('claude') to see the real-time statusline.")
if has_gemini:
print("• \033[1mGemini CLI\033[0m: Session/rate statusline hooks are fully enabled.")
if has_aider:
print("• \033[1mAider\033[0m: Analytics log and co-author attribution configured (~/.aider.conf.yml).")
print("• \033[1mVS Code Extension\033[0m: Reload VS Code — the AI status bar and sidebar panel will activate.")
print("• \033[1mUnified report\033[0m: Run 'cognitive-report' to generate your corporate resource dashboard.")
# Path warning for users
path_checked = False
path_env = os.environ.get("PATH", "")
local_bin_str = str(local_bin_dir)
if is_windows:
# Check if %USERPROFILE%\.local\bin is in PATH
userprofile = os.environ.get("USERPROFILE", "")
win_local_bin = f"{userprofile}\\.local\\bin"
if win_local_bin.lower() in path_env.lower() or "%USERPROFILE%\\.local\\bin".lower() in path_env.lower():
path_checked = True
else:
# Check if ~/.local/bin or $HOME/.local/bin is in PATH
home_local_bin = f"{home}/.local/bin"
if home_local_bin in path_env or "~/.local/bin" in path_env:
path_checked = True
if not path_checked:
print("\n\033[1;33m⚠️ PATH Configuration Notice:\033[0m")
if is_windows:
print(f" The directory '{local_bin_dir}' is not detected in your Path environment variable.")
print(" Please add it to your Path so you can run 'cognitive-report' from any command prompt.")
else:
print(f" The directory '{local_bin_dir}' is not detected in your $PATH.")
print(" To run 'cognitive-report' globally, add the following to your profile (~/.bashrc or ~/.zshrc):")
print(" export PATH=\"$HOME/.local/bin:$PATH\"")
print()
if __name__ == "__main__":
main()