| name | project-creator |
| description | Creates new projects from BUILDER stack template. Auto-triggers on Dashboard "New Project" button or API call /api/projects/create. Handles clone, port assignment, PM2 setup, deployment. |
| allowed-tools | Read, Write, Bash, Glob |
Skill: Project Creator
Auto-loaded by: EXECUTOR when Dashboard API calls project creation Purpose: Create new project from BUILDER stack, deploy automatically Version: 1.0.0
Trigger Conditions
EXECUTOR charges ce skill quand:
- API call:
POST /api/projects/create - Dashboard button: "+ New Project"
- Command:
create-project [name](via Dashboard terminal)
Workflow (STRICT - 5 Steps)
Step 1: Validate Project Name
// OBLIGATOIRE avant toute création
function validateProjectName(name: string): boolean {
// Règles:
// - Kebab-case uniquement (lowercase + hyphens)
// - 3-50 caractères
// - Pas de caractères spéciaux
// - Pas de doublon (check si existe déjà)
const REGEX = /^[a-z0-9-]{3,50}$/
if (!REGEX.test(name)) {
throw new Error(`Invalid project name: ${name}. Must be kebab-case.`)
}
const projectPath = `/home/pilote/projet/secondaire/${name}`
if (fs.existsSync(projectPath)) {
throw new Error(`Project ${name} already exists.`)
}
return true
}
Step 2: Clone BUILDER Stack
#!/bin/bash
PROJECT_NAME="$1"
PROJECT_PATH="/home/pilote/projet/secondaire/$PROJECT_NAME"
BUILDER_STACK="/home/pilote/projet/primaire/BUILDER/.stack"
echo "📦 Cloning BUILDER stack..."
# Create project directory
mkdir -p "$PROJECT_PATH"
# Clone stack (57 shadcn components + Next.js 16)
cp -r "$BUILDER_STACK"/* "$PROJECT_PATH"/
# Verify clone succeeded
if [ ! -f "$PROJECT_PATH/package.json" ]; then
echo "❌ Clone failed"
exit 1
fi
echo "✅ Stack cloned"
Fichiers clonés:
projet/secondaire/mon-app/
├── components/ui/ (57 composants shadcn)
├── app/
│ ├── globals.css
│ ├── themes.css
│ └── layout.tsx
├── lib/utils.ts
├── package.json
├── tsconfig.json
├── next.config.ts
└── tailwind.config.ts
Step 3: Initialize .build/ Structure
echo "📋 Initializing .build/..."
mkdir -p "$PROJECT_PATH/.build/decisions"
# context.md
cat > "$PROJECT_PATH/.build/context.md" << 'EOF'
# Project Context
## Stack Technique
- Frontend: Next.js 16 + shadcn/ui (57 components)
- Styling: Tailwind CSS v4
- Dark mode: Included (themes.css)
## Architecture Actuelle
(Will be populated after first features)
## Conventions Établies
- Components: components/ui/ (shadcn)
- Pages: app/
- Utils: lib/utils.ts
EOF
# timeline.md
cat > "$PROJECT_PATH/.build/timeline.md" << EOF
# Timeline
## $(date +"%Y-%m-%d %H:%M") - Project Initialization
**Type**: Setup
**Status**: ✓ Completed
**Source**: Dashboard GUI
### Changes
- Project created via Dashboard
- BUILDER stack cloned (57 shadcn components)
- .build/ structure initialized
- Deployed to PM2
### Notes
- Ready for feature development
- Preview: http://89.116.27.88:[PORT]
EOF
# tasks.md, issues.md, specs.md (templates vides)
touch "$PROJECT_PATH/.build/tasks.md"
touch "$PROJECT_PATH/.build/issues.md"
touch "$PROJECT_PATH/.build/specs.md"
echo "✅ .build/ initialized"
Step 4: Install Dependencies + Build
echo "📥 Installing dependencies..."
cd "$PROJECT_PATH"
# Install (silent mode)
npm install --silent
# Verify install succeeded
if [ ! -d "node_modules" ]; then
echo "❌ npm install failed"
exit 1
fi
echo "✅ Dependencies installed"
echo "📦 Building production..."
# Clean old builds
rm -rf .next
# Build production
npm run build
# Verify build succeeded
if [ ! -f ".next/BUILD_ID" ]; then
echo "❌ Build failed"
exit 1
fi
echo "✅ Production build ready"
Step 5: Deploy to PM2 (Auto Port Assignment)
echo "🚀 Deploying to PM2..."
# Get next available port
HIGHEST_PORT=$(pm2 jlist 2>/dev/null | node -e "
let data = '';
process.stdin.on('data', chunk => data += chunk);
process.stdin.on('end', () => {
try {
const procs = JSON.parse(data);
const ports = procs
.map(p => p.pm2_env.PORT)
.filter(p => p && !isNaN(p))
.map(Number);
console.log(Math.max(3000, ...ports));
} catch(e) { console.log(3000); }
});
")
PORT=$((HIGHEST_PORT + 1))
# Save port to .env
echo "PORT=$PORT" > .env
# Create ecosystem.config.js
cat > ecosystem.config.js << EOF
module.exports = {
apps: [{
name: '$PROJECT_NAME',
script: 'npm',
args: 'start',
cwd: '$PROJECT_PATH',
instances: 1,
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: $PORT
},
error_file: '~/.pm2/logs/$PROJECT_NAME-error.log',
out_file: '~/.pm2/logs/$PROJECT_NAME-out.log',
merge_logs: true,
autorestart: true,
watch: false
}]
}
EOF
# Start PM2
pm2 start ecosystem.config.js
# Save PM2 config (persist reboot)
pm2 save
# Health check (max 15 seconds)
echo "🔍 Health check..."
for i in {1..15}; do
sleep 1
# Check PM2 status
STATUS=$(pm2 jlist 2>/dev/null | node -e "
let data = '';
process.stdin.on('data', chunk => data += chunk);
process.stdin.on('end', () => {
try {
const procs = JSON.parse(data);
const proc = procs.find(p => p.name === '$PROJECT_NAME');
console.log(proc ? proc.pm2_env.status : 'not_found');
} catch(e) { console.log('error'); }
});
")
if [ "$STATUS" == "online" ]; then
# Check HTTP 200
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:$PORT)
if [ "$HTTP_CODE" == "200" ]; then
echo "✅ Health check passed"
break
fi
fi
if [ $i -eq 15 ]; then
echo "⚠️ Health check timeout (still starting...)"
fi
done
echo ""
echo "✅ Project deployed successfully!"
echo ""
echo "📍 Details:"
echo " Name: $PROJECT_NAME"
echo " Path: $PROJECT_PATH"
echo " Port: $PORT"
echo " Preview: http://89.116.27.88:$PORT"
echo " PM2: pm2 logs $PROJECT_NAME"
echo ""
Step 6: Setup Chrome DevTools Container (Optional)
Si tests automatiques demandés:
# Calculate next noVNC port (6081, 6082, 6083...)
NOVNC_BASE=6080
CHROME_BASE=9223
NEXT_NOVNC=$((NOVNC_BASE + PORT - 3000))
NEXT_CHROME=$((CHROME_BASE + PORT - 3000))
# Start noVNC container for this project
docker run -d \
--name "chrome-$PROJECT_NAME" \
--restart=always \
-p $NEXT_NOVNC:80 \
-p $NEXT_CHROME:9223 \
-e RESOLUTION=1920x1080 \
-e VNC_PASSWORD=Voiture789 \
--shm-size=2gb \
dorowu/ubuntu-desktop-lxde-vnc
# Wait for container ready
sleep 5
# Start Chrome + socat in container
docker exec "chrome-$PROJECT_NAME" bash -c "
# Install Chrome if not present
if ! command -v google-chrome &> /dev/null; then
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' > /etc/apt/sources.list.d/google-chrome.list
apt-get update -qq
apt-get install -y google-chrome-stable socat
fi
# Start Chrome with remote debugging
DISPLAY=:1 google-chrome \
--remote-debugging-port=9222 \
--no-sandbox \
--disable-dev-shm-usage \
--start-maximized \
http://89.116.27.88:$PORT &
# Tunnel port 9222 → 9223
socat TCP-LISTEN:9223,fork,bind=0.0.0.0 TCP:127.0.0.1:9222 &
"
echo "✅ Chrome DevTools ready"
echo " noVNC: http://89.116.27.88:$NEXT_NOVNC"
echo " DevTools: http://89.116.27.88:$NEXT_CHROME"
Return Format (JSON API Response)
{
"success": true,
"project": {
"name": "mon-app",
"path": "/home/pilote/projet/secondaire/mon-app",
"port": 3004,
"preview_url": "http://89.116.27.88:3004",
"pm2_status": "online",
"devtools": {
"novnc_url": "http://89.116.27.88:6084",
"chrome_port": 9227
},
"created_at": "2025-11-11T17:30:00Z"
}
}
Error Handling
Error 1: Invalid Project Name
{
"success": false,
"error": "INVALID_NAME",
"message": "Project name must be kebab-case (lowercase, hyphens only)",
"example": "my-awesome-app"
}
Error 2: Project Already Exists
{
"success": false,
"error": "PROJECT_EXISTS",
"message": "Project 'mon-app' already exists",
"existing_port": 3002
}
Error 3: Build Failed
{
"success": false,
"error": "BUILD_FAILED",
"message": "Production build failed",
"logs": "npm ERR! ..."
}
Error 4: PM2 Deploy Failed
{
"success": false,
"error": "DEPLOY_FAILED",
"message": "PM2 deployment failed",
"logs": "pm2 ERR! ..."
}
Integration avec Dashboard
Dashboard API Call
// Dashboard: app/api/projects/create/route.ts
import { exec } from 'child_process'
import { promisify } from 'util'
const execAsync = promisify(exec)
export async function POST(req: Request) {
const { name } = await req.json()
try {
// Validate name
if (!/^[a-z0-9-]{3,50}$/.test(name)) {
return Response.json({
success: false,
error: 'INVALID_NAME',
message: 'Project name must be kebab-case'
}, { status: 400 })
}
// Execute creation script (invokes EXECUTOR with this skill)
const script = `/home/pilote/projet/primaire/BUILDER/bin/create-project-api`
const { stdout, stderr } = await execAsync(`${script} ${name}`)
// Parse output (JSON)
const result = JSON.parse(stdout)
return Response.json(result)
} catch (error) {
return Response.json({
success: false,
error: 'EXECUTION_FAILED',
message: error.message
}, { status: 500 })
}
}
Dashboard Frontend
// Dashboard: components/NewProjectDialog.tsx
'use client'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
export function NewProjectDialog({ onSuccess }: { onSuccess: () => void }) {
const [name, setName] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const createProject = async () => {
setLoading(true)
setError('')
try {
const res = await fetch('/api/projects/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
})
const data = await res.json()
if (data.success) {
onSuccess() // Refresh project list
setName('')
} else {
setError(data.message)
}
} catch (err) {
setError('Failed to create project')
} finally {
setLoading(false)
}
}
return (
<Dialog>
<DialogTrigger asChild>
<Button>+ New Project</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Project</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Input
placeholder="project-name (kebab-case)"
value={name}
onChange={(e) => setName(e.target.value)}
/>
{error && (
<p className="text-red-500 text-sm">{error}</p>
)}
<Button
onClick={createProject}
disabled={loading || !name}
className="w-full"
>
{loading ? 'Creating...' : 'Create Project'}
</Button>
</div>
</DialogContent>
</Dialog>
)
}
Auto-Refresh Dashboard on New Project
// Dashboard: hooks/useProjects.ts
'use client'
import { useState, useEffect } from 'react'
export function useProjects() {
const [projects, setProjects] = useState([])
const refreshProjects = async () => {
const res = await fetch('/api/projects/list')
const data = await res.json()
setProjects(data.projects)
}
useEffect(() => {
// Initial load
refreshProjects()
// Poll every 5 seconds (detect new projects)
const interval = setInterval(refreshProjects, 5000)
return () => clearInterval(interval)
}, [])
return { projects, refreshProjects }
}
Conventions EXECUTOR
Quand EXECUTOR reçoit cette task:
- Charge skill automatiquement (détection keyword "create project")
- Exécute workflow 5 steps (validate → clone → build → deploy → devtools)
- Return JSON response (succès ou erreur)
- Update .build/timeline.md (log création projet)
- Confirme à Dashboard via API response
EXECUTOR ne demande PAS de validation user (création automatique).
Version: 1.0.0 Last updated: 2025-11-11 Maintainer: BUILDER System