chore: normalize all tracked files to LF line endings
Mechanical application of the `.gitattributes` rules from the prior commit. All 50 files differ only in line endings — verified by `git diff --cached --ignore-all-space` being empty. Before: main had 42 CRLF + 27 LF Python files plus mixed-ending in YAML, templates, and shell scripts. After: every text file is LF (except the Windows-native *.ps1, *.bat scripts which remain CRLF per gitattributes). This eliminates the noisy-diff failure mode seen in PR #102, where a small logic change produced a 918-line diff due to whole-file CRLF→LF conversion.
This commit is contained in:
@@ -1,255 +1,255 @@
|
||||
"""
|
||||
Project-related command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import pcbnew # type: ignore
|
||||
|
||||
logger = logging.getLogger("kicad_interface")
|
||||
|
||||
|
||||
class ProjectCommands:
|
||||
"""Handles project-related KiCAD operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def create_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Create a new KiCAD project"""
|
||||
try:
|
||||
# Accept both 'name' (from MCP tool) and 'projectName' (legacy)
|
||||
project_name = params.get("name") or params.get("projectName", "New_Project")
|
||||
path = params.get("path", os.getcwd())
|
||||
template = params.get("template")
|
||||
|
||||
# Generate the full project path
|
||||
project_path = os.path.join(path, project_name)
|
||||
if not project_path.endswith(".kicad_pro"):
|
||||
project_path += ".kicad_pro"
|
||||
|
||||
# Create project directory if it doesn't exist
|
||||
os.makedirs(os.path.dirname(project_path), exist_ok=True)
|
||||
|
||||
# Create a new board
|
||||
board = pcbnew.BOARD()
|
||||
|
||||
# Set project properties
|
||||
board.GetTitleBlock().SetTitle(project_name)
|
||||
|
||||
# Set current date with proper parameter
|
||||
from datetime import datetime
|
||||
|
||||
current_date = datetime.now().strftime("%Y-%m-%d")
|
||||
board.GetTitleBlock().SetDate(current_date)
|
||||
|
||||
# If template is specified, try to load it
|
||||
if template:
|
||||
template_path = os.path.expanduser(template)
|
||||
if os.path.exists(template_path):
|
||||
template_board = pcbnew.LoadBoard(template_path)
|
||||
# Copy settings from template
|
||||
board.SetDesignSettings(template_board.GetDesignSettings())
|
||||
board.SetLayerStack(template_board.GetLayerStack())
|
||||
|
||||
# Save the board
|
||||
board_path = project_path.replace(".kicad_pro", ".kicad_pcb")
|
||||
board.SetFileName(board_path)
|
||||
pcbnew.SaveBoard(board_path, board)
|
||||
|
||||
# Create schematic from template (use expanded template with symbol definitions)
|
||||
schematic_path = project_path.replace(".kicad_pro", ".kicad_sch")
|
||||
template_sch_path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"..",
|
||||
"templates",
|
||||
"template_with_symbols_expanded.kicad_sch",
|
||||
)
|
||||
|
||||
if os.path.exists(template_sch_path):
|
||||
# Copy template schematic
|
||||
shutil.copy(template_sch_path, schematic_path)
|
||||
|
||||
# Regenerate UUID to ensure uniqueness for each created project
|
||||
import re
|
||||
import uuid as uuid_module
|
||||
|
||||
with open(schematic_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
new_uuid = str(uuid_module.uuid4())
|
||||
content = re.sub(
|
||||
r"\(uuid [0-9a-fA-F-]+\)",
|
||||
f"(uuid {new_uuid})",
|
||||
content,
|
||||
count=1, # Only replace first (schematic) UUID
|
||||
)
|
||||
with open(schematic_path, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(content)
|
||||
|
||||
logger.info(f"Created schematic from template: {schematic_path}")
|
||||
else:
|
||||
# Fallback: create minimal schematic
|
||||
logger.warning(
|
||||
f"Template not found at {template_sch_path}, creating minimal schematic"
|
||||
)
|
||||
import uuid as uuid_module
|
||||
|
||||
schematic_uuid = str(uuid_module.uuid4())
|
||||
with open(schematic_path, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write('(kicad_sch (version 20250114) (generator "KiCAD-MCP-Server")\n\n')
|
||||
f.write(f" (uuid {schematic_uuid})\n\n")
|
||||
f.write(' (paper "A4")\n\n')
|
||||
f.write(" (lib_symbols\n )\n\n")
|
||||
f.write(' (sheet_instances\n (path "/" (page "1"))\n )\n')
|
||||
f.write(")\n")
|
||||
|
||||
# Create project file with schematic reference
|
||||
with open(project_path, "w") as f:
|
||||
f.write("{\n")
|
||||
f.write(' "board": {\n')
|
||||
f.write(f' "filename": "{os.path.basename(board_path)}"\n')
|
||||
f.write(" },\n")
|
||||
f.write(' "sheets": [\n')
|
||||
f.write(f' ["root", "{os.path.basename(schematic_path)}"]\n')
|
||||
f.write(" ]\n")
|
||||
f.write("}\n")
|
||||
|
||||
self.board = board
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Created project: {project_name}",
|
||||
"project": {
|
||||
"name": project_name,
|
||||
"path": project_path,
|
||||
"boardPath": board_path,
|
||||
"schematicPath": schematic_path,
|
||||
},
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating project: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to create project",
|
||||
"errorDetails": str(e),
|
||||
}
|
||||
|
||||
def open_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Open an existing KiCAD project"""
|
||||
try:
|
||||
filename = params.get("filename")
|
||||
if not filename:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No filename provided",
|
||||
"errorDetails": "The filename parameter is required",
|
||||
}
|
||||
|
||||
# Expand user path and make absolute
|
||||
filename = os.path.abspath(os.path.expanduser(filename))
|
||||
|
||||
# If it's a project file, get the board file
|
||||
if filename.endswith(".kicad_pro"):
|
||||
board_path = filename.replace(".kicad_pro", ".kicad_pcb")
|
||||
else:
|
||||
board_path = filename
|
||||
|
||||
# Load the board
|
||||
board = pcbnew.LoadBoard(board_path)
|
||||
self.board = board
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Opened project: {os.path.basename(board_path)}",
|
||||
"project": {
|
||||
"name": os.path.splitext(os.path.basename(board_path))[0],
|
||||
"path": filename,
|
||||
"boardPath": board_path,
|
||||
},
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error opening project: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to open project",
|
||||
"errorDetails": str(e),
|
||||
}
|
||||
|
||||
def save_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Save the current KiCAD project"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first",
|
||||
}
|
||||
|
||||
filename = params.get("filename")
|
||||
if filename:
|
||||
# Save to new location
|
||||
filename = os.path.abspath(os.path.expanduser(filename))
|
||||
self.board.SetFileName(filename)
|
||||
|
||||
# Save the board
|
||||
pcbnew.SaveBoard(self.board.GetFileName(), self.board)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Saved project to: {self.board.GetFileName()}",
|
||||
"project": {
|
||||
"name": os.path.splitext(os.path.basename(self.board.GetFileName()))[0],
|
||||
"path": self.board.GetFileName(),
|
||||
},
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving project: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to save project",
|
||||
"errorDetails": str(e),
|
||||
}
|
||||
|
||||
def get_project_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get information about the current project"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first",
|
||||
}
|
||||
|
||||
title_block = self.board.GetTitleBlock()
|
||||
filename = self.board.GetFileName()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"project": {
|
||||
"name": os.path.splitext(os.path.basename(filename))[0],
|
||||
"path": filename,
|
||||
"title": title_block.GetTitle(),
|
||||
"date": title_block.GetDate(),
|
||||
"revision": title_block.GetRevision(),
|
||||
"company": title_block.GetCompany(),
|
||||
"comment1": title_block.GetComment(0),
|
||||
"comment2": title_block.GetComment(1),
|
||||
"comment3": title_block.GetComment(2),
|
||||
"comment4": title_block.GetComment(3),
|
||||
},
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting project info: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get project information",
|
||||
"errorDetails": str(e),
|
||||
}
|
||||
"""
|
||||
Project-related command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import pcbnew # type: ignore
|
||||
|
||||
logger = logging.getLogger("kicad_interface")
|
||||
|
||||
|
||||
class ProjectCommands:
|
||||
"""Handles project-related KiCAD operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def create_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Create a new KiCAD project"""
|
||||
try:
|
||||
# Accept both 'name' (from MCP tool) and 'projectName' (legacy)
|
||||
project_name = params.get("name") or params.get("projectName", "New_Project")
|
||||
path = params.get("path", os.getcwd())
|
||||
template = params.get("template")
|
||||
|
||||
# Generate the full project path
|
||||
project_path = os.path.join(path, project_name)
|
||||
if not project_path.endswith(".kicad_pro"):
|
||||
project_path += ".kicad_pro"
|
||||
|
||||
# Create project directory if it doesn't exist
|
||||
os.makedirs(os.path.dirname(project_path), exist_ok=True)
|
||||
|
||||
# Create a new board
|
||||
board = pcbnew.BOARD()
|
||||
|
||||
# Set project properties
|
||||
board.GetTitleBlock().SetTitle(project_name)
|
||||
|
||||
# Set current date with proper parameter
|
||||
from datetime import datetime
|
||||
|
||||
current_date = datetime.now().strftime("%Y-%m-%d")
|
||||
board.GetTitleBlock().SetDate(current_date)
|
||||
|
||||
# If template is specified, try to load it
|
||||
if template:
|
||||
template_path = os.path.expanduser(template)
|
||||
if os.path.exists(template_path):
|
||||
template_board = pcbnew.LoadBoard(template_path)
|
||||
# Copy settings from template
|
||||
board.SetDesignSettings(template_board.GetDesignSettings())
|
||||
board.SetLayerStack(template_board.GetLayerStack())
|
||||
|
||||
# Save the board
|
||||
board_path = project_path.replace(".kicad_pro", ".kicad_pcb")
|
||||
board.SetFileName(board_path)
|
||||
pcbnew.SaveBoard(board_path, board)
|
||||
|
||||
# Create schematic from template (use expanded template with symbol definitions)
|
||||
schematic_path = project_path.replace(".kicad_pro", ".kicad_sch")
|
||||
template_sch_path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"..",
|
||||
"templates",
|
||||
"template_with_symbols_expanded.kicad_sch",
|
||||
)
|
||||
|
||||
if os.path.exists(template_sch_path):
|
||||
# Copy template schematic
|
||||
shutil.copy(template_sch_path, schematic_path)
|
||||
|
||||
# Regenerate UUID to ensure uniqueness for each created project
|
||||
import re
|
||||
import uuid as uuid_module
|
||||
|
||||
with open(schematic_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
new_uuid = str(uuid_module.uuid4())
|
||||
content = re.sub(
|
||||
r"\(uuid [0-9a-fA-F-]+\)",
|
||||
f"(uuid {new_uuid})",
|
||||
content,
|
||||
count=1, # Only replace first (schematic) UUID
|
||||
)
|
||||
with open(schematic_path, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(content)
|
||||
|
||||
logger.info(f"Created schematic from template: {schematic_path}")
|
||||
else:
|
||||
# Fallback: create minimal schematic
|
||||
logger.warning(
|
||||
f"Template not found at {template_sch_path}, creating minimal schematic"
|
||||
)
|
||||
import uuid as uuid_module
|
||||
|
||||
schematic_uuid = str(uuid_module.uuid4())
|
||||
with open(schematic_path, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write('(kicad_sch (version 20250114) (generator "KiCAD-MCP-Server")\n\n')
|
||||
f.write(f" (uuid {schematic_uuid})\n\n")
|
||||
f.write(' (paper "A4")\n\n')
|
||||
f.write(" (lib_symbols\n )\n\n")
|
||||
f.write(' (sheet_instances\n (path "/" (page "1"))\n )\n')
|
||||
f.write(")\n")
|
||||
|
||||
# Create project file with schematic reference
|
||||
with open(project_path, "w") as f:
|
||||
f.write("{\n")
|
||||
f.write(' "board": {\n')
|
||||
f.write(f' "filename": "{os.path.basename(board_path)}"\n')
|
||||
f.write(" },\n")
|
||||
f.write(' "sheets": [\n')
|
||||
f.write(f' ["root", "{os.path.basename(schematic_path)}"]\n')
|
||||
f.write(" ]\n")
|
||||
f.write("}\n")
|
||||
|
||||
self.board = board
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Created project: {project_name}",
|
||||
"project": {
|
||||
"name": project_name,
|
||||
"path": project_path,
|
||||
"boardPath": board_path,
|
||||
"schematicPath": schematic_path,
|
||||
},
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating project: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to create project",
|
||||
"errorDetails": str(e),
|
||||
}
|
||||
|
||||
def open_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Open an existing KiCAD project"""
|
||||
try:
|
||||
filename = params.get("filename")
|
||||
if not filename:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No filename provided",
|
||||
"errorDetails": "The filename parameter is required",
|
||||
}
|
||||
|
||||
# Expand user path and make absolute
|
||||
filename = os.path.abspath(os.path.expanduser(filename))
|
||||
|
||||
# If it's a project file, get the board file
|
||||
if filename.endswith(".kicad_pro"):
|
||||
board_path = filename.replace(".kicad_pro", ".kicad_pcb")
|
||||
else:
|
||||
board_path = filename
|
||||
|
||||
# Load the board
|
||||
board = pcbnew.LoadBoard(board_path)
|
||||
self.board = board
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Opened project: {os.path.basename(board_path)}",
|
||||
"project": {
|
||||
"name": os.path.splitext(os.path.basename(board_path))[0],
|
||||
"path": filename,
|
||||
"boardPath": board_path,
|
||||
},
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error opening project: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to open project",
|
||||
"errorDetails": str(e),
|
||||
}
|
||||
|
||||
def save_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Save the current KiCAD project"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first",
|
||||
}
|
||||
|
||||
filename = params.get("filename")
|
||||
if filename:
|
||||
# Save to new location
|
||||
filename = os.path.abspath(os.path.expanduser(filename))
|
||||
self.board.SetFileName(filename)
|
||||
|
||||
# Save the board
|
||||
pcbnew.SaveBoard(self.board.GetFileName(), self.board)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Saved project to: {self.board.GetFileName()}",
|
||||
"project": {
|
||||
"name": os.path.splitext(os.path.basename(self.board.GetFileName()))[0],
|
||||
"path": self.board.GetFileName(),
|
||||
},
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving project: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to save project",
|
||||
"errorDetails": str(e),
|
||||
}
|
||||
|
||||
def get_project_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get information about the current project"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first",
|
||||
}
|
||||
|
||||
title_block = self.board.GetTitleBlock()
|
||||
filename = self.board.GetFileName()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"project": {
|
||||
"name": os.path.splitext(os.path.basename(filename))[0],
|
||||
"path": filename,
|
||||
"title": title_block.GetTitle(),
|
||||
"date": title_block.GetDate(),
|
||||
"revision": title_block.GetRevision(),
|
||||
"company": title_block.GetCompany(),
|
||||
"comment1": title_block.GetComment(0),
|
||||
"comment2": title_block.GetComment(1),
|
||||
"comment3": title_block.GetComment(2),
|
||||
"comment4": title_block.GetComment(3),
|
||||
},
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting project info: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get project information",
|
||||
"errorDetails": str(e),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user