Skip to content

Commit 4bc2f0b

Browse files
authored
feat: refactor replacements.json structure to eliminate duplication (#8)
- Consolidate project storage to single 'projects' section with rich metadata - Add automatic migration from old format to new format - Update all replacement functions to use new unified structure - Add comprehensive default commands and patterns - Update README documentation with new configuration format Closes #6
1 parent 314ca86 commit 4bc2f0b

17 files changed

Lines changed: 1149 additions & 919 deletions

README.md

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,25 +113,45 @@ Customize how project names and commands are pronounced by editing `~/.claude/cc
113113

114114
```json
115115
{
116-
"project_names": {
117-
"agent-zero": "agent zero",
118-
"ccnotify": "CC notify",
119-
"roo-code": "roo code"
116+
"projects": {
117+
"ccnotify": {
118+
"folder": "-Users-helmi-code-ccnotify",
119+
"display_name": "CCNotify",
120+
"pronunciation": "CC notify"
121+
},
122+
"agent-zero": {
123+
"folder": "-Users-helmi-code-agent-zero",
124+
"display_name": "Agent Zero",
125+
"pronunciation": "agent zero"
126+
}
120127
},
121128
"commands": {
122129
"ls": "list",
123130
"cd": "change directory",
124-
"rm": "remove"
131+
"rm": "remove",
132+
"mkdir": "make directory",
133+
"npm": "N P M",
134+
"uvx": "U V X"
125135
},
126136
"patterns": [
127137
{
128138
"pattern": "npm run (\\w+)",
129-
"replacement": "npm run {1}"
139+
"replacement": "N P M run {1}"
140+
},
141+
{
142+
"pattern": "git (push|pull|commit)",
143+
"replacement": "git {1}"
144+
},
145+
{
146+
"pattern": "(.+)\\.py",
147+
"replacement": "{1} python file"
130148
}
131149
]
132150
}
133151
```
134152

153+
**Note:** Existing configurations will be automatically migrated to the new format on first load.
154+
135155
## Requirements
136156

137157
- macOS or Linux

src/ccnotify/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,20 @@
66
__author__ = "Helmi"
77
__license__ = "MIT"
88

9+
910
# Lazy imports to avoid heavy dependencies during CLI usage
1011
def get_tts_provider(*args, **kwargs):
1112
"""Lazy import wrapper for TTS provider"""
1213
from .tts import get_tts_provider as _get_tts_provider
14+
1315
return _get_tts_provider(*args, **kwargs)
1416

17+
1518
def get_tts_provider_class():
1619
"""Lazy import wrapper for TTS provider class"""
1720
from .tts import TTSProvider
21+
1822
return TTSProvider
1923

20-
__all__ = ["get_tts_provider", "get_tts_provider_class"]
24+
25+
__all__ = ["get_tts_provider", "get_tts_provider_class"]

src/ccnotify/cli.py

Lines changed: 58 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -29,68 +29,54 @@ def main():
2929
uvx ccnotify install --force # Force complete reinstallation
3030
uvx ccnotify install --config-only # Only update configuration
3131
uvx ccnotify install --quiet # Minimal output mode
32-
"""
32+
""",
3333
)
34-
34+
3535
# Simplified argument structure
3636
parser.add_argument(
37-
"command",
38-
nargs='?',
37+
"command",
38+
nargs="?",
3939
default="install",
4040
choices=["install"],
41-
help="Command to execute (default: install)"
42-
)
43-
44-
parser.add_argument(
45-
"--force",
46-
action="store_true",
47-
help="Force complete reinstallation"
48-
)
49-
50-
parser.add_argument(
51-
"--config-only",
52-
action="store_true",
53-
help="Only update configuration, skip script updates"
41+
help="Command to execute (default: install)",
5442
)
55-
56-
parser.add_argument(
57-
"--quiet",
58-
action="store_true",
59-
help="Minimal output mode"
60-
)
61-
43+
44+
parser.add_argument("--force", action="store_true", help="Force complete reinstallation")
45+
6246
parser.add_argument(
63-
"--logging",
64-
action="store_true",
65-
help="Enable logging to file (off by default)"
47+
"--config-only", action="store_true", help="Only update configuration, skip script updates"
6648
)
67-
49+
50+
parser.add_argument("--quiet", action="store_true", help="Minimal output mode")
51+
6852
parser.add_argument(
69-
"--version",
70-
action="version",
71-
version=f"CCNotify {get_package_version()}"
53+
"--logging", action="store_true", help="Enable logging to file (off by default)"
7254
)
73-
55+
56+
parser.add_argument("--version", action="version", version=f"CCNotify {get_package_version()}")
57+
7458
args = parser.parse_args()
75-
59+
7660
# Always execute install command with intelligent detection
7761
success = execute_install_command(args.force, args.config_only, args.quiet, args.logging)
78-
62+
7963
if not success:
8064
sys.exit(1)
8165

8266

83-
def execute_install_command(force: bool = False, config_only: bool = False, quiet: bool = False, logging: bool = False) -> bool:
67+
def execute_install_command(
68+
force: bool = False, config_only: bool = False, quiet: bool = False, logging: bool = False
69+
) -> bool:
8470
"""Execute the intelligent install command with detection logic."""
8571
# Validate parameters
8672
if not isinstance(logging, bool):
8773
raise TypeError("logging parameter must be a boolean")
88-
74+
8975
try:
9076
# Detect existing installation
9177
detector = InstallationDetector()
9278
status = detector.check_existing_installation()
93-
79+
9480
if status.exists and not force:
9581
# Existing installation - run update flow
9682
update_flow = UpdateFlow()
@@ -99,7 +85,7 @@ def execute_install_command(force: bool = False, config_only: bool = False, quie
9985
# No installation or force requested - run first-time flow
10086
first_time_flow = FirstTimeFlow()
10187
return first_time_flow.run(force=force, quiet=quiet, logging=logging)
102-
88+
10389
except KeyboardInterrupt:
10490
if not quiet:
10591
display_error_message("Installation cancelled by user")
@@ -116,17 +102,19 @@ def get_notify_template() -> str:
116102
from .notify import main as notify_main
117103
from .version import get_package_version
118104
import inspect
119-
105+
120106
# Get the complete notify.py file content
121107
from pathlib import Path
108+
122109
notify_file = Path(__file__).parent / "notify.py"
123-
110+
124111
if notify_file.exists():
125112
content = notify_file.read_text()
126113
# Embed version in the generated script
127114
from .version import embed_version_in_script
115+
128116
return embed_version_in_script(content, get_package_version())
129-
117+
130118
# Fallback minimal template
131119
version = get_package_version()
132120
return f'''#!/usr/bin/env python3
@@ -188,60 +176,57 @@ def update_claude_settings(script_path: str, logging: bool = False) -> bool:
188176
import json
189177
import shutil
190178
from pathlib import Path
191-
179+
192180
claude_dir = Path.home() / ".claude"
193181
settings_file = claude_dir / "settings.json"
194-
182+
195183
try:
196184
if settings_file.exists():
197185
# Create backup first
198-
backup_file = settings_file.with_suffix('.json.ccnotify.bak')
186+
backup_file = settings_file.with_suffix(".json.ccnotify.bak")
199187
shutil.copy2(settings_file, backup_file)
200-
201-
with open(settings_file, 'r') as f:
188+
189+
with open(settings_file, "r") as f:
202190
settings = json.load(f)
203191
else:
204192
settings = {}
205-
193+
206194
# Add our hook configuration
207195
if "hooks" not in settings:
208196
settings["hooks"] = {}
209-
197+
210198
# Configure ccnotify hook for relevant events
211199
# Add --logging flag to command if logging is enabled
212200
command = f"uv run {script_path}"
213201
if logging:
214202
command += " --logging"
215-
216-
hook_config = {
217-
"type": "command",
218-
"command": command
219-
}
220-
203+
204+
hook_config = {"type": "command", "command": command}
205+
221206
events_to_hook = ["PreToolUse", "PostToolUse", "Stop", "SubagentStop", "Notification"]
222207
hooks_added = False
223-
208+
224209
for event in events_to_hook:
225210
if event not in settings["hooks"]:
226211
settings["hooks"][event] = []
227-
212+
228213
# Check if our hook is already configured and update if needed
229214
# Hook structure: {"matcher": ".*", "hooks": [{"type": "command", "command": "..."}]}
230215
hook_updated = False
231216
hook_exists = False
232-
217+
233218
for i, entry in enumerate(settings["hooks"][event]):
234219
if not isinstance(entry, dict):
235220
continue
236-
221+
237222
hooks_list = entry.get("hooks", [])
238223
if not isinstance(hooks_list, list):
239224
continue
240-
225+
241226
for j, hook in enumerate(hooks_list):
242227
if not isinstance(hook, dict):
243228
continue
244-
229+
245230
existing_command = hook.get("command", "")
246231
# Check if this is our ccnotify hook
247232
if "ccnotify.py" in existing_command or str(script_path) in existing_command:
@@ -254,33 +239,33 @@ def update_claude_settings(script_path: str, logging: bool = False) -> bool:
254239
hooks_added = True
255240
except (KeyError, IndexError) as e:
256241
# Log error but continue processing
257-
print(f"Warning: Could not update hook for {event}: {e}", file=sys.stderr)
242+
print(
243+
f"Warning: Could not update hook for {event}: {e}",
244+
file=sys.stderr,
245+
)
258246
break
259-
247+
260248
if hook_exists:
261249
break
262-
250+
263251
if not hook_exists:
264-
settings["hooks"][event].append({
265-
"matcher": ".*",
266-
"hooks": [hook_config]
267-
})
252+
settings["hooks"][event].append({"matcher": ".*", "hooks": [hook_config]})
268253
hooks_added = True
269-
254+
270255
# Enable hooks if not already enabled
271256
if not settings.get("hooksEnabled", False):
272257
settings["hooksEnabled"] = True
273258
hooks_added = True
274-
259+
275260
if hooks_added:
276-
with open(settings_file, 'w') as f:
261+
with open(settings_file, "w") as f:
277262
json.dump(settings, f, indent=2)
278-
263+
279264
return True
280-
265+
281266
except Exception:
282267
return False
283268

284269

285270
if __name__ == "__main__":
286-
main()
271+
main()

0 commit comments

Comments
 (0)