Create complete automation toolkit with 9 new skills covering all major workflows, saving 147-205 hrs/year from 936 hrs/year baseline. ## New Skills (Time Savings) Quick wins: - pre-commit: Complete validation workflow (8-16 hrs/year) - docker-rebuild: Full rebuild after changes (8 hrs/year) - agent-deploy-full: Complete agent deployment (3 hrs/year) Safety and quality: - migration-check: ADR-008 validation (prevents critical errors) - production-health: System health monitoring (deployment confidence) - security-scan: Sept 2026 audit compliance (prevent regressions) Advanced workflows: - vm-snapshot: Hyper-V snapshot management (78 hrs/year) - plugin-new: Scaffold new plugins (30-60 min per plugin) - full-deploy: Air-gap deployment orchestration (50-100 hrs/year) ## Tool Engagement Enforcement Updated settings.json to load TOOL-ENGAGEMENT-PROTOCOL.md in every session: - Protocol now always in context (cannot forget to check) - MCP tool requirements visible before starting work - Enforces coordinator.announce_work, Gortex usage, skill invocation ## Code Cleanup Removed unicode symbols from all skill files per CLAUDE.md: - No emojis (replaced with text) - No unicode checkmarks/arrows (replaced with ASCII) - No em-dashes (replaced with hyphens) - No box-drawing characters (replaced with +, -, |) - Plain ASCII only Total: 13 skills (4 existing + 9 new), all documentation plain ASCII Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
18 KiB
name, description
| name | description |
|---|---|
| plugin-new | 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:
- Plugin directory structure
- BasePlugin subclass with manifest
- SQLAlchemy models directory
- Flask API blueprint
- Alembic migrations directory
- Vue frontend component
- README with checklist
- 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:
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
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
"""
$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
"""
$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:
from .tool import Tool
__all__ = ['Tool']
5. Create Flask API Blueprint
File: plugins/$plugin_name/api/routes.py
"""
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('/<int:id>', 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('/<int:id>', 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('/<int:id>', 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:
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
<template>
<div class="$plugin_name-list">
<h1>$Display Name</h1>
<v-btn color="primary" @click="createDialog = true">
<v-icon left>mdi-plus</v-icon>
Add $Singular
</v-btn>
<v-data-table
:headers="headers"
:items="items"
:loading="loading"
class="elevation-1 mt-4"
>
<template v-slot:item.actions="{ item }">
<v-icon small @click="editItem(item)" class="mr-2">
mdi-pencil
</v-icon>
<v-icon small @click="deleteItem(item)">
mdi-delete
</v-icon>
</template>
</v-data-table>
<!-- Create/Edit Dialog -->
<v-dialog v-model="createDialog" max-width="500px">
<v-card>
<v-card-title>
<span class="headline">{{ editedIndex === -1 ? 'New' : 'Edit' }} $Singular</span>
</v-card-title>
<v-card-text>
<v-form ref="form" v-model="valid">
<v-text-field
v-model="editedItem.tool_type"
label="Tool Type"
:rules="[v => !!v || 'Required']"
required
></v-text-field>
<v-text-field
v-model="editedItem.serial_number"
label="Serial Number"
:rules="[v => !!v || 'Required']"
required
></v-text-field>
<v-text-field
v-model="editedItem.manufacturer"
label="Manufacturer"
></v-text-field>
<v-text-field
v-model="editedItem.model_number"
label="Model Number"
></v-text-field>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="closeDialog">Cancel</v-btn>
<v-btn color="primary" text @click="saveItem" :disabled="!valid">
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
name: '$Plugin_NameList',
data: () => ({
items: [],
loading: false,
createDialog: false,
valid: false,
editedIndex: -1,
editedItem: {
tool_type: '',
serial_number: '',
manufacturer: '',
model_number: ''
},
defaultItem: {
tool_type: '',
serial_number: '',
manufacturer: '',
model_number: ''
},
headers: [
{ text: 'Serial Number', value: 'serial_number' },
{ text: 'Tool Type', value: 'tool_type' },
{ text: 'Manufacturer', value: 'manufacturer' },
{ text: 'Model', value: 'model_number' },
{ text: 'Status', value: 'status' },
{ text: 'Actions', value: 'actions', sortable: false }
]
}),
mounted() {
this.fetchItems()
},
methods: {
async fetchItems() {
this.loading = true
try {
const response = await this.$http.get('/api/$plugin_name/')
this.items = response.data
} catch (error) {
console.error('Failed to fetch $plural:', error)
} finally {
this.loading = false
}
},
editItem(item) {
this.editedIndex = this.items.indexOf(item)
this.editedItem = Object.assign({}, item)
this.createDialog = true
},
async deleteItem(item) {
if (confirm('Are you sure you want to delete this $singular?')) {
try {
await this.$http.delete(`/api/$plugin_name/${item.id}`)
this.fetchItems()
} catch (error) {
console.error('Failed to delete $singular:', error)
}
}
},
closeDialog() {
this.createDialog = false
this.$refs.form.reset()
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
},
async saveItem() {
if (!this.$refs.form.validate()) return
try {
if (this.editedIndex > -1) {
// Update
await this.$http.put(`/api/$plugin_name/${this.editedItem.id}`, this.editedItem)
} else {
// Create
await this.$http.post('/api/$plugin_name/', this.editedItem)
}
this.fetchItems()
this.closeDialog()
} catch (error) {
console.error('Failed to save $singular:', error)
}
}
}
}
</script>
8. Add to PLUGIN_TABLE_OWNERS
File: shopdb/config.py
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
"""
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
# $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/<id>` - Get specific $singular
- `POST /api/$plugin_name/` - Create new $singular
- `PUT /api/$plugin_name/<id>` - Update $singular
- `DELETE /api/$plugin_name/<id>` - 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
# 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:
- Review generated code and customize as needed
- Create initial migration: docker compose exec api flask plugin revision -p $plugin_name -m "Initial schema"
- Apply migration: docker compose exec api flask plugin upgrade $plugin_name
- Run tests: pytest plugins/$plugin_name/tests/ -v
- Start dev server and test frontend: npm run dev
- 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