diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..8955e7721 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,19 @@ +{ + "worktree": { + "baseRef": "fresh", + "symlinkDirectories": ["node_modules"] + }, + "hooks": { + "PostToolUse": [ + { + "matcher": "EnterWorktree", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR/scripts/assign-worktree-port.sh\"" + } + ] + } + ] + } +} diff --git a/.claude/skills/fix-issue/SKILL.md b/.claude/skills/fix-issue/SKILL.md new file mode 100644 index 000000000..524c490ee --- /dev/null +++ b/.claude/skills/fix-issue/SKILL.md @@ -0,0 +1,39 @@ +--- +name: fix-issue +description: Implement a GitHub issue with reproduction and verification +allowed-tools: Bash, Edit, Write, Read, Glob, Grep, mcp__playwright__*, mcp__dokploy__* +--- + +The issue number is passed as $1. An isolated instance is running at $DOKPLOY_BASE_URL. + +## Tools + +- `mcp__dokploy__*` — the Dokploy API of the running instance. Use it to set up + state (create a project, an app, an env var) and to verify backend behavior. + Search for the tool you need; they are not all loaded upfront. +- `mcp__playwright__*` — the browser at $DOKPLOY_BASE_URL. Use it for anything + a user would see or click. + +Pick by where the bug lives, not by convenience: + +- Bug in the UI (rendering, forms, navigation, state) → reproduce in Playwright. + The API returning correct data proves nothing here. +- Bug in the API, deploy logic, or data → reproduce with the Dokploy MCP. + A green screenshot proves nothing here. +- Unclear → do both. + +Use the MCP to reach the state you need quickly, then verify in the UI. Do not +click through ten screens to create a project the API can create in one call. + +## Steps + +1. Run `gh issue view $1` and read the full issue, including comments. +2. Reproduce the bug with the appropriate tool. If you cannot reproduce it, + comment on the issue explaining what you tried and STOP. + Do not implement anything. +3. Implement the fix. Keep the change minimal and scoped to the issue. +4. Run `pnpm test`, then re-run the same reproduction from step 2. +5. Only if both pass: commit and run `gh pr create`. The PR description must + include the before/after reproduction steps and reference the issue. + +Never skip step 2. A fix you cannot reproduce and then verify is not a fix. \ No newline at end of file diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 000000000..c195f5d73 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,13 @@ +{ + "mcpServers": { + "dokploy": { + "type": "http", + "url": "${DOKPLOY_BASE_URL}/api/mcp", + "headers": { "Authorization": "Bearer ${DOKPLOY_TOKEN}" } + }, + "playwright": { + "command": "npx", + "args": ["-y", "@playwright/mcp@latest", "--headless", "--isolated"] + } + } +} diff --git a/.worktreeinclude b/.worktreeinclude new file mode 100644 index 000000000..e3d34d2fb --- /dev/null +++ b/.worktreeinclude @@ -0,0 +1,2 @@ +.env +.env.local \ No newline at end of file diff --git a/scripts/assign-worktree-port.sh b/scripts/assign-worktree-port.sh new file mode 100755 index 000000000..0bd544db9 --- /dev/null +++ b/scripts/assign-worktree-port.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +PAYLOAD=$(cat) +WORKTREE_PATH=$(echo "$PAYLOAD" | jq -r '.tool_response.worktreePath // empty') + +if [ -z "$WORKTREE_PATH" ]; then + exit 0 +fi + +ENV_FILE="$WORKTREE_PATH/apps/dokploy/.env" +if [ ! -f "$ENV_FILE" ]; then + exit 0 +fi + +FREE_PORT=$(node "$CLAUDE_PROJECT_DIR/scripts/find-free-port.mjs") + +sed -i.bak "s/^PORT=.*/PORT=$FREE_PORT/" "$ENV_FILE" +sed -i.bak -E "s#^(BETTER_AUTH_URL=https?://[^:/]+):[0-9]+#\1:$FREE_PORT#" "$ENV_FILE" +rm -f "$ENV_FILE.bak" + +echo "Worktree $WORKTREE_PATH -> dokploy dev PORT=$FREE_PORT" diff --git a/scripts/find-free-port.mjs b/scripts/find-free-port.mjs new file mode 100644 index 000000000..2e647f504 --- /dev/null +++ b/scripts/find-free-port.mjs @@ -0,0 +1,23 @@ +import { createServer } from "node:net"; + +function isFree(port) { + return new Promise((resolve) => { + const server = createServer(); + server.once("error", () => resolve(false)); + server.listen(port, "0.0.0.0", () => { + server.close(() => resolve(true)); + }); + }); +} + +async function findFreePort(start) { + let port = start; + while (!(await isFree(port))) { + port++; + } + return port; +} + +const start = Number.parseInt(process.argv[2] || process.env.PORT || "3000", 10); +const port = await findFreePort(start); +process.stdout.write(String(port));