- **Smart JSON Merging for VS Code Settings**: .vscode/settings.json is now intelligently merged instead of being overwritten during specify init --here or specify init .

- Existing settings are preserved
  - New Spec Kit settings are added
  - Nested objects are merged recursively
  - Prevents accidental loss of custom VS Code workspace configurations
This commit is contained in:
danielshih
2025-10-17 00:16:04 +08:00
parent 3b000fce4d
commit 315269d9a8

View File

@@ -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):
"""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"
@@ -676,6 +743,10 @@ 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)
# 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)