1416 lines
27 KiB
Bash
Executable File
1416 lines
27 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
PROJECT_RAW="${1:-}"
|
|
|
|
if [[ -z "$PROJECT_RAW" ]]; then
|
|
echo "Usage: $0 <project-name> [--no-install]"
|
|
echo "Example: $0 surveyable"
|
|
exit 1
|
|
fi
|
|
|
|
NO_INSTALL=false
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--no-install) NO_INSTALL=true ;;
|
|
esac
|
|
done
|
|
|
|
require_cmd() {
|
|
if ! command -v "$1" >/dev/null 2>&1; then
|
|
echo "Missing required command: $1"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
require_cmd dotnet
|
|
require_cmd npm
|
|
|
|
# Fix old npm config warning:
|
|
# npm warn Unknown builtin config "globalignorefile"
|
|
if npm config get globalignorefile >/dev/null 2>&1; then
|
|
npm config delete globalignorefile >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
to_pascal_case() {
|
|
echo "$1" \
|
|
| tr '[:upper:]' '[:lower:]' \
|
|
| sed -E 's/[^a-z0-9]+/ /g' \
|
|
| awk '{
|
|
out="";
|
|
for (i=1; i<=NF; i++) {
|
|
word=$i;
|
|
out = out toupper(substr(word,1,1)) substr(word,2);
|
|
}
|
|
print out;
|
|
}'
|
|
}
|
|
|
|
PROJECT_DIR="$PROJECT_RAW"
|
|
PASCAL="$(to_pascal_case "$PROJECT_RAW")"
|
|
|
|
if [[ -z "$PASCAL" ]]; then
|
|
echo "Project name must contain at least one letter or number."
|
|
exit 1
|
|
fi
|
|
|
|
API_NAME="${PASCAL}.Api"
|
|
TEST_NAME="${PASCAL}.Tests"
|
|
SLNX_NAME="${PASCAL}.slnx"
|
|
|
|
BACKEND_PORT=5080
|
|
FRONTEND_PORT=5173
|
|
COMPOSE_PORT=8080
|
|
|
|
echo "Project directory: $PROJECT_DIR"
|
|
echo ".NET solution: $SLNX_NAME"
|
|
echo "API project: $API_NAME"
|
|
echo "Test project: $TEST_NAME"
|
|
echo "Backend dev URL: http://0.0.0.0:$BACKEND_PORT"
|
|
echo "Frontend dev URL: http://0.0.0.0:$FRONTEND_PORT"
|
|
echo
|
|
|
|
mkdir -p "$PROJECT_DIR"
|
|
cd "$PROJECT_DIR"
|
|
|
|
mkdir -p backend/src backend/tests docs deploy/caddy shared/openapi scripts
|
|
|
|
########################################
|
|
# Backend: .NET 10 API + Tests + .slnx
|
|
########################################
|
|
cd backend
|
|
|
|
dotnet new sln -n "$PASCAL"
|
|
|
|
if [[ ! -f "$SLNX_NAME" ]]; then
|
|
echo "Expected $SLNX_NAME, but dotnet did not create it."
|
|
echo "Detected SDK:"
|
|
dotnet --version
|
|
exit 1
|
|
fi
|
|
|
|
dotnet new webapi -n "$API_NAME" -o "src/$API_NAME" --framework net10.0
|
|
dotnet new xunit -n "$TEST_NAME" -o "tests/$TEST_NAME" --framework net10.0
|
|
|
|
dotnet sln "$SLNX_NAME" add "src/$API_NAME/$API_NAME.csproj"
|
|
dotnet sln "$SLNX_NAME" add "tests/$TEST_NAME/$TEST_NAME.csproj"
|
|
|
|
dotnet add "tests/$TEST_NAME/$TEST_NAME.csproj" reference "src/$API_NAME/$API_NAME.csproj"
|
|
|
|
API_DIR="src/$API_NAME"
|
|
rm -f "$API_DIR/WeatherForecast.cs" || true
|
|
|
|
mkdir -p "$API_DIR/Endpoints/Health"
|
|
mkdir -p "$API_DIR/Contracts/Health"
|
|
mkdir -p "$API_DIR/Domain"
|
|
mkdir -p "$API_DIR/Infrastructure"
|
|
mkdir -p "$API_DIR/Properties"
|
|
|
|
cat > "$API_DIR/Properties/launchSettings.json" <<EOF
|
|
{
|
|
"\$schema": "https://json.schemastore.org/launchsettings.json",
|
|
"profiles": {
|
|
"$API_NAME": {
|
|
"commandName": "Project",
|
|
"dotnetRunMessages": true,
|
|
"launchBrowser": false,
|
|
"applicationUrl": "http://0.0.0.0:$BACKEND_PORT",
|
|
"environmentVariables": {
|
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
EOF
|
|
|
|
cat > "$API_DIR/Contracts/Health/HealthResponse.cs" <<EOF
|
|
namespace $API_NAME.Contracts.Health;
|
|
|
|
public sealed record HealthResponse(
|
|
string Status,
|
|
DateTimeOffset ServerTimeUtc
|
|
);
|
|
EOF
|
|
|
|
cat > "$API_DIR/Contracts/Health/HelloResponse.cs" <<EOF
|
|
namespace $API_NAME.Contracts.Health;
|
|
|
|
public sealed record HelloResponse(
|
|
string Message
|
|
);
|
|
EOF
|
|
|
|
cat > "$API_DIR/Endpoints/Health/HealthEndpoints.cs" <<EOF
|
|
using $API_NAME.Contracts.Health;
|
|
|
|
namespace $API_NAME.Endpoints.Health;
|
|
|
|
public static class HealthEndpoints
|
|
{
|
|
public static RouteGroupBuilder MapHealthEndpoints(this IEndpointRouteBuilder app)
|
|
{
|
|
var group = app.MapGroup("/api");
|
|
|
|
group.MapGet("/health", () =>
|
|
Results.Ok(new HealthResponse("ok", DateTimeOffset.UtcNow)))
|
|
.WithName("GetHealth")
|
|
.WithOpenApi();
|
|
|
|
group.MapGet("/hello", () =>
|
|
Results.Ok(new HelloResponse("Hello from the .NET API")))
|
|
.WithName("GetHello")
|
|
.WithOpenApi();
|
|
|
|
return group;
|
|
}
|
|
}
|
|
EOF
|
|
|
|
cat > "$API_DIR/Program.cs" <<EOF
|
|
using Microsoft.AspNetCore.Http.Json;
|
|
using $API_NAME.Endpoints.Health;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
builder.Services.AddOpenApi();
|
|
|
|
builder.Services.Configure<JsonOptions>(options =>
|
|
{
|
|
options.SerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase;
|
|
});
|
|
|
|
const string DevCorsPolicy = "DevCors";
|
|
|
|
builder.Services.AddCors(options =>
|
|
{
|
|
options.AddPolicy(DevCorsPolicy, policy =>
|
|
{
|
|
policy
|
|
.AllowAnyHeader()
|
|
.AllowAnyMethod()
|
|
.AllowCredentials()
|
|
.SetIsOriginAllowed(origin =>
|
|
{
|
|
if (string.IsNullOrWhiteSpace(origin)) return false;
|
|
|
|
var uri = new Uri(origin);
|
|
|
|
return uri.Host is "localhost" or "127.0.0.1"
|
|
|| uri.Host.StartsWith("192.168.")
|
|
|| uri.Host.StartsWith("10.")
|
|
|| uri.Host.StartsWith("172.");
|
|
});
|
|
});
|
|
});
|
|
|
|
var app = builder.Build();
|
|
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
app.MapOpenApi();
|
|
app.UseCors(DevCorsPolicy);
|
|
}
|
|
|
|
app.MapHealthEndpoints();
|
|
|
|
app.Run();
|
|
EOF
|
|
|
|
cat > "$API_DIR/Dockerfile" <<EOF
|
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
|
WORKDIR /src
|
|
|
|
COPY backend/$SLNX_NAME backend/
|
|
COPY backend/src/$API_NAME/$API_NAME.csproj backend/src/$API_NAME/
|
|
COPY backend/tests/$TEST_NAME/$TEST_NAME.csproj backend/tests/$TEST_NAME/
|
|
|
|
RUN dotnet restore backend/$SLNX_NAME
|
|
|
|
COPY backend/ backend/
|
|
|
|
RUN dotnet publish backend/src/$API_NAME/$API_NAME.csproj \\
|
|
-c Release \\
|
|
-o /app/publish \\
|
|
--no-restore
|
|
|
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
|
WORKDIR /app
|
|
COPY --from=build /app/publish .
|
|
|
|
ENV ASPNETCORE_URLS=http://0.0.0.0:8080
|
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
|
|
|
EXPOSE 8080
|
|
ENTRYPOINT ["dotnet", "$API_NAME.dll"]
|
|
EOF
|
|
|
|
cd ..
|
|
|
|
########################################
|
|
# Frontend: Vue + TypeScript + OpenAPI
|
|
########################################
|
|
npm create vite@latest frontend -- --template vue-ts --no-interactive
|
|
|
|
cd frontend
|
|
|
|
npm pkg set scripts.dev="vite --host 0.0.0.0 --port $FRONTEND_PORT"
|
|
npm pkg set scripts.preview="vite preview --host 0.0.0.0 --port 4173"
|
|
npm pkg set scripts.api:schema="node scripts/fetch-openapi.mjs"
|
|
npm pkg set scripts.api:types="openapi-typescript ../shared/openapi/openapi.json -o src/api/schema.d.ts"
|
|
npm pkg set scripts.api:update="npm run api:schema && npm run api:types"
|
|
|
|
npm pkg set devDependencies.typescript="~5.9.3"
|
|
npm pkg set devDependencies.openapi-typescript="7.13.0"
|
|
npm pkg set dependencies.openapi-fetch="0.17.0"
|
|
|
|
mkdir -p src/app src/pages src/features/health/components src/components src/layouts src/router src/stores src/api scripts
|
|
|
|
rm -f src/main.ts
|
|
|
|
cat > src/app/main.ts <<'EOF'
|
|
import { createApp } from 'vue'
|
|
import App from '../App.vue'
|
|
|
|
createApp(App).mount('#app')
|
|
EOF
|
|
|
|
sed -i 's|/src/main.ts|/src/app/main.ts|g' index.html
|
|
|
|
cat > src/App.vue <<'EOF'
|
|
<script setup lang="ts">
|
|
import HomePage from './pages/HomePage.vue'
|
|
</script>
|
|
|
|
<template>
|
|
<HomePage />
|
|
</template>
|
|
EOF
|
|
|
|
cat > src/pages/HomePage.vue <<'EOF'
|
|
<script setup lang="ts">
|
|
import HealthPanel from '../features/health/components/HealthPanel.vue'
|
|
</script>
|
|
|
|
<template>
|
|
<main class="page">
|
|
<section class="hero">
|
|
<h1>Project is ready</h1>
|
|
<p>Vue + TypeScript frontend, .NET backend, OpenAPI sync, Docker Compose, Caddy, and agentic workflow docs.</p>
|
|
</section>
|
|
|
|
<HealthPanel />
|
|
</main>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.page {
|
|
max-width: 960px;
|
|
margin: 0 auto;
|
|
padding: 3rem 1.5rem;
|
|
font-family: system-ui, sans-serif;
|
|
}
|
|
|
|
.hero {
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.hero h1 {
|
|
margin: 0 0 0.5rem;
|
|
font-size: 2.5rem;
|
|
}
|
|
|
|
.hero p {
|
|
margin: 0;
|
|
color: #555;
|
|
}
|
|
</style>
|
|
EOF
|
|
|
|
cat > src/features/health/types.ts <<'EOF'
|
|
export type HealthStatus = {
|
|
status: string
|
|
serverTimeUtc: string
|
|
}
|
|
EOF
|
|
|
|
cat > src/features/health/api.ts <<EOF
|
|
import { api } from '../../api/client'
|
|
|
|
export async function getHealth() {
|
|
const { data, error } = await api.GET('/api/health')
|
|
|
|
if (error) {
|
|
throw error
|
|
}
|
|
|
|
return data
|
|
}
|
|
EOF
|
|
|
|
cat > src/features/health/components/HealthPanel.vue <<'EOF'
|
|
<script setup lang="ts">
|
|
import { onMounted, ref } from 'vue'
|
|
import { getHealth } from '../api'
|
|
|
|
const loading = ref(true)
|
|
const error = ref<string | null>(null)
|
|
const status = ref<string | null>(null)
|
|
const serverTimeUtc = ref<string | null>(null)
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const result = await getHealth()
|
|
status.value = result?.status ?? null
|
|
serverTimeUtc.value = result?.serverTimeUtc ?? null
|
|
} catch (err) {
|
|
error.value = err instanceof Error ? err.message : 'Failed to call backend'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<section class="panel">
|
|
<h2>Backend health</h2>
|
|
|
|
<p v-if="loading">Checking backend...</p>
|
|
<p v-else-if="error" class="error">{{ error }}</p>
|
|
|
|
<dl v-else>
|
|
<div>
|
|
<dt>Status</dt>
|
|
<dd>{{ status }}</dd>
|
|
</div>
|
|
|
|
<div>
|
|
<dt>Server UTC time</dt>
|
|
<dd>{{ serverTimeUtc }}</dd>
|
|
</div>
|
|
</dl>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.panel {
|
|
border: 1px solid #ddd;
|
|
border-radius: 12px;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.panel h2 {
|
|
margin-top: 0;
|
|
}
|
|
|
|
dl {
|
|
display: grid;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
dt {
|
|
font-weight: 700;
|
|
}
|
|
|
|
dd {
|
|
margin: 0.25rem 0 0;
|
|
}
|
|
|
|
.error {
|
|
color: #b00020;
|
|
}
|
|
</style>
|
|
EOF
|
|
|
|
cat > src/api/client.ts <<EOF
|
|
import createClient from 'openapi-fetch'
|
|
import type { paths } from './schema'
|
|
|
|
export const api = createClient<paths>({
|
|
baseUrl: import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:$BACKEND_PORT',
|
|
})
|
|
EOF
|
|
|
|
cat > src/api/schema.d.ts <<'EOF'
|
|
export interface paths {
|
|
'/api/health': {
|
|
get: {
|
|
responses: {
|
|
200: {
|
|
content: {
|
|
'application/json': {
|
|
status: string
|
|
serverTimeUtc: string
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
EOF
|
|
|
|
cat > scripts/fetch-openapi.mjs <<EOF
|
|
import { writeFile, mkdir } from 'node:fs/promises';
|
|
|
|
const url = process.env.OPENAPI_URL ?? 'http://localhost:$BACKEND_PORT/openapi/v1.json';
|
|
const output = new URL('../../shared/openapi/openapi.json', import.meta.url);
|
|
|
|
console.log(\`Fetching OpenAPI schema from \${url}\`);
|
|
|
|
const response = await fetch(url);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(\`Failed to fetch OpenAPI schema: \${response.status} \${response.statusText}\`);
|
|
}
|
|
|
|
const schema = await response.text();
|
|
|
|
await mkdir(new URL('../../shared/openapi', import.meta.url), { recursive: true });
|
|
await writeFile(output, schema);
|
|
|
|
console.log(\`Wrote \${output.pathname}\`);
|
|
EOF
|
|
|
|
cat > vite.config.ts <<EOF
|
|
import { defineConfig } from 'vite'
|
|
import vue from '@vitejs/plugin-vue'
|
|
|
|
export default defineConfig({
|
|
plugins: [vue()],
|
|
server: {
|
|
host: '0.0.0.0',
|
|
port: $FRONTEND_PORT,
|
|
strictPort: false,
|
|
proxy: {
|
|
'/api': {
|
|
target: 'http://localhost:$BACKEND_PORT',
|
|
changeOrigin: true,
|
|
},
|
|
},
|
|
},
|
|
preview: {
|
|
host: '0.0.0.0',
|
|
},
|
|
})
|
|
EOF
|
|
|
|
cat > Dockerfile <<EOF
|
|
FROM node:22-alpine AS build
|
|
WORKDIR /app
|
|
|
|
COPY frontend/package*.json ./
|
|
RUN npm ci
|
|
|
|
COPY frontend/ ./
|
|
RUN npm run build
|
|
|
|
FROM caddy:2-alpine AS runtime
|
|
COPY --from=build /app/dist /srv
|
|
EOF
|
|
|
|
if [[ "$NO_INSTALL" == "false" ]]; then
|
|
npm install
|
|
fi
|
|
|
|
cd ..
|
|
|
|
########################################
|
|
# Root scripts
|
|
########################################
|
|
cat > scripts/dev-backend.sh <<EOF
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
dotnet run --project backend/src/$API_NAME/$API_NAME.csproj --urls http://0.0.0.0:$BACKEND_PORT
|
|
EOF
|
|
|
|
cat > scripts/dev-frontend.sh <<EOF
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
cd frontend
|
|
npm run dev
|
|
EOF
|
|
|
|
cat > scripts/update-openapi.sh <<EOF
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
cd frontend
|
|
npm run api:update
|
|
EOF
|
|
|
|
chmod +x scripts/*.sh
|
|
|
|
########################################
|
|
# Docker Compose + Caddy
|
|
########################################
|
|
cat > docker-compose.yml <<EOF
|
|
services:
|
|
api:
|
|
build:
|
|
context: .
|
|
dockerfile: backend/src/$API_NAME/Dockerfile
|
|
environment:
|
|
ASPNETCORE_ENVIRONMENT: Production
|
|
ASPNETCORE_URLS: http://0.0.0.0:8080
|
|
expose:
|
|
- "8080"
|
|
|
|
web:
|
|
build:
|
|
context: .
|
|
dockerfile: frontend/Dockerfile
|
|
depends_on:
|
|
- api
|
|
ports:
|
|
- "$COMPOSE_PORT:80"
|
|
volumes:
|
|
- ./deploy/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
|
EOF
|
|
|
|
cat > deploy/caddy/Caddyfile <<'EOF'
|
|
:80 {
|
|
root * /srv
|
|
encode gzip zstd
|
|
|
|
handle /api/* {
|
|
reverse_proxy api:8080
|
|
}
|
|
|
|
handle {
|
|
try_files {path} /index.html
|
|
file_server
|
|
}
|
|
}
|
|
EOF
|
|
|
|
########################################
|
|
# Docs + Agentic workflow
|
|
########################################
|
|
mkdir -p docs/FEATURES docs/TASKS docs/PROMPTS docs/DECISIONS
|
|
|
|
cat > README.md <<EOF
|
|
# $PASCAL
|
|
|
|
Monorepo with:
|
|
|
|
- Backend: .NET 10 Web API
|
|
- Frontend: Vue 3 + TypeScript + Vite
|
|
- API contract: OpenAPI generated from backend, TypeScript generated for frontend
|
|
- Deployment: Docker Compose + Caddy
|
|
- Agentic workflow docs for Codex, Claude, ChatGPT, and other coding agents
|
|
|
|
## Local development
|
|
|
|
Terminal 1:
|
|
|
|
\`\`\`bash
|
|
./scripts/dev-backend.sh
|
|
\`\`\`
|
|
|
|
Terminal 2:
|
|
|
|
\`\`\`bash
|
|
./scripts/dev-frontend.sh
|
|
\`\`\`
|
|
|
|
Frontend:
|
|
|
|
\`\`\`txt
|
|
http://localhost:$FRONTEND_PORT
|
|
http://<this-machine-lan-ip>:$FRONTEND_PORT
|
|
\`\`\`
|
|
|
|
Backend:
|
|
|
|
\`\`\`txt
|
|
http://localhost:$BACKEND_PORT
|
|
http://<this-machine-lan-ip>:$BACKEND_PORT
|
|
\`\`\`
|
|
|
|
## Update frontend API types from backend
|
|
|
|
The backend must be running first.
|
|
|
|
\`\`\`bash
|
|
./scripts/update-openapi.sh
|
|
\`\`\`
|
|
|
|
This writes:
|
|
|
|
\`\`\`txt
|
|
shared/openapi/openapi.json
|
|
frontend/src/api/schema.d.ts
|
|
\`\`\`
|
|
|
|
## Docker Compose
|
|
|
|
\`\`\`bash
|
|
docker compose up --build
|
|
\`\`\`
|
|
|
|
Then open:
|
|
|
|
\`\`\`txt
|
|
http://localhost:$COMPOSE_PORT
|
|
http://<this-machine-lan-ip>:$COMPOSE_PORT
|
|
\`\`\`
|
|
|
|
## Solution
|
|
|
|
\`\`\`bash
|
|
dotnet build backend/$SLNX_NAME
|
|
dotnet test backend/$SLNX_NAME
|
|
\`\`\`
|
|
|
|
## Agentic workflow
|
|
|
|
Start here:
|
|
|
|
\`\`\`txt
|
|
docs/AGENTIC_WORKFLOW.md
|
|
\`\`\`
|
|
|
|
Use feature specs, task files, and prompt templates instead of asking agents to work from vague chat history.
|
|
EOF
|
|
|
|
cat > docs/ARCHITECTURE.md <<EOF
|
|
# Architecture
|
|
|
|
## Backend
|
|
|
|
\`\`\`txt
|
|
backend/
|
|
├─ $SLNX_NAME
|
|
├─ src/$API_NAME/
|
|
│ ├─ Contracts/
|
|
│ ├─ Domain/
|
|
│ ├─ Endpoints/
|
|
│ └─ Infrastructure/
|
|
└─ tests/$TEST_NAME/
|
|
\`\`\`
|
|
|
|
The backend intentionally starts with one API project plus one test project.
|
|
|
|
Split into more projects only when the pressure is real:
|
|
|
|
- domain logic becomes hard to isolate
|
|
- infrastructure dependencies pollute the API project
|
|
- tests need clearer boundaries
|
|
- multiple apps need shared backend code
|
|
|
|
## Frontend
|
|
|
|
\`\`\`txt
|
|
frontend/src/
|
|
├─ api/
|
|
├─ app/
|
|
├─ components/
|
|
├─ features/
|
|
├─ layouts/
|
|
├─ pages/
|
|
├─ router/
|
|
└─ stores/
|
|
\`\`\`
|
|
|
|
## Feature pattern
|
|
|
|
Frontend features should usually look like:
|
|
|
|
\`\`\`txt
|
|
features/example/
|
|
├─ api.ts
|
|
├─ types.ts
|
|
└─ components/
|
|
\`\`\`
|
|
|
|
Backend endpoints should usually look like:
|
|
|
|
\`\`\`txt
|
|
Endpoints/Example/
|
|
Contracts/Example/
|
|
\`\`\`
|
|
|
|
## API contract
|
|
|
|
The backend exposes OpenAPI at:
|
|
|
|
\`\`\`txt
|
|
/openapi/v1.json
|
|
\`\`\`
|
|
|
|
The frontend updates its TypeScript API model with:
|
|
|
|
\`\`\`bash
|
|
./scripts/update-openapi.sh
|
|
\`\`\`
|
|
|
|
## Contract rule
|
|
|
|
Do not manually duplicate backend DTOs into the frontend.
|
|
|
|
Flow:
|
|
|
|
\`\`\`txt
|
|
Backend contracts -> OpenAPI -> frontend TypeScript types
|
|
\`\`\`
|
|
EOF
|
|
|
|
cat > docs/DEVELOPMENT_WORKFLOW.md <<EOF
|
|
# Development Workflow
|
|
|
|
## Before coding
|
|
|
|
Read:
|
|
|
|
1. README.md
|
|
2. AGENTS.md
|
|
3. docs/ARCHITECTURE.md
|
|
4. docs/AGENTIC_WORKFLOW.md
|
|
5. docs/DEVELOPMENT_WORKFLOW.md
|
|
6. Relevant feature spec in docs/FEATURES/
|
|
7. Relevant task file in docs/TASKS/
|
|
|
|
## Backend
|
|
|
|
\`\`\`bash
|
|
./scripts/dev-backend.sh
|
|
\`\`\`
|
|
|
|
## Frontend
|
|
|
|
\`\`\`bash
|
|
./scripts/dev-frontend.sh
|
|
\`\`\`
|
|
|
|
## API model sync
|
|
|
|
When backend request/response models change:
|
|
|
|
\`\`\`bash
|
|
./scripts/update-openapi.sh
|
|
\`\`\`
|
|
|
|
Then check the frontend TypeScript errors.
|
|
|
|
## Recommended first commit
|
|
|
|
After verifying the scaffold:
|
|
|
|
\`\`\`bash
|
|
git init
|
|
git add .
|
|
git commit -m "chore: bootstrap project"
|
|
\`\`\`
|
|
EOF
|
|
|
|
cat > docs/AGENTIC_WORKFLOW.md <<'EOF'
|
|
# Agentic Workflow
|
|
|
|
This repository is designed to be worked on by humans and coding agents.
|
|
|
|
The goal is to avoid the common failure mode where the first AI prompt creates a lot of code and every later prompt degrades because the agent loses architectural context.
|
|
|
|
## Core rule
|
|
|
|
Agents do not work from vague chat history.
|
|
|
|
Agents work from repository files:
|
|
|
|
1. `AGENTS.md`
|
|
2. `docs/ARCHITECTURE.md`
|
|
3. `docs/DEVELOPMENT_WORKFLOW.md`
|
|
4. `docs/FEATURES/<feature>.md`
|
|
5. `docs/TASKS/<feature>/<task>.md`
|
|
6. `docs/PROMPTS/<prompt-template>.md`
|
|
|
|
## Workflow loop
|
|
|
|
For every meaningful change:
|
|
|
|
1. Define or update a feature spec in `docs/FEATURES/`
|
|
2. Create a small task in `docs/TASKS/<feature>/`
|
|
3. Use the correct prompt template from `docs/PROMPTS/`
|
|
4. Ask the agent to implement only that task
|
|
5. Review the diff
|
|
6. Run tests/builds
|
|
7. Update OpenAPI when backend contracts change
|
|
8. Commit the task separately
|
|
|
|
## Unit of work
|
|
|
|
A task should usually be small enough to finish in one agent session.
|
|
|
|
Good task examples:
|
|
|
|
- Add `POST /api/auth/login`
|
|
- Add `LoginPage.vue`
|
|
- Add typed API wrapper for auth
|
|
- Add validation for registration email
|
|
- Add integration test for health endpoint
|
|
|
|
Bad task examples:
|
|
|
|
- Build auth
|
|
- Make the whole SaaS
|
|
- Redesign the app
|
|
- Add billing and dashboard and admin
|
|
|
|
## Backend task pattern
|
|
|
|
Backend changes should generally touch:
|
|
|
|
```txt
|
|
backend/src/<App>.Api/Endpoints/<Feature>/
|
|
backend/src/<App>.Api/Contracts/<Feature>/
|
|
backend/tests/<App>.Tests/
|
|
```
|
|
|
|
Do not create new backend projects unless there is a clear architectural reason.
|
|
|
|
## Frontend task pattern
|
|
|
|
Frontend changes should generally touch:
|
|
|
|
```txt
|
|
frontend/src/features/<feature>/
|
|
frontend/src/pages/
|
|
frontend/src/api/
|
|
```
|
|
|
|
Prefer feature folders over global folders.
|
|
|
|
## Contract workflow
|
|
|
|
When backend endpoints or contracts change:
|
|
|
|
```bash
|
|
./scripts/dev-backend.sh
|
|
./scripts/update-openapi.sh
|
|
```
|
|
|
|
Then check frontend TypeScript errors and fix affected frontend code.
|
|
|
|
## Agent guardrails
|
|
|
|
Agents must not:
|
|
|
|
- invent architecture
|
|
- perform broad unrelated refactors
|
|
- manually duplicate generated API types
|
|
- ignore existing docs
|
|
- change deployment defaults without updating docs
|
|
- silently replace the chosen backend/frontend structure
|
|
|
|
Agents should:
|
|
|
|
- make small changes
|
|
- update feature specs when behavior changes
|
|
- update task files if implementation scope changes
|
|
- run or list validation commands
|
|
EOF
|
|
|
|
cat > docs/FEATURE_TEMPLATE.md <<'EOF'
|
|
# Feature: <Feature Name>
|
|
|
|
## Status
|
|
|
|
Draft | In Progress | Complete
|
|
|
|
## Goal
|
|
|
|
Describe the user-facing outcome.
|
|
|
|
## User stories
|
|
|
|
- As a <user>, I want <capability>, so that <benefit>.
|
|
|
|
## Backend
|
|
|
|
### Endpoints
|
|
|
|
- `GET /api/...`
|
|
- `POST /api/...`
|
|
|
|
### Contracts
|
|
|
|
- `<RequestName>`
|
|
- `<ResponseName>`
|
|
|
|
### Domain rules
|
|
|
|
- Rule 1
|
|
- Rule 2
|
|
|
|
## Frontend
|
|
|
|
### Pages
|
|
|
|
- `<PageName>`
|
|
|
|
### Components
|
|
|
|
- `<ComponentName>`
|
|
|
|
### State
|
|
|
|
- Local component state
|
|
- Store state, if needed
|
|
|
|
## API contract
|
|
|
|
Describe what needs to appear in OpenAPI.
|
|
|
|
## Done when
|
|
|
|
- [ ] Backend endpoint implemented
|
|
- [ ] Backend tests added
|
|
- [ ] OpenAPI updated
|
|
- [ ] Frontend calls generated API types/client
|
|
- [ ] Frontend UI implemented
|
|
- [ ] Build/test passes
|
|
|
|
## Notes
|
|
|
|
Architectural notes, edge cases, and decisions.
|
|
EOF
|
|
|
|
cat > docs/TASK_TEMPLATE.md <<'EOF'
|
|
# Task: <Task Name>
|
|
|
|
## Feature
|
|
|
|
`docs/FEATURES/<feature>.md`
|
|
|
|
## Goal
|
|
|
|
One concrete change.
|
|
|
|
## Context
|
|
|
|
What the agent needs to know before coding.
|
|
|
|
## Files likely to change
|
|
|
|
- `backend/src/...`
|
|
- `frontend/src/...`
|
|
|
|
## Constraints
|
|
|
|
- Follow existing architecture.
|
|
- Do not refactor unrelated files.
|
|
- Do not manually duplicate generated API types.
|
|
- Keep the diff focused.
|
|
|
|
## Implementation steps
|
|
|
|
1. Step one
|
|
2. Step two
|
|
3. Step three
|
|
|
|
## Done when
|
|
|
|
- [ ] Code implemented
|
|
- [ ] Tests/build pass
|
|
- [ ] OpenAPI regenerated if backend contracts changed
|
|
- [ ] Docs updated if behavior changed
|
|
|
|
## Validation commands
|
|
|
|
```bash
|
|
dotnet test backend/*.slnx
|
|
cd frontend && npm run build
|
|
```
|
|
EOF
|
|
|
|
cat > docs/PROMPT_TEMPLATE.md <<'EOF'
|
|
# General Agent Prompt Template
|
|
|
|
You are working in this repository.
|
|
|
|
## Read first
|
|
|
|
- `AGENTS.md`
|
|
- `README.md`
|
|
- `docs/ARCHITECTURE.md`
|
|
- `docs/DEVELOPMENT_WORKFLOW.md`
|
|
- `docs/AGENTIC_WORKFLOW.md`
|
|
- `<feature spec>`
|
|
- `<task file>`
|
|
|
|
## Task
|
|
|
|
Implement:
|
|
|
|
`<paste task title or path>`
|
|
|
|
## Rules
|
|
|
|
- Implement only this task.
|
|
- Do not refactor unrelated code.
|
|
- Follow existing backend/frontend structure.
|
|
- Backend feature code goes under `Endpoints/<Feature>` and `Contracts/<Feature>`.
|
|
- Frontend feature code goes under `frontend/src/features/<feature>`.
|
|
- If backend contracts change, update OpenAPI using `./scripts/update-openapi.sh`.
|
|
- Do not manually duplicate generated API types.
|
|
|
|
## Output expected
|
|
|
|
- Code changes
|
|
- Tests where appropriate
|
|
- Minimal docs update if behavior changed
|
|
- Summary of what changed
|
|
- Validation commands run
|
|
EOF
|
|
|
|
cat > docs/PROMPTS/backend-task.md <<'EOF'
|
|
# Backend Task Prompt
|
|
|
|
You are implementing a backend task in this repository.
|
|
|
|
## Read first
|
|
|
|
- `AGENTS.md`
|
|
- `docs/ARCHITECTURE.md`
|
|
- `docs/DEVELOPMENT_WORKFLOW.md`
|
|
- `docs/AGENTIC_WORKFLOW.md`
|
|
- Relevant feature spec in `docs/FEATURES/`
|
|
- Relevant task file in `docs/TASKS/`
|
|
|
|
## Instructions
|
|
|
|
- Implement only the requested backend task.
|
|
- Use the existing one-project backend structure.
|
|
- Put endpoints under `Endpoints/<Feature>/`.
|
|
- Put request/response contracts under `Contracts/<Feature>/`.
|
|
- Add or update tests under `backend/tests/`.
|
|
- Do not introduce repository/service layers unless the task explicitly requires it.
|
|
- Do not split the backend into multiple projects.
|
|
|
|
## After coding
|
|
|
|
Run or recommend:
|
|
|
|
```bash
|
|
dotnet build backend/*.slnx
|
|
dotnet test backend/*.slnx
|
|
./scripts/update-openapi.sh
|
|
```
|
|
EOF
|
|
|
|
cat > docs/PROMPTS/frontend-task.md <<'EOF'
|
|
# Frontend Task Prompt
|
|
|
|
You are implementing a frontend task in this repository.
|
|
|
|
## Read first
|
|
|
|
- `AGENTS.md`
|
|
- `docs/ARCHITECTURE.md`
|
|
- `docs/DEVELOPMENT_WORKFLOW.md`
|
|
- `docs/AGENTIC_WORKFLOW.md`
|
|
- Relevant feature spec in `docs/FEATURES/`
|
|
- Relevant task file in `docs/TASKS/`
|
|
|
|
## Instructions
|
|
|
|
- Implement only the requested frontend task.
|
|
- Put feature code under `frontend/src/features/<feature>/`.
|
|
- Put route-level pages under `frontend/src/pages/`.
|
|
- Use generated API types from `frontend/src/api/schema.d.ts`.
|
|
- Do not manually recreate backend DTOs.
|
|
- Do not create global services unless the feature truly needs shared behavior.
|
|
|
|
## After coding
|
|
|
|
Run or recommend:
|
|
|
|
```bash
|
|
cd frontend
|
|
npm run build
|
|
```
|
|
EOF
|
|
|
|
cat > docs/PROMPTS/debug.md <<'EOF'
|
|
# Debug Prompt
|
|
|
|
You are debugging a specific failure in this repository.
|
|
|
|
## Read first
|
|
|
|
- `AGENTS.md`
|
|
- `docs/ARCHITECTURE.md`
|
|
- `docs/DEVELOPMENT_WORKFLOW.md`
|
|
|
|
## Error
|
|
|
|
Paste the full error here.
|
|
|
|
## Instructions
|
|
|
|
- Identify the most likely root cause.
|
|
- Make the smallest safe fix.
|
|
- Do not refactor unrelated code.
|
|
- Explain how to verify the fix.
|
|
|
|
## Validation
|
|
|
|
List the exact command(s) to run after the fix.
|
|
EOF
|
|
|
|
cat > docs/PROMPTS/refactor.md <<'EOF'
|
|
# Refactor Prompt
|
|
|
|
You are refactoring part of this repository.
|
|
|
|
## Read first
|
|
|
|
- `AGENTS.md`
|
|
- `docs/ARCHITECTURE.md`
|
|
- `docs/AGENTIC_WORKFLOW.md`
|
|
- Relevant feature spec
|
|
|
|
## Scope
|
|
|
|
Describe the exact refactor boundary.
|
|
|
|
## Rules
|
|
|
|
- Preserve behavior.
|
|
- Do not add features.
|
|
- Keep the diff focused.
|
|
- Update docs if structure or workflow changes.
|
|
- Run tests/builds after.
|
|
|
|
## Validation
|
|
|
|
```bash
|
|
dotnet test backend/*.slnx
|
|
cd frontend && npm run build
|
|
```
|
|
EOF
|
|
|
|
cat > docs/FEATURES/health.md <<'EOF'
|
|
# Feature: Health
|
|
|
|
## Status
|
|
|
|
Complete
|
|
|
|
## Goal
|
|
|
|
Verify that the frontend can call the backend through the generated OpenAPI TypeScript contract.
|
|
|
|
## Backend
|
|
|
|
### Endpoints
|
|
|
|
- `GET /api/health`
|
|
- `GET /api/hello`
|
|
|
|
### Contracts
|
|
|
|
- `HealthResponse`
|
|
- `HelloResponse`
|
|
|
|
## Frontend
|
|
|
|
### Components
|
|
|
|
- `HealthPanel.vue`
|
|
|
|
### API
|
|
|
|
- `features/health/api.ts`
|
|
|
|
## Done when
|
|
|
|
- [x] Backend exposes health endpoint
|
|
- [x] Frontend calls health endpoint
|
|
- [x] OpenAPI can generate frontend types
|
|
EOF
|
|
|
|
mkdir -p docs/TASKS/health
|
|
|
|
cat > docs/TASKS/health/001-verify-health-slice.md <<'EOF'
|
|
# Task: Verify health vertical slice
|
|
|
|
## Feature
|
|
|
|
`docs/FEATURES/health.md`
|
|
|
|
## Goal
|
|
|
|
Verify the generated project works end-to-end.
|
|
|
|
## Steps
|
|
|
|
1. Run the backend.
|
|
2. Run the frontend.
|
|
3. Open the frontend from localhost and optionally from another LAN machine.
|
|
4. Regenerate OpenAPI types.
|
|
5. Build/test.
|
|
|
|
## Validation commands
|
|
|
|
```bash
|
|
./scripts/dev-backend.sh
|
|
./scripts/dev-frontend.sh
|
|
./scripts/update-openapi.sh
|
|
dotnet test backend/*.slnx
|
|
cd frontend && npm run build
|
|
```
|
|
|
|
## Done when
|
|
|
|
- [ ] Health panel shows backend status
|
|
- [ ] OpenAPI types regenerate successfully
|
|
- [ ] Backend tests pass
|
|
- [ ] Frontend build passes
|
|
EOF
|
|
|
|
cat > docs/DECISIONS/0001-use-simple-dotnet-vue-monorepo.md <<EOF
|
|
# Decision 0001: Use simple .NET + Vue monorepo
|
|
|
|
## Status
|
|
|
|
Accepted
|
|
|
|
## Context
|
|
|
|
The project needs to move fast while remaining understandable to humans and coding agents.
|
|
|
|
## Decision
|
|
|
|
Use:
|
|
|
|
- one .NET API project
|
|
- one .NET test project
|
|
- one Vue TypeScript frontend
|
|
- OpenAPI as the backend/frontend contract
|
|
- feature-first frontend folders
|
|
- endpoint/contract backend folders
|
|
|
|
## Consequences
|
|
|
|
Good:
|
|
|
|
- low ceremony
|
|
- easy for agents to understand
|
|
- easy to split later if real pressure appears
|
|
|
|
Tradeoff:
|
|
|
|
- the API project may grow large over time
|
|
|
|
## Revisit when
|
|
|
|
- multiple backend apps need shared code
|
|
- infrastructure dependencies become hard to manage
|
|
- domain logic becomes too large for one project
|
|
EOF
|
|
|
|
cat > AGENTS.md <<EOF
|
|
# AGENTS
|
|
|
|
This repository is designed for human + AI agent collaboration.
|
|
|
|
## Read order
|
|
|
|
Before making code changes, read:
|
|
|
|
1. README.md
|
|
2. docs/AGENTIC_WORKFLOW.md
|
|
3. docs/ARCHITECTURE.md
|
|
4. docs/DEVELOPMENT_WORKFLOW.md
|
|
5. Relevant file in docs/FEATURES/
|
|
6. Relevant file in docs/TASKS/
|
|
|
|
## Core rules
|
|
|
|
- Do not invent architecture.
|
|
- Keep the backend simple unless a task explicitly asks for a split.
|
|
- Backend default is one API project plus one test project.
|
|
- The solution file is \`backend/$SLNX_NAME\`.
|
|
- Backend feature code goes under \`Endpoints/<Feature>\` and \`Contracts/<Feature>\`.
|
|
- Frontend feature code goes under \`frontend/src/features/<feature>\`.
|
|
- Frontend API types are generated from backend OpenAPI.
|
|
- Do not manually duplicate backend DTOs into frontend code.
|
|
- If backend DTOs or endpoints change, run \`./scripts/update-openapi.sh\`.
|
|
- Dev servers must use HTTP and bind to \`0.0.0.0\` for LAN access.
|
|
- Avoid broad refactors unless the task explicitly asks for one.
|
|
|
|
## Task discipline
|
|
|
|
Agents should work from task files in \`docs/TASKS/\`.
|
|
|
|
A good task:
|
|
|
|
- has a clear goal
|
|
- names the relevant feature
|
|
- has a small scope
|
|
- lists validation commands
|
|
|
|
If no task exists, create one before implementing a meaningful feature.
|
|
|
|
## Validation
|
|
|
|
For backend changes:
|
|
|
|
\`\`\`bash
|
|
dotnet build backend/$SLNX_NAME
|
|
dotnet test backend/$SLNX_NAME
|
|
\`\`\`
|
|
|
|
For frontend changes:
|
|
|
|
\`\`\`bash
|
|
cd frontend
|
|
npm run build
|
|
\`\`\`
|
|
|
|
For contract changes:
|
|
|
|
\`\`\`bash
|
|
./scripts/update-openapi.sh
|
|
\`\`\`
|
|
EOF
|
|
|
|
cat > .gitignore <<'EOF'
|
|
# .NET
|
|
bin/
|
|
obj/
|
|
TestResults/
|
|
*.user
|
|
*.suo
|
|
|
|
# Node
|
|
node_modules/
|
|
dist/
|
|
.vite/
|
|
|
|
# Env
|
|
.env
|
|
.env.*
|
|
|
|
# IDE
|
|
.vscode/
|
|
.idea/
|
|
|
|
# OS
|
|
.DS_Store
|
|
Thumbs.db
|
|
EOF
|
|
|
|
echo
|
|
echo "Bootstrap complete."
|
|
echo
|
|
echo "Next:"
|
|
echo " cd $PROJECT_DIR"
|
|
echo " ./scripts/dev-backend.sh"
|
|
echo " ./scripts/dev-frontend.sh"
|
|
echo
|
|
echo "After backend is running:"
|
|
echo " ./scripts/update-openapi.sh"
|
|
echo
|
|
echo "Agentic workflow:"
|
|
echo " docs/AGENTIC_WORKFLOW.md"
|