.PHONY: deploy/klaus
deploy/klaus: ##H @Remote Deploy Klaus (systemd + nginx) and install deps
@echo "Uploading deployment bundle..."
- tar cz -C etc/systemd/system klaus.service -C ../../nginx/conf.d klaus.conf | ssh $(VPS) "cat > /tmp/klaus-deploy.tgz"
+ tar cz -C etc/systemd/system klaus.service -C ../../nginx/conf.d klaus.conf -C ../../../scripts klaus_app.py | ssh $(VPS) "cat > /tmp/klaus-deploy.tgz"
@echo "Installing on $(VPS_HOST)..."
ssh -t $(VPS) "cd /tmp && tar xz -f klaus-deploy.tgz && \
- sudo pip3 install klaus gunicorn && \
- sudo mv klaus.service /etc/systemd/system/klaus.service && \
- sudo systemctl daemon-reload && \
- sudo systemctl enable --now klaus && \
- sudo mv /etc/nginx/conf.d/git-http.conf /etc/nginx/conf.d/git-http.conf.disabled 2>/dev/null || true && \
- sudo mv klaus.conf /etc/nginx/conf.d/klaus.conf && \
- sudo nginx -t && \
- sudo systemctl reload nginx && \
+ sudo bash -c '# apt-get update && apt-get install -y universal-ctags && \
+ pip3 install klaus gunicorn markdown && \
+ mv klaus_app.py /usr/local/bin/klaus_app.py && \
+ mv klaus.service /etc/systemd/system/klaus.service && \
+ systemctl daemon-reload && \
+ systemctl enable --now klaus && \
+ systemctl restart klaus && \
+ mv /etc/nginx/conf.d/git-http.conf /etc/nginx/conf.d/git-http.conf.disabled 2>/dev/null || true && \
+ mv klaus.conf /etc/nginx/conf.d/klaus.conf && \
+ nginx -t && \
+ systemctl reload nginx' && \
rm klaus-deploy.tgz"
@echo "Klaus deployed!"
.PHONY: git/list
git/list: ##H @Local List tracked repositories
@python3 scripts/manage_repos.py list
+
+.PHONY: git/sync
+git/sync: ##H @Local Sync remote repositories to local JSON
+ @python3 scripts/manage_repos.py --remote $(VPS) sync
print(f"Scanning {remote}:{GIT_ROOT}...")
- # 1. Find all .git directories
- # We look for something ending in .git.
- # Use -maxdepth 3 to catch projects/foo.git but avoid deep nesting
- find_cmd = ['ssh', remote, f'find {GIT_ROOT} -maxdepth 3 -name "*.git" -type d']
+ # Remote python script to gather all metadata in one go
+ remote_script = f"""
+import os, json, subprocess
+
+root = "{GIT_ROOT}"
+results = {{}}
+
+for dirpath, dirnames, filenames in os.walk(root):
+ for d in dirnames:
+ if d.endswith('.git'):
+ full_path = os.path.join(dirpath, d)
+ rel_path = os.path.relpath(full_path, root)
+
+ # Get description
+ desc = ""
+ desc_file = os.path.join(full_path, 'description')
+ if os.path.exists(desc_file):
+ try:
+ with open(desc_file, 'r') as f:
+ desc = f.read().strip()
+ if "Unnamed repository" in desc: desc = ""
+ except: pass
+
+ # Get owner/origin via git config
+ owner = ""
+ origin = ""
+ try:
+ # We use git config to read keys.
+ # Note: 'git config' might fail if safe.directory issues, but usually fine for reading files directly if we parse?
+ # Safer to use git command.
+ # Only run if directory seems valid.
+ cmd = ['git', 'config', '--file', os.path.join(full_path, 'config'), '--list']
+ out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL, universal_newlines=True)
+ for line in out.splitlines():
+ if line.startswith('gitweb.owner='):
+ owner = line.split('=', 1)[1]
+ if line.startswith('remote.origin.url='):
+ origin = line.split('=', 1)[1]
+ except:
+ pass
+
+ results[rel_path] = {{
+ 'description': desc,
+ 'owner': owner,
+ 'remotes': {{'origin': origin}} if origin else {{}}
+ }}
+
+print(json.dumps(results))
+"""
+
+ # Run the script remotely via SSH
+ cmd = ['ssh', remote, 'python3', '-c', shlex.quote(remote_script)]
+
try:
- res = subprocess.check_output(find_cmd, universal_newlines=True)
+ output = subprocess.check_output(cmd, universal_newlines=True)
+ remote_data = json.loads(output)
except subprocess.CalledProcessError as e:
- print(f"Error scanning remote: {e}")
- sys.exit(1)
-
- paths = [p.strip() for p in res.splitlines() if p.strip()]
-
+ print(f"Error executing remote fetch: {e}")
+ return
+ except json.JSONDecodeError as e:
+ print(f"Error parsing remote response: {e}")
+ print("Raw output:", output)
+ return
+
+ # Update local data
data = load_repos()
updated_count = 0
new_count = 0
- for full_path in paths:
- if not full_path.startswith(GIT_ROOT):
- continue
-
- rel_path = os.path.relpath(full_path, GIT_ROOT)
- # normalize
- if not rel_path.endswith('.git'): rel_path += '.git'
-
- # Check if we assume 'projects/' prefix if it's missing?
- # The script generally enforces strict paths.
-
- print(f"Processing {rel_path}...")
-
- # Get Description
- description = ""
- try:
- # cat description file. Silence stderr in case missing.
- desc_cmd = ['ssh', remote, f'cat {shlex.quote(os.path.join(full_path, "description"))}']
- description = subprocess.check_output(desc_cmd, stderr=subprocess.DEVNULL, universal_newlines=True).strip()
- # Default git description is usually "Unnamed repository..."
- if "Unnamed repository" in description:
- description = ""
- except subprocess.CalledProcessError:
- pass
-
- # Get Owner
- owner = ""
- try:
- owner_cmd = ['ssh', remote, f'git config --file {shlex.quote(os.path.join(full_path, "config"))} gitweb.owner']
- owner = subprocess.check_output(owner_cmd, stderr=subprocess.DEVNULL, universal_newlines=True).strip()
- except subprocess.CalledProcessError:
- pass
-
- # Update Data
+ for rel_path, info in remote_data.items():
if rel_path not in data:
data[rel_path] = {}
new_count += 1
else:
updated_count += 1
- # Only overwrite if we found something useful, or if we want to sync truth?
- # Let's trust remote as truth for now.
- if description:
- data[rel_path]['description'] = description
- if owner:
- data[rel_path]['owner'] = owner
-
- # We can't easily guess origin URL from the bare repo unless we look at the config
- # but bare repos usually don't have 'origin' remotes that point upstream,
- # unless they were cloned with --mirror.
- # Let's check for 'remote.origin.url'
- try:
- origin_cmd = ['ssh', remote, f'git config --file {shlex.quote(os.path.join(full_path, "config"))} remote.origin.url']
- origin = subprocess.check_output(origin_cmd, stderr=subprocess.DEVNULL, universal_newlines=True).strip()
- if origin:
- if 'remotes' not in data[rel_path]: data[rel_path]['remotes'] = {}
- data[rel_path]['remotes']['origin'] = origin
- except:
- pass
-
+ data[rel_path]['description'] = info['description']
+ data[rel_path]['owner'] = info['owner']
+ if info.get('remotes'):
+ if 'remotes' not in data[rel_path]: data[rel_path]['remotes'] = {}
+ data[rel_path]['remotes'].update(info['remotes'])
+
save_repos(data)
- print(f"\nSync complete. Added {new_count}, Scanned {updated_count}.")
+ print(f"\nSync complete. Added {new_count}, Scanned {updated_count}. (Single SSH connection used)")
def cmd_list(args, remote):
data = migrate_csv_if_needed()
"owner": "Shane",
"description": "SvelteKit fork with legacy browser support (IE11) and static builds",
"remotes": {}
+ },
+ "gamesguru/ffpass.git": {
+ "description": "CLI to export/manage Firefox & Quantum passwords.",
+ "owner": "Shane J"
+ },
+ "gamesguru/vps-root.git": {
+ "description": "Configuration files for setting up nginx and more",
+ "owner": "Shane J"
+ },
+ "gamesguru/git-remote-gcrypt.git": {
+ "description": "Progressive fork of `git-remote-gcrypt`",
+ "owner": "Shane J"
+ },
+ "@nutratech/usda-sqlite.git": {
+ "description": "Portable USDA database, based on SR28",
+ "owner": "Shane J"
+ },
+ "@nutratech/cli.git": {
+ "description": "CLI for SR28 nutrient DB, tracking/calculations",
+ "owner": "Shane J"
+ },
+ "@nutratech/nt-sqlite.git": {
+ "description": "Portable USDA database, based on SR28",
+ "owner": "Shane J"
+ },
+ "@tg-svelte/kit.git": {
+ "description": "SvelteKit fork with legacy browser support (IE11) and static builds",
+ "owner": "Shane J"
+ },
+ "@tg-svelte/website-template-svkit-v2-legacy.git": {
+ "description": "SvelteKit v2 website sample/template with IE11 legacy support configuration.",
+ "owner": "Shane J"
}
}
\ No newline at end of file