config-map 1.0.0__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.
- config_map/__init__.py +7 -0
- config_map/__main__.py +728 -0
- config_map-1.0.0.dist-info/METADATA +97 -0
- config_map-1.0.0.dist-info/RECORD +8 -0
- config_map-1.0.0.dist-info/WHEEL +5 -0
- config_map-1.0.0.dist-info/entry_points.txt +2 -0
- config_map-1.0.0.dist-info/licenses/LICENSE +21 -0
- config_map-1.0.0.dist-info/top_level.txt +1 -0
config_map/__init__.py
ADDED
config_map/__main__.py
ADDED
|
@@ -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,8 @@
|
|
|
1
|
+
config_map/__init__.py,sha256=5bEf-igar6Kt1ttzfgGUlwVdb4Hti8O_bZL9wK5rktM,171
|
|
2
|
+
config_map/__main__.py,sha256=nKixAmAnhdT8HCVphmp0g_SWm487MmR7C94QwIm-8cI,28413
|
|
3
|
+
config_map-1.0.0.dist-info/licenses/LICENSE,sha256=uP-DN_EdWzvh1jN1b25FjXe5hhdJn9F1_cMv3ylRq8Q,1110
|
|
4
|
+
config_map-1.0.0.dist-info/METADATA,sha256=dySxJt39z-VvG-zrUpvQ7dzlzMDDnV10GW7zBuo5HLI,3165
|
|
5
|
+
config_map-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
config_map-1.0.0.dist-info/entry_points.txt,sha256=quFRti01dxXlnN0_ixSIABETyFkJ3gSX4drP1fBSl44,56
|
|
7
|
+
config_map-1.0.0.dist-info/top_level.txt,sha256=IJ0jKUHGK0LdlP6OqpZ1ax3IbLyTo9MOWyCaw3PcIqQ,11
|
|
8
|
+
config_map-1.0.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
config_map
|