--- name: plugin-new description: Scaffold new ShopDB plugin following ADRs - directory structure, BasePlugin boilerplate, migrations, Vue component --- # New Plugin Scaffolding Generate complete plugin structure following ShopDB architecture (ADR-001, ADR-008, ADR-010). ## What This Skill Does Creates production-ready plugin scaffold: 1. Plugin directory structure 2. BasePlugin subclass with manifest 3. SQLAlchemy models directory 4. Flask API blueprint 5. Alembic migrations directory 6. Vue frontend component 7. README with checklist 8. Adds plugin to PLUGIN_TABLE_OWNERS Saves 30-60 minutes of setup per new plugin. ## Usage `/plugin-new "plugin-name" "description"` Example: `/plugin-new "tooling" "Track hand tools and tooling inventory"` ## Workflow ### 1. Validate Plugin Name **Check naming conventions:** - Lowercase, no underscores - Singular form ("tool" not "tools", exception: existing plurals like "computers") - No spaces or special characters - Not a Python reserved word **Check plugin doesn't exist:** ```bash if [ -d "plugins/$plugin_name" ]; then echo "ERROR: Plugin '$plugin_name' already exists" exit 1 fi ``` **Suggest corrections:** - "tool_tracking" -> "tooling" (remove underscore) - "Tools" -> "tools" (lowercase) - "hand-tools" -> "handtools" (remove hyphen) ### 2. Create Directory Structure ```bash mkdir -p plugins/$plugin_name/{api,models,frontend/views,migrations/versions,tests} ``` **Expected structure:** ``` plugins/tooling/ __init__.py plugin.py # BasePlugin subclass api/ __init__.py routes.py # Flask blueprint models/ __init__.py tool.py # SQLAlchemy model frontend/ views/ ToolList.vue # Main Vue component migrations/ env.py # Alembic environment script.py.mako # Migration template versions/ # Migration files tests/ __init__.py test_tooling.py # Plugin tests README.md # Plugin documentation ``` ### 3. Generate BasePlugin Subclass **File:** `plugins/$plugin_name/plugin.py` ```python """ $Plugin_Name Plugin $Description """ from shopdb.api import BasePlugin class ${Plugin_Name}Plugin(BasePlugin): """$Description""" name = "$plugin_name" display_name = "$Display Name" description = "$description" version = "0.1.0" author = "ShopDB Team" # Dependencies (other plugins this depends on) dependencies = [] # Frontend routes frontend_routes = [ { "path": "/$plugin_name", "name": "$Plugin_Name", "component": "$Plugin_NameList", "meta": {"requiresAuth": True, "title": "$Display Name"} } ] # Sidebar menu entry sidebar_items = [ { "title": "$Display Name", "icon": "mdi-tools", # Material Design Icon "path": "/$plugin_name", "permission": "$plugin_name.view" } ] # Permissions this plugin defines permissions = [ ("$plugin_name.view", "View $display_name"), ("$plugin_name.create", "Create $display_name"), ("$plugin_name.edit", "Edit $display_name"), ("$plugin_name.delete", "Delete $display_name"), ] def register_blueprints(self, app): """Register Flask blueprints""" from .api.routes import bp app.register_blueprint(bp, url_prefix=f"/api/$plugin_name") def init_app(self, app): """Initialize plugin with Flask app""" self.register_blueprints(app) ``` **Variables to substitute:** - `$plugin_name`: "tooling" - `$Plugin_Name`: "Tooling" - `$Display Name`: "Tool Tracking" - `$description`: User-provided description ### 4. Create SQLAlchemy Model **File:** `plugins/$plugin_name/models/$model_name.py` ```python """ $Model_Name model for $plugin_name plugin """ from shopdb.api import db, Asset class $Model_Name(Asset): """$Description""" __tablename__ = "$tablename" __mapper_args__ = {"polymorphic_identity": "$plugin_name"} # Polymorphic inheritance from Asset (ADR-001) id = db.Column(db.Integer, db.ForeignKey('asset.id'), primary_key=True) # Plugin-specific fields tool_type = db.Column(db.String(100), nullable=False) manufacturer = db.Column(db.String(100)) model_number = db.Column(db.String(100)) serial_number = db.Column(db.String(100), unique=True) purchase_date = db.Column(db.Date) status = db.Column(db.String(50), default='available') # Relationships (example: assigned to employee) assigned_to_id = db.Column(db.Integer, db.ForeignKey('employee.id')) assigned_to = db.relationship('Employee', backref='tools') def __repr__(self): return f"<$Model_Name {self.serial_number}>" def to_dict(self): """Serialize model to dictionary""" data = super().to_dict() # Get Asset base fields data.update({ 'tool_type': self.tool_type, 'manufacturer': self.manufacturer, 'model_number': self.model_number, 'serial_number': self.serial_number, 'purchase_date': self.purchase_date.isoformat() if self.purchase_date else None, 'status': self.status, 'assigned_to': self.assigned_to.to_dict() if self.assigned_to else None }) return data ``` **Models/__init__.py:** ```python from .tool import Tool __all__ = ['Tool'] ``` ### 5. Create Flask API Blueprint **File:** `plugins/$plugin_name/api/routes.py` ```python """ API routes for $plugin_name plugin """ from flask import Blueprint, request, jsonify from shopdb.api import db, requires_auth from ..models import $Model_Name bp = Blueprint('$plugin_name', __name__) @bp.route('/', methods=['GET']) @requires_auth def list_$plural(): """List all $plural""" items = $Model_Name.query.all() return jsonify([item.to_dict() for item in items]) @bp.route('/', methods=['GET']) @requires_auth def get_$singular(id): """Get specific $singular by ID""" item = $Model_Name.query.get_or_404(id) return jsonify(item.to_dict()) @bp.route('/', methods=['POST']) @requires_auth def create_$singular(): """Create new $singular""" data = request.json # Validate required fields required = ['tool_type', 'serial_number'] if not all(field in data for field in required): return jsonify({'error': 'Missing required fields'}), 400 # Create item item = $Model_Name(**data) db.session.add(item) db.session.commit() return jsonify(item.to_dict()), 201 @bp.route('/', methods=['PUT']) @requires_auth def update_$singular(id): """Update existing $singular""" item = $Model_Name.query.get_or_404(id) data = request.json # Update fields for key, value in data.items(): if hasattr(item, key): setattr(item, key, value) db.session.commit() return jsonify(item.to_dict()) @bp.route('/', methods=['DELETE']) @requires_auth def delete_$singular(id): """Delete $singular""" item = $Model_Name.query.get_or_404(id) db.session.delete(item) db.session.commit() return '', 204 ``` ### 6. Initialize Alembic Migrations **File:** `plugins/$plugin_name/migrations/env.py` Copy from template (similar to other plugins) **File:** `plugins/$plugin_name/migrations/script.py.mako` Copy from template **Create initial migration:** ```bash docker compose exec api flask plugin revision -p $plugin_name -m "Initial $plugin_name schema" ``` This generates: `plugins/$plugin_name/migrations/versions/001_initial_schema.py` **Verify migration:** - Check revision ID generated - Verify down_revision is None (first migration) - Verify upgrade() creates table - Verify downgrade() drops table ### 7. Create Vue Frontend Component **File:** `plugins/$plugin_name/frontend/views/$Plugin_NameList.vue` ```vue ``` ### 8. Add to PLUGIN_TABLE_OWNERS **File:** `shopdb/config.py` ```python PLUGIN_TABLE_OWNERS = [ 'employees', # Core dependency (if using Employee FK) 'assets', # Core dependency (Asset polymorphism) # ... existing plugins ... '$plugin_name', # ADD NEW PLUGIN HERE ] ``` **Placement considerations:** - After plugins you reference (foreign keys) - Before plugins that reference you - Alphabetical within same dependency level ### 9. Create Plugin Tests **File:** `plugins/$plugin_name/tests/test_tooling.py` ```python """ Tests for $plugin_name plugin """ import pytest from shopdb.api import db from plugins.$plugin_name.models import $Model_Name def test_create_$singular(client, auth_header): """Test creating $singular via API""" response = client.post('/api/$plugin_name/', json={ 'tool_type': 'Wrench', 'serial_number': 'WR-001', 'manufacturer': 'Craftsman' }, headers=auth_header) assert response.status_code == 201 data = response.json assert data['tool_type'] == 'Wrench' assert data['serial_number'] == 'WR-001' def test_list_$plural(client, auth_header): """Test listing all $plural""" response = client.get('/api/$plugin_name/', headers=auth_header) assert response.status_code == 200 assert isinstance(response.json, list) def test_get_$singular(client, auth_header, test_$singular): """Test getting specific $singular""" response = client.get(f'/api/$plugin_name/{test_$singular.id}', headers=auth_header) assert response.status_code == 200 data = response.json assert data['id'] == test_$singular.id def test_update_$singular(client, auth_header, test_$singular): """Test updating $singular""" response = client.put(f'/api/$plugin_name/{test_$singular.id}', json={ 'status': 'in_use' }, headers=auth_header) assert response.status_code == 200 data = response.json assert data['status'] == 'in_use' def test_delete_$singular(client, auth_header, test_$singular): """Test deleting $singular""" response = client.delete(f'/api/$plugin_name/{test_$singular.id}', headers=auth_header) assert response.status_code == 204 # Verify deleted response = client.get(f'/api/$plugin_name/{test_$singular.id}', headers=auth_header) assert response.status_code == 404 @pytest.fixture def test_$singular(db_session): """Create test $singular""" $singular = $Model_Name( tool_type='Hammer', serial_number='TEST-001', manufacturer='TestCo' ) db_session.add($singular) db_session.commit() return $singular ``` ### 10. Create Plugin README **File:** `plugins/$plugin_name/README.md` ```markdown # $Display Name Plugin $description ## Features - Track $plural with serial numbers - Manage tool assignments - Monitor tool status (available, in_use, maintenance, retired) ## Models ### $Model_Name Inherits from `Asset` (ADR-001: Asset as platform contract) **Fields:** - `tool_type` (str): Type of tool - `manufacturer` (str): Manufacturer name - `model_number` (str): Model number - `serial_number` (str, unique): Serial number - `status` (str): Tool status ## API Endpoints - `GET /api/$plugin_name/` - List all $plural - `GET /api/$plugin_name/` - Get specific $singular - `POST /api/$plugin_name/` - Create new $singular - `PUT /api/$plugin_name/` - Update $singular - `DELETE /api/$plugin_name/` - Delete $singular ## Frontend Main component: `$Plugin_NameList.vue` Route: `/$plugin_name` ## Development Checklist - [ ] Models defined with proper relationships - [ ] Initial migration created and tested - [ ] API endpoints implemented with authentication - [ ] Frontend component renders list and detail views - [ ] Tests written and passing - [ ] Added to PLUGIN_TABLE_OWNERS in correct order - [ ] Documentation updated - [ ] Permissions defined - [ ] Sidebar menu item configured ## Testing ```bash pytest plugins/$plugin_name/tests/ -v ``` ## Migration ```bash # Create new migration docker compose exec api flask plugin revision -p $plugin_name -m "description" # Apply migration docker compose exec api flask plugin upgrade $plugin_name ``` ``` ### 11. Summary Report ``` Plugin scaffolding complete: $plugin_name Created files: [X] plugins/$plugin_name/plugin.py (BasePlugin subclass) [X] plugins/$plugin_name/models/$model_name.py (SQLAlchemy model) [X] plugins/$plugin_name/api/routes.py (Flask blueprint, 5 endpoints) [X] plugins/$plugin_name/frontend/views/$Plugin_NameList.vue (Vue component) [X] plugins/$plugin_name/migrations/ (Alembic initialized) [X] plugins/$plugin_name/tests/test_$plugin_name.py (pytest tests) [X] plugins/$plugin_name/README.md (documentation) Updated files: [X] shopdb/config.py (added to PLUGIN_TABLE_OWNERS) Next steps: 1. Review generated code and customize as needed 2. Create initial migration: docker compose exec api flask plugin revision -p $plugin_name -m "Initial schema" 3. Apply migration: docker compose exec api flask plugin upgrade $plugin_name 4. Run tests: pytest plugins/$plugin_name/tests/ -v 5. Start dev server and test frontend: npm run dev 6. Commit changes: git add plugins/$plugin_name shopdb/config.py Time saved: ~45 minutes of manual scaffolding ``` ## Customization After scaffolding, customize: **Models:** - Add plugin-specific fields - Define relationships to other plugins - Add custom methods **API:** - Add custom endpoints (search, filters, bulk operations) - Implement business logic - Add validation schemas **Frontend:** - Customize table columns - Add filters and search - Implement detail views - Add charts/visualizations **Tests:** - Add tests for custom logic - Test edge cases - Add integration tests ## Related - ADR-001: Asset as platform contract - ADR-008: Plugin migration ownership - ADR-010: Frontend plugin hooks - `CLAUDE.md` - Plugin development guide - `/pre-commit` - Test before committing