config-map 1.0.0__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.
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2026 Toshon Jennings
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: config-map
3
+ Version: 1.0.0
4
+ Summary: A beautiful terminal map of your configuration files
5
+ Author-email: Toshon Jennings <mstly.pcfl@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/toshonjennings/config-map
8
+ Project-URL: Repository, https://github.com/toshonjennings/config-map
9
+ Project-URL: Issues, https://github.com/toshonjennings/config-map/issues
10
+ Keywords: config,dotfiles,terminal,cli,system,audit,map
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.7
20
+ Classifier: Programming Language :: Python :: 3.8
21
+ Classifier: Programming Language :: Python :: 3.9
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Topic :: System :: Systems Administration
26
+ Classifier: Topic :: Utilities
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.7
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: rich>=10.0
32
+ Dynamic: license-file
33
+
34
+ # config-map
35
+
36
+ A beautiful terminal map of your configuration files.
37
+
38
+ Scans `~/.config/` recursively and your top-level dotfiles, then displays each
39
+ file with path, size, last-modified, format, line count, and a type-specific
40
+ summary (extracted keys for structured formats, counts for `.env` / shell
41
+ configs, etc.).
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ pip install config-map
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ```bash
52
+ config-map # full report (~3s)
53
+ config-map --search herme # only files with "herme" in the path
54
+ config-map --min-size 100k # only files >= 100 KB
55
+ config-map --config # only ~/.config/
56
+ config-map --dotfiles # only top-level dotfiles
57
+ config-map --version # show version
58
+ ```
59
+
60
+ ## What it shows
61
+
62
+ | Field | Description |
63
+ |-------|-------------|
64
+ | File | Path (relative to ~) |
65
+ | Format | JSON / YAML / TOML / ENV / SHELL / PLIST / INI etc. |
66
+ | Size | Human-readable (B/KB/MB) |
67
+ | Lines | Line count (skips files >5 MB) |
68
+ | Modified | Last modified date/time |
69
+ | Purpose / Summary | Extracted keys, alias/export counts, git sections, etc. |
70
+
71
+ ## Type-specific summaries
72
+
73
+ - **JSON** → top 6 keys
74
+ - **YAML** → top 6 top-level keys
75
+ - **TOML** → top 6 sections/keys
76
+ - **.env** → variable count
77
+ - **Shell configs** → alias / export / function counts
78
+ - **.gitconfig** → section list
79
+ - **SSH config** → Host entries
80
+ - **plists** → top-level key count
81
+
82
+ ## Performance
83
+
84
+ Large cache/model dirs (`.hermes`, `.vscode`, `.lmstudio`, `.gemini`, etc.) are
85
+ scanned shallowly (top-level files only) to avoid walking gigabytes of models.
86
+ Files over 5 MB are never read — they show size/format only.
87
+
88
+ Full scan runs in ~3 seconds on a typical macOS system.
89
+
90
+ ## Requirements
91
+
92
+ - Python 3.7+
93
+ - `rich` library (installed automatically)
94
+
95
+ ## License
96
+
97
+ MIT
@@ -0,0 +1,64 @@
1
+ # config-map
2
+
3
+ A beautiful terminal map of your configuration files.
4
+
5
+ Scans `~/.config/` recursively and your top-level dotfiles, then displays each
6
+ file with path, size, last-modified, format, line count, and a type-specific
7
+ summary (extracted keys for structured formats, counts for `.env` / shell
8
+ configs, etc.).
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install config-map
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```bash
19
+ config-map # full report (~3s)
20
+ config-map --search herme # only files with "herme" in the path
21
+ config-map --min-size 100k # only files >= 100 KB
22
+ config-map --config # only ~/.config/
23
+ config-map --dotfiles # only top-level dotfiles
24
+ config-map --version # show version
25
+ ```
26
+
27
+ ## What it shows
28
+
29
+ | Field | Description |
30
+ |-------|-------------|
31
+ | File | Path (relative to ~) |
32
+ | Format | JSON / YAML / TOML / ENV / SHELL / PLIST / INI etc. |
33
+ | Size | Human-readable (B/KB/MB) |
34
+ | Lines | Line count (skips files >5 MB) |
35
+ | Modified | Last modified date/time |
36
+ | Purpose / Summary | Extracted keys, alias/export counts, git sections, etc. |
37
+
38
+ ## Type-specific summaries
39
+
40
+ - **JSON** → top 6 keys
41
+ - **YAML** → top 6 top-level keys
42
+ - **TOML** → top 6 sections/keys
43
+ - **.env** → variable count
44
+ - **Shell configs** → alias / export / function counts
45
+ - **.gitconfig** → section list
46
+ - **SSH config** → Host entries
47
+ - **plists** → top-level key count
48
+
49
+ ## Performance
50
+
51
+ Large cache/model dirs (`.hermes`, `.vscode`, `.lmstudio`, `.gemini`, etc.) are
52
+ scanned shallowly (top-level files only) to avoid walking gigabytes of models.
53
+ Files over 5 MB are never read — they show size/format only.
54
+
55
+ Full scan runs in ~3 seconds on a typical macOS system.
56
+
57
+ ## Requirements
58
+
59
+ - Python 3.7+
60
+ - `rich` library (installed automatically)
61
+
62
+ ## License
63
+
64
+ MIT
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "config-map"
7
+ version = "1.0.0"
8
+ description = "A beautiful terminal map of your configuration files"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.7"
12
+ authors = [
13
+ {name = "Toshon Jennings", email = "mstly.pcfl@gmail.com"},
14
+ ]
15
+ keywords = ["config", "dotfiles", "terminal", "cli", "system", "audit", "map"]
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "Intended Audience :: System Administrators",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Operating System :: MacOS",
23
+ "Operating System :: POSIX :: Linux",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.7",
26
+ "Programming Language :: Python :: 3.8",
27
+ "Programming Language :: Python :: 3.9",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Topic :: System :: Systems Administration",
32
+ "Topic :: Utilities",
33
+ "Typing :: Typed",
34
+ ]
35
+ dependencies = [
36
+ "rich>=10.0",
37
+ ]
38
+
39
+ [project.scripts]
40
+ config-map = "config_map.__main__:main"
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/toshonjennings/config-map"
44
+ Repository = "https://github.com/toshonjennings/config-map"
45
+ Issues = "https://github.com/toshonjennings/config-map/issues"
46
+
47
+ [tool.setuptools.packages.find]
48
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """config-map — A beautiful terminal map of your configuration files."""
2
+
3
+ __version__ = "1.0.0"
4
+
5
+ from config_map.__main__ import main
6
+
7
+ __all__ = ["main", "__version__"]
@@ -0,0 +1,728 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ config-map — A beautiful terminal map of your configuration files.
4
+
5
+ Scans ~/.config/ recursively and your top-level dotfiles, then displays
6
+ each file with path, size, last-modified, format, line count, and a
7
+ type-specific summary (extracted keys for structured formats, counts for
8
+ .env / shell configs, etc.).
9
+
10
+ Requirements: Python 3.7+ and the `rich` library (pip install rich)
11
+
12
+ Usage:
13
+ config-map # full report
14
+ config-map --dotfiles # only top-level dotfiles
15
+ config-map --config # only ~/.config
16
+ config-map --search herme # filter by path/name substring
17
+ config-map --min-size 10k # only files >= this size
18
+ """
19
+
20
+ import argparse
21
+ import json
22
+ import os
23
+ import plistlib
24
+ import re
25
+ import sys
26
+ from collections import OrderedDict
27
+ from datetime import datetime
28
+ from pathlib import Path
29
+ from typing import Any, Dict, List, Optional, Tuple
30
+
31
+ try:
32
+ from rich.console import Console
33
+ from rich.table import Table
34
+ from rich.panel import Panel
35
+ from rich.text import Text
36
+ from rich.tree import Tree
37
+ from rich import box
38
+ from rich.columns import Columns
39
+ from rich.rule import Rule
40
+ except ImportError:
41
+ sys.stderr.write(
42
+ "\n[ERROR] The 'rich' library is required but not installed.\n"
43
+ "Install it with: pip install rich\n\n"
44
+ )
45
+ sys.exit(1)
46
+
47
+
48
+ __version__ = "1.0.0"
49
+
50
+ HOME = Path.home()
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Known-config registry: maps filename/dir patterns to a label + extractor
55
+ # ---------------------------------------------------------------------------
56
+
57
+ def _first_yaml_keys(path: Path, max_keys: int = 6) -> Optional[str]:
58
+ """Extract top-level YAML keys (simple line scanner, no PyYAML needed)."""
59
+ try:
60
+ keys = []
61
+ for line in path.read_text(errors="replace").splitlines():
62
+ stripped = line.rstrip()
63
+ if not stripped or stripped.lstrip().startswith("#"):
64
+ continue
65
+ # top-level key: starts at column 0, ends with ':'
66
+ if re.match(r"^[a-zA-Z0-9_\-]+\s*:", stripped):
67
+ keys.append(stripped.split(":", 1)[0].strip())
68
+ if len(keys) >= max_keys:
69
+ break
70
+ if keys:
71
+ shown = ", ".join(keys[:max_keys])
72
+ return shown + ("…" if len(keys) >= max_keys else "")
73
+ except Exception:
74
+ pass
75
+ return None
76
+
77
+
78
+ def _first_toml_keys(path: Path, max_keys: int = 6) -> Optional[str]:
79
+ """Extract top-level TOML keys/sections."""
80
+ try:
81
+ keys = []
82
+ for line in path.read_text(errors="replace").splitlines():
83
+ stripped = line.strip()
84
+ if not stripped or stripped.startswith("#"):
85
+ continue
86
+ m = re.match(r"^\[([^\]]+)\]", stripped)
87
+ if m:
88
+ keys.append("[" + m.group(1) + "]")
89
+ continue
90
+ m = re.match(r"^([a-zA-Z0-9_\-]+)\s*=", stripped)
91
+ if m:
92
+ keys.append(m.group(1))
93
+ if len(keys) >= max_keys:
94
+ break
95
+ if keys:
96
+ shown = ", ".join(keys[:max_keys])
97
+ return shown + ("…" if len(keys) >= max_keys else "")
98
+ except Exception:
99
+ pass
100
+ return None
101
+
102
+
103
+ def _first_json_keys(path: Path, max_keys: int = 6) -> Optional[str]:
104
+ """Parse JSON and return top-level keys."""
105
+ try:
106
+ data = json.loads(path.read_text(errors="replace"))
107
+ if isinstance(data, dict):
108
+ keys = list(data.keys())
109
+ shown = ", ".join(str(k) for k in keys[:max_keys])
110
+ return shown + ("…" if len(keys) > max_keys else "")
111
+ except Exception:
112
+ pass
113
+ return None
114
+
115
+
116
+ def _env_summary(path: Path) -> Optional[str]:
117
+ """Count variables in a .env-style file."""
118
+ try:
119
+ lines = path.read_text(errors="replace").splitlines()
120
+ vars = [
121
+ l for l in lines
122
+ if l.strip() and not l.strip().startswith("#") and "=" in l
123
+ ]
124
+ if vars:
125
+ return f"{len(vars)} variable{'s' if len(vars) != 1 else ''}"
126
+ except Exception:
127
+ pass
128
+ return None
129
+
130
+
131
+ def _shell_summary(path: Path) -> Optional[str]:
132
+ """Count aliases, exports, and functions in a shell config."""
133
+ try:
134
+ text = path.read_text(errors="replace")
135
+ aliases = len(re.findall(r"^\s*alias\s+", text, re.MULTILINE))
136
+ exports = len(re.findall(r"^\s*export\s+", text, re.MULTILINE))
137
+ funcs = len(re.findall(r"^\s*(function\s+\w+|\w+\s*\(\))", text, re.MULTILINE))
138
+ parts = []
139
+ if aliases:
140
+ parts.append(f"{aliases} alias{'es' if aliases != 1 else ''}")
141
+ if exports:
142
+ parts.append(f"{exports} export{'s' if exports != 1 else ''}")
143
+ if funcs:
144
+ parts.append(f"{funcs} func{'s' if funcs != 1 else ''}")
145
+ return ", ".join(parts) if parts else None
146
+ except Exception:
147
+ pass
148
+ return None
149
+
150
+
151
+ def _ssh_config_summary(path: Path) -> Optional[str]:
152
+ """Count Host entries in an SSH config."""
153
+ try:
154
+ text = path.read_text(errors="replace")
155
+ hosts = re.findall(r"^\s*Host\s+(.+)", text, re.MULTILINE)
156
+ if hosts:
157
+ shown = ", ".join(hosts[:4])
158
+ return f"{len(hosts)} host{'s' if len(hosts) != 1 else ''}: {shown}" + (
159
+ "…" if len(hosts) > 4 else ""
160
+ )
161
+ except Exception:
162
+ pass
163
+ return None
164
+
165
+
166
+ def _gitconfig_summary(path: Path) -> Optional[str]:
167
+ """Extract sections from a gitconfig."""
168
+ try:
169
+ text = path.read_text(errors="replace")
170
+ sections = re.findall(r"^\[([^\]]+)\]", text, re.MULTILINE)
171
+ if sections:
172
+ shown = ", ".join(sections[:5])
173
+ return shown + ("…" if len(sections) > 5 else "")
174
+ except Exception:
175
+ pass
176
+ return None
177
+
178
+
179
+ def _plist_summary(path: Path) -> Optional[str]:
180
+ """Peek at plist — just report the top-level keys."""
181
+ try:
182
+ with open(path, "rb") as f:
183
+ data = plistlib.load(f)
184
+ if isinstance(data, dict):
185
+ keys = list(data.keys())
186
+ shown = ", ".join(str(k) for k in keys[:5])
187
+ return f"{len(keys)} keys: {shown}" + ("…" if len(keys) > 5 else "")
188
+ except Exception:
189
+ pass
190
+ return None
191
+
192
+
193
+ def _ini_summary(path: Path, max_sections: int = 5) -> Optional[str]:
194
+ """Extract INI-style section headers."""
195
+ try:
196
+ sections = []
197
+ for line in path.read_text(errors="replace").splitlines():
198
+ m = re.match(r"^\[([^\]]+)\]", line.strip())
199
+ if m:
200
+ sections.append(m.group(1))
201
+ if len(sections) >= max_sections:
202
+ break
203
+ if sections:
204
+ shown = ", ".join(sections[:max_sections])
205
+ return shown + ("…" if len(sections) >= max_sections else "")
206
+ except Exception:
207
+ pass
208
+ return None
209
+
210
+
211
+ # (pattern, label, summary_extractor)
212
+ # Patterns are matched against the file's path string (relative or absolute).
213
+ KNOWN_CONFIGS: List[Tuple[str, str, Any]] = [
214
+ (".zshrc", "Zsh shell config (login shell)", _shell_summary),
215
+ (".zshenv", "Zsh shell env (always sourced)", _shell_summary),
216
+ (".zprofile", "Zsh shell profile", _shell_summary),
217
+ (".profile", "Generic shell profile", _shell_summary),
218
+ (".bash_profile", "Bash shell profile", _shell_summary),
219
+ (".bashrc", "Bash shell config", _shell_summary),
220
+ (".tcshrc", "Tcsh shell config", _shell_summary),
221
+ (".gitconfig", "Git identity & settings", _gitconfig_summary),
222
+ (".npmrc", "NPM package manager config", _env_summary),
223
+ (".docker/", "Docker configuration", None),
224
+ (".ssh/", "SSH keys & config", _ssh_config_summary),
225
+ (".hermes/", "Hermes Agent configuration", None),
226
+ (".claude/", "Claude Code configuration", None),
227
+ (".claude.json", "Claude Code settings", _first_json_keys),
228
+ (".cursor/", "Cursor editor configuration", None),
229
+ (".vscode/", "VS Code settings/", None),
230
+ ("config.yaml", "YAML config", _first_yaml_keys),
231
+ ("config.yml", "YAML config", _first_yaml_keys),
232
+ ("config.json", "JSON config", _first_json_keys),
233
+ ("config.toml", "TOML config", _first_toml_keys),
234
+ (".env", "Environment variables", _env_summary),
235
+ (".env.", "Environment variables", _env_summary),
236
+ ("settings.json", "Application settings (JSON)", _first_json_keys),
237
+ ("preferences.json", "Application preferences (JSON)", _first_json_keys),
238
+ (".orbstack/", "OrbStack (Docker replacement)", None),
239
+ (".openclaw/", "OpenClaw AI agent", None),
240
+ (".orbit/", "Orbit personal assistant", None),
241
+ (".perci/", "Perci (Electron app)", None),
242
+ (".lmstudio/", "LM Studio local LLM", None),
243
+ (".ollama/", "Ollama local LLM", None),
244
+ (".logseq/", "Logseq knowledge base", None),
245
+ (".obsidian-cli.sock", "Obsidian CLI socket", None),
246
+ (".gk", "GitKraken client", None),
247
+ (".gitkraken/", "GitKraken client", None),
248
+ (".codex/", "OpenAI Codex CLI", None),
249
+ (".continue/", "Continue.dev (VS Code)", None),
250
+ (".cline/", "Cline (VS Code agent)", None),
251
+ (".copilot/", "GitHub Copilot", None),
252
+ (".gemini/", "Google Gemini CLI", None),
253
+ (".aionui", "AI One UI config", None),
254
+ (".agents/", "Agents directory", None),
255
+ (".autoforge/", "Autoforge", None),
256
+ (".vibe/", "Vibe coding tool", None),
257
+ (".trae/", "Trae IDE", None),
258
+ (".securecoder/", "SecureCoder", None),
259
+ (".antigravity/", "Antigravity IDE", None),
260
+ (".commandcode/", "CommandCode", None),
261
+ (".aibom/", "AI BOM tool", None),
262
+ (".dendron/", "Dendron knowledge base", None),
263
+ (".mempalace/", "MemPalace memory store", None),
264
+ (".graphify/", "Graphify knowledge graph", None),
265
+ (".hyperframes/", "Hyperframes", None),
266
+ (".openbb_platform/", "OpenBB Platform (finance)", None),
267
+ (".streamlit/", "Streamlit config", None),
268
+ (".matplotlib/", "Matplotlib config", None),
269
+ (".cups/", "macOS printing system (CUPS)", None),
270
+ (".cargo/", "Rust Cargo package manager", None),
271
+ (".rustup/", "Rust toolchain manager", None),
272
+ (".bun/", "Bun JS runtime", None),
273
+ (".npm/", "NPM cache/config", None),
274
+ (".local/", "User-local data", None),
275
+ (".cache/", "User cache directory", None),
276
+ (".py/", "Python bytecode cache", None),
277
+ ]
278
+
279
+
280
+ def classify(path: Path) -> Tuple[str, Optional[Any]]:
281
+ """Return (label, summary_extractor) for a given path."""
282
+ rel = str(path)
283
+ for pattern, label, extractor in KNOWN_CONFIGS:
284
+ if pattern in rel:
285
+ return label, extractor
286
+ return None, None
287
+
288
+
289
+ # ---------------------------------------------------------------------------
290
+ # File discovery
291
+ # ---------------------------------------------------------------------------
292
+
293
+ # Directories to skip entirely (noise / huge / binary)
294
+ SKIP_DIRS = {
295
+ "__pycache__", "node_modules", ".git", "Cache", "Caches",
296
+ "Code Cache", "GPUCache", "Service Worker", "Crash Reports",
297
+ "logs", "tmp", "cache", "CachedData", "Code Cache",
298
+ "DawnCache", "GrShaderCache", "ShaderCache",
299
+ "extensions", "CachedExtensionVSIXs", "CachedProfilesData",
300
+ "Code Storage", "Network Persistent State", "Session Storage",
301
+ "Sessions", "shared_blob", "blob_storage", "VideoCapture",
302
+ "GPUCache", "optimization_cache", "model_cache",
303
+ "node_modules.connector", "packages", "out", "dist",
304
+ "media", "images", "assets", "fonts",
305
+ }
306
+
307
+ # Dotdirs that are mostly cache/data/models — only scan top-level files, don't recurse
308
+ SHALLOW_DOTDIRS = {
309
+ ".hermes", ".vscode", ".cursor", ".openclaw", ".claude",
310
+ ".cache", ".npm", ".cargo", ".rustup", ".bun", ".local",
311
+ ".cups", ".matplotlib", ".streamlit", ".pytest_cache",
312
+ ".agent-browser", ".abacusai-chromium-profile",
313
+ ".lmstudio", ".gemini", ".codex", ".antigravity", ".antigravity-ide",
314
+ ".trae", ".supacode", ".orbit", ".autoforge", ".copilot",
315
+ ".openclaude", ".opalcode", ".securecoder", ".aionui", ".commandcode",
316
+ ".vibe", ".aibom", ".logseq", ".dendron", ".mempalace", ".graphify",
317
+ ".hyperframes", ".openbb_platform", ".gk", ".gitkraken",
318
+ ".ssh", ".docker", ".orbstack",
319
+ ".continue", ".cline", ".abacusai",
320
+ }
321
+
322
+ # Maximum file size to read/line-count (5 MB) — anything larger just shows size+format
323
+ MAX_READ_SIZE = 5 * 1024 * 1024
324
+
325
+ # File extensions to skip (binary / non-config)
326
+ SKIP_EXTENSIONS = {
327
+ ".png", ".jpg", ".jpeg", ".gif", ".ico", ".icns", ".svg",
328
+ ".woff", ".woff2", ".ttf", ".otf", ".eot",
329
+ ".mp3", ".mp4", ".wav", ".avi", ".mov",
330
+ ".zip", ".tar", ".gz", ".bz2", ".xz", ".7z", ".rar",
331
+ ".exe", ".dll", ".so", ".dylib", ".bin",
332
+ ".pyc", ".pyo", ".class", ".o", ".a",
333
+ ".db", ".sqlite", ".sqlite3", ".lock",
334
+ ".sock",
335
+ }
336
+
337
+ # Dotfiles/dirs to always skip at top level
338
+ SKIP_DOTFILES = {
339
+ ".DS_Store", ".Trash", ".cache", ".local", ".npm", ".cargo",
340
+ ".rustup", ".bun", ".py", ".pytest_cache", ".cups",
341
+ ".bash_history", ".zsh_history", ".zcompdump",
342
+ ".bash_sessions", ".zsh_sessions", ".viminfo",
343
+ ".CFUserTextEncoding", ".electron-gyp", ".swiftpm",
344
+ ".u2net", ".pdf-toolkit-files",
345
+ ".claude.json.backup", ".lmstudio-home-pointer",
346
+ ".obsidian-cli.sock",
347
+ }
348
+
349
+
350
+ def discover_files(
351
+ roots: List[Path],
352
+ include_dotfiles: bool = True,
353
+ include_config: bool = True,
354
+ ) -> List[Path]:
355
+ """Walk the given roots and return a sorted list of config files."""
356
+ files: List[Path] = []
357
+
358
+ if include_config:
359
+ config_root = HOME / ".config"
360
+ if config_root.exists():
361
+ for dirpath, dirnames, filenames in os.walk(config_root):
362
+ # prune skip dirs in-place
363
+ dirnames[:] = sorted(
364
+ d for d in dirnames if d not in SKIP_DIRS
365
+ )
366
+ for fn in sorted(filenames):
367
+ fp = Path(dirpath) / fn
368
+ if fp.suffix.lower() in SKIP_EXTENSIONS:
369
+ continue
370
+ files.append(fp)
371
+
372
+ if include_dotfiles:
373
+ for entry in sorted(HOME.iterdir()):
374
+ name = entry.name
375
+ if name in SKIP_DOTFILES:
376
+ continue
377
+ if name.startswith(".") and not name.startswith(".."):
378
+ if entry.is_file():
379
+ if entry.suffix.lower() in SKIP_EXTENSIONS:
380
+ continue
381
+ files.append(entry)
382
+ elif entry.is_dir() and name not in SKIP_DOTFILES:
383
+ # For shallow dotdirs, only scan top-level files (no recursion)
384
+ if name.lower() in SHALLOW_DOTDIRS:
385
+ for child in sorted(entry.iterdir()):
386
+ if child.is_file():
387
+ if child.suffix.lower() in SKIP_EXTENSIONS:
388
+ continue
389
+ files.append(child)
390
+ else:
391
+ # walk the dotdir recursively
392
+ for dirpath, dirnames, filenames in os.walk(entry):
393
+ dirnames[:] = sorted(
394
+ d for d in dirnames if d not in SKIP_DIRS
395
+ )
396
+ for fn in sorted(filenames):
397
+ fp = Path(dirpath) / fn
398
+ if fp.suffix.lower() in SKIP_EXTENSIONS:
399
+ continue
400
+ files.append(fp)
401
+
402
+ # deduplicate and sort
403
+ seen = set()
404
+ unique = []
405
+ for f in files:
406
+ resolved = f.resolve()
407
+ if resolved not in seen:
408
+ seen.add(resolved)
409
+ unique.append(f)
410
+ unique.sort(key=lambda p: str(p))
411
+ return unique
412
+
413
+
414
+ # ---------------------------------------------------------------------------
415
+ # File info extraction
416
+ # ---------------------------------------------------------------------------
417
+
418
+ def human_size(n: int) -> str:
419
+ """Human-readable file size."""
420
+ for unit in ("B", "KB", "MB", "GB"):
421
+ if n < 1024:
422
+ return f"{n:.0f} B" if unit == "B" else f"{n:.1f} {unit}"
423
+ n /= 1024
424
+ return f"{n:.1f} TB"
425
+
426
+
427
+ def detect_format(path: Path) -> str:
428
+ """Detect config format from extension / filename."""
429
+ name = path.name.lower()
430
+ suffix = path.suffix.lower()
431
+ mapping = {
432
+ ".json": "JSON",
433
+ ".yaml": "YAML",
434
+ ".yml": "YAML",
435
+ ".toml": "TOML",
436
+ ".ini": "INI",
437
+ ".cfg": "INI",
438
+ ".conf": "CONF",
439
+ ".env": "ENV",
440
+ ".plist": "PLIST",
441
+ ".xml": "XML",
442
+ ".sh": "SHELL",
443
+ ".zsh": "SHELL",
444
+ ".bash": "SHELL",
445
+ ".fish": "SHELL",
446
+ ".py": "PYTHON",
447
+ ".js": "JS",
448
+ ".ts": "TS",
449
+ ".cjs": "JS",
450
+ ".mjs": "JS",
451
+ ".md": "MARKDOWN",
452
+ ".lock": "LOCK",
453
+ }
454
+ if name == ".gitconfig" or name.endswith("config") and suffix == "":
455
+ return "GITCONFIG"
456
+ if name.startswith(".env"):
457
+ return "ENV"
458
+ if name in ("config", "settings", "preferences"):
459
+ return "CONF"
460
+ return mapping.get(suffix, "TEXT")
461
+
462
+
463
+ def count_lines(path: Path) -> Optional[int]:
464
+ """Count lines in a text file; return None on binary/read error."""
465
+ try:
466
+ # fast line count
467
+ count = 0
468
+ with open(path, "rb") as f:
469
+ for _ in f:
470
+ count += 1
471
+ return count
472
+ except Exception:
473
+ return None
474
+
475
+
476
+ def get_summary(path: Path, extractor: Optional[Any]) -> Optional[str]:
477
+ """Get a type-specific summary string for the file."""
478
+ if extractor is not None:
479
+ try:
480
+ result = extractor(path)
481
+ if result:
482
+ return result
483
+ except Exception:
484
+ pass
485
+ # fallback heuristics based on format
486
+ fmt = detect_format(path)
487
+ if fmt == "JSON":
488
+ return _first_json_keys(path)
489
+ if fmt in ("YAML",):
490
+ return _first_yaml_keys(path)
491
+ if fmt == "TOML":
492
+ return _first_toml_keys(path)
493
+ if fmt == "ENV":
494
+ return _env_summary(path)
495
+ if fmt == "PLIST":
496
+ return _plist_summary(path)
497
+ if fmt == "INI":
498
+ return _ini_summary(path)
499
+ if fmt == "GITCONFIG":
500
+ return _gitconfig_summary(path)
501
+ if fmt == "SHELL":
502
+ return _shell_summary(path)
503
+ return None
504
+
505
+
506
+ def file_info(path: Path) -> Optional[Dict[str, Any]]:
507
+ """Collect all info about a config file."""
508
+ try:
509
+ stat = path.stat()
510
+ except Exception:
511
+ return None
512
+
513
+ size = stat.st_size
514
+ mtime = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M")
515
+ fmt = detect_format(path)
516
+ lines = count_lines(path) if size <= MAX_READ_SIZE else None # skip huge files
517
+ label, extractor = classify(path)
518
+ summary = get_summary(path, extractor) if size <= MAX_READ_SIZE else None
519
+
520
+ return {
521
+ "path": path,
522
+ "size": size,
523
+ "size_human": human_size(size),
524
+ "mtime": mtime,
525
+ "format": fmt,
526
+ "lines": lines,
527
+ "label": label or "",
528
+ "summary": summary or "",
529
+ }
530
+
531
+
532
+ # ---------------------------------------------------------------------------
533
+ # Display
534
+ # ---------------------------------------------------------------------------
535
+
536
+ def make_table_for_group(group_name: str, items: List[Dict]) -> Table:
537
+ """Build a rich Table for a group of config files."""
538
+ table = Table(
539
+ title=f"[bold cyan]{group_name}[/bold cyan] [dim]({len(items)} file{'s' if len(items) != 1 else ''})[/dim]",
540
+ box=box.ROUNDED,
541
+ show_lines=False,
542
+ title_style="bold",
543
+ border_style="bright_black",
544
+ pad_edge=True,
545
+ expand=True,
546
+ )
547
+ table.add_column("File", style="bold white", no_wrap=True, max_width=40)
548
+ table.add_column("Format", style="magenta", justify="center", max_width=10)
549
+ table.add_column("Size", style="green", justify="right", max_width=8)
550
+ table.add_column("Lines", style="yellow", justify="right", max_width=7)
551
+ table.add_column("Modified", style="dim", max_width=16)
552
+ table.add_column("Purpose / Summary", style="italic white", ratio=2, no_wrap=False)
553
+
554
+ for item in items:
555
+ rel = str(item["path"]).replace(str(HOME), "~")
556
+ # shorten if too long
557
+ if len(rel) > 50:
558
+ rel = "…" + rel[-49:]
559
+
560
+ lines_str = f"{item['lines']:,}" if item["lines"] is not None else "—"
561
+ summary_parts = []
562
+ if item["label"]:
563
+ summary_parts.append(f"[dim]{item['label']}[/dim]")
564
+ if item["summary"]:
565
+ summary_parts.append(f"[italic]{item['summary']}[/italic]")
566
+ summary = " • ".join(summary_parts) if summary_parts else "—"
567
+
568
+ table.add_row(
569
+ rel,
570
+ item["format"],
571
+ item["size_human"],
572
+ lines_str,
573
+ item["mtime"],
574
+ summary,
575
+ )
576
+ return table
577
+
578
+
579
+ def group_by_parent(items: List[Dict]) -> OrderedDict:
580
+ """Group items by their parent directory."""
581
+ groups = OrderedDict()
582
+ for item in items:
583
+ parent = str(item["path"].parent).replace(str(HOME), "~")
584
+ if parent not in groups:
585
+ groups[parent] = []
586
+ groups[parent].append(item)
587
+ return groups
588
+
589
+
590
+ def render_full_report(
591
+ items: List[Dict],
592
+ console: Console,
593
+ title: str = "Config Map",
594
+ ):
595
+ """Render the full beautiful report."""
596
+ # Header
597
+ console.print()
598
+ console.print(
599
+ Rule(
600
+ title=f"[bold white] {title} [/bold white]",
601
+ style="bold cyan",
602
+ align="center",
603
+ )
604
+ )
605
+ console.print(
606
+ f"[dim] Scanned [bold]{len(items)}[/bold] config files • "
607
+ f"[bold]{datetime.now().strftime('%Y-%m-%d %H:%M')}[/bold][/dim]"
608
+ )
609
+ console.print()
610
+
611
+ groups = group_by_parent(items)
612
+
613
+ # Summary stats
614
+ total_size = sum(i["size"] for i in items)
615
+ formats: Dict[str, int] = {}
616
+ for i in items:
617
+ formats[i["format"]] = formats.get(i["format"], 0) + 1
618
+
619
+ fmt_summary = ", ".join(
620
+ f"[bold]{k}[/bold] [dim]({v})[/dim]" for k, v in sorted(formats.items(), key=lambda x: -x[1])
621
+ )
622
+
623
+ stats = Table.grid(padding=(0, 2))
624
+ stats.add_column(style="dim", justify="right")
625
+ stats.add_column(style="white")
626
+ stats.add_row("Total files:", f"[bold]{len(items)}[/bold]")
627
+ stats.add_row("Total size:", f"[bold]{human_size(total_size)}[/bold]")
628
+ stats.add_row("Formats:", fmt_summary)
629
+ stats.add_row("Directories:", f"[bold]{len(groups)}[/bold]")
630
+
631
+ console.print(Panel(stats, title="[bold]Summary[/bold]", border_style="cyan", expand=True))
632
+ console.print()
633
+
634
+ # Render each group
635
+ for group_name, group_items in groups.items():
636
+ # sort by size descending within group
637
+ group_items.sort(key=lambda x: -x["size"])
638
+ table = make_table_for_group(group_name, group_items)
639
+ console.print(table)
640
+ console.print()
641
+
642
+ console.print(Rule(style="dim"))
643
+
644
+
645
+ def main():
646
+ parser = argparse.ArgumentParser(
647
+ description="Beautiful terminal map of your configuration files.",
648
+ formatter_class=argparse.RawDescriptionHelpFormatter,
649
+ )
650
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
651
+ parser.add_argument("--dotfiles", action="store_true", help="Only scan top-level dotfiles")
652
+ parser.add_argument("--config", action="store_true", help="Only scan ~/.config/")
653
+ parser.add_argument("--search", type=str, default=None, help="Filter by path/name substring")
654
+ parser.add_argument(
655
+ "--min-size",
656
+ type=str,
657
+ default=None,
658
+ help="Minimum file size (e.g., '1k', '100b', '1m')",
659
+ )
660
+ parser.add_argument("--no-color", action="store_true", help="Disable color output")
661
+ args = parser.parse_args()
662
+
663
+ # Determine scan scope
664
+ if args.dotfiles and args.config:
665
+ sys.stderr.write("[ERROR] --dotfiles and --config are mutually exclusive\n")
666
+ sys.exit(1)
667
+ include_dotfiles = not args.config
668
+ include_config = not args.dotfiles
669
+
670
+ console = Console(
671
+ color_system="truecolor" if not args.no_color else None,
672
+ force_terminal=not args.no_color,
673
+ )
674
+
675
+ files = discover_files(
676
+ roots=[],
677
+ include_dotfiles=include_dotfiles,
678
+ include_config=include_config,
679
+ )
680
+
681
+ items = []
682
+ total = len(files)
683
+ with console.status("[bold cyan]Analyzing 0/{total} files…[/bold cyan]", spinner="dots") as status:
684
+ for idx, fp in enumerate(files, 1):
685
+ if idx % 50 == 0 or idx == total:
686
+ status.update(f"[bold cyan]Analyzing {idx}/{total} files…[/bold cyan]")
687
+ info = file_info(fp)
688
+ if info:
689
+ items.append(info)
690
+
691
+ # Apply filters
692
+ if args.search:
693
+ needle = args.search.lower()
694
+ items = [i for i in items if needle in str(i["path"]).lower()]
695
+
696
+ if args.min_size:
697
+ size_str = args.min_size.lower().strip()
698
+ multipliers = {"b": 1, "k": 1024, "kb": 1024, "m": 1024**2, "mb": 1024**2, "g": 1024**3}
699
+ m = re.match(r"^(\d+(?:\.\d+)?)\s*([a-z]*)$", size_str)
700
+ if m:
701
+ num = float(m.group(1))
702
+ unit = m.group(2) or "b"
703
+ min_bytes = int(num * multipliers.get(unit, 1))
704
+ items = [i for i in items if i["size"] >= min_bytes]
705
+ else:
706
+ sys.stderr.write(f"[WARN] Could not parse --min-size '{args.min_size}', ignoring\n")
707
+
708
+ if not items:
709
+ console.print("[yellow]No config files found matching criteria.[/yellow]")
710
+ sys.exit(0)
711
+
712
+ title_parts = []
713
+ if args.dotfiles:
714
+ title_parts.append("Dotfiles")
715
+ elif args.config:
716
+ title_parts.append("~/.config")
717
+ else:
718
+ title_parts.append("System Config")
719
+ if args.search:
720
+ title_parts.append(f"matching '{args.search}'")
721
+ if args.min_size:
722
+ title_parts.append(f">= {args.min_size}")
723
+
724
+ render_full_report(items, console, title=" • ".join(title_parts))
725
+
726
+
727
+ if __name__ == "__main__":
728
+ main()
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: config-map
3
+ Version: 1.0.0
4
+ Summary: A beautiful terminal map of your configuration files
5
+ Author-email: Toshon Jennings <mstly.pcfl@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/toshonjennings/config-map
8
+ Project-URL: Repository, https://github.com/toshonjennings/config-map
9
+ Project-URL: Issues, https://github.com/toshonjennings/config-map/issues
10
+ Keywords: config,dotfiles,terminal,cli,system,audit,map
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.7
20
+ Classifier: Programming Language :: Python :: 3.8
21
+ Classifier: Programming Language :: Python :: 3.9
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Topic :: System :: Systems Administration
26
+ Classifier: Topic :: Utilities
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.7
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: rich>=10.0
32
+ Dynamic: license-file
33
+
34
+ # config-map
35
+
36
+ A beautiful terminal map of your configuration files.
37
+
38
+ Scans `~/.config/` recursively and your top-level dotfiles, then displays each
39
+ file with path, size, last-modified, format, line count, and a type-specific
40
+ summary (extracted keys for structured formats, counts for `.env` / shell
41
+ configs, etc.).
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ pip install config-map
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ```bash
52
+ config-map # full report (~3s)
53
+ config-map --search herme # only files with "herme" in the path
54
+ config-map --min-size 100k # only files >= 100 KB
55
+ config-map --config # only ~/.config/
56
+ config-map --dotfiles # only top-level dotfiles
57
+ config-map --version # show version
58
+ ```
59
+
60
+ ## What it shows
61
+
62
+ | Field | Description |
63
+ |-------|-------------|
64
+ | File | Path (relative to ~) |
65
+ | Format | JSON / YAML / TOML / ENV / SHELL / PLIST / INI etc. |
66
+ | Size | Human-readable (B/KB/MB) |
67
+ | Lines | Line count (skips files >5 MB) |
68
+ | Modified | Last modified date/time |
69
+ | Purpose / Summary | Extracted keys, alias/export counts, git sections, etc. |
70
+
71
+ ## Type-specific summaries
72
+
73
+ - **JSON** → top 6 keys
74
+ - **YAML** → top 6 top-level keys
75
+ - **TOML** → top 6 sections/keys
76
+ - **.env** → variable count
77
+ - **Shell configs** → alias / export / function counts
78
+ - **.gitconfig** → section list
79
+ - **SSH config** → Host entries
80
+ - **plists** → top-level key count
81
+
82
+ ## Performance
83
+
84
+ Large cache/model dirs (`.hermes`, `.vscode`, `.lmstudio`, `.gemini`, etc.) are
85
+ scanned shallowly (top-level files only) to avoid walking gigabytes of models.
86
+ Files over 5 MB are never read — they show size/format only.
87
+
88
+ Full scan runs in ~3 seconds on a typical macOS system.
89
+
90
+ ## Requirements
91
+
92
+ - Python 3.7+
93
+ - `rich` library (installed automatically)
94
+
95
+ ## License
96
+
97
+ MIT
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/config_map/__init__.py
5
+ src/config_map/__main__.py
6
+ src/config_map.egg-info/PKG-INFO
7
+ src/config_map.egg-info/SOURCES.txt
8
+ src/config_map.egg-info/dependency_links.txt
9
+ src/config_map.egg-info/entry_points.txt
10
+ src/config_map.egg-info/requires.txt
11
+ src/config_map.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ config-map = config_map.__main__:main
@@ -0,0 +1 @@
1
+ rich>=10.0
@@ -0,0 +1 @@
1
+ config_map