Merge branch 'main' into create-new-feature
@@ -84,6 +84,8 @@ uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME
|
|||||||
|
|
||||||
### 2. Establish project principles
|
### 2. Establish project principles
|
||||||
|
|
||||||
|
Launch your AI assistant in the project directory. The `/speckit.*` commands are available in the assistant.
|
||||||
|
|
||||||
Use the **`/speckit.constitution`** command to create your project's governing principles and development guidelines that will guide all subsequent development.
|
Use the **`/speckit.constitution`** command to create your project's governing principles and development guidelines that will guide all subsequent development.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 529 KiB After Width: | Height: | Size: 302 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 3.0 MiB After Width: | Height: | Size: 1.7 MiB |
@@ -203,4 +203,4 @@ else
|
|||||||
echo "SPEC_FILE: $SPEC_FILE"
|
echo "SPEC_FILE: $SPEC_FILE"
|
||||||
echo "FEATURE_NUM: $FEATURE_NUM"
|
echo "FEATURE_NUM: $FEATURE_NUM"
|
||||||
echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME"
|
echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ get_commands_for_language() {
|
|||||||
echo "cargo test && cargo clippy"
|
echo "cargo test && cargo clippy"
|
||||||
;;
|
;;
|
||||||
*"JavaScript"*|*"TypeScript"*)
|
*"JavaScript"*|*"TypeScript"*)
|
||||||
echo "npm test \&\& npm run lint"
|
echo "npm test \\&\\& npm run lint"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "# Add commands for $lang"
|
echo "# Add commands for $lang"
|
||||||
|
|||||||
@@ -485,6 +485,73 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> Tuple[bool, Option
|
|||||||
finally:
|
finally:
|
||||||
os.chdir(original_cwd)
|
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]:
|
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_owner = "github"
|
||||||
repo_name = "spec-kit"
|
repo_name = "spec-kit"
|
||||||
@@ -676,7 +743,11 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
|
|||||||
rel_path = sub_item.relative_to(item)
|
rel_path = sub_item.relative_to(item)
|
||||||
dest_file = dest_path / rel_path
|
dest_file = dest_path / rel_path
|
||||||
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
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:
|
else:
|
||||||
shutil.copytree(item, dest_path)
|
shutil.copytree(item, dest_path)
|
||||||
else:
|
else:
|
||||||
@@ -1093,18 +1164,25 @@ def check():
|
|||||||
|
|
||||||
tracker.add("git", "Git version control")
|
tracker.add("git", "Git version control")
|
||||||
git_ok = check_tool("git", tracker=tracker)
|
git_ok = check_tool("git", tracker=tracker)
|
||||||
|
|
||||||
agent_results = {}
|
agent_results = {}
|
||||||
for agent_key, agent_config in AGENT_CONFIG.items():
|
for agent_key, agent_config in AGENT_CONFIG.items():
|
||||||
agent_name = agent_config["name"]
|
agent_name = agent_config["name"]
|
||||||
|
requires_cli = agent_config["requires_cli"]
|
||||||
|
|
||||||
tracker.add(agent_key, agent_name)
|
tracker.add(agent_key, agent_name)
|
||||||
agent_results[agent_key] = check_tool(agent_key, tracker=tracker)
|
|
||||||
|
if requires_cli:
|
||||||
|
agent_results[agent_key] = check_tool(agent_key, tracker=tracker)
|
||||||
|
else:
|
||||||
|
# IDE-based agent - skip CLI check and mark as optional
|
||||||
|
tracker.skip(agent_key, "IDE-based, no CLI check")
|
||||||
|
agent_results[agent_key] = False # Don't count IDE agents as "found"
|
||||||
|
|
||||||
# Check VS Code variants (not in agent config)
|
# Check VS Code variants (not in agent config)
|
||||||
tracker.add("code", "Visual Studio Code")
|
tracker.add("code", "Visual Studio Code")
|
||||||
code_ok = check_tool("code", tracker=tracker)
|
code_ok = check_tool("code", tracker=tracker)
|
||||||
|
|
||||||
tracker.add("code-insiders", "Visual Studio Code Insiders")
|
tracker.add("code-insiders", "Visual Studio Code Insiders")
|
||||||
code_insiders_ok = check_tool("code-insiders", tracker=tracker)
|
code_insiders_ok = check_tool("code-insiders", tracker=tracker)
|
||||||
|
|
||||||
|
|||||||