Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,73 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> Tuple[bool, Option
finally:
os.chdir(original_cwd)

def handle_vscode_settings(sub_item, dest_file, rel_path, verbose=False, tracker=None) -> None:
"""Handle merging or copying of .vscode/settings.json files."""
def log(message, color="green"):
if verbose and not tracker:
console.print(f"[{color}]{message}[/] {rel_path}")

try:
with open(sub_item, 'r', encoding='utf-8') as f:
new_settings = json.load(f)

if dest_file.exists():
merged = merge_json_files(dest_file, new_settings, verbose=verbose and not tracker)
with open(dest_file, 'w', encoding='utf-8') as f:
json.dump(merged, f, indent=4)
f.write('\n')
log("Merged:", "green")
else:
shutil.copy2(sub_item, dest_file)
log("Copied (no existing settings.json):", "blue")

except Exception as e:
log(f"Warning: Could not merge, copying instead: {e}", "yellow")
shutil.copy2(sub_item, dest_file)

def merge_json_files(existing_path: Path, new_content: dict, verbose: bool = False) -> dict:
"""Merge new JSON content into existing JSON file.

Performs a deep merge where:
- New keys are added
- Existing keys are preserved unless overwritten by new content
- Nested dictionaries are merged recursively
- Lists and other values are replaced (not merged)

Args:
existing_path: Path to existing JSON file
new_content: New JSON content to merge in
verbose: Whether to print merge details

Returns:
Merged JSON content as dict
"""
try:
with open(existing_path, 'r', encoding='utf-8') as f:
existing_content = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
# If file doesn't exist or is invalid, just use new content
return new_content

def deep_merge(base: dict, update: dict) -> dict:
"""Recursively merge update dict into base dict."""
result = base.copy()
for key, value in update.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
# Recursively merge nested dictionaries
result[key] = deep_merge(result[key], value)
else:
# Add new key or replace existing value
result[key] = value
return result

merged = deep_merge(existing_content, new_content)

if verbose:
console.print(f"[cyan]Merged JSON file:[/cyan] {existing_path.name}")

return merged

def download_template_from_github(ai_assistant: str, download_dir: Path, *, script_type: str = "sh", verbose: bool = True, show_progress: bool = True, client: httpx.Client = None, debug: bool = False, github_token: str = None) -> Tuple[Path, dict]:
repo_owner = "github"
repo_name = "spec-kit"
Expand Down Expand Up @@ -676,7 +743,11 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
rel_path = sub_item.relative_to(item)
dest_file = dest_path / rel_path
dest_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(sub_item, dest_file)
# Special handling for .vscode/settings.json - merge instead of overwrite
if dest_file.name == "settings.json" and dest_file.parent.name == ".vscode":
handle_vscode_settings(sub_item, dest_file, rel_path, verbose, tracker)
else:
shutil.copy2(sub_item, dest_file)
else:
shutil.copytree(item, dest_path)
else:
Expand Down