works but klaus is basic/ugly, too, like gitweb
authorShane <chown_tee@proton.me>
Sun, 11 Jan 2026 19:42:54 +0000 (19:42 +0000)
committerShane <chown_tee@proton.me>
Sun, 11 Jan 2026 19:48:20 +0000 (19:48 +0000)
Makefile
etc/systemd/system/klaus.service
scripts/klaus_app.py [new file with mode: 0644]
scripts/manage_repos.py
scripts/repos.json

index d91b17abeaad32e6c0a79f1dc6b65f0d7f85690a..dfe33aed1f6474838ada54e045772011d6fbb724 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -65,17 +65,20 @@ test/nginx: ##H @Remote Test staged configuration without deploying
 .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!"
 
@@ -164,3 +167,7 @@ endif
 .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
index 61c9a8c63b0c85a08319bf68597501c7cf9570f5..e608798c94322187c9e114d125f2495242546551 100644 (file)
Binary files a/etc/systemd/system/klaus.service and b/etc/systemd/system/klaus.service differ
diff --git a/scripts/klaus_app.py b/scripts/klaus_app.py
new file mode 100644 (file)
index 0000000..6f3561a
--- /dev/null
@@ -0,0 +1,34 @@
+import os
+import klaus
+from klaus.contrib.wsgi import make_app
+
+# Root directory for repositories
+REPO_ROOT = os.environ.get('KLAUS_REPOS_ROOT', '/srv/git')
+SITE_NAME = os.environ.get('KLAUS_SITE_NAME', 'Git Repos')
+
+def find_git_repos(root_dir):
+    """
+    Recursively find all git repositories (directories ending in .git)
+    """
+    repos = []
+    for root, dirs, files in os.walk(root_dir):
+        # Scan directories
+        for d in dirs:
+            if d.endswith('.git'):
+                full_path = os.path.join(root, d)
+                repos.append(full_path)
+    return sorted(repos)
+
+# Discover repositories
+repositories = find_git_repos(REPO_ROOT)
+
+if not repositories:
+    print(f"Warning: No repositories found in {REPO_ROOT}")
+else:
+    print(f"Found {len(repositories)} repositories: {repositories}")
+
+# Create the WSGI application
+application = make_app(
+    repositories,
+    SITE_NAME,
+)
index 57cf1a54de4ac18d2e6a10dc00461e3f00235c7f..22f371d6675fa4cb8bfff99e7082660178bdd7b2 100755 (executable)
@@ -235,56 +235,76 @@ def cmd_sync(args, remote):
         
     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
@@ -292,28 +312,14 @@ def cmd_sync(args, remote):
         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()
index 16698f48ee998eaa2dceb953fe0d46d643317c45..0e07304fbd305d205ad767e394a617b56be4433e 100644 (file)
@@ -8,5 +8,37 @@
     "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