Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 19f1534e42 | |||
| a48d80d160 | |||
| 4cab01697d |
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"fingerprint": "639c16d45210921c3c8ece071ef18bbe0c426ea2"
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Skill Registry — workspace
|
|
||||||
|
|
||||||
<!-- Auto-generated by gentle-pi extensions/skill-registry.ts. Run /skill-registry:refresh to regenerate. -->
|
|
||||||
|
|
||||||
Last updated: 2026-06-05
|
|
||||||
|
|
||||||
## Sources scanned
|
|
||||||
|
|
||||||
- .opencode/skills
|
|
||||||
- .claude/skills
|
|
||||||
|
|
||||||
## Contract
|
|
||||||
|
|
||||||
**Delegator use only.** This registry is an index, not a summary. Any agent that launches subagents reads it to select relevant skills, then passes exact `SKILL.md` paths for the subagent to read before work.
|
|
||||||
|
|
||||||
`SKILL.md` remains the source of truth. Do not inject generated summaries or compact rules by default; pass paths so subagents load the full runtime contract and preserve author intent.
|
|
||||||
|
|
||||||
## Skills
|
|
||||||
|
|
||||||
| Skill | Trigger / description | Scope | Path |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `openspec-apply-change` | Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks. | project | `/workspace/.opencode/skills/openspec-apply-change/SKILL.md` |
|
|
||||||
| `openspec-archive-change` | Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete. | project | `/workspace/.opencode/skills/openspec-archive-change/SKILL.md` |
|
|
||||||
| `openspec-explore` | Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change. | project | `/workspace/.opencode/skills/openspec-explore/SKILL.md` |
|
|
||||||
| `openspec-propose` | Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation. | project | `/workspace/.opencode/skills/openspec-propose/SKILL.md` |
|
|
||||||
| `sift-backlog` | Triage and organize backlog tasks into actionable plans. Use when asked to review the backlog, prioritize tasks, create plans from backlog items, or move tasks from backlog to open status. Handles the full workflow of listing backlog tasks, grouping related tasks into plans, setting priorities and dependencies, activating plans, and changing task status from backlog to open. | project | `/workspace/.claude/skills/sift-backlog/SKILL.md` |
|
|
||||||
|
|
||||||
## Loading protocol
|
|
||||||
|
|
||||||
1. Match task context and target files against the `Trigger / description` column.
|
|
||||||
2. Pass only the matching `Path` values to the subagent under `## Skills to load before work`.
|
|
||||||
3. Instruct the subagent to read those exact `SKILL.md` files before reading, writing, reviewing, testing, or creating artifacts.
|
|
||||||
4. If no matching skill exists, proceed without project skill injection and report `skill_resolution: none`.
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# .claude (index)
|
|
||||||
dir: .claude
|
|
||||||
|
|
||||||
## role
|
|
||||||
Configuration directory for Claude AI assistant integration and custom instructions.
|
|
||||||
## parent
|
|
||||||
index: ./.pi-map.index.md
|
|
||||||
map: ./.pi-map.md
|
|
||||||
## children
|
|
||||||
- .claude/skills
|
|
||||||
index: .claude/skills/.pi-map.index.md
|
|
||||||
map: .claude/skills/.pi-map.md
|
|
||||||
## files
|
|
||||||
## links
|
|
||||||
index: .claude/.pi-map.index.md
|
|
||||||
map: .claude/.pi-map.md
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# .claude
|
|
||||||
dir: .claude
|
|
||||||
|
|
||||||
index: .claude/.pi-map.index.md
|
|
||||||
|
|
||||||
## role
|
|
||||||
Configuration directory for Claude AI assistant integration and custom instructions.
|
|
||||||
## files
|
|
||||||
## arch
|
|
||||||
Project-specific AI tooling configuration using convention-based file organization for assistant context and behavior customization.
|
|
||||||
## tags
|
|
||||||
-
|
|
||||||
## symbols
|
|
||||||
-
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# .claude/skills (index)
|
|
||||||
dir: .claude/skills
|
|
||||||
|
|
||||||
## role
|
|
||||||
Contains reusable AI skill definitions and prompt templates that configure Claude's specialized capabilities for specific development tasks.
|
|
||||||
## parent
|
|
||||||
index: .claude/.pi-map.index.md
|
|
||||||
map: .claude/.pi-map.md
|
|
||||||
## children
|
|
||||||
- .claude/skills/sift-backlog
|
|
||||||
index: .claude/skills/sift-backlog/.pi-map.index.md
|
|
||||||
map: .claude/skills/sift-backlog/.pi-map.md
|
|
||||||
## files
|
|
||||||
## links
|
|
||||||
index: .claude/skills/.pi-map.index.md
|
|
||||||
map: .claude/skills/.pi-map.md
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# .claude/skills
|
|
||||||
dir: .claude/skills
|
|
||||||
|
|
||||||
index: .claude/skills/.pi-map.index.md
|
|
||||||
|
|
||||||
## role
|
|
||||||
Contains reusable AI skill definitions and prompt templates that configure Claude's specialized capabilities for specific development tasks.
|
|
||||||
## files
|
|
||||||
## arch
|
|
||||||
Modular skill-based architecture using declarative configuration files (likely YAML/JSON) to define context-specific behaviors, tool access patterns, and system prompts for different operational modes.
|
|
||||||
## tags
|
|
||||||
-
|
|
||||||
## symbols
|
|
||||||
-
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# .claude/skills/sift-backlog (index)
|
|
||||||
dir: .claude/skills/sift-backlog
|
|
||||||
|
|
||||||
## role
|
|
||||||
Defines a workflow skill for triaging, organizing, and activating backlog tasks into actionable plans using a custom CLI tool.
|
|
||||||
## parent
|
|
||||||
index: .claude/skills/.pi-map.index.md
|
|
||||||
map: .claude/skills/.pi-map.md
|
|
||||||
## children
|
|
||||||
-
|
|
||||||
## files
|
|
||||||
- SKILL.md
|
|
||||||
## links
|
|
||||||
index: .claude/skills/sift-backlog/.pi-map.index.md
|
|
||||||
map: .claude/skills/sift-backlog/.pi-map.md
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# .claude/skills/sift-backlog
|
|
||||||
dir: .claude/skills/sift-backlog
|
|
||||||
|
|
||||||
index: .claude/skills/sift-backlog/.pi-map.index.md
|
|
||||||
|
|
||||||
## role
|
|
||||||
Defines a workflow skill for triaging, organizing, and activating backlog tasks into actionable plans using a custom CLI tool.
|
|
||||||
## files
|
|
||||||
- SKILL.md | Defines a workflow skill for triaging, organizing, and activating backlog tasks into actionable plans using a custom CLI tool. | dep: sf (custom CLI tool), task management system, plan management system
|
|
||||||
## arch
|
|
||||||
Documentation-driven skill definition using structured markdown with command specifications, workflow stages, and integration patterns for Claude CLI tooling.
|
|
||||||
## tags
|
|
||||||
skill, defines, workflow, triaging, organizing, activating, backlog, tasks
|
|
||||||
## symbols
|
|
||||||
-
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
---
|
|
||||||
name: sift-backlog
|
|
||||||
description: Triage and organize backlog tasks into actionable plans. Use when asked to review the backlog, prioritize tasks, create plans from backlog items, or move tasks from backlog to open status. Handles the full workflow of listing backlog tasks, grouping related tasks into plans, setting priorities and dependencies, activating plans, and changing task status from backlog to open.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Sift Backlog
|
|
||||||
|
|
||||||
Triage backlog tasks: prioritize, group into plans, set dependencies, and activate.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
1. List backlog tasks (`sf task backlog`)
|
|
||||||
2. Clarify and enrich each task (titles, descriptions)
|
|
||||||
3. Identify groupings and create draft plans
|
|
||||||
4. Add tasks to plans and set dependencies
|
|
||||||
5. Activate plans
|
|
||||||
6. Set task status to open
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
### Step 1: List Backlog Tasks
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sf task backlog
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Clarify and Enrich Tasks
|
|
||||||
|
|
||||||
Backlog tasks often have only a brief title with no description. Before organizing, ensure each task is well-defined.
|
|
||||||
|
|
||||||
**For each task, evaluate:**
|
|
||||||
|
|
||||||
- Is the title clear and actionable?
|
|
||||||
- Is there a description? Check with `sf task describe <task-id> --show`
|
|
||||||
- Is the scope unambiguous?
|
|
||||||
|
|
||||||
**If the title is unclear**, update it:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sf update <task-id> --title "Clear, actionable title"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Add a description** with context, scope, and acceptance criteria:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sf task describe <task-id> --content "Description with:
|
|
||||||
- What needs to be done
|
|
||||||
- Why it matters
|
|
||||||
- Acceptance criteria
|
|
||||||
- Any relevant context"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Use your best judgment** to interpret tasks and make reasonable decisions about scope, grouping, and priority. You have context about the codebase, project patterns, and typical development practices—leverage this knowledge rather than deferring to the user for routine decisions.
|
|
||||||
|
|
||||||
**Only ask the user for clarity when absolutely necessary:**
|
|
||||||
|
|
||||||
- The task is fundamentally ambiguous (multiple mutually exclusive interpretations)
|
|
||||||
- Critical business logic or user-facing behavior that could go wrong in meaningful ways
|
|
||||||
- External dependencies or integrations you cannot verify
|
|
||||||
|
|
||||||
**Do NOT ask about:**
|
|
||||||
|
|
||||||
- Implementation details you can reasonably infer
|
|
||||||
- Priority or grouping decisions—use your judgment
|
|
||||||
- Standard development practices (testing, code style, etc.)
|
|
||||||
- Tasks where a reasonable interpretation exists
|
|
||||||
|
|
||||||
### Step 3: Create Draft Plans
|
|
||||||
|
|
||||||
Group related tasks into plans using your best judgment. Plans start as drafts (tasks won't be dispatched until activated).
|
|
||||||
|
|
||||||
**Grouping guidance:**
|
|
||||||
|
|
||||||
- Group tasks that share a common theme, feature area, or goal
|
|
||||||
- Consider technical dependencies when grouping (tasks that touch the same files/modules)
|
|
||||||
- Separate unrelated work into distinct plans for parallel execution
|
|
||||||
- Don't over-group—if tasks are truly independent, separate plans enable better parallelism
|
|
||||||
- Don't under-group—related tasks benefit from shared context and coordinated execution
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sf plan create --title "Plan Name"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sf plan create --title "Authentication Improvements"
|
|
||||||
# Output: Created plan el-abc123
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Add Tasks to Plans
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sf plan add-task <plan-id> <task-id>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sf plan add-task el-abc123 el-task1
|
|
||||||
sf plan add-task el-abc123 el-task2
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 5: Set Dependencies Between Tasks
|
|
||||||
|
|
||||||
Use `blocks` dependency when one task must complete before another can start.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sf dependency add <blocked-id> <blocker-id> --type blocks
|
|
||||||
```
|
|
||||||
|
|
||||||
**Semantics:** The first ID is blocked BY the second ID. The blocker must complete first.
|
|
||||||
|
|
||||||
**Example:** Task 2 can't start until Task 1 completes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sf dependency add el-task2 el-task1 --type blocks
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 6: Update Priorities
|
|
||||||
|
|
||||||
Set priorities based on your assessment of impact, urgency, and dependencies. Use your judgment—you don't need user confirmation for routine prioritization.
|
|
||||||
|
|
||||||
**Priority guidance:**
|
|
||||||
|
|
||||||
- **Critical (1):** Blocking issues, security vulnerabilities, production bugs
|
|
||||||
- **High (2):** Important features with deadlines, significant user impact
|
|
||||||
- **Medium (3):** Standard feature work, most tasks default here
|
|
||||||
- **Low (4):** Nice-to-haves, minor improvements, tech debt
|
|
||||||
- **Minimal (5):** Backlog cleanup, documentation, exploratory work
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sf update <task-id> --priority <1-5>
|
|
||||||
```
|
|
||||||
|
|
||||||
| Value | Level |
|
|
||||||
| ----- | -------- |
|
|
||||||
| 1 | Critical |
|
|
||||||
| 2 | High |
|
|
||||||
| 3 | Medium |
|
|
||||||
| 4 | Low |
|
|
||||||
| 5 | Minimal |
|
|
||||||
|
|
||||||
### Step 7: Activate Plans
|
|
||||||
|
|
||||||
Once tasks are organized with dependencies set, activate plans to enable dispatch.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sf plan activate <plan-id>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 8: Set Task Status to Open
|
|
||||||
|
|
||||||
Move tasks from backlog to open so they become ready for work.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sf update <id> --status open
|
|
||||||
```
|
|
||||||
|
|
||||||
## Other Actions
|
|
||||||
|
|
||||||
**Close obsolete tasks:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sf task close <id> --reason "Won't do: <reason>"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Defer tasks:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sf task defer <id> --until <date>
|
|
||||||
```
|
|
||||||
|
|
||||||
**View existing plans:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sf plan list
|
|
||||||
```
|
|
||||||
|
|
||||||
**View tasks in a plan:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sf plan tasks <plan-id>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tips
|
|
||||||
|
|
||||||
- **Use your best judgment** for grouping, prioritization, and task interpretation—don't defer routine decisions to the user
|
|
||||||
- **Only escalate to the user** when ambiguity is fundamental and could lead to wasted work (mutually exclusive interpretations, critical business decisions)
|
|
||||||
- Make reasonable inferences about implementation details, scope, and priority based on codebase context
|
|
||||||
- Create plans before setting dependencies to avoid dispatch race conditions
|
|
||||||
- Always activate plans after dependencies are set
|
|
||||||
- Focus on oldest backlog items first (sorted by creation date)
|
|
||||||
- Every task should have a clear title and description before activation
|
|
||||||
- When uncertain about a minor detail, make a reasonable choice and document it in the task description—workers can ask if needed
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.py]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
+36
-43
@@ -1,49 +1,42 @@
|
|||||||
# Database Configuration
|
# App identity
|
||||||
POSTGRES_USER=headquarter
|
APP_NAME=Headquarter
|
||||||
POSTGRES_PASSWORD=change-me-in-production
|
ROOT_DOMAIN=localhost
|
||||||
|
TOOL_DOMAIN=tools.localhost
|
||||||
|
|
||||||
|
# API / Web URLs
|
||||||
|
API_URL=http://localhost:8000
|
||||||
|
WEB_URL=http://localhost:5173
|
||||||
|
CORS_ORIGINS=http://localhost:5173
|
||||||
|
|
||||||
|
# Database (local development)
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_PASSWORD=postgres
|
||||||
POSTGRES_DB=headquarter
|
POSTGRES_DB=headquarter
|
||||||
|
# DATABASE_URL uses a literal value because Pydantic Settings does not expand
|
||||||
|
# shell-style variable interpolation from .env files.
|
||||||
|
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/headquarter
|
||||||
|
|
||||||
# Redis Configuration
|
# Authentik OIDC placeholders (wire in FN-004)
|
||||||
REDIS_URL=redis://redis:6379/0
|
AUTHENTIK_ISSUER_URL=https://auth.example.com/application/o/headquarter/
|
||||||
|
AUTHENTIK_CLIENT_ID=your-client-id
|
||||||
|
AUTHENTIK_CLIENT_SECRET=your-client-secret
|
||||||
|
|
||||||
# Session Configuration
|
# Traefik / deployment placeholders (wire in FN-006)
|
||||||
SESSION_SECRET=change-me-in-production
|
TRAEFIK_NETWORK=traefik
|
||||||
SESSION_TTL_HOURS=24
|
TRAEFIK_ENTRYPOINT=websecure
|
||||||
|
TRAEFIK_CERT_RESOLVER=letsencrypt
|
||||||
|
TRAEFIK_LOG_LEVEL=INFO
|
||||||
|
TRAEFIK_ACME_EMAIL=admin@example.com
|
||||||
|
TOOL_SUBDOMAIN_PATTERN={tool}-{project}-{user}.tools.localhost
|
||||||
|
|
||||||
# Application Configuration
|
# Frontend build-time variables (passed to web container)
|
||||||
APP_ENV=development
|
VITE_API_URL=http://localhost:8000
|
||||||
DEBUG=true
|
VITE_OIDC_ISSUER=https://auth.example.com/application/o/headquarter/
|
||||||
LOG_LEVEL=info
|
VITE_OIDC_CLIENT_ID=your-client-id
|
||||||
REPO_BASE_PATH=/data/repos
|
VITE_OIDC_REDIRECT_URI=https://headquarter.commumedia.org/callback
|
||||||
|
|
||||||
# Domain Configuration (for both development and traefik modes)
|
# Secrets (generate strong random values for production)
|
||||||
API_DOMAIN=localhost
|
SECRET_ENCRYPTION_KEY=change-me-in-production
|
||||||
WEB_DOMAIN=localhost
|
|
||||||
AUTHENTIK_DOMAIN=authentik.local
|
|
||||||
|
|
||||||
# Public URLs (optional - will be constructed from domains if not set)
|
# Auth dev bypass (local development only — NEVER enable in production)
|
||||||
# API_PUBLIC_URL=https://api.example.com
|
AUTH_DEV_BYPASS=false
|
||||||
# WEB_PUBLIC_URL=https://app.example.com
|
|
||||||
|
|
||||||
# Authentik Configuration
|
|
||||||
# Client ID: The OAuth client ID from Authentik (may be a UUID)
|
|
||||||
AUTHENTIK_CLIENT_ID=headquarter-web
|
|
||||||
AUTHENTIK_CLIENT_SECRET=change-me
|
|
||||||
# Application Slug: The URL-friendly identifier used in Authentik URLs
|
|
||||||
# This is often the same as the application identifier/slug in Authentik
|
|
||||||
# e.g., if your Authentik app URL is /application/o/headquarter-web/, use "headquarter-web"
|
|
||||||
AUTHENTIK_APPLICATION_SLUG=headquarter-web
|
|
||||||
# Override Authentik URLs if they differ from the default pattern
|
|
||||||
# AUTHENTIK_AUTHORIZE_URL=https://authentik.example.com/application/o/authorize/
|
|
||||||
# AUTHENTIK_TOKEN_URL=https://authentik.example.com/application/o/token/
|
|
||||||
|
|
||||||
# Frontend Configuration
|
|
||||||
VITE_API_BASE_URL=http://localhost:8000
|
|
||||||
VITE_APP_URL=http://localhost:3000
|
|
||||||
|
|
||||||
# Docker Configuration
|
|
||||||
COMPOSE_PROJECT_NAME=headquarter
|
|
||||||
|
|
||||||
# Traefik Configuration (for docker-compose.traefik.yml)
|
|
||||||
# PROXY_WEB_NAME=headquarter-web
|
|
||||||
# TRAEFIK_NETWORK=traefik
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
web-ci:
|
||||||
|
name: Web CI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: pnpm --filter @headquarter/web lint
|
||||||
|
|
||||||
|
- name: Typecheck
|
||||||
|
run: pnpm --filter @headquarter/web typecheck
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: pnpm --filter @headquarter/web test
|
||||||
|
|
||||||
|
api-ci:
|
||||||
|
name: API CI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: headquarter_test
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
|
||||||
|
- name: Install API dev dependencies
|
||||||
|
working-directory: apps/api
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
|
||||||
|
- name: Ruff check
|
||||||
|
working-directory: apps/api
|
||||||
|
run: ruff check app/ tests/
|
||||||
|
|
||||||
|
- name: Mypy
|
||||||
|
working-directory: apps/api
|
||||||
|
run: mypy app/ tests/
|
||||||
|
|
||||||
|
- name: Pytest
|
||||||
|
working-directory: apps/api
|
||||||
|
env:
|
||||||
|
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/headquarter_test
|
||||||
|
run: pytest
|
||||||
+51
-49
@@ -1,46 +1,20 @@
|
|||||||
|
# Dependencies
|
||||||
# Beads / Dolt files (added by bd init)
|
|
||||||
.dolt/
|
|
||||||
*.db
|
|
||||||
.beads-credential-key
|
|
||||||
|
|
||||||
# Environment files
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
# Python
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*.pyo
|
|
||||||
*.pyd
|
|
||||||
*.so
|
|
||||||
.python-version
|
|
||||||
.venv/
|
|
||||||
.venv-test/
|
|
||||||
venv/
|
|
||||||
env/
|
|
||||||
.pytest_cache/
|
|
||||||
.mypy_cache/
|
|
||||||
.ruff_cache/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
htmlcov/
|
|
||||||
|
|
||||||
# Python packaging
|
|
||||||
*.egg-info/
|
|
||||||
build/
|
|
||||||
dist/
|
|
||||||
|
|
||||||
# Node / frontend
|
|
||||||
node_modules/
|
node_modules/
|
||||||
npm-debug.log*
|
.pnpm-store/
|
||||||
yarn-debug.log*
|
package-lock.json
|
||||||
yarn-error.log*
|
yarn.lock
|
||||||
pnpm-debug.log*
|
|
||||||
apps/web/dist/
|
|
||||||
|
|
||||||
# IDE / editor
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
*.swp
|
*.swp
|
||||||
@@ -49,12 +23,40 @@ apps/web/dist/
|
|||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
/.stoneforge/.worktrees/
|
|
||||||
# Pi / agent cache
|
# Logs
|
||||||
.pi/
|
*.log
|
||||||
.atl/
|
logs/
|
||||||
.sisyphus/
|
|
||||||
.pi-lens/
|
# Testing
|
||||||
minerv3/
|
coverage/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
.egg-info/
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Docker volumes
|
||||||
|
docker-volumes/
|
||||||
|
|
||||||
|
# Fusion internals
|
||||||
|
.fusion/
|
||||||
|
|
||||||
|
# Misc
|
||||||
.cache/
|
.cache/
|
||||||
openspec-audit-report.md
|
.temp/
|
||||||
|
tmp/
|
||||||
|
.local-bin/
|
||||||
|
|
||||||
|
# OpenCode / Sisyphus
|
||||||
|
.opencode/
|
||||||
|
.sisyphus/
|
||||||
|
AGENTS.md
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
# .opencode (index)
|
|
||||||
dir: .opencode
|
|
||||||
|
|
||||||
## role
|
|
||||||
Hidden directory for OpenCode IDE/editor configuration and workspace metadata
|
|
||||||
## parent
|
|
||||||
index: ./.pi-map.index.md
|
|
||||||
map: ./.pi-map.md
|
|
||||||
## children
|
|
||||||
- .opencode/commands
|
|
||||||
index: .opencode/commands/.pi-map.index.md
|
|
||||||
map: .opencode/commands/.pi-map.md
|
|
||||||
- .opencode/skills
|
|
||||||
index: .opencode/skills/.pi-map.index.md
|
|
||||||
map: .opencode/skills/.pi-map.md
|
|
||||||
## files
|
|
||||||
## links
|
|
||||||
index: .opencode/.pi-map.index.md
|
|
||||||
map: .opencode/.pi-map.md
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# .opencode
|
|
||||||
dir: .opencode
|
|
||||||
|
|
||||||
index: .opencode/.pi-map.index.md
|
|
||||||
|
|
||||||
## role
|
|
||||||
Hidden directory for OpenCode IDE/editor configuration and workspace metadata
|
|
||||||
## files
|
|
||||||
## arch
|
|
||||||
IDE-specific dot-directory pattern, no active code architecture; stores tool preferences and ephemeral state
|
|
||||||
## tags
|
|
||||||
-
|
|
||||||
## symbols
|
|
||||||
-
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# .opencode/commands (index)
|
|
||||||
dir: .opencode/commands
|
|
||||||
|
|
||||||
## role
|
|
||||||
Defines experimental workflow skills and AI assistant stances for an OpenSpec-based development system with structured change management.
|
|
||||||
## parent
|
|
||||||
index: .opencode/.pi-map.index.md
|
|
||||||
map: .opencode/.pi-map.md
|
|
||||||
## children
|
|
||||||
-
|
|
||||||
## files
|
|
||||||
- opsx-apply.md
|
|
||||||
- opsx-archive.md
|
|
||||||
- opsx-explore.md
|
|
||||||
- opsx-propose.md
|
|
||||||
## links
|
|
||||||
index: .opencode/commands/.pi-map.index.md
|
|
||||||
map: .opencode/commands/.pi-map.md
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# .opencode/commands
|
|
||||||
dir: .opencode/commands
|
|
||||||
|
|
||||||
index: .opencode/commands/.pi-map.index.md
|
|
||||||
|
|
||||||
## role
|
|
||||||
Defines experimental workflow skills and AI assistant stances for an OpenSpec-based development system with structured change management.
|
|
||||||
## files
|
|
||||||
- opsx-apply.md | Defines an experimental workflow skill for implementing tasks from an OpenSpec change through a structured, interactive process with CLI integration and progress tracking. | dep: openspec CLI, AskUserQuestion tool, filesystem (for reading context files)
|
|
||||||
- opsx-archive.md | Defines a workflow for archiving completed changes in an experimental openspec-based development system | dep: openspec CLI, AskUserQuestion tool, Task tool, Skill tool, filesystem (mkdir, mv), JSON parsing
|
|
||||||
- opsx-explore.md | Defines the "explore mode" stance for an AI assistant - a thinking/discovery mode for investigating problems and clarifying requirements without implementing code | dep: OpenSpec system
|
|
||||||
- opsx-propose.md | Defines a workflow for proposing new changes in the openspec system by creating a change directory and generating all required artifacts (proposal.md, design.md, tasks.md) in dependency order | dep: openspec CLI, AskUserQuestion tool, TodoWrite tool, JSON parsing
|
|
||||||
## arch
|
|
||||||
Markdown-based command definitions using a workflow pattern with interactive CLI integration, progress tracking, and dependency-ordered artifact generation across explore/propose/apply/archive lifecycle phases.
|
|
||||||
## tags
|
|
||||||
opsx, defines, workflow, openspec, openspec cli, askuserquestion tool, explore, experimental
|
|
||||||
## symbols
|
|
||||||
-
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
---
|
|
||||||
description: Implement tasks from an OpenSpec change (Experimental)
|
|
||||||
---
|
|
||||||
|
|
||||||
Implement tasks from an OpenSpec change.
|
|
||||||
|
|
||||||
**Input**: Optionally specify a change name (e.g., `/opsx-apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
|
||||||
|
|
||||||
**Steps**
|
|
||||||
|
|
||||||
1. **Select the change**
|
|
||||||
|
|
||||||
If a name is provided, use it. Otherwise:
|
|
||||||
- Infer from conversation context if the user mentioned a change
|
|
||||||
- Auto-select if only one active change exists
|
|
||||||
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
|
||||||
|
|
||||||
Always announce: "Using change: <name>" and how to override (e.g., `/opsx-apply <other>`).
|
|
||||||
|
|
||||||
2. **Check status to understand the schema**
|
|
||||||
```bash
|
|
||||||
openspec status --change "<name>" --json
|
|
||||||
```
|
|
||||||
Parse the JSON to understand:
|
|
||||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
|
||||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
|
||||||
|
|
||||||
3. **Get apply instructions**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
openspec instructions apply --change "<name>" --json
|
|
||||||
```
|
|
||||||
|
|
||||||
This returns:
|
|
||||||
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema)
|
|
||||||
- Progress (total, complete, remaining)
|
|
||||||
- Task list with status
|
|
||||||
- Dynamic instruction based on current state
|
|
||||||
|
|
||||||
**Handle states:**
|
|
||||||
- If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx-continue`
|
|
||||||
- If `state: "all_done"`: congratulate, suggest archive
|
|
||||||
- Otherwise: proceed to implementation
|
|
||||||
|
|
||||||
4. **Read context files**
|
|
||||||
|
|
||||||
Read every file path listed under `contextFiles` from the apply instructions output.
|
|
||||||
The files depend on the schema being used:
|
|
||||||
- **spec-driven**: proposal, specs, design, tasks
|
|
||||||
- Other schemas: follow the contextFiles from CLI output
|
|
||||||
|
|
||||||
5. **Show current progress**
|
|
||||||
|
|
||||||
Display:
|
|
||||||
- Schema being used
|
|
||||||
- Progress: "N/M tasks complete"
|
|
||||||
- Remaining tasks overview
|
|
||||||
- Dynamic instruction from CLI
|
|
||||||
|
|
||||||
6. **Implement tasks (loop until done or blocked)**
|
|
||||||
|
|
||||||
For each pending task:
|
|
||||||
- Show which task is being worked on
|
|
||||||
- Make the code changes required
|
|
||||||
- Keep changes minimal and focused
|
|
||||||
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
|
||||||
- Continue to next task
|
|
||||||
|
|
||||||
**Pause if:**
|
|
||||||
- Task is unclear → ask for clarification
|
|
||||||
- Implementation reveals a design issue → suggest updating artifacts
|
|
||||||
- Error or blocker encountered → report and wait for guidance
|
|
||||||
- User interrupts
|
|
||||||
|
|
||||||
7. **On completion or pause, show status**
|
|
||||||
|
|
||||||
Display:
|
|
||||||
- Tasks completed this session
|
|
||||||
- Overall progress: "N/M tasks complete"
|
|
||||||
- If all done: suggest archive
|
|
||||||
- If paused: explain why and wait for guidance
|
|
||||||
|
|
||||||
**Output During Implementation**
|
|
||||||
|
|
||||||
```
|
|
||||||
## Implementing: <change-name> (schema: <schema-name>)
|
|
||||||
|
|
||||||
Working on task 3/7: <task description>
|
|
||||||
[...implementation happening...]
|
|
||||||
✓ Task complete
|
|
||||||
|
|
||||||
Working on task 4/7: <task description>
|
|
||||||
[...implementation happening...]
|
|
||||||
✓ Task complete
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output On Completion**
|
|
||||||
|
|
||||||
```
|
|
||||||
## Implementation Complete
|
|
||||||
|
|
||||||
**Change:** <change-name>
|
|
||||||
**Schema:** <schema-name>
|
|
||||||
**Progress:** 7/7 tasks complete ✓
|
|
||||||
|
|
||||||
### Completed This Session
|
|
||||||
- [x] Task 1
|
|
||||||
- [x] Task 2
|
|
||||||
...
|
|
||||||
|
|
||||||
All tasks complete! You can archive this change with `/opsx-archive`.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output On Pause (Issue Encountered)**
|
|
||||||
|
|
||||||
```
|
|
||||||
## Implementation Paused
|
|
||||||
|
|
||||||
**Change:** <change-name>
|
|
||||||
**Schema:** <schema-name>
|
|
||||||
**Progress:** 4/7 tasks complete
|
|
||||||
|
|
||||||
### Issue Encountered
|
|
||||||
<description of the issue>
|
|
||||||
|
|
||||||
**Options:**
|
|
||||||
1. <option 1>
|
|
||||||
2. <option 2>
|
|
||||||
3. Other approach
|
|
||||||
|
|
||||||
What would you like to do?
|
|
||||||
```
|
|
||||||
|
|
||||||
**Guardrails**
|
|
||||||
- Keep going through tasks until done or blocked
|
|
||||||
- Always read context files before starting (from the apply instructions output)
|
|
||||||
- If task is ambiguous, pause and ask before implementing
|
|
||||||
- If implementation reveals issues, pause and suggest artifact updates
|
|
||||||
- Keep code changes minimal and scoped to each task
|
|
||||||
- Update task checkbox immediately after completing each task
|
|
||||||
- Pause on errors, blockers, or unclear requirements - don't guess
|
|
||||||
- Use contextFiles from CLI output, don't assume specific file names
|
|
||||||
|
|
||||||
**Fluid Workflow Integration**
|
|
||||||
|
|
||||||
This skill supports the "actions on a change" model:
|
|
||||||
|
|
||||||
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
|
||||||
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
---
|
|
||||||
description: Archive a completed change in the experimental workflow
|
|
||||||
---
|
|
||||||
|
|
||||||
Archive a completed change in the experimental workflow.
|
|
||||||
|
|
||||||
**Input**: Optionally specify a change name after `/opsx-archive` (e.g., `/opsx-archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
|
||||||
|
|
||||||
**Steps**
|
|
||||||
|
|
||||||
1. **If no change name provided, prompt for selection**
|
|
||||||
|
|
||||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
|
||||||
|
|
||||||
Show only active changes (not already archived).
|
|
||||||
Include the schema used for each change if available.
|
|
||||||
|
|
||||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
|
||||||
|
|
||||||
2. **Check artifact completion status**
|
|
||||||
|
|
||||||
Run `openspec status --change "<name>" --json` to check artifact completion.
|
|
||||||
|
|
||||||
Parse the JSON to understand:
|
|
||||||
- `schemaName`: The workflow being used
|
|
||||||
- `artifacts`: List of artifacts with their status (`done` or other)
|
|
||||||
|
|
||||||
**If any artifacts are not `done`:**
|
|
||||||
- Display warning listing incomplete artifacts
|
|
||||||
- Prompt user for confirmation to continue
|
|
||||||
- Proceed if user confirms
|
|
||||||
|
|
||||||
3. **Check task completion status**
|
|
||||||
|
|
||||||
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
|
||||||
|
|
||||||
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
|
||||||
|
|
||||||
**If incomplete tasks found:**
|
|
||||||
- Display warning showing count of incomplete tasks
|
|
||||||
- Prompt user for confirmation to continue
|
|
||||||
- Proceed if user confirms
|
|
||||||
|
|
||||||
**If no tasks file exists:** Proceed without task-related warning.
|
|
||||||
|
|
||||||
4. **Assess delta spec sync state**
|
|
||||||
|
|
||||||
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
|
|
||||||
|
|
||||||
**If delta specs exist:**
|
|
||||||
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
|
|
||||||
- Determine what changes would be applied (adds, modifications, removals, renames)
|
|
||||||
- Show a combined summary before prompting
|
|
||||||
|
|
||||||
**Prompt options:**
|
|
||||||
- If changes needed: "Sync now (recommended)", "Archive without syncing"
|
|
||||||
- If already synced: "Archive now", "Sync anyway", "Cancel"
|
|
||||||
|
|
||||||
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
|
|
||||||
|
|
||||||
5. **Perform the archive**
|
|
||||||
|
|
||||||
Create the archive directory if it doesn't exist:
|
|
||||||
```bash
|
|
||||||
mkdir -p openspec/changes/archive
|
|
||||||
```
|
|
||||||
|
|
||||||
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
|
||||||
|
|
||||||
**Check if target already exists:**
|
|
||||||
- If yes: Fail with error, suggest renaming existing archive or using different date
|
|
||||||
- If no: Move the change directory to archive
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
|
||||||
```
|
|
||||||
|
|
||||||
6. **Display summary**
|
|
||||||
|
|
||||||
Show archive completion summary including:
|
|
||||||
- Change name
|
|
||||||
- Schema that was used
|
|
||||||
- Archive location
|
|
||||||
- Spec sync status (synced / sync skipped / no delta specs)
|
|
||||||
- Note about any warnings (incomplete artifacts/tasks)
|
|
||||||
|
|
||||||
**Output On Success**
|
|
||||||
|
|
||||||
```
|
|
||||||
## Archive Complete
|
|
||||||
|
|
||||||
**Change:** <change-name>
|
|
||||||
**Schema:** <schema-name>
|
|
||||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
|
||||||
**Specs:** ✓ Synced to main specs
|
|
||||||
|
|
||||||
All artifacts complete. All tasks complete.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output On Success (No Delta Specs)**
|
|
||||||
|
|
||||||
```
|
|
||||||
## Archive Complete
|
|
||||||
|
|
||||||
**Change:** <change-name>
|
|
||||||
**Schema:** <schema-name>
|
|
||||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
|
||||||
**Specs:** No delta specs
|
|
||||||
|
|
||||||
All artifacts complete. All tasks complete.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output On Success With Warnings**
|
|
||||||
|
|
||||||
```
|
|
||||||
## Archive Complete (with warnings)
|
|
||||||
|
|
||||||
**Change:** <change-name>
|
|
||||||
**Schema:** <schema-name>
|
|
||||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
|
||||||
**Specs:** Sync skipped (user chose to skip)
|
|
||||||
|
|
||||||
**Warnings:**
|
|
||||||
- Archived with 2 incomplete artifacts
|
|
||||||
- Archived with 3 incomplete tasks
|
|
||||||
- Delta spec sync was skipped (user chose to skip)
|
|
||||||
|
|
||||||
Review the archive if this was not intentional.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output On Error (Archive Exists)**
|
|
||||||
|
|
||||||
```
|
|
||||||
## Archive Failed
|
|
||||||
|
|
||||||
**Change:** <change-name>
|
|
||||||
**Target:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
|
||||||
|
|
||||||
Target archive directory already exists.
|
|
||||||
|
|
||||||
**Options:**
|
|
||||||
1. Rename the existing archive
|
|
||||||
2. Delete the existing archive if it's a duplicate
|
|
||||||
3. Wait until a different date to archive
|
|
||||||
```
|
|
||||||
|
|
||||||
**Guardrails**
|
|
||||||
- Always prompt for change selection if not provided
|
|
||||||
- Use artifact graph (openspec status --json) for completion checking
|
|
||||||
- Don't block archive on warnings - just inform and confirm
|
|
||||||
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
|
||||||
- Show clear summary of what happened
|
|
||||||
- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven)
|
|
||||||
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
---
|
|
||||||
description: Enter explore mode - think through ideas, investigate problems, clarify requirements
|
|
||||||
---
|
|
||||||
|
|
||||||
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
|
||||||
|
|
||||||
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
|
|
||||||
|
|
||||||
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
|
||||||
|
|
||||||
**Input**: The argument after `/opsx-explore` is whatever the user wants to think about. Could be:
|
|
||||||
- A vague idea: "real-time collaboration"
|
|
||||||
- A specific problem: "the auth system is getting unwieldy"
|
|
||||||
- A change name: "add-dark-mode" (to explore in context of that change)
|
|
||||||
- A comparison: "postgres vs sqlite for this"
|
|
||||||
- Nothing (just enter explore mode)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The Stance
|
|
||||||
|
|
||||||
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
|
||||||
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
|
||||||
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
|
||||||
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
|
||||||
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
|
||||||
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What You Might Do
|
|
||||||
|
|
||||||
Depending on what the user brings, you might:
|
|
||||||
|
|
||||||
**Explore the problem space**
|
|
||||||
- Ask clarifying questions that emerge from what they said
|
|
||||||
- Challenge assumptions
|
|
||||||
- Reframe the problem
|
|
||||||
- Find analogies
|
|
||||||
|
|
||||||
**Investigate the codebase**
|
|
||||||
- Map existing architecture relevant to the discussion
|
|
||||||
- Find integration points
|
|
||||||
- Identify patterns already in use
|
|
||||||
- Surface hidden complexity
|
|
||||||
|
|
||||||
**Compare options**
|
|
||||||
- Brainstorm multiple approaches
|
|
||||||
- Build comparison tables
|
|
||||||
- Sketch tradeoffs
|
|
||||||
- Recommend a path (if asked)
|
|
||||||
|
|
||||||
**Visualize**
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────┐
|
|
||||||
│ Use ASCII diagrams liberally │
|
|
||||||
├─────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ ┌────────┐ ┌────────┐ │
|
|
||||||
│ │ State │────────▶│ State │ │
|
|
||||||
│ │ A │ │ B │ │
|
|
||||||
│ └────────┘ └────────┘ │
|
|
||||||
│ │
|
|
||||||
│ System diagrams, state machines, │
|
|
||||||
│ data flows, architecture sketches, │
|
|
||||||
│ dependency graphs, comparison tables │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Surface risks and unknowns**
|
|
||||||
- Identify what could go wrong
|
|
||||||
- Find gaps in understanding
|
|
||||||
- Suggest spikes or investigations
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## OpenSpec Awareness
|
|
||||||
|
|
||||||
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
|
||||||
|
|
||||||
### Check for context
|
|
||||||
|
|
||||||
At the start, quickly check what exists:
|
|
||||||
```bash
|
|
||||||
openspec list --json
|
|
||||||
```
|
|
||||||
|
|
||||||
This tells you:
|
|
||||||
- If there are active changes
|
|
||||||
- Their names, schemas, and status
|
|
||||||
- What the user might be working on
|
|
||||||
|
|
||||||
If the user mentioned a specific change name, read its artifacts for context.
|
|
||||||
|
|
||||||
### When no change exists
|
|
||||||
|
|
||||||
Think freely. When insights crystallize, you might offer:
|
|
||||||
|
|
||||||
- "This feels solid enough to start a change. Want me to create a proposal?"
|
|
||||||
- Or keep exploring - no pressure to formalize
|
|
||||||
|
|
||||||
### When a change exists
|
|
||||||
|
|
||||||
If the user mentions a change or you detect one is relevant:
|
|
||||||
|
|
||||||
1. **Read existing artifacts for context**
|
|
||||||
- `openspec/changes/<name>/proposal.md`
|
|
||||||
- `openspec/changes/<name>/design.md`
|
|
||||||
- `openspec/changes/<name>/tasks.md`
|
|
||||||
- etc.
|
|
||||||
|
|
||||||
2. **Reference them naturally in conversation**
|
|
||||||
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
|
||||||
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
|
||||||
|
|
||||||
3. **Offer to capture when decisions are made**
|
|
||||||
|
|
||||||
| Insight Type | Where to Capture |
|
|
||||||
|----------------------------|--------------------------------|
|
|
||||||
| New requirement discovered | `specs/<capability>/spec.md` |
|
|
||||||
| Requirement changed | `specs/<capability>/spec.md` |
|
|
||||||
| Design decision made | `design.md` |
|
|
||||||
| Scope changed | `proposal.md` |
|
|
||||||
| New work identified | `tasks.md` |
|
|
||||||
| Assumption invalidated | Relevant artifact |
|
|
||||||
|
|
||||||
Example offers:
|
|
||||||
- "That's a design decision. Capture it in design.md?"
|
|
||||||
- "This is a new requirement. Add it to specs?"
|
|
||||||
- "This changes scope. Update the proposal?"
|
|
||||||
|
|
||||||
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What You Don't Have To Do
|
|
||||||
|
|
||||||
- Follow a script
|
|
||||||
- Ask the same questions every time
|
|
||||||
- Produce a specific artifact
|
|
||||||
- Reach a conclusion
|
|
||||||
- Stay on topic if a tangent is valuable
|
|
||||||
- Be brief (this is thinking time)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ending Discovery
|
|
||||||
|
|
||||||
There's no required ending. Discovery might:
|
|
||||||
|
|
||||||
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
|
|
||||||
- **Result in artifact updates**: "Updated design.md with these decisions"
|
|
||||||
- **Just provide clarity**: User has what they need, moves on
|
|
||||||
- **Continue later**: "We can pick this up anytime"
|
|
||||||
|
|
||||||
When things crystallize, you might offer a summary - but it's optional. Sometimes the thinking IS the value.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Guardrails
|
|
||||||
|
|
||||||
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
|
|
||||||
- **Don't fake understanding** - If something is unclear, dig deeper
|
|
||||||
- **Don't rush** - Discovery is thinking time, not task time
|
|
||||||
- **Don't force structure** - Let patterns emerge naturally
|
|
||||||
- **Don't auto-capture** - Offer to save insights, don't just do it
|
|
||||||
- **Do visualize** - A good diagram is worth many paragraphs
|
|
||||||
- **Do explore the codebase** - Ground discussions in reality
|
|
||||||
- **Do question assumptions** - Including the user's and your own
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
---
|
|
||||||
description: Propose a new change - create it and generate all artifacts in one step
|
|
||||||
---
|
|
||||||
|
|
||||||
Propose a new change - create the change and generate all artifacts in one step.
|
|
||||||
|
|
||||||
I'll create a change with artifacts:
|
|
||||||
- proposal.md (what & why)
|
|
||||||
- design.md (how)
|
|
||||||
- tasks.md (implementation steps)
|
|
||||||
|
|
||||||
When ready to implement, run /opsx-apply
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Input**: The argument after `/opsx-propose` is the change name (kebab-case), OR a description of what the user wants to build.
|
|
||||||
|
|
||||||
**Steps**
|
|
||||||
|
|
||||||
1. **If no input provided, ask what they want to build**
|
|
||||||
|
|
||||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
|
||||||
> "What change do you want to work on? Describe what you want to build or fix."
|
|
||||||
|
|
||||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
|
||||||
|
|
||||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
|
||||||
|
|
||||||
2. **Create the change directory**
|
|
||||||
```bash
|
|
||||||
openspec new change "<name>"
|
|
||||||
```
|
|
||||||
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
|
||||||
|
|
||||||
3. **Get the artifact build order**
|
|
||||||
```bash
|
|
||||||
openspec status --change "<name>" --json
|
|
||||||
```
|
|
||||||
Parse the JSON to get:
|
|
||||||
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
|
||||||
- `artifacts`: list of all artifacts with their status and dependencies
|
|
||||||
|
|
||||||
4. **Create artifacts in sequence until apply-ready**
|
|
||||||
|
|
||||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
|
||||||
|
|
||||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
|
||||||
|
|
||||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
|
||||||
- Get instructions:
|
|
||||||
```bash
|
|
||||||
openspec instructions <artifact-id> --change "<name>" --json
|
|
||||||
```
|
|
||||||
- The instructions JSON includes:
|
|
||||||
- `context`: Project background (constraints for you - do NOT include in output)
|
|
||||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
|
||||||
- `template`: The structure to use for your output file
|
|
||||||
- `instruction`: Schema-specific guidance for this artifact type
|
|
||||||
- `outputPath`: Where to write the artifact
|
|
||||||
- `dependencies`: Completed artifacts to read for context
|
|
||||||
- Read any completed dependency files for context
|
|
||||||
- Create the artifact file using `template` as the structure
|
|
||||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
|
||||||
- Show brief progress: "Created <artifact-id>"
|
|
||||||
|
|
||||||
b. **Continue until all `applyRequires` artifacts are complete**
|
|
||||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
|
||||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
|
||||||
- Stop when all `applyRequires` artifacts are done
|
|
||||||
|
|
||||||
c. **If an artifact requires user input** (unclear context):
|
|
||||||
- Use **AskUserQuestion tool** to clarify
|
|
||||||
- Then continue with creation
|
|
||||||
|
|
||||||
5. **Show final status**
|
|
||||||
```bash
|
|
||||||
openspec status --change "<name>"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output**
|
|
||||||
|
|
||||||
After completing all artifacts, summarize:
|
|
||||||
- Change name and location
|
|
||||||
- List of artifacts created with brief descriptions
|
|
||||||
- What's ready: "All artifacts created! Ready for implementation."
|
|
||||||
- Prompt: "Run `/opsx-apply` to start implementing."
|
|
||||||
|
|
||||||
**Artifact Creation Guidelines**
|
|
||||||
|
|
||||||
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
|
||||||
- The schema defines what each artifact should contain - follow it
|
|
||||||
- Read dependency artifacts for context before creating new ones
|
|
||||||
- Use `template` as the structure for your output file - fill in its sections
|
|
||||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
|
||||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
|
||||||
- These guide what you write, but should never appear in the output
|
|
||||||
|
|
||||||
**Guardrails**
|
|
||||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
|
||||||
- Always read dependency artifacts before creating a new one
|
|
||||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
|
||||||
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
|
||||||
- Verify each artifact file exists after writing before proceeding to next
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# .opencode/skills (index)
|
|
||||||
dir: .opencode/skills
|
|
||||||
|
|
||||||
## role
|
|
||||||
Contains reusable AI skill modules that provide specialized capabilities for the OpenCode assistant.
|
|
||||||
## parent
|
|
||||||
index: .opencode/.pi-map.index.md
|
|
||||||
map: .opencode/.pi-map.md
|
|
||||||
## children
|
|
||||||
- .opencode/skills/openspec-apply-change
|
|
||||||
index: .opencode/skills/openspec-apply-change/.pi-map.index.md
|
|
||||||
map: .opencode/skills/openspec-apply-change/.pi-map.md
|
|
||||||
- .opencode/skills/openspec-archive-change
|
|
||||||
index: .opencode/skills/openspec-archive-change/.pi-map.index.md
|
|
||||||
map: .opencode/skills/openspec-archive-change/.pi-map.md
|
|
||||||
- .opencode/skills/openspec-explore
|
|
||||||
index: .opencode/skills/openspec-explore/.pi-map.index.md
|
|
||||||
map: .opencode/skills/openspec-explore/.pi-map.md
|
|
||||||
- .opencode/skills/openspec-propose
|
|
||||||
index: .opencode/skills/openspec-propose/.pi-map.index.md
|
|
||||||
map: .opencode/skills/openspec-propose/.pi-map.md
|
|
||||||
## files
|
|
||||||
## links
|
|
||||||
index: .opencode/skills/.pi-map.index.md
|
|
||||||
map: .opencode/skills/.pi-map.md
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# .opencode/skills
|
|
||||||
dir: .opencode/skills
|
|
||||||
|
|
||||||
index: .opencode/skills/.pi-map.index.md
|
|
||||||
|
|
||||||
## role
|
|
||||||
Contains reusable AI skill modules that provide specialized capabilities for the OpenCode assistant.
|
|
||||||
## files
|
|
||||||
## arch
|
|
||||||
Modular plugin-based architecture where each skill is a self-contained module with defined interfaces, enabling dynamic loading and composition of AI capabilities.
|
|
||||||
## tags
|
|
||||||
-
|
|
||||||
## symbols
|
|
||||||
-
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# .opencode/skills/openspec-apply-change (index)
|
|
||||||
dir: .opencode/skills/openspec-apply-change
|
|
||||||
|
|
||||||
## role
|
|
||||||
Defines an AI assistant skill that implements OpenSpec changes through a spec-driven workflow with structured planning, validation, and execution phases.
|
|
||||||
## parent
|
|
||||||
index: .opencode/skills/.pi-map.index.md
|
|
||||||
map: .opencode/skills/.pi-map.md
|
|
||||||
## children
|
|
||||||
-
|
|
||||||
## files
|
|
||||||
- SKILL.md
|
|
||||||
## links
|
|
||||||
index: .opencode/skills/openspec-apply-change/.pi-map.index.md
|
|
||||||
map: .opencode/skills/openspec-apply-change/.pi-map.md
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# .opencode/skills/openspec-apply-change
|
|
||||||
dir: .opencode/skills/openspec-apply-change
|
|
||||||
|
|
||||||
index: .opencode/skills/openspec-apply-change/.pi-map.index.md
|
|
||||||
|
|
||||||
## role
|
|
||||||
Defines an AI assistant skill that implements OpenSpec changes through a spec-driven workflow with structured planning, validation, and execution phases.
|
|
||||||
## files
|
|
||||||
- SKILL.md | Defines an AI assistant skill for implementing tasks from an OpenSpec change using a spec-driven workflow | dep: openspec CLI, AskUserQuestion tool, filesystem access
|
|
||||||
## arch
|
|
||||||
Template-based skill definition using markdown documentation with structured workflow phases (planning, validation, execution) and integration points for external tools (OpenSpec CLI, OpenCode agent).
|
|
||||||
## tags
|
|
||||||
skill, defines, assistant, implementing, tasks, openspec, change, spec
|
|
||||||
## symbols
|
|
||||||
-
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
---
|
|
||||||
name: openspec-apply-change
|
|
||||||
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
|
|
||||||
license: MIT
|
|
||||||
compatibility: Requires openspec CLI.
|
|
||||||
metadata:
|
|
||||||
author: openspec
|
|
||||||
version: "1.0"
|
|
||||||
generatedBy: "1.3.1"
|
|
||||||
---
|
|
||||||
|
|
||||||
Implement tasks from an OpenSpec change.
|
|
||||||
|
|
||||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
|
||||||
|
|
||||||
**Steps**
|
|
||||||
|
|
||||||
1. **Select the change**
|
|
||||||
|
|
||||||
If a name is provided, use it. Otherwise:
|
|
||||||
- Infer from conversation context if the user mentioned a change
|
|
||||||
- Auto-select if only one active change exists
|
|
||||||
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
|
||||||
|
|
||||||
Always announce: "Using change: <name>" and how to override (e.g., `/opsx-apply <other>`).
|
|
||||||
|
|
||||||
2. **Check status to understand the schema**
|
|
||||||
```bash
|
|
||||||
openspec status --change "<name>" --json
|
|
||||||
```
|
|
||||||
Parse the JSON to understand:
|
|
||||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
|
||||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
|
||||||
|
|
||||||
3. **Get apply instructions**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
openspec instructions apply --change "<name>" --json
|
|
||||||
```
|
|
||||||
|
|
||||||
This returns:
|
|
||||||
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
|
|
||||||
- Progress (total, complete, remaining)
|
|
||||||
- Task list with status
|
|
||||||
- Dynamic instruction based on current state
|
|
||||||
|
|
||||||
**Handle states:**
|
|
||||||
- If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
|
|
||||||
- If `state: "all_done"`: congratulate, suggest archive
|
|
||||||
- Otherwise: proceed to implementation
|
|
||||||
|
|
||||||
4. **Read context files**
|
|
||||||
|
|
||||||
Read every file path listed under `contextFiles` from the apply instructions output.
|
|
||||||
The files depend on the schema being used:
|
|
||||||
- **spec-driven**: proposal, specs, design, tasks
|
|
||||||
- Other schemas: follow the contextFiles from CLI output
|
|
||||||
|
|
||||||
5. **Show current progress**
|
|
||||||
|
|
||||||
Display:
|
|
||||||
- Schema being used
|
|
||||||
- Progress: "N/M tasks complete"
|
|
||||||
- Remaining tasks overview
|
|
||||||
- Dynamic instruction from CLI
|
|
||||||
|
|
||||||
6. **Implement tasks (loop until done or blocked)**
|
|
||||||
|
|
||||||
For each pending task:
|
|
||||||
- Show which task is being worked on
|
|
||||||
- Make the code changes required
|
|
||||||
- Keep changes minimal and focused
|
|
||||||
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
|
||||||
- Continue to next task
|
|
||||||
|
|
||||||
**Pause if:**
|
|
||||||
- Task is unclear → ask for clarification
|
|
||||||
- Implementation reveals a design issue → suggest updating artifacts
|
|
||||||
- Error or blocker encountered → report and wait for guidance
|
|
||||||
- User interrupts
|
|
||||||
|
|
||||||
7. **On completion or pause, show status**
|
|
||||||
|
|
||||||
Display:
|
|
||||||
- Tasks completed this session
|
|
||||||
- Overall progress: "N/M tasks complete"
|
|
||||||
- If all done: suggest archive
|
|
||||||
- If paused: explain why and wait for guidance
|
|
||||||
|
|
||||||
**Output During Implementation**
|
|
||||||
|
|
||||||
```
|
|
||||||
## Implementing: <change-name> (schema: <schema-name>)
|
|
||||||
|
|
||||||
Working on task 3/7: <task description>
|
|
||||||
[...implementation happening...]
|
|
||||||
✓ Task complete
|
|
||||||
|
|
||||||
Working on task 4/7: <task description>
|
|
||||||
[...implementation happening...]
|
|
||||||
✓ Task complete
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output On Completion**
|
|
||||||
|
|
||||||
```
|
|
||||||
## Implementation Complete
|
|
||||||
|
|
||||||
**Change:** <change-name>
|
|
||||||
**Schema:** <schema-name>
|
|
||||||
**Progress:** 7/7 tasks complete ✓
|
|
||||||
|
|
||||||
### Completed This Session
|
|
||||||
- [x] Task 1
|
|
||||||
- [x] Task 2
|
|
||||||
...
|
|
||||||
|
|
||||||
All tasks complete! Ready to archive this change.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output On Pause (Issue Encountered)**
|
|
||||||
|
|
||||||
```
|
|
||||||
## Implementation Paused
|
|
||||||
|
|
||||||
**Change:** <change-name>
|
|
||||||
**Schema:** <schema-name>
|
|
||||||
**Progress:** 4/7 tasks complete
|
|
||||||
|
|
||||||
### Issue Encountered
|
|
||||||
<description of the issue>
|
|
||||||
|
|
||||||
**Options:**
|
|
||||||
1. <option 1>
|
|
||||||
2. <option 2>
|
|
||||||
3. Other approach
|
|
||||||
|
|
||||||
What would you like to do?
|
|
||||||
```
|
|
||||||
|
|
||||||
**Guardrails**
|
|
||||||
- Keep going through tasks until done or blocked
|
|
||||||
- Always read context files before starting (from the apply instructions output)
|
|
||||||
- If task is ambiguous, pause and ask before implementing
|
|
||||||
- If implementation reveals issues, pause and suggest artifact updates
|
|
||||||
- Keep code changes minimal and scoped to each task
|
|
||||||
- Update task checkbox immediately after completing each task
|
|
||||||
- Pause on errors, blockers, or unclear requirements - don't guess
|
|
||||||
- Use contextFiles from CLI output, don't assume specific file names
|
|
||||||
|
|
||||||
**Fluid Workflow Integration**
|
|
||||||
|
|
||||||
This skill supports the "actions on a change" model:
|
|
||||||
|
|
||||||
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
|
||||||
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# .opencode/skills/openspec-archive-change (index)
|
|
||||||
dir: .opencode/skills/openspec-archive-change
|
|
||||||
|
|
||||||
## role
|
|
||||||
Provides a reusable automation skill for archiving completed experimental changes via the openspec CLI
|
|
||||||
## parent
|
|
||||||
index: .opencode/skills/.pi-map.index.md
|
|
||||||
map: .opencode/skills/.pi-map.md
|
|
||||||
## children
|
|
||||||
-
|
|
||||||
## files
|
|
||||||
- SKILL.md
|
|
||||||
## links
|
|
||||||
index: .opencode/skills/openspec-archive-change/.pi-map.index.md
|
|
||||||
map: .opencode/skills/openspec-archive-change/.pi-map.md
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# .opencode/skills/openspec-archive-change
|
|
||||||
dir: .opencode/skills/openspec-archive-change
|
|
||||||
|
|
||||||
index: .opencode/skills/openspec-archive-change/.pi-map.index.md
|
|
||||||
|
|
||||||
## role
|
|
||||||
Provides a reusable automation skill for archiving completed experimental changes via the openspec CLI
|
|
||||||
## files
|
|
||||||
- SKILL.md | Defines a skill for archiving completed changes in an experimental workflow using the openspec CLI. | dep: openspec CLI, AskUserQuestion tool, Task tool, file system (mkdir, mv, read), JSON parsing
|
|
||||||
## arch
|
|
||||||
Skill-based modular automation pattern using markdown-defined CLI operations with structured metadata and command templates
|
|
||||||
## tags
|
|
||||||
skill, defines, archiving, completed, changes, experimental, workflow, openspec
|
|
||||||
## symbols
|
|
||||||
-
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
---
|
|
||||||
name: openspec-archive-change
|
|
||||||
description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.
|
|
||||||
license: MIT
|
|
||||||
compatibility: Requires openspec CLI.
|
|
||||||
metadata:
|
|
||||||
author: openspec
|
|
||||||
version: "1.0"
|
|
||||||
generatedBy: "1.3.1"
|
|
||||||
---
|
|
||||||
|
|
||||||
Archive a completed change in the experimental workflow.
|
|
||||||
|
|
||||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
|
||||||
|
|
||||||
**Steps**
|
|
||||||
|
|
||||||
1. **If no change name provided, prompt for selection**
|
|
||||||
|
|
||||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
|
||||||
|
|
||||||
Show only active changes (not already archived).
|
|
||||||
Include the schema used for each change if available.
|
|
||||||
|
|
||||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
|
||||||
|
|
||||||
2. **Check artifact completion status**
|
|
||||||
|
|
||||||
Run `openspec status --change "<name>" --json` to check artifact completion.
|
|
||||||
|
|
||||||
Parse the JSON to understand:
|
|
||||||
- `schemaName`: The workflow being used
|
|
||||||
- `artifacts`: List of artifacts with their status (`done` or other)
|
|
||||||
|
|
||||||
**If any artifacts are not `done`:**
|
|
||||||
- Display warning listing incomplete artifacts
|
|
||||||
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
|
||||||
- Proceed if user confirms
|
|
||||||
|
|
||||||
3. **Check task completion status**
|
|
||||||
|
|
||||||
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
|
||||||
|
|
||||||
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
|
||||||
|
|
||||||
**If incomplete tasks found:**
|
|
||||||
- Display warning showing count of incomplete tasks
|
|
||||||
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
|
||||||
- Proceed if user confirms
|
|
||||||
|
|
||||||
**If no tasks file exists:** Proceed without task-related warning.
|
|
||||||
|
|
||||||
4. **Assess delta spec sync state**
|
|
||||||
|
|
||||||
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
|
|
||||||
|
|
||||||
**If delta specs exist:**
|
|
||||||
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
|
|
||||||
- Determine what changes would be applied (adds, modifications, removals, renames)
|
|
||||||
- Show a combined summary before prompting
|
|
||||||
|
|
||||||
**Prompt options:**
|
|
||||||
- If changes needed: "Sync now (recommended)", "Archive without syncing"
|
|
||||||
- If already synced: "Archive now", "Sync anyway", "Cancel"
|
|
||||||
|
|
||||||
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
|
|
||||||
|
|
||||||
5. **Perform the archive**
|
|
||||||
|
|
||||||
Create the archive directory if it doesn't exist:
|
|
||||||
```bash
|
|
||||||
mkdir -p openspec/changes/archive
|
|
||||||
```
|
|
||||||
|
|
||||||
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
|
||||||
|
|
||||||
**Check if target already exists:**
|
|
||||||
- If yes: Fail with error, suggest renaming existing archive or using different date
|
|
||||||
- If no: Move the change directory to archive
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
|
||||||
```
|
|
||||||
|
|
||||||
6. **Display summary**
|
|
||||||
|
|
||||||
Show archive completion summary including:
|
|
||||||
- Change name
|
|
||||||
- Schema that was used
|
|
||||||
- Archive location
|
|
||||||
- Whether specs were synced (if applicable)
|
|
||||||
- Note about any warnings (incomplete artifacts/tasks)
|
|
||||||
|
|
||||||
**Output On Success**
|
|
||||||
|
|
||||||
```
|
|
||||||
## Archive Complete
|
|
||||||
|
|
||||||
**Change:** <change-name>
|
|
||||||
**Schema:** <schema-name>
|
|
||||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
|
||||||
**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped")
|
|
||||||
|
|
||||||
All artifacts complete. All tasks complete.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Guardrails**
|
|
||||||
- Always prompt for change selection if not provided
|
|
||||||
- Use artifact graph (openspec status --json) for completion checking
|
|
||||||
- Don't block archive on warnings - just inform and confirm
|
|
||||||
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
|
||||||
- Show clear summary of what happened
|
|
||||||
- If sync is requested, use openspec-sync-specs approach (agent-driven)
|
|
||||||
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# .opencode/skills/openspec-explore (index)
|
|
||||||
dir: .opencode/skills/openspec-explore
|
|
||||||
|
|
||||||
## role
|
|
||||||
Defines a conversational AI skill/persona for "explore mode" that serves as a thinking partner for exploring ideas, investigating problems, and clarifying requirements without implementing code.
|
|
||||||
## parent
|
|
||||||
index: .opencode/skills/.pi-map.index.md
|
|
||||||
map: .opencode/skills/.pi-map.md
|
|
||||||
## children
|
|
||||||
-
|
|
||||||
## files
|
|
||||||
- SKILL.md
|
|
||||||
## links
|
|
||||||
index: .opencode/skills/openspec-explore/.pi-map.index.md
|
|
||||||
map: .opencode/skills/openspec-explore/.pi-map.md
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# .opencode/skills/openspec-explore
|
|
||||||
dir: .opencode/skills/openspec-explore
|
|
||||||
|
|
||||||
index: .opencode/skills/openspec-explore/.pi-map.index.md
|
|
||||||
|
|
||||||
## role
|
|
||||||
Defines a conversational AI skill/persona for "explore mode" that serves as a thinking partner for exploring ideas, investigating problems, and clarifying requirements without implementing code.
|
|
||||||
## files
|
|
||||||
- SKILL.md | Defines a conversational AI skill/persona for "explore mode" - a thinking partner for exploring ideas, investigating problems, and clarifying requirements without implementing code. | dep: openspec CLI
|
|
||||||
## arch
|
|
||||||
Single-file skill definition using markdown-based persona specification with structured sections for description, usage guidelines, and behavioral constraints.
|
|
||||||
## tags
|
|
||||||
skill, defines, conversational, persona, explore, mode, thinking, partner
|
|
||||||
## symbols
|
|
||||||
-
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
---
|
|
||||||
name: openspec-explore
|
|
||||||
description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.
|
|
||||||
license: MIT
|
|
||||||
compatibility: Requires openspec CLI.
|
|
||||||
metadata:
|
|
||||||
author: openspec
|
|
||||||
version: "1.0"
|
|
||||||
generatedBy: "1.3.1"
|
|
||||||
---
|
|
||||||
|
|
||||||
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
|
||||||
|
|
||||||
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
|
|
||||||
|
|
||||||
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The Stance
|
|
||||||
|
|
||||||
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
|
||||||
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
|
||||||
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
|
||||||
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
|
||||||
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
|
||||||
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What You Might Do
|
|
||||||
|
|
||||||
Depending on what the user brings, you might:
|
|
||||||
|
|
||||||
**Explore the problem space**
|
|
||||||
- Ask clarifying questions that emerge from what they said
|
|
||||||
- Challenge assumptions
|
|
||||||
- Reframe the problem
|
|
||||||
- Find analogies
|
|
||||||
|
|
||||||
**Investigate the codebase**
|
|
||||||
- Map existing architecture relevant to the discussion
|
|
||||||
- Find integration points
|
|
||||||
- Identify patterns already in use
|
|
||||||
- Surface hidden complexity
|
|
||||||
|
|
||||||
**Compare options**
|
|
||||||
- Brainstorm multiple approaches
|
|
||||||
- Build comparison tables
|
|
||||||
- Sketch tradeoffs
|
|
||||||
- Recommend a path (if asked)
|
|
||||||
|
|
||||||
**Visualize**
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────┐
|
|
||||||
│ Use ASCII diagrams liberally │
|
|
||||||
├─────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ ┌────────┐ ┌────────┐ │
|
|
||||||
│ │ State │────────▶│ State │ │
|
|
||||||
│ │ A │ │ B │ │
|
|
||||||
│ └────────┘ └────────┘ │
|
|
||||||
│ │
|
|
||||||
│ System diagrams, state machines, │
|
|
||||||
│ data flows, architecture sketches, │
|
|
||||||
│ dependency graphs, comparison tables │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Surface risks and unknowns**
|
|
||||||
- Identify what could go wrong
|
|
||||||
- Find gaps in understanding
|
|
||||||
- Suggest spikes or investigations
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## OpenSpec Awareness
|
|
||||||
|
|
||||||
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
|
||||||
|
|
||||||
### Check for context
|
|
||||||
|
|
||||||
At the start, quickly check what exists:
|
|
||||||
```bash
|
|
||||||
openspec list --json
|
|
||||||
```
|
|
||||||
|
|
||||||
This tells you:
|
|
||||||
- If there are active changes
|
|
||||||
- Their names, schemas, and status
|
|
||||||
- What the user might be working on
|
|
||||||
|
|
||||||
### When no change exists
|
|
||||||
|
|
||||||
Think freely. When insights crystallize, you might offer:
|
|
||||||
|
|
||||||
- "This feels solid enough to start a change. Want me to create a proposal?"
|
|
||||||
- Or keep exploring - no pressure to formalize
|
|
||||||
|
|
||||||
### When a change exists
|
|
||||||
|
|
||||||
If the user mentions a change or you detect one is relevant:
|
|
||||||
|
|
||||||
1. **Read existing artifacts for context**
|
|
||||||
- `openspec/changes/<name>/proposal.md`
|
|
||||||
- `openspec/changes/<name>/design.md`
|
|
||||||
- `openspec/changes/<name>/tasks.md`
|
|
||||||
- etc.
|
|
||||||
|
|
||||||
2. **Reference them naturally in conversation**
|
|
||||||
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
|
||||||
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
|
||||||
|
|
||||||
3. **Offer to capture when decisions are made**
|
|
||||||
|
|
||||||
| Insight Type | Where to Capture |
|
|
||||||
|----------------------------|--------------------------------|
|
|
||||||
| New requirement discovered | `specs/<capability>/spec.md` |
|
|
||||||
| Requirement changed | `specs/<capability>/spec.md` |
|
|
||||||
| Design decision made | `design.md` |
|
|
||||||
| Scope changed | `proposal.md` |
|
|
||||||
| New work identified | `tasks.md` |
|
|
||||||
| Assumption invalidated | Relevant artifact |
|
|
||||||
|
|
||||||
Example offers:
|
|
||||||
- "That's a design decision. Capture it in design.md?"
|
|
||||||
- "This is a new requirement. Add it to specs?"
|
|
||||||
- "This changes scope. Update the proposal?"
|
|
||||||
|
|
||||||
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What You Don't Have To Do
|
|
||||||
|
|
||||||
- Follow a script
|
|
||||||
- Ask the same questions every time
|
|
||||||
- Produce a specific artifact
|
|
||||||
- Reach a conclusion
|
|
||||||
- Stay on topic if a tangent is valuable
|
|
||||||
- Be brief (this is thinking time)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Handling Different Entry Points
|
|
||||||
|
|
||||||
**User brings a vague idea:**
|
|
||||||
```
|
|
||||||
User: I'm thinking about adding real-time collaboration
|
|
||||||
|
|
||||||
You: Real-time collab is a big space. Let me think about this...
|
|
||||||
|
|
||||||
COLLABORATION SPECTRUM
|
|
||||||
════════════════════════════════════════════
|
|
||||||
|
|
||||||
Awareness Coordination Sync
|
|
||||||
│ │ │
|
|
||||||
▼ ▼ ▼
|
|
||||||
┌────────┐ ┌────────┐ ┌────────┐
|
|
||||||
│Presence│ │Cursors │ │ CRDT │
|
|
||||||
│ "3 │ │ Multi │ │Conflict│
|
|
||||||
│online" │ │ select │ │ free │
|
|
||||||
└────────┘ └────────┘ └────────┘
|
|
||||||
│ │ │
|
|
||||||
trivial moderate complex
|
|
||||||
|
|
||||||
Where's your head at?
|
|
||||||
```
|
|
||||||
|
|
||||||
**User brings a specific problem:**
|
|
||||||
```
|
|
||||||
User: The auth system is a mess
|
|
||||||
|
|
||||||
You: [reads codebase]
|
|
||||||
|
|
||||||
┌─────────────────────────────────────────────┐
|
|
||||||
│ CURRENT AUTH FLOW │
|
|
||||||
└─────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌─────────────┼─────────────┐
|
|
||||||
▼ ▼ ▼
|
|
||||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
|
||||||
│ Google │ │ GitHub │ │ Email │
|
|
||||||
│ OAuth │ │ OAuth │ │ Magic │
|
|
||||||
└────┬────┘ └────┬────┘ └────┬────┘
|
|
||||||
│ │ │
|
|
||||||
└─────────────┼─────────────┘
|
|
||||||
▼
|
|
||||||
┌───────────┐
|
|
||||||
│ Session │
|
|
||||||
└─────┬─────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌───────────┐
|
|
||||||
│ Perms │
|
|
||||||
└───────────┘
|
|
||||||
|
|
||||||
I see three tangles. Which one's burning?
|
|
||||||
```
|
|
||||||
|
|
||||||
**User is stuck mid-implementation:**
|
|
||||||
```
|
|
||||||
User: /opsx-explore add-auth-system
|
|
||||||
The OAuth integration is more complex than expected
|
|
||||||
|
|
||||||
You: [reads change artifacts]
|
|
||||||
|
|
||||||
You're on task 4: "Implement OAuth flow"
|
|
||||||
|
|
||||||
Let me trace what's involved...
|
|
||||||
|
|
||||||
[draws diagram, explores options, suggests paths]
|
|
||||||
|
|
||||||
Want to update the design to reflect this?
|
|
||||||
Or add a spike task to investigate?
|
|
||||||
```
|
|
||||||
|
|
||||||
**User wants to compare options:**
|
|
||||||
```
|
|
||||||
User: Should we use Postgres or SQLite?
|
|
||||||
|
|
||||||
You: Generic answer is boring. What's the context?
|
|
||||||
|
|
||||||
User: A CLI tool that tracks local dev environments
|
|
||||||
|
|
||||||
You: That changes everything.
|
|
||||||
|
|
||||||
┌─────────────────────────────────────────────────┐
|
|
||||||
│ CLI TOOL DATA STORAGE │
|
|
||||||
└─────────────────────────────────────────────────┘
|
|
||||||
|
|
||||||
Key constraints:
|
|
||||||
• No daemon running
|
|
||||||
• Must work offline
|
|
||||||
• Single user
|
|
||||||
|
|
||||||
SQLite Postgres
|
|
||||||
Deployment embedded ✓ needs server ✗
|
|
||||||
Offline yes ✓ no ✗
|
|
||||||
Single file yes ✓ no ✗
|
|
||||||
|
|
||||||
SQLite. Not even close.
|
|
||||||
|
|
||||||
Unless... is there a sync component?
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ending Discovery
|
|
||||||
|
|
||||||
There's no required ending. Discovery might:
|
|
||||||
|
|
||||||
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
|
|
||||||
- **Result in artifact updates**: "Updated design.md with these decisions"
|
|
||||||
- **Just provide clarity**: User has what they need, moves on
|
|
||||||
- **Continue later**: "We can pick this up anytime"
|
|
||||||
|
|
||||||
When it feels like things are crystallizing, you might summarize:
|
|
||||||
|
|
||||||
```
|
|
||||||
## What We Figured Out
|
|
||||||
|
|
||||||
**The problem**: [crystallized understanding]
|
|
||||||
|
|
||||||
**The approach**: [if one emerged]
|
|
||||||
|
|
||||||
**Open questions**: [if any remain]
|
|
||||||
|
|
||||||
**Next steps** (if ready):
|
|
||||||
- Create a change proposal
|
|
||||||
- Keep exploring: just keep talking
|
|
||||||
```
|
|
||||||
|
|
||||||
But this summary is optional. Sometimes the thinking IS the value.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Guardrails
|
|
||||||
|
|
||||||
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
|
|
||||||
- **Don't fake understanding** - If something is unclear, dig deeper
|
|
||||||
- **Don't rush** - Discovery is thinking time, not task time
|
|
||||||
- **Don't force structure** - Let patterns emerge naturally
|
|
||||||
- **Don't auto-capture** - Offer to save insights, don't just do it
|
|
||||||
- **Do visualize** - A good diagram is worth many paragraphs
|
|
||||||
- **Do explore the codebase** - Ground discussions in reality
|
|
||||||
- **Do question assumptions** - Including the user's and your own
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# .opencode/skills/openspec-propose (index)
|
|
||||||
dir: .opencode/skills/openspec-propose
|
|
||||||
|
|
||||||
## role
|
|
||||||
Provides a structured workflow skill for proposing new changes using the openspec CLI with artifact generation in dependency order.
|
|
||||||
## parent
|
|
||||||
index: .opencode/skills/.pi-map.index.md
|
|
||||||
map: .opencode/skills/.pi-map.md
|
|
||||||
## children
|
|
||||||
-
|
|
||||||
## files
|
|
||||||
- SKILL.md
|
|
||||||
## links
|
|
||||||
index: .opencode/skills/openspec-propose/.pi-map.index.md
|
|
||||||
map: .opencode/skills/openspec-propose/.pi-map.md
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# .opencode/skills/openspec-propose
|
|
||||||
dir: .opencode/skills/openspec-propose
|
|
||||||
|
|
||||||
index: .opencode/skills/openspec-propose/.pi-map.index.md
|
|
||||||
|
|
||||||
## role
|
|
||||||
Provides a structured workflow skill for proposing new changes using the openspec CLI with artifact generation in dependency order.
|
|
||||||
## files
|
|
||||||
- SKILL.md | Defines a structured workflow for proposing new changes using the openspec CLI, generating proposal, design, and task artifacts in dependency order. | dep: openspec CLI, AskUserQuestion tool, TodoWrite tool
|
|
||||||
## arch
|
|
||||||
Template-based skill definition using markdown documentation with sequential artifact generation (proposal → design → tasks) following dependency ordering.
|
|
||||||
## tags
|
|
||||||
skill, defines, structured, workflow, proposing, new, changes, openspec
|
|
||||||
## symbols
|
|
||||||
-
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
---
|
|
||||||
name: openspec-propose
|
|
||||||
description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.
|
|
||||||
license: MIT
|
|
||||||
compatibility: Requires openspec CLI.
|
|
||||||
metadata:
|
|
||||||
author: openspec
|
|
||||||
version: "1.0"
|
|
||||||
generatedBy: "1.3.1"
|
|
||||||
---
|
|
||||||
|
|
||||||
Propose a new change - create the change and generate all artifacts in one step.
|
|
||||||
|
|
||||||
I'll create a change with artifacts:
|
|
||||||
- proposal.md (what & why)
|
|
||||||
- design.md (how)
|
|
||||||
- tasks.md (implementation steps)
|
|
||||||
|
|
||||||
When ready to implement, run /opsx-apply
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
|
||||||
|
|
||||||
**Steps**
|
|
||||||
|
|
||||||
1. **If no clear input provided, ask what they want to build**
|
|
||||||
|
|
||||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
|
||||||
> "What change do you want to work on? Describe what you want to build or fix."
|
|
||||||
|
|
||||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
|
||||||
|
|
||||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
|
||||||
|
|
||||||
2. **Create the change directory**
|
|
||||||
```bash
|
|
||||||
openspec new change "<name>"
|
|
||||||
```
|
|
||||||
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
|
||||||
|
|
||||||
3. **Get the artifact build order**
|
|
||||||
```bash
|
|
||||||
openspec status --change "<name>" --json
|
|
||||||
```
|
|
||||||
Parse the JSON to get:
|
|
||||||
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
|
||||||
- `artifacts`: list of all artifacts with their status and dependencies
|
|
||||||
|
|
||||||
4. **Create artifacts in sequence until apply-ready**
|
|
||||||
|
|
||||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
|
||||||
|
|
||||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
|
||||||
|
|
||||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
|
||||||
- Get instructions:
|
|
||||||
```bash
|
|
||||||
openspec instructions <artifact-id> --change "<name>" --json
|
|
||||||
```
|
|
||||||
- The instructions JSON includes:
|
|
||||||
- `context`: Project background (constraints for you - do NOT include in output)
|
|
||||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
|
||||||
- `template`: The structure to use for your output file
|
|
||||||
- `instruction`: Schema-specific guidance for this artifact type
|
|
||||||
- `outputPath`: Where to write the artifact
|
|
||||||
- `dependencies`: Completed artifacts to read for context
|
|
||||||
- Read any completed dependency files for context
|
|
||||||
- Create the artifact file using `template` as the structure
|
|
||||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
|
||||||
- Show brief progress: "Created <artifact-id>"
|
|
||||||
|
|
||||||
b. **Continue until all `applyRequires` artifacts are complete**
|
|
||||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
|
||||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
|
||||||
- Stop when all `applyRequires` artifacts are done
|
|
||||||
|
|
||||||
c. **If an artifact requires user input** (unclear context):
|
|
||||||
- Use **AskUserQuestion tool** to clarify
|
|
||||||
- Then continue with creation
|
|
||||||
|
|
||||||
5. **Show final status**
|
|
||||||
```bash
|
|
||||||
openspec status --change "<name>"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output**
|
|
||||||
|
|
||||||
After completing all artifacts, summarize:
|
|
||||||
- Change name and location
|
|
||||||
- List of artifacts created with brief descriptions
|
|
||||||
- What's ready: "All artifacts created! Ready for implementation."
|
|
||||||
- Prompt: "Run `/opsx-apply` or ask me to implement to start working on the tasks."
|
|
||||||
|
|
||||||
**Artifact Creation Guidelines**
|
|
||||||
|
|
||||||
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
|
||||||
- The schema defines what each artifact should contain - follow it
|
|
||||||
- Read dependency artifacts for context before creating new ones
|
|
||||||
- Use `template` as the structure for your output file - fill in its sections
|
|
||||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
|
||||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
|
||||||
- These guide what you write, but should never appear in the output
|
|
||||||
|
|
||||||
**Guardrails**
|
|
||||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
|
||||||
- Always read dependency artifacts before creating a new one
|
|
||||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
|
||||||
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
|
||||||
- Verify each artifact file exists after writing before proceeding to next
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
# . (index)
|
|
||||||
dir: .
|
|
||||||
|
|
||||||
## Project Map Protocol
|
|
||||||
|
|
||||||
1. Read this protocol and the root `.pi-map.index.md` first.
|
|
||||||
2. Use `index:` / `map:` references to open relevant directory indexes and maps.
|
|
||||||
3. Load indexes before rich maps during task-start navigation.
|
|
||||||
4. Read the local rich map and actual source before editing.
|
|
||||||
5. Treat non-empty `## dirty` sections in either artifact as stale.
|
|
||||||
6. If source and generated artifacts disagree, trust source.
|
|
||||||
7. If map and index disagree, trust neither blindly; verify from source and regenerate the pair.
|
|
||||||
8. After editing source, run `project_map_patch` for each changed file.
|
|
||||||
9. Before broad architectural claims or final handoff, run `project_map_validate` when freshness matters.
|
|
||||||
|
|
||||||
Trust boundary: index routes, map orients, source decides.
|
|
||||||
|
|
||||||
## role
|
|
||||||
Infrastructure and deployment configuration package for a self-hosted project management platform with OAuth2 authentication, providing containerized orchestration, environment templates, and development tooling.
|
|
||||||
## parent
|
|
||||||
-
|
|
||||||
## children
|
|
||||||
- .atl
|
|
||||||
index: .atl/.pi-map.index.md
|
|
||||||
map: .atl/.pi-map.md
|
|
||||||
- .claude
|
|
||||||
index: .claude/.pi-map.index.md
|
|
||||||
map: .claude/.pi-map.md
|
|
||||||
- .opencode
|
|
||||||
index: .opencode/.pi-map.index.md
|
|
||||||
map: .opencode/.pi-map.md
|
|
||||||
- .pi
|
|
||||||
index: .pi/.pi-map.index.md
|
|
||||||
map: .pi/.pi-map.md
|
|
||||||
- .sisyphus
|
|
||||||
index: .sisyphus/.pi-map.index.md
|
|
||||||
map: .sisyphus/.pi-map.md
|
|
||||||
- .stoneforge
|
|
||||||
index: .stoneforge/.pi-map.index.md
|
|
||||||
map: .stoneforge/.pi-map.md
|
|
||||||
- apps
|
|
||||||
index: apps/.pi-map.index.md
|
|
||||||
map: apps/.pi-map.md
|
|
||||||
- docs
|
|
||||||
index: docs/.pi-map.index.md
|
|
||||||
map: docs/.pi-map.md
|
|
||||||
- e2e
|
|
||||||
index: e2e/.pi-map.index.md
|
|
||||||
map: e2e/.pi-map.md
|
|
||||||
- minerv3
|
|
||||||
index: minerv3/.pi-map.index.md
|
|
||||||
map: minerv3/.pi-map.md
|
|
||||||
- openspec
|
|
||||||
index: openspec/.pi-map.index.md
|
|
||||||
map: openspec/.pi-map.md
|
|
||||||
- scripts
|
|
||||||
index: scripts/.pi-map.index.md
|
|
||||||
map: scripts/.pi-map.md
|
|
||||||
- tool-images
|
|
||||||
index: tool-images/.pi-map.index.md
|
|
||||||
map: tool-images/.pi-map.md
|
|
||||||
- uploads
|
|
||||||
index: uploads/.pi-map.index.md
|
|
||||||
map: uploads/.pi-map.md
|
|
||||||
## files
|
|
||||||
- .env.example
|
|
||||||
- .gitignore
|
|
||||||
- AGENTS.md
|
|
||||||
- CHANGELOG.md
|
|
||||||
- Makefile
|
|
||||||
- README.md
|
|
||||||
- docker-compose.traefik.yml
|
|
||||||
- docker-compose.yml
|
|
||||||
- progress.md
|
|
||||||
- swap-pane
|
|
||||||
## links
|
|
||||||
index: ./.pi-map.index.md
|
|
||||||
map: ./.pi-map.md
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
-42
@@ -1,42 +0,0 @@
|
|||||||
# .
|
|
||||||
dir: .
|
|
||||||
|
|
||||||
index: ./.pi-map.index.md
|
|
||||||
|
|
||||||
## Project Map Protocol
|
|
||||||
|
|
||||||
1. Read this protocol and the root `.pi-map.index.md` first.
|
|
||||||
2. Use `index:` / `map:` references to open relevant directory indexes and maps.
|
|
||||||
3. Load indexes before rich maps during task-start navigation.
|
|
||||||
4. Read the local rich map and actual source before editing.
|
|
||||||
5. Treat non-empty `## dirty` sections in either artifact as stale.
|
|
||||||
6. If source and generated artifacts disagree, trust source.
|
|
||||||
7. If map and index disagree, trust neither blindly; verify from source and regenerate the pair.
|
|
||||||
8. After editing source, run `project_map_patch` for each changed file.
|
|
||||||
9. Before broad architectural claims or final handoff, run `project_map_validate` when freshness matters.
|
|
||||||
|
|
||||||
Trust boundary: index routes, map orients, source decides.
|
|
||||||
|
|
||||||
## role
|
|
||||||
Infrastructure and deployment configuration package for a self-hosted project management platform with OAuth2 authentication, providing containerized orchestration, environment templates, and development tooling.
|
|
||||||
## files
|
|
||||||
- .env.example | Provides a template of environment variables for configuring a Headquarter application with PostgreSQL, Redis, Authentik SSO, and Docker/Traefik deployment
|
|
||||||
- .gitignore | Specifies files and directories for Git to ignore across a multi-language project with Python, Node, and custom tooling | dep: Git
|
|
||||||
- AGENTS.md | Defines operational rules, workflows, and constraints for AI agents working within an OpenSpec-driven software development project. | dep: OpenSpec, superpowers, git, docker compose, conventional commits
|
|
||||||
- CHANGELOG.md | Documents version history and notable changes for a Git-based project management web application
|
|
||||||
- Makefile | Provides standard development commands for containerized web application lifecycle management via Docker Compose | dep: docker compose, alembic, pytest, ruff, mypy, playwright, npm, postgres, redis
|
|
||||||
- README.md | A self-hosted platform for managing projects, git repositories, and development tools with OAuth2 authentication. | dep: FastAPI, SQLAlchemy, Pydantic, Alembic, python-jose, React, TypeScript, Vite, React Router, Docker, PostgreSQL, Traefik, Authentik, Git
|
|
||||||
- docker-compose.traefik.yml | Deploys a multi-service web application (frontend, API, PostgreSQL, Redis) behind an existing Traefik reverse proxy with TLS termination and environment-configurable domains. | dep: docker, traefik, postgres, redis, authentik, docker-compose
|
|
||||||
- docker-compose.yml | Defines a multi-service Docker Compose stack with PostgreSQL, Redis, API backend, and web frontend services for a "headquarter" application | dep: Docker, PostgreSQL, Redis, Vite, asyncpg, nginx
|
|
||||||
- progress.md | Tracks completed and remaining tasks for a backend-frontend code refactoring project organized in 7 phases
|
|
||||||
- swap-pane | Empty file with no functionality
|
|
||||||
## arch
|
|
||||||
Docker Compose-based microservices architecture with frontend/backend separation, PostgreSQL/Redis data layer, Traefik reverse proxy integration, and environment-driven configuration management following twelve-factor app principles.
|
|
||||||
## tags
|
|
||||||
docker, redis, git, application, postgresql, compose, traefik, project
|
|
||||||
## symbols
|
|
||||||
-
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"sessionID": "ses_1da2608b1ffergOzow3NQt1mGr",
|
|
||||||
"updatedAt": "2026-05-15T23:50:42.832Z",
|
|
||||||
"sources": {
|
|
||||||
"background-task": {
|
|
||||||
"state": "idle",
|
|
||||||
"updatedAt": "2026-05-15T23:50:42.832Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
262629
|
|
||||||
1779624255076
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# Runtime data
|
|
||||||
*.db
|
|
||||||
*.db-journal
|
|
||||||
*.db-wal
|
|
||||||
*.db-shm
|
|
||||||
daemon-state.json
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# .stoneforge (index)
|
|
||||||
dir: .stoneforge
|
|
||||||
|
|
||||||
## role
|
|
||||||
Internal configuration and state tracking directory for the Stoneforge application
|
|
||||||
## parent
|
|
||||||
index: ./.pi-map.index.md
|
|
||||||
map: ./.pi-map.md
|
|
||||||
## children
|
|
||||||
- .stoneforge/sync
|
|
||||||
index: .stoneforge/sync/.pi-map.index.md
|
|
||||||
map: .stoneforge/sync/.pi-map.md
|
|
||||||
## files
|
|
||||||
- .dashboard-opened
|
|
||||||
- .gitignore
|
|
||||||
- config.yaml
|
|
||||||
## links
|
|
||||||
index: .stoneforge/.pi-map.index.md
|
|
||||||
map: .stoneforge/.pi-map.md
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# .stoneforge
|
|
||||||
dir: .stoneforge
|
|
||||||
|
|
||||||
index: .stoneforge/.pi-map.index.md
|
|
||||||
|
|
||||||
## role
|
|
||||||
Internal configuration and state tracking directory for the Stoneforge application
|
|
||||||
## files
|
|
||||||
- .dashboard-opened | Stores timestamp and identifier data for tracking when a dashboard was opened
|
|
||||||
- .gitignore | Specifies files and patterns for Git to ignore in version control
|
|
||||||
- config.yaml | Configuration file for the Stoneforge application defining database, sync, playbook, identity, merge, workflow, and agent settings.
|
|
||||||
## arch
|
|
||||||
Simple dot-directory pattern storing metadata (.dashboard-opened), version control exclusions (.gitignore), and hierarchical YAML configuration (config.yaml) with domain-separated settings
|
|
||||||
## tags
|
|
||||||
config, stores, timestamp, identifier, data, tracking, dashboard, was
|
|
||||||
## symbols
|
|
||||||
-
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Stoneforge Configuration
|
|
||||||
|
|
||||||
database: stoneforge.db
|
|
||||||
sync:
|
|
||||||
auto_export: true
|
|
||||||
elements_file: elements.jsonl
|
|
||||||
dependencies_file: dependencies.jsonl
|
|
||||||
playbooks:
|
|
||||||
paths:
|
|
||||||
- playbooks
|
|
||||||
identity:
|
|
||||||
mode: soft
|
|
||||||
merge:
|
|
||||||
auto_merge: true
|
|
||||||
target_branch: null
|
|
||||||
require_approval: false
|
|
||||||
workflow:
|
|
||||||
preset: auto
|
|
||||||
agents:
|
|
||||||
permission_model: unrestricted
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# .stoneforge/sync (index)
|
|
||||||
dir: .stoneforge/sync
|
|
||||||
|
|
||||||
## role
|
|
||||||
Persists distributed task execution state by storing dependency graphs and ephemeral worker agent records for a collaborative workflow system.
|
|
||||||
## parent
|
|
||||||
index: .stoneforge/.pi-map.index.md
|
|
||||||
map: .stoneforge/.pi-map.md
|
|
||||||
## children
|
|
||||||
-
|
|
||||||
## files
|
|
||||||
- dependencies.jsonl
|
|
||||||
- elements.jsonl
|
|
||||||
## links
|
|
||||||
index: .stoneforge/sync/.pi-map.index.md
|
|
||||||
map: .stoneforge/sync/.pi-map.md
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# .stoneforge/sync
|
|
||||||
dir: .stoneforge/sync
|
|
||||||
|
|
||||||
index: .stoneforge/sync/.pi-map.index.md
|
|
||||||
|
|
||||||
## role
|
|
||||||
Persists distributed task execution state by storing dependency graphs and ephemeral worker agent records for a collaborative workflow system.
|
|
||||||
## files
|
|
||||||
- dependencies.jsonl | Stores a sequence of dependency relationships between entities in JSON Lines format, tracking parent-child, blocking, and reply relationships with timestamps and creators.
|
|
||||||
- elements.jsonl | Stores JSONL records of ephemeral worker agents with their session history, worktree assignments, and lifecycle metadata for a distributed task execution system.
|
|
||||||
## arch
|
|
||||||
Event-sourced JSONL append-only logs with entity-relationship modeling (parent-child, blocking, reply) and session-based worker lifecycle tracking.
|
|
||||||
## tags
|
|
||||||
stores, relationships, dependencies, elements, sequence, dependency, entities, json
|
|
||||||
## symbols
|
|
||||||
-
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
{"blockedId":"el-1of","blockerId":"el-258","type":"parent-child","createdAt":"2026-05-24T09:44:58.759Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-5fe","blockerId":"el-20no","type":"parent-child","createdAt":"2026-05-24T12:44:40.892Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-1nj","blockerId":"el-20no","type":"parent-child","createdAt":"2026-05-24T12:44:41.010Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-1bn","blockerId":"el-20no","type":"parent-child","createdAt":"2026-05-24T12:44:41.127Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-4hr","blockerId":"el-20no","type":"parent-child","createdAt":"2026-05-24T12:44:41.244Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-62c","blockerId":"el-20no","type":"parent-child","createdAt":"2026-05-24T12:44:41.372Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-5z8","blockerId":"el-20no","type":"parent-child","createdAt":"2026-05-24T12:44:41.490Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-1t7","blockerId":"el-20no","type":"parent-child","createdAt":"2026-05-24T12:44:41.607Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-5j5","blockerId":"el-20no","type":"parent-child","createdAt":"2026-05-24T12:44:41.726Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-2xl","blockerId":"el-20no","type":"parent-child","createdAt":"2026-05-24T12:44:41.844Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-4bc","blockerId":"el-20no","type":"parent-child","createdAt":"2026-05-24T12:44:41.959Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-107","blockerId":"el-20no","type":"parent-child","createdAt":"2026-05-24T12:44:42.074Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-32e","blockerId":"el-20no","type":"parent-child","createdAt":"2026-05-24T12:44:42.195Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-3ou","blockerId":"el-20no","type":"parent-child","createdAt":"2026-05-24T12:44:42.311Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-14w","blockerId":"el-20no","type":"parent-child","createdAt":"2026-05-24T12:44:42.425Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-1ou","blockerId":"el-20no","type":"parent-child","createdAt":"2026-05-24T12:44:42.541Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-1nj","blockerId":"el-5fe","type":"blocks","createdAt":"2026-05-24T12:44:42.651Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-1bn","blockerId":"el-5fe","type":"blocks","createdAt":"2026-05-24T12:44:42.761Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-4hr","blockerId":"el-5fe","type":"blocks","createdAt":"2026-05-24T12:44:42.868Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-62c","blockerId":"el-1nj","type":"blocks","createdAt":"2026-05-24T12:44:42.979Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-62c","blockerId":"el-1bn","type":"blocks","createdAt":"2026-05-24T12:44:43.092Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-5z8","blockerId":"el-1nj","type":"blocks","createdAt":"2026-05-24T12:44:43.205Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-5z8","blockerId":"el-4hr","type":"blocks","createdAt":"2026-05-24T12:44:43.313Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-1t7","blockerId":"el-1bn","type":"blocks","createdAt":"2026-05-24T12:44:43.422Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-1t7","blockerId":"el-4hr","type":"blocks","createdAt":"2026-05-24T12:44:43.529Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-1t7","blockerId":"el-62c","type":"blocks","createdAt":"2026-05-24T12:44:43.647Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-5j5","blockerId":"el-1t7","type":"blocks","createdAt":"2026-05-24T12:44:43.758Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-2xl","blockerId":"el-1t7","type":"blocks","createdAt":"2026-05-24T12:44:43.876Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-4bc","blockerId":"el-1nj","type":"blocks","createdAt":"2026-05-24T12:44:43.987Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-4bc","blockerId":"el-1bn","type":"blocks","createdAt":"2026-05-24T12:44:44.096Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-4bc","blockerId":"el-62c","type":"blocks","createdAt":"2026-05-24T12:44:44.208Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-107","blockerId":"el-5z8","type":"blocks","createdAt":"2026-05-24T12:44:44.319Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-32e","blockerId":"el-5j5","type":"blocks","createdAt":"2026-05-24T12:44:44.429Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-32e","blockerId":"el-2xl","type":"blocks","createdAt":"2026-05-24T12:44:44.539Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-3ou","blockerId":"el-4bc","type":"blocks","createdAt":"2026-05-24T12:44:44.650Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-3ou","blockerId":"el-107","type":"blocks","createdAt":"2026-05-24T12:44:44.761Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-14w","blockerId":"el-32e","type":"blocks","createdAt":"2026-05-24T12:44:44.873Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-1ou","blockerId":"el-3ou","type":"blocks","createdAt":"2026-05-24T12:44:44.987Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-1ou","blockerId":"el-14w","type":"blocks","createdAt":"2026-05-24T12:44:45.107Z","createdBy":"el-2jua"}
|
|
||||||
{"blockedId":"el-375","blockerId":"el-26p","type":"replies-to","createdAt":"2026-05-24T13:21:42.486Z","createdBy":"el-2i1s"}
|
|
||||||
{"blockedId":"el-3n4","blockerId":"el-31p","type":"replies-to","createdAt":"2026-05-24T13:21:46.044Z","createdBy":"el-13ju"}
|
|
||||||
{"blockedId":"el-3jer","blockerId":"el-1xx","type":"replies-to","createdAt":"2026-05-24T13:24:47.580Z","createdBy":"el-4350"}
|
|
||||||
{"blockedId":"el-1afv","blockerId":"el-1ozw","type":"replies-to","createdAt":"2026-05-24T13:32:42.658Z","createdBy":"el-51a8"}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,158 +0,0 @@
|
|||||||
# AGENTS.md
|
|
||||||
|
|
||||||
## Core rule
|
|
||||||
|
|
||||||
OpenSpec is the source of truth. Superpowers is the default workflow. Keep changes small, scoped, and verified.
|
|
||||||
|
|
||||||
## Communication
|
|
||||||
|
|
||||||
All agent output, code comments, commit messages, documentation, and artifacts must be in **English** unless the user explicitly requests another language.
|
|
||||||
|
|
||||||
## Priority order
|
|
||||||
|
|
||||||
1. Current user instruction
|
|
||||||
2. OpenSpec proposal, tasks, and spec deltas
|
|
||||||
3. This `AGENTS.md`
|
|
||||||
4. Existing project conventions
|
|
||||||
5. Agent assumptions
|
|
||||||
|
|
||||||
When instructions conflict, follow the higher-priority source. Do not silently expand scope.
|
|
||||||
|
|
||||||
## Default workflow
|
|
||||||
|
|
||||||
For any non-trivial change:
|
|
||||||
|
|
||||||
1. Read the relevant OpenSpec change, tasks, and spec deltas.
|
|
||||||
2. Use `brainstorming` if scope, design, or requirements are unclear.
|
|
||||||
3. Use `writing-plans` before implementation.
|
|
||||||
4. Implement only the selected task or clearly requested change.
|
|
||||||
5. Use tests, typecheck, lint, or targeted checks to verify.
|
|
||||||
6. Use `verification-before-completion` before claiming completion.
|
|
||||||
|
|
||||||
If namespacing is required, use:
|
|
||||||
|
|
||||||
* `superpowers:brainstorming`
|
|
||||||
* `superpowers:writing-plans`
|
|
||||||
* `superpowers:test-driven-development`
|
|
||||||
* `superpowers:systematic-debugging`
|
|
||||||
* `superpowers:verification-before-completion`
|
|
||||||
|
|
||||||
## When OpenSpec is required
|
|
||||||
|
|
||||||
Create or update an OpenSpec change before implementing:
|
|
||||||
|
|
||||||
* New features
|
|
||||||
* Behavior changes
|
|
||||||
* API changes
|
|
||||||
* Database/schema changes
|
|
||||||
* Auth, security, billing, permissions, or data handling changes
|
|
||||||
* Architecture changes
|
|
||||||
* Large refactors
|
|
||||||
* Anything with unclear acceptance criteria
|
|
||||||
|
|
||||||
Small local fixes may skip OpenSpec if they do not change behavior or public contracts.
|
|
||||||
|
|
||||||
## Superpowers usage
|
|
||||||
|
|
||||||
Use:
|
|
||||||
|
|
||||||
* `brainstorming` for ambiguity, design choices, or scope questions.
|
|
||||||
* `writing-plans` for multi-step or multi-file work.
|
|
||||||
* `test-driven-development` for behavior changes and bug fixes where practical.
|
|
||||||
* `systematic-debugging` for failing tests or unclear bugs.
|
|
||||||
* `verification-before-completion` before final completion claims.
|
|
||||||
* `using-git-worktrees` only for isolated risky or parallel work.
|
|
||||||
* `dispatching-parallel-agents` only for independent subtasks with clear boundaries.
|
|
||||||
|
|
||||||
If a skill is unavailable, follow its intent manually and say so.
|
|
||||||
|
|
||||||
## Scope discipline
|
|
||||||
|
|
||||||
Do not:
|
|
||||||
|
|
||||||
* Implement outside the selected OpenSpec task.
|
|
||||||
* Mix unrelated cleanup with feature work.
|
|
||||||
* Introduce new dependencies without clear justification.
|
|
||||||
* Treat existing code as more authoritative than OpenSpec for intended behavior.
|
|
||||||
* Decide product behavior silently when the spec is unclear.
|
|
||||||
* Run `docker compose` commands (build, up, down, etc.) without explicit user approval and proper isolation (e.g., feature branches, separate worktrees, or staged rollouts). Docker Compose operations are deployment-level changes that can affect running services, shared volumes, and network state. Always ask first.
|
|
||||||
|
|
||||||
If scope must change, propose an OpenSpec update first.
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
Before completion, report:
|
|
||||||
|
|
||||||
* What changed
|
|
||||||
* Which OpenSpec task/change it addresses
|
|
||||||
* Tests/checks run
|
|
||||||
* Any failures, skipped checks, assumptions, or risks
|
|
||||||
|
|
||||||
Do not claim completion without verification evidence.
|
|
||||||
|
|
||||||
## Git workflow
|
|
||||||
|
|
||||||
### Branching strategy
|
|
||||||
|
|
||||||
For every spec change or new functionality:
|
|
||||||
|
|
||||||
1. Create a new branch from `dev` with a proper prefix:
|
|
||||||
- `feat/` for new features (e.g., `feat/tool-workshop`)
|
|
||||||
- `fix/` for bug fixes (e.g., `fix/terminal-tty`)
|
|
||||||
- `refactor/` for refactors (e.g., `refactor/api-cleanup`)
|
|
||||||
- `docs/` for documentation (e.g., `docs/api-guide`)
|
|
||||||
- `chore/` for maintenance (e.g., `chore/update-deps`)
|
|
||||||
2. Branch name should reference the OpenSpec change name when applicable.
|
|
||||||
3. Do not commit directly to `main` or `dev`.
|
|
||||||
|
|
||||||
### Completion and merge
|
|
||||||
|
|
||||||
When implementation is complete and verified:
|
|
||||||
|
|
||||||
1. Ensure all tests pass and quality gates are met.
|
|
||||||
2. Stage all changes with `git add -A`.
|
|
||||||
3. Create a commit with a proper conventional commit message (see below).
|
|
||||||
4. Switch to `dev`: `git checkout dev`.
|
|
||||||
5. Merge the feature branch: `git merge --no-ff <branch-name>`.
|
|
||||||
6. Push to remote: `git push origin dev`.
|
|
||||||
7. Delete the local feature branch if desired: `git branch -d <branch-name>`.
|
|
||||||
|
|
||||||
### Auto-commit on spec completion
|
|
||||||
|
|
||||||
When an OpenSpec change is fully implemented and all tasks are complete:
|
|
||||||
|
|
||||||
1. Stage all changes with `git add -A`
|
|
||||||
2. Create a commit with a proper conventional commit message
|
|
||||||
3. The commit message should:
|
|
||||||
- Use conventional commit format (`feat:`, `fix:`, `refactor:`, etc.)
|
|
||||||
- Reference the OpenSpec change name and relevant user stories
|
|
||||||
- Include a brief summary of what changed
|
|
||||||
- Mention quality gate results (tests passed, etc.)
|
|
||||||
- Example:
|
|
||||||
```
|
|
||||||
feat: implement user profile management
|
|
||||||
|
|
||||||
- Add authenticated profile endpoints (GET/PUT /users/me)
|
|
||||||
- Add avatar upload with file validation
|
|
||||||
- Create frontend profile page
|
|
||||||
|
|
||||||
Quality gates: pytest (50 passed), ruff, mypy
|
|
||||||
```
|
|
||||||
|
|
||||||
### Commit scope
|
|
||||||
|
|
||||||
- One commit per completed OpenSpec change (or related group of changes)
|
|
||||||
- Do not commit untested or broken code
|
|
||||||
- Do not commit secrets, .env files, or credentials
|
|
||||||
|
|
||||||
## Definition of done
|
|
||||||
|
|
||||||
A task is done when:
|
|
||||||
|
|
||||||
* It matches OpenSpec.
|
|
||||||
* The diff is focused.
|
|
||||||
* Relevant tests/checks passed or limitations are stated.
|
|
||||||
* No unrelated scope was added.
|
|
||||||
* Remaining risks or follow-ups are documented.
|
|
||||||
* Changes are committed with a proper conventional commit message.
|
|
||||||
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- **Project Management** - Create and manage projects with dashboard view
|
|
||||||
- **Git Repository Management** - Bare repository initialization and mirror cloning with smart URL parsing
|
|
||||||
- **Repository Workspace** - File browser with syntax highlighting, branch switching, and file editing
|
|
||||||
- **Git History Visualization** - Commit history with graph visualization and diff viewing
|
|
||||||
- **Git Control** - Branch management, commit, fetch/pull/push, merge operations
|
|
||||||
- **File Editor** - Syntax highlighting for 50+ languages with edit/commit workflow
|
|
||||||
- **Smart Git URL Parsing** - Automatic detection and correction of browser URLs to git clone URLs
|
|
||||||
- **OAuth2 Authentication** - Session-based authentication via Authentik with simplified flow
|
|
||||||
- **User Profile** - Profile management with avatar upload
|
|
||||||
- **User Settings** - Theme selection, git identity, and preference management
|
|
||||||
- **SSH Key Management** - Ed25519 key generation with secure storage
|
|
||||||
- **Tool Types** - Built-in development tools (code-server, jupyter-notebook) with custom type support
|
|
||||||
- **Comprehensive Documentation** - Architecture, API, deployment, and development guides
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Simplified authentication from JWT to session-based cookies
|
|
||||||
- Restructured test infrastructure with unit/integration/system separation
|
|
||||||
- Improved Docker deployment with Traefik integration
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Database migration chain errors
|
|
||||||
- Cross-origin cookie handling for OAuth flow
|
|
||||||
- Nginx permission issues in container
|
|
||||||
|
|
||||||
## [0.1.0] - 2026-05-19
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Initial release with core project and repository management
|
|
||||||
- OAuth2 authentication with Authentik
|
|
||||||
- Basic file browsing and git history viewing
|
|
||||||
- Development tool type definitions
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
.PHONY: help up down logs migrate test test-unit test-integration test-system test-e2e lint clean build
|
|
||||||
|
|
||||||
# Default target
|
|
||||||
help:
|
|
||||||
@echo "Headquarter Development Commands"
|
|
||||||
@echo "================================"
|
|
||||||
@echo "make up - Start all services"
|
|
||||||
@echo "make down - Stop all services"
|
|
||||||
@echo "make logs - View service logs"
|
|
||||||
@echo "make migrate - Run database migrations"
|
|
||||||
@echo "make test - Run all test suites"
|
|
||||||
@echo "make test-unit - Run unit tests only"
|
|
||||||
@echo "make test-integration - Run integration tests only"
|
|
||||||
@echo "make test-system - Run system tests only"
|
|
||||||
@echo "make test-e2e - Run E2E tests (Playwright)"
|
|
||||||
@echo "make lint - Run linting"
|
|
||||||
@echo "make build - Build all Docker images"
|
|
||||||
@echo "make clean - Remove containers and volumes"
|
|
||||||
@echo "make shell - Open shell in API container"
|
|
||||||
|
|
||||||
# Start services
|
|
||||||
up:
|
|
||||||
docker compose up -d
|
|
||||||
@echo "Services starting..."
|
|
||||||
@echo "API: http://localhost:8000"
|
|
||||||
@echo "Web: http://localhost:3000"
|
|
||||||
@echo "Postgres: localhost:5432"
|
|
||||||
@echo "Redis: localhost:6379"
|
|
||||||
|
|
||||||
# Stop services
|
|
||||||
down:
|
|
||||||
docker compose down
|
|
||||||
|
|
||||||
# View logs
|
|
||||||
logs:
|
|
||||||
docker compose logs -f
|
|
||||||
|
|
||||||
# View specific service logs
|
|
||||||
logs-api:
|
|
||||||
docker compose logs -f api
|
|
||||||
|
|
||||||
logs-web:
|
|
||||||
docker compose logs -f web
|
|
||||||
|
|
||||||
logs-db:
|
|
||||||
docker compose logs -f postgres
|
|
||||||
|
|
||||||
# Run database migrations
|
|
||||||
migrate:
|
|
||||||
docker compose exec api alembic upgrade head
|
|
||||||
|
|
||||||
# Create new migration
|
|
||||||
migration:
|
|
||||||
docker compose exec api alembic revision --autogenerate -m "$(message)"
|
|
||||||
|
|
||||||
# Run all tests
|
|
||||||
test:
|
|
||||||
docker compose exec api pytest -v
|
|
||||||
|
|
||||||
# Run unit tests only (fast, no external dependencies)
|
|
||||||
test-unit:
|
|
||||||
docker compose exec api pytest -v -m unit tests/unit/
|
|
||||||
|
|
||||||
# Run integration tests only (requires database)
|
|
||||||
test-integration:
|
|
||||||
docker compose exec api pytest -v -m integration tests/integration/
|
|
||||||
|
|
||||||
# Run system tests only (full stack)
|
|
||||||
test-system:
|
|
||||||
docker compose exec api pytest -v -m system tests/system/
|
|
||||||
|
|
||||||
# Run E2E tests (requires full application stack)
|
|
||||||
test-e2e:
|
|
||||||
cd e2e && npx playwright test
|
|
||||||
|
|
||||||
# Run linting
|
|
||||||
lint:
|
|
||||||
docker compose exec api ruff check .
|
|
||||||
docker compose exec api mypy .
|
|
||||||
cd apps/web && npm run lint
|
|
||||||
|
|
||||||
# Type checking
|
|
||||||
typecheck:
|
|
||||||
docker compose exec api mypy .
|
|
||||||
cd apps/web && npm run typecheck
|
|
||||||
|
|
||||||
# Build all images
|
|
||||||
build:
|
|
||||||
docker compose build
|
|
||||||
|
|
||||||
# Build specific service
|
|
||||||
build-api:
|
|
||||||
docker compose build api
|
|
||||||
|
|
||||||
build-web:
|
|
||||||
docker compose build web
|
|
||||||
|
|
||||||
# Clean up
|
|
||||||
clean:
|
|
||||||
docker compose down -v --remove-orphans
|
|
||||||
docker system prune -f
|
|
||||||
|
|
||||||
# Open shell in API container
|
|
||||||
shell:
|
|
||||||
docker compose exec api /bin/sh
|
|
||||||
|
|
||||||
# Database shell
|
|
||||||
db-shell:
|
|
||||||
docker compose exec postgres psql -U $(POSTGRES_USER) -d $(POSTGRES_DB)
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
health:
|
|
||||||
@echo "Checking service health..."
|
|
||||||
@docker compose ps
|
|
||||||
@@ -1,195 +1,137 @@
|
|||||||
# Headquarter
|
# Headquarter
|
||||||
|
|
||||||
A self-hosted platform for managing projects, git repositories, and development tools with OAuth2 authentication.
|
Hosted workspace and tool-orchestration platform where authenticated users create projects, connect Git repositories, and spawn self-hosted tools such as OpenCode and code-server.
|
||||||
|
|
||||||
## Overview
|
## Current Status
|
||||||
|
|
||||||
Headquarter provides a centralized workspace for development teams to:
|
This repository provides:
|
||||||
- Manage projects and their associated git repositories
|
|
||||||
- Browse repository files and view git history
|
|
||||||
- Spawn development tools (VS Code Server, Jupyter Notebook, etc.)
|
|
||||||
- Manage SSH keys and user preferences
|
|
||||||
|
|
||||||
## Features
|
- React + Vite + TypeScript frontend (`apps/web`)
|
||||||
|
- FastAPI + Python backend (`apps/api`)
|
||||||
|
- Manifest-driven tool registry with built-in OpenCode and code-server definitions
|
||||||
|
- Root monorepo tooling (pnpm workspace, Makefile)
|
||||||
|
- Docker Compose local development stack
|
||||||
|
- Deployment skeleton for Portainer + Traefik
|
||||||
|
- Automated tests (Vitest + pytest)
|
||||||
|
|
||||||
### Project Management
|
## Repository Layout
|
||||||
- Create and manage projects
|
|
||||||
- View all projects in a dashboard
|
|
||||||
- Click any project to open its workspace
|
|
||||||
|
|
||||||
### Git Repository Management
|
```text
|
||||||
- Initialize bare repositories
|
├── apps/
|
||||||
- Clone repositories (including mirror clones)
|
│ ├── web/ # React frontend
|
||||||
- Smart URL parsing (converts browser URLs to git URLs)
|
│ └── api/ # FastAPI backend
|
||||||
- View repository history and commit details
|
├── packages/ # Shared packages (future)
|
||||||
|
├── docs/ # Architecture, development, and deployment docs
|
||||||
|
├── deploy/ # Portainer/Traefik deployment examples
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── docker-compose.traefik.yml
|
||||||
|
├── package.json # Root monorepo scripts
|
||||||
|
├── Makefile # Common local workflows
|
||||||
|
└── .env.example # Shared environment variables
|
||||||
|
```
|
||||||
|
|
||||||
### Repository Workspace
|
## Prerequisites
|
||||||
- Browse files and directories
|
|
||||||
- View file contents with syntax highlighting
|
|
||||||
- Switch between branches
|
|
||||||
- Quick file editing with automatic commits
|
|
||||||
|
|
||||||
### Git History Visualization
|
- Node.js ≥ 20 and pnpm ≥ 9
|
||||||
- View commit history with branch graph
|
- Python ≥ 3.11
|
||||||
- See commit details, statistics, and diffs
|
- Docker and Docker Compose (optional, for local Postgres)
|
||||||
- Filter by branch
|
|
||||||
|
|
||||||
### Authentication
|
## Quickstart
|
||||||
- OAuth2 via Authentik
|
|
||||||
- Session-based authentication
|
|
||||||
- User profile management
|
|
||||||
|
|
||||||
### Tool Management
|
```bash
|
||||||
- Built-in tool types (code-server, jupyter-notebook)
|
# Install dependencies
|
||||||
- Create custom tool types with Docker Compose templates
|
make install
|
||||||
- Template validation
|
|
||||||
|
|
||||||
### User Settings
|
# Copy environment examples
|
||||||
- Theme selection (system/light/dark)
|
cp .env.example .env
|
||||||
- Git identity configuration
|
cp apps/web/.env.example apps/web/.env
|
||||||
- Default editor preference
|
|
||||||
|
|
||||||
### SSH Key Management
|
# Run tests
|
||||||
- Generate Ed25519 key pairs
|
make test
|
||||||
- Copy public keys to clipboard
|
|
||||||
- Delete keys
|
|
||||||
|
|
||||||
## Quick Start
|
# Start frontend and backend in development mode
|
||||||
|
make dev
|
||||||
|
```
|
||||||
|
|
||||||
### Prerequisites
|
### Docker Compose
|
||||||
- Docker and Docker Compose
|
|
||||||
- Git
|
|
||||||
|
|
||||||
### Local Development
|
```bash
|
||||||
|
docker compose up --build -d
|
||||||
|
```
|
||||||
|
|
||||||
1. **Clone the repository:**
|
This starts the API, web frontend, and PostgreSQL.
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd headquarter
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Set up environment:**
|
## Commands
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
# Edit .env with your settings
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Start services:**
|
| Command | Description |
|
||||||
```bash
|
|---------|-------------|
|
||||||
docker compose up -d
|
| `make install` | Install Node and Python dependencies |
|
||||||
```
|
| `make dev` | Start frontend and backend in parallel |
|
||||||
|
| `make test` | Run frontend and backend tests |
|
||||||
|
| `make lint` | Run linters |
|
||||||
|
| `make typecheck` | Run type checkers |
|
||||||
|
| `make build` | Build frontend and backend |
|
||||||
|
| `make compose-up` | Start Docker Compose stack |
|
||||||
|
| `make compose-down` | Stop Docker Compose stack |
|
||||||
|
|
||||||
4. **Access the application:**
|
## Continuous Integration
|
||||||
- Frontend: http://localhost:5173
|
|
||||||
- API: http://localhost:8000
|
|
||||||
- API Docs: http://localhost:8000/docs
|
|
||||||
|
|
||||||
### Production Deployment
|
All pull requests and pushes to `main` are validated by a GitHub Actions workflow (`.github/workflows/ci.yml`). The workflow runs the frontend and backend quality gates in parallel:
|
||||||
|
|
||||||
See [Deployment Guide](docs/deployment/) for production setup with Traefik and Authentik.
|
- **Web CI** — lint, typecheck, and test the React frontend.
|
||||||
|
- **API CI** — lint with `ruff`, typecheck with `mypy`, and run `pytest` against a PostgreSQL service container.
|
||||||
|
|
||||||
## Tech Stack
|
See [Development](docs/development.md) for details on running these checks locally.
|
||||||
|
|
||||||
### Backend
|
|
||||||
- **FastAPI** - Python web framework
|
|
||||||
- **SQLAlchemy** - ORM with async PostgreSQL support
|
|
||||||
- **Pydantic** - Data validation
|
|
||||||
- **Alembic** - Database migrations
|
|
||||||
- **python-jose** - JWT handling
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- **React** - UI library
|
|
||||||
- **TypeScript** - Type safety
|
|
||||||
- **Vite** - Build tool
|
|
||||||
- **React Router** - Client-side routing
|
|
||||||
|
|
||||||
### Infrastructure
|
|
||||||
- **Docker** - Containerization
|
|
||||||
- **PostgreSQL** - Database
|
|
||||||
- **Traefik** - Reverse proxy (production)
|
|
||||||
- **Authentik** - Identity provider
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [User Guide](docs/features/) - Feature documentation
|
- [Architecture](docs/architecture.md) — System design and MVP phases
|
||||||
- [API Reference](docs/api/) - API endpoints
|
- [Development](docs/development.md) — Local setup and day-to-day commands
|
||||||
- [Architecture](docs/architecture/) - System design
|
- [Deployment](docs/deployment.md) — Portainer/Traefik assumptions
|
||||||
- [Deployment](docs/deployment/) - Setup guides
|
|
||||||
- [Development](docs/development/) - Contributing
|
|
||||||
|
|
||||||
## Project Structure
|
## Frontend Environment Variables
|
||||||
|
|
||||||
```
|
The frontend (`apps/web`) requires these environment variables:
|
||||||
.
|
|
||||||
├── apps/
|
|
||||||
│ ├── api/ # FastAPI backend
|
|
||||||
│ │ ├── src/
|
|
||||||
│ │ │ ├── api/ # API routes
|
|
||||||
│ │ │ ├── auth/ # Authentication
|
|
||||||
│ │ │ ├── models/ # Database models
|
|
||||||
│ │ │ └── utils/ # Utilities
|
|
||||||
│ │ ├── tests/ # Test suite
|
|
||||||
│ │ └── Dockerfile
|
|
||||||
│ └── web/ # React frontend
|
|
||||||
│ ├── src/
|
|
||||||
│ │ ├── api/ # API clients
|
|
||||||
│ │ ├── components/# UI components
|
|
||||||
│ │ └── pages/ # Page components
|
|
||||||
│ └── Dockerfile
|
|
||||||
├── docs/ # Documentation
|
|
||||||
├── docker-compose.yml # Development setup
|
|
||||||
├── docker-compose.traefik.yml # Production setup
|
|
||||||
└── Makefile # Common commands
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `VITE_API_URL` | Backend API base URL |
|
||||||
|
| `VITE_OIDC_ISSUER` | OIDC provider issuer URL |
|
||||||
|
| `VITE_OIDC_CLIENT_ID` | OIDC client ID |
|
||||||
|
| `VITE_OIDC_REDIRECT_URI` | Post-login redirect URL |
|
||||||
|
|
||||||
|
Copy `apps/web/.env.example` to `apps/web/.env` and fill in your values.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Deploy to production using Docker Compose:
|
||||||
|
|
||||||
### Backend Development
|
|
||||||
```bash
|
```bash
|
||||||
cd apps/api
|
# Copy and configure production environment
|
||||||
python -m venv .venv
|
cp deploy/.env.example deploy/.env
|
||||||
source .venv/bin/activate
|
# Edit deploy/.env with your domain and secrets
|
||||||
pip install -e ".[dev]"
|
|
||||||
uvicorn src.main:app --reload
|
# Deploy locally for testing
|
||||||
|
docker compose -f docker-compose.prod.yml up --build -d
|
||||||
|
|
||||||
|
# Or deploy via Portainer using deploy/portainer-stack.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
### Frontend Development
|
See [Deployment Guide](docs/deployment.md) for full details.
|
||||||
```bash
|
|
||||||
cd apps/web
|
|
||||||
npm install
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running Tests
|
## Scope Boundaries
|
||||||
```bash
|
|
||||||
# Backend tests
|
|
||||||
make test
|
|
||||||
|
|
||||||
# Frontend tests
|
This scaffold intentionally defers detailed implementation to follow-up tasks:
|
||||||
make test-web
|
|
||||||
|
|
||||||
# All quality gates
|
- **FN-004** — Backend domain models, database migrations, API endpoints, auth integration
|
||||||
make lint
|
- **FN-005** — Frontend dashboard navigation, project creation, authenticated flows
|
||||||
make typecheck
|
- **FN-006** — Full deployment automation, dynamic Traefik labels for spawned tool containers
|
||||||
```
|
- **FN-003** — Manifest-driven tool registry
|
||||||
|
- **FN-007** — Provider-independent Git connection model
|
||||||
## Configuration
|
- **FN-008** — OpenCode terminal environment proof of concept
|
||||||
|
- **FN-009** — Persistent config and secrets handling
|
||||||
Key environment variables:
|
- **FN-010** — code-server manifest and spawn flow
|
||||||
|
|
||||||
| Variable | Description | Default |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `API_DOMAIN` | API domain | `localhost` |
|
|
||||||
| `WEB_DOMAIN` | Web domain | `localhost` |
|
|
||||||
| `AUTHENTIK_DOMAIN` | Authentik domain | - |
|
|
||||||
| `AUTHENTIK_CLIENT_ID` | OAuth client ID | - |
|
|
||||||
| `AUTHENTIK_CLIENT_SECRET` | OAuth client secret | - |
|
|
||||||
| `DATABASE_URL` | PostgreSQL URL | - |
|
|
||||||
| `JWT_SECRET` | JWT signing secret | - |
|
|
||||||
| `REPO_BASE_PATH` | Repository storage path | `/data/repos` |
|
|
||||||
|
|
||||||
See [Environment Variables](docs/deployment/environment.md) for complete list.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[License information]
|
TBD
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
# apps (index)
|
|
||||||
dir: apps
|
|
||||||
|
|
||||||
## role
|
|
||||||
Contains the main deployable application modules or entry points for the project.
|
|
||||||
## parent
|
|
||||||
index: ./.pi-map.index.md
|
|
||||||
map: ./.pi-map.md
|
|
||||||
## children
|
|
||||||
- apps/api
|
|
||||||
index: apps/api/.pi-map.index.md
|
|
||||||
map: apps/api/.pi-map.md
|
|
||||||
- apps/web
|
|
||||||
index: apps/web/.pi-map.index.md
|
|
||||||
map: apps/web/.pi-map.md
|
|
||||||
## files
|
|
||||||
## links
|
|
||||||
index: apps/.pi-map.index.md
|
|
||||||
map: apps/.pi-map.md
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# apps
|
|
||||||
dir: apps
|
|
||||||
|
|
||||||
index: apps/.pi-map.index.md
|
|
||||||
|
|
||||||
## role
|
|
||||||
Contains the main deployable application modules or entry points for the project.
|
|
||||||
## files
|
|
||||||
## arch
|
|
||||||
Modular monolith or microservices architecture with separate application boundaries, each potentially having its own configuration, dependencies, and lifecycle.
|
|
||||||
## tags
|
|
||||||
-
|
|
||||||
## symbols
|
|
||||||
-
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
+5
-32
@@ -1,42 +1,15 @@
|
|||||||
# Python cache
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
# Virtual environments
|
|
||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
|
ENV/
|
||||||
env/
|
env/
|
||||||
|
*.egg-info/
|
||||||
# Test artifacts
|
dist/
|
||||||
.pytest_cache/
|
build/
|
||||||
.coverage
|
|
||||||
htmlcov/
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
|
|
||||||
# Git
|
|
||||||
.git/
|
.git/
|
||||||
.gitignore
|
|
||||||
|
|
||||||
# Local env files
|
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
*.log
|
||||||
# Alembic cache
|
|
||||||
alembic/versions/__pycache__/
|
|
||||||
|
|
||||||
# Pi lens cache
|
|
||||||
.pi-lens/
|
|
||||||
|
|
||||||
# Documentation
|
|
||||||
docs/
|
|
||||||
*.md
|
|
||||||
|
|
||||||
# Scripts not needed in container
|
|
||||||
scripts/
|
|
||||||
|
|||||||
-568
@@ -1,568 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "v2",
|
|
||||||
"timestamp": 1779889907001,
|
|
||||||
"ruleHash": "fd9b2b15f2ac8993",
|
|
||||||
"queries": [
|
|
||||||
{
|
|
||||||
"id": "bare-except",
|
|
||||||
"name": "Bare Except Clause",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "Bare 'except:' clause — catches SystemExit, KeyboardInterrupt",
|
|
||||||
"query": " (except_clause\n \"except\") @CLAUSE",
|
|
||||||
"metavars": [
|
|
||||||
"CLAUSE"
|
|
||||||
],
|
|
||||||
"post_filter": "bare_except_only",
|
|
||||||
"defect_class": "silent-error",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/bare-except.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "eval-exec",
|
|
||||||
"name": "Eval/Exec Usage",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "{{FUNC}}() detected — security risk, code injection vulnerability",
|
|
||||||
"query": " (call\n function: (identifier) @FUNC\n (#match? @FUNC \"^(eval|exec)$\")\n arguments: (argument_list) @ARGS)",
|
|
||||||
"metavars": [
|
|
||||||
"FUNC",
|
|
||||||
"ARGS"
|
|
||||||
],
|
|
||||||
"defect_class": "injection",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/eval-exec.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "exit-signature-check",
|
|
||||||
"name": "__exit__ Missing Parameters",
|
|
||||||
"severity": "error",
|
|
||||||
"language": "python",
|
|
||||||
"message": "__exit__ should accept type, value, and traceback arguments",
|
|
||||||
"query": " (function_definition\n name: (identifier) @NAME (#eq? @NAME \"__exit__\")\n parameters: (parameters\n (_) @SELF\n . (_) @PARAM1?\n . (_) @PARAM2?\n . (_) @PARAM3?))",
|
|
||||||
"metavars": [
|
|
||||||
"NAME",
|
|
||||||
"SELF",
|
|
||||||
"PARAM1",
|
|
||||||
"PARAM2",
|
|
||||||
"PARAM3"
|
|
||||||
],
|
|
||||||
"post_filter": "exit_params_insufficient",
|
|
||||||
"defect_class": "correctness",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/exit-signature-check.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "in-operator-unsupported",
|
|
||||||
"name": "In and Not In Operators Should Be Used on Valid Objects",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "'in' operator used on object that may not support containment",
|
|
||||||
"query": " (comparison_operator\n (identifier) @OBJ\n \"in\"\n (identifier) @TARGET)\n (comparison_operator\n (identifier) @OBJ\n \"not\"\n \"in\"\n (identifier) @TARGET)",
|
|
||||||
"metavars": [
|
|
||||||
"OBJ",
|
|
||||||
"TARGET"
|
|
||||||
],
|
|
||||||
"post_filter": "check_in_operator_types",
|
|
||||||
"defect_class": "correctness",
|
|
||||||
"inline_tier": "warning",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/in-operator-unsupported.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "is-vs-equals",
|
|
||||||
"name": "Is vs Equals for Literals",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "Using 'is' with literal — use '==' for value comparison",
|
|
||||||
"query": " (comparison_operator\n (identifier)\n (\"is\")\n (string) @LITERAL)\n (comparison_operator\n (identifier)\n (\"is not\")\n (string) @LITERAL)\n (comparison_operator\n (identifier)\n (\"is\")\n (integer) @LITERAL)\n (comparison_operator\n (identifier)\n (\"is not\")\n (integer) @LITERAL)",
|
|
||||||
"metavars": [
|
|
||||||
"LITERAL"
|
|
||||||
],
|
|
||||||
"defect_class": "correctness",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/is-vs-equals.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "iter-return-iterator",
|
|
||||||
"name": "__iter__ Should Return Iterator",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "__iter__ should return an iterator (object with __next__ method)",
|
|
||||||
"query": " (function_definition\n name: (identifier) @NAME (#eq? @NAME \"__iter__\")\n body: (block\n (return_statement) @RETURN))",
|
|
||||||
"metavars": [
|
|
||||||
"NAME",
|
|
||||||
"RETURN"
|
|
||||||
],
|
|
||||||
"defect_class": "correctness",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/iter-return-iterator.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "mutable-default-arg",
|
|
||||||
"name": "Mutable Default Argument",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "Mutable default argument — list/dict/set as default value",
|
|
||||||
"query": " (function_definition\n (parameters\n (default_parameter\n (identifier) @PARAM\n [(list) (dictionary) (set)] @MUTABLE)))",
|
|
||||||
"metavars": [
|
|
||||||
"PARAM",
|
|
||||||
"MUTABLE"
|
|
||||||
],
|
|
||||||
"defect_class": "correctness",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/mutable-default-arg.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "no-super-torchscript",
|
|
||||||
"name": "super Should Not Be Used in TorchScript Methods",
|
|
||||||
"severity": "error",
|
|
||||||
"language": "python",
|
|
||||||
"message": "super() calls should not be used in TorchScript methods",
|
|
||||||
"query": " (function_definition\n (decorator\n (call\n function: (identifier) @DEC (#match? @DEC \"^(torch\\.jit\\.script|jit\\.script)$\")))\n body: (block\n (call\n function: (identifier) @FUNC (#eq? @FUNC \"super\")) @CALL))",
|
|
||||||
"metavars": [
|
|
||||||
"DEC",
|
|
||||||
"FUNC",
|
|
||||||
"CALL"
|
|
||||||
],
|
|
||||||
"defect_class": "correctness",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/no-super-torchscript.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "notimplemented-boolean-context",
|
|
||||||
"name": "NotImplemented in Boolean Context",
|
|
||||||
"severity": "error",
|
|
||||||
"language": "python",
|
|
||||||
"message": "NotImplemented should not be used in boolean contexts",
|
|
||||||
"query": " (if_statement\n condition: (identifier) @COND (#eq? @COND \"NotImplemented\"))\n (while_statement\n condition: (identifier) @COND (#eq? @COND \"NotImplemented\"))\n (binary_operator\n (identifier) @COND (#eq? @COND \"NotImplemented\")\n (\"and\" | \"or\"))\n (boolean_operator\n (identifier) @COND (#eq? @COND \"NotImplemented\"))\n (unary_operator\n operator: (\"not\")\n argument: (identifier) @COND (#eq? @COND \"NotImplemented\"))",
|
|
||||||
"metavars": [
|
|
||||||
"COND"
|
|
||||||
],
|
|
||||||
"defect_class": "correctness",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/notimplemented-boolean-context.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-assert-production",
|
|
||||||
"name": "Assert in Production Code",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "assert statement stripped by Python -O flag — use explicit checks with exceptions in production code",
|
|
||||||
"query": " (assert_statement) @ASSERT",
|
|
||||||
"metavars": [
|
|
||||||
"ASSERT"
|
|
||||||
],
|
|
||||||
"defect_class": "correctness",
|
|
||||||
"inline_tier": "warning",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-assert-production.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-command-injection",
|
|
||||||
"name": "Command Injection Sink",
|
|
||||||
"severity": "error",
|
|
||||||
"language": "python",
|
|
||||||
"message": "Potential command injection sink — avoid shell execution with dynamic input",
|
|
||||||
"query": " (call\n function: (attribute\n object: (identifier) @MOD\n attribute: (identifier) @FN)\n arguments: (argument_list) @ARGS\n (#eq? @MOD \"os\")\n (#match? @FN \"^(system|popen)$\"))\n\n (call\n function: (attribute\n object: (identifier) @MOD\n attribute: (identifier) @FN)\n arguments: (argument_list\n (keyword_argument\n name: (identifier) @KW\n value: (true)))\n (#eq? @MOD \"subprocess\")\n (#match? @FN \"^(run|Popen|call|check_output|check_call)$\")\n (#eq? @KW \"shell\"))",
|
|
||||||
"metavars": [
|
|
||||||
"MOD",
|
|
||||||
"FN",
|
|
||||||
"ARGS",
|
|
||||||
"KW"
|
|
||||||
],
|
|
||||||
"post_filter": "py_command_injection_sink",
|
|
||||||
"defect_class": "injection",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-command-injection.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-cross-language-method",
|
|
||||||
"name": "Cross-Language Method Leakage",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "'{METHOD}' is not a Python method — likely a {LANG} idiom leaking in",
|
|
||||||
"query": " (call\n function: (attribute\n object: (_) @OBJ\n attribute: (identifier) @METHOD)\n (#match? @METHOD \"^(push|forEach|indexOf|charAt|substring|hasOwnProperty|unshift|flatMap|padStart|padEnd|trimStart|trimEnd|equals|isEmpty|println|printf|getClass|hashCode|toCharArray|getBytes|compareTo|equalsIgnoreCase|startsWith|endsWith|each|collect|select|reject|detect|inject|chomp|chop|gsub|upcase|downcase|present|blank|Add|Contains|ToLower|ToUpper|Trim|Substring|WriteLine|ReadLine|TryParse|forEach|includes|assign|freeze|splice|unshift|shift|flatMap)$\"))",
|
|
||||||
"metavars": [
|
|
||||||
"OBJ",
|
|
||||||
"METHOD"
|
|
||||||
],
|
|
||||||
"post_filter": "match_captures",
|
|
||||||
"post_filter_params": {
|
|
||||||
"METHOD": "^(push|forEach|indexOf|charAt|substring|hasOwnProperty|unshift|flatMap|padStart|padEnd|trimStart|trimEnd|equals|isEmpty|println|printf|getClass|hashCode|toCharArray|getBytes|compareTo|equalsIgnoreCase|startsWith|endsWith|each|collect|select|reject|detect|inject|chomp|chop|gsub|upcase|downcase|present|blank|Add|Contains|ToLower|ToUpper|Trim|Substring|WriteLine|ReadLine|TryParse|forEach|includes|assign|freeze|splice|unshift|shift|flatMap)$"
|
|
||||||
},
|
|
||||||
"defect_class": "hallucination",
|
|
||||||
"inline_tier": "warning",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-cross-language-method.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-debugger",
|
|
||||||
"name": "Debugger Statement",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "Debugger call '{{FUNC}}' — remove before committing",
|
|
||||||
"query": " (call\n function: (identifier) @FUNC\n (#eq? @FUNC \"breakpoint\"))\n\n (call\n function: (attribute\n object: (identifier) @MOD\n attribute: (identifier) @FUNC)\n (#eq? @MOD \"pdb\")\n (#match? @FUNC \"^(set_trace|post_mortem|pm|run|runcall)$\"))",
|
|
||||||
"metavars": [
|
|
||||||
"FUNC",
|
|
||||||
"MOD"
|
|
||||||
],
|
|
||||||
"defect_class": "safety",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-debugger.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-empty-except",
|
|
||||||
"name": "Empty Except Block",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "Except block only contains 'pass' — handle or re-raise the exception",
|
|
||||||
"query": " (try_statement\n (except_clause\n body: (block) @BODY))",
|
|
||||||
"metavars": [
|
|
||||||
"BODY"
|
|
||||||
],
|
|
||||||
"post_filter": "python_empty_except",
|
|
||||||
"defect_class": "silent-error",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-empty-except.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-hallucinated-import",
|
|
||||||
"name": "Hallucinated Import",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "Hallucinated import — '{NAME}' does not exist in '{MODULE}'",
|
|
||||||
"query": " (import_from_statement\n module_name: (dotted_name) @MODULE\n name: (dotted_name) @NAME)",
|
|
||||||
"metavars": [
|
|
||||||
"MODULE",
|
|
||||||
"NAME"
|
|
||||||
],
|
|
||||||
"post_filter": "match_captures",
|
|
||||||
"post_filter_params": {
|
|
||||||
"MODULE": "^(requests|flask|django|typing|collections|asyncio|json|unittest|pytest|urllib|sqlalchemy)$",
|
|
||||||
"NAME": "^(JSONResponse|HTMLResponse|RedirectResponse|StreamingResponse|Depends|Query|Path|Body|Header|Cookie|Form|File|UploadFile|FastAPI|APIRouter|HTTPException|BackgroundTasks|dataclass|fields|BaseModel|Field|validator|aiohttp|parse|stringify|fixture|TestCase|get|post|put|delete|Model|Session|Column|Integer|String)$"
|
|
||||||
},
|
|
||||||
"defect_class": "hallucination",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-hallucinated-import.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-hardcoded-secrets",
|
|
||||||
"name": "Hardcoded Secret",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "Hardcoded {{VARNAME}} — use environment variables or a secrets manager",
|
|
||||||
"query": " (assignment\n left: (identifier) @VARNAME\n right: (string) @VALUE)",
|
|
||||||
"metavars": [
|
|
||||||
"VARNAME",
|
|
||||||
"VALUE"
|
|
||||||
],
|
|
||||||
"post_filter": "check_secret_pattern",
|
|
||||||
"defect_class": "secrets",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-hardcoded-secrets.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-insecure-deserialization",
|
|
||||||
"name": "Insecure Deserialization",
|
|
||||||
"severity": "error",
|
|
||||||
"language": "python",
|
|
||||||
"message": "Potential insecure deserialization sink — avoid unsafe loaders",
|
|
||||||
"query": " (call\n function: (attribute\n object: (identifier) @MOD\n attribute: (identifier) @FN)\n arguments: (argument_list (_) @DATA)\n (#match? @MOD \"^(pickle|yaml)$\")\n (#match? @FN \"^(load|loads|unsafe_load)$\"))",
|
|
||||||
"metavars": [
|
|
||||||
"MOD",
|
|
||||||
"FN",
|
|
||||||
"DATA"
|
|
||||||
],
|
|
||||||
"post_filter": "py_insecure_deserialization_sink",
|
|
||||||
"defect_class": "injection",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-insecure-deserialization.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-insecure-random",
|
|
||||||
"name": "Insecure Randomness",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "Insecure randomness source detected — use secrets or os.urandom for security-sensitive values",
|
|
||||||
"query": " (call\n function: (attribute\n object: (identifier) @MOD\n attribute: (identifier) @FN)\n arguments: (argument_list) @ARGS\n (#eq? @MOD \"random\")\n (#match? @FN \"^(random|randint|randrange|choice|choices)$\"))",
|
|
||||||
"metavars": [
|
|
||||||
"MOD",
|
|
||||||
"FN",
|
|
||||||
"ARGS"
|
|
||||||
],
|
|
||||||
"defect_class": "injection",
|
|
||||||
"inline_tier": "warning",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-insecure-random.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-mutable-class-attr",
|
|
||||||
"name": "Mutable Class Attribute",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "Class attribute '{{VARNAME}}' is mutable — shared across all instances",
|
|
||||||
"query": " (class_definition\n body: (block\n (expression_statement\n (assignment\n left: (identifier) @VARNAME\n right: [\n (list) @VALUE\n (dictionary) @VALUE\n (set) @VALUE\n ]))))",
|
|
||||||
"metavars": [
|
|
||||||
"VARNAME",
|
|
||||||
"VALUE"
|
|
||||||
],
|
|
||||||
"post_filter": "not_in_function",
|
|
||||||
"defect_class": "correctness",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-mutable-class-attr.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-path-traversal",
|
|
||||||
"name": "Path Traversal Risk",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "Potential path traversal sink — sanitize and constrain file paths",
|
|
||||||
"query": " [\n (call\n function: (identifier) @FN\n arguments: (argument_list\n [(identifier) (binary_operator) (call)] @PATH))\n (call\n function: (attribute\n object: (identifier) @MOD\n attribute: (identifier) @FN)\n arguments: (argument_list\n [(identifier) (binary_operator) (call)] @PATH))\n ]\n (#match? @FN \"^(open|read_text|read_bytes|write_text|write_bytes|remove|unlink|rmdir)$\")",
|
|
||||||
"metavars": [
|
|
||||||
"MOD",
|
|
||||||
"FN",
|
|
||||||
"PATH"
|
|
||||||
],
|
|
||||||
"post_filter": "py_path_traversal_sink",
|
|
||||||
"defect_class": "injection",
|
|
||||||
"inline_tier": "warning",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-path-traversal.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-print-statement",
|
|
||||||
"name": "Print Statement in Production",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "print() — remove debug output before committing",
|
|
||||||
"query": " (call\n function: (identifier) @FUNC\n (#eq? @FUNC \"print\")\n arguments: (argument_list) @ARGS)",
|
|
||||||
"metavars": [
|
|
||||||
"FUNC",
|
|
||||||
"ARGS"
|
|
||||||
],
|
|
||||||
"post_filter": "not_in_test_block",
|
|
||||||
"defect_class": "safety",
|
|
||||||
"inline_tier": "warning",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-print-statement.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-raise-string",
|
|
||||||
"name": "Raise String Instead of Exception",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "raise with string literal — Python 3 requires exception instances",
|
|
||||||
"query": " (raise_statement\n (string) @VALUE)",
|
|
||||||
"metavars": [
|
|
||||||
"VALUE"
|
|
||||||
],
|
|
||||||
"defect_class": "correctness",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-raise-string.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-sleep-in-test",
|
|
||||||
"name": "time.sleep in Test",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "time.sleep() in test — use synchronisation primitives or polling helpers instead of fixed sleeps",
|
|
||||||
"query": " (call\n function: (attribute\n object: (identifier) @MOD\n attribute: (identifier) @FN)\n (#eq? @MOD \"time\")\n (#eq? @FN \"sleep\")) @CALL",
|
|
||||||
"metavars": [
|
|
||||||
"MOD",
|
|
||||||
"FN",
|
|
||||||
"CALL"
|
|
||||||
],
|
|
||||||
"defect_class": "async-misuse",
|
|
||||||
"inline_tier": "warning",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-sleep-in-test.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-sql-injection",
|
|
||||||
"name": "SQL Injection Risk",
|
|
||||||
"severity": "error",
|
|
||||||
"language": "python",
|
|
||||||
"message": "Potential SQL injection sink — use parameterized queries",
|
|
||||||
"query": " (call\n function: (attribute\n object: (_) @OBJ\n attribute: (identifier) @FN)\n arguments: (argument_list\n [(binary_operator) (identifier) (call)] @SQL\n (_)*))",
|
|
||||||
"metavars": [
|
|
||||||
"OBJ",
|
|
||||||
"FN",
|
|
||||||
"SQL"
|
|
||||||
],
|
|
||||||
"post_filter": "py_sql_injection_sink",
|
|
||||||
"defect_class": "injection",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-sql-injection.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-ssrf",
|
|
||||||
"name": "SSRF Risk",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "Potential SSRF sink — validate/allowlist outbound URLs",
|
|
||||||
"query": " (call\n function: (attribute\n object: (identifier) @MOD\n attribute: (identifier) @FN)\n arguments: (argument_list\n [(identifier) (subscript) (call)] @URL)\n (#eq? @MOD \"requests\")\n (#match? @FN \"^(get|post|put|patch|delete|request|head|options)$\"))",
|
|
||||||
"metavars": [
|
|
||||||
"MOD",
|
|
||||||
"FN",
|
|
||||||
"URL"
|
|
||||||
],
|
|
||||||
"post_filter": "py_ssrf_sink",
|
|
||||||
"defect_class": "injection",
|
|
||||||
"inline_tier": "warning",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-ssrf.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-subprocess-shell",
|
|
||||||
"name": "subprocess with shell=True",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "subprocess called with shell=True — command injection risk if any argument is user-controlled",
|
|
||||||
"query": " (call\n function: (attribute\n object: (identifier) @MOD\n attribute: (identifier) @FN)\n arguments: (argument_list\n (keyword_argument\n name: (identifier) @KW\n value: (true) @VAL))\n (#eq? @MOD \"subprocess\")\n (#match? @FN \"^(run|Popen|call|check_output|check_call)$\")\n (#eq? @KW \"shell\"))",
|
|
||||||
"metavars": [
|
|
||||||
"MOD",
|
|
||||||
"FN",
|
|
||||||
"KW"
|
|
||||||
],
|
|
||||||
"defect_class": "injection",
|
|
||||||
"inline_tier": "warning",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-subprocess-shell.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-thread-global-write",
|
|
||||||
"name": "Threaded Shared State Risk",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "Thread creation detected — ensure shared state mutations are synchronized",
|
|
||||||
"query": " (call\n function: (attribute\n object: (identifier) @MOD\n attribute: (identifier) @FN)\n arguments: (argument_list) @ARGS)\n (#eq? @MOD \"threading\")\n (#eq? @FN \"Thread\")",
|
|
||||||
"metavars": [
|
|
||||||
"MOD",
|
|
||||||
"FN",
|
|
||||||
"ARGS"
|
|
||||||
],
|
|
||||||
"defect_class": "async-misuse",
|
|
||||||
"inline_tier": "warning",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-thread-global-write.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-unsafe-regex",
|
|
||||||
"name": "Unsafe Dynamic Regex",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "re.{{FUNC}}() with variable pattern — ReDoS risk if pattern is user-controlled",
|
|
||||||
"query": " (call\n function: (attribute\n object: (identifier) @MOD\n attribute: (identifier) @FUNC)\n arguments: (argument_list\n (identifier) @PATTERN)\n (#eq? @MOD \"re\")\n (#match? @FUNC \"^(compile|match|search|fullmatch|findall|finditer|sub|subn|split)$\"))",
|
|
||||||
"metavars": [
|
|
||||||
"MOD",
|
|
||||||
"FUNC",
|
|
||||||
"PATTERN"
|
|
||||||
],
|
|
||||||
"defect_class": "injection",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-unsafe-regex.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-weak-hash",
|
|
||||||
"name": "Weak Hash Primitive",
|
|
||||||
"severity": "error",
|
|
||||||
"language": "python",
|
|
||||||
"message": "Weak hash primitive detected (MD5/SHA1) — use SHA-256+ for security-sensitive contexts",
|
|
||||||
"query": " (call\n function: (attribute\n object: (identifier) @MOD\n attribute: (identifier) @FN)\n arguments: (argument_list) @ARGS\n (#eq? @MOD \"hashlib\")\n (#match? @FN \"^(md5|sha1)$\"))",
|
|
||||||
"metavars": [
|
|
||||||
"MOD",
|
|
||||||
"FN",
|
|
||||||
"ARGS"
|
|
||||||
],
|
|
||||||
"defect_class": "injection",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/python-weak-hash.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "return-in-generator",
|
|
||||||
"name": "Return with Value in Generator",
|
|
||||||
"severity": "error",
|
|
||||||
"language": "python",
|
|
||||||
"message": "'return' with a value should not be used in a generator function",
|
|
||||||
"query": " (function_definition\n body: (block\n (return_statement\n (_) @RETURN_VAL) @RETURN)) @FUNCTION",
|
|
||||||
"metavars": [
|
|
||||||
"FUNCTION",
|
|
||||||
"RETURN",
|
|
||||||
"RETURN_VAL"
|
|
||||||
],
|
|
||||||
"post_filter": "is_generator_with_valued_return",
|
|
||||||
"defect_class": "correctness",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/return-in-generator.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "return-in-init",
|
|
||||||
"name": "Return Value in __init__",
|
|
||||||
"severity": "error",
|
|
||||||
"language": "python",
|
|
||||||
"message": "__init__ should not return a value — it must always return None",
|
|
||||||
"query": " (function_definition\n name: (identifier) @NAME (#eq? @NAME \"__init__\")\n body: (block\n (return_statement\n (_) @RETURN_VAL) @RETURN))",
|
|
||||||
"metavars": [
|
|
||||||
"NAME",
|
|
||||||
"RETURN",
|
|
||||||
"RETURN_VAL"
|
|
||||||
],
|
|
||||||
"post_filter": "has_return_value",
|
|
||||||
"defect_class": "correctness",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/return-in-init.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "send-file-mimetype",
|
|
||||||
"name": "send_file Should Specify Mimetype or Download Name",
|
|
||||||
"severity": "error",
|
|
||||||
"language": "python",
|
|
||||||
"message": "send_file should specify 'mimetype' or 'download_name' when used with file-like objects",
|
|
||||||
"query": " (call\n function: (identifier) @FUNC (#eq? @FUNC \"send_file\")\n arguments: (argument_list\n (_) @FIRST_ARG\n (keyword_argument)? @KW))",
|
|
||||||
"metavars": [
|
|
||||||
"FUNC",
|
|
||||||
"FIRST_ARG",
|
|
||||||
"KW"
|
|
||||||
],
|
|
||||||
"post_filter": "missing_mimetype_and_download_name",
|
|
||||||
"defect_class": "correctness",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/send-file-mimetype.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "unreachable-except",
|
|
||||||
"name": "Unreachable Except Clause",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "Unreachable except clause — earlier except catches all",
|
|
||||||
"query": " (try_statement\n (except_clause\n \"except\") @GENERAL\n (except_clause\n \"except\"\n (identifier) @SPECIFIC))",
|
|
||||||
"metavars": [
|
|
||||||
"GENERAL",
|
|
||||||
"SPECIFIC"
|
|
||||||
],
|
|
||||||
"defect_class": "correctness",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/unreachable-except.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "wildcard-import",
|
|
||||||
"name": "Wildcard Import",
|
|
||||||
"severity": "warning",
|
|
||||||
"language": "python",
|
|
||||||
"message": "Wildcard import — pollutes namespace, hard to track origin",
|
|
||||||
"query": " (import_from_statement\n module_name: (dotted_name) @MODULE\n (wildcard_import) @WILDCARD)",
|
|
||||||
"metavars": [
|
|
||||||
"MODULE",
|
|
||||||
"WILDCARD"
|
|
||||||
],
|
|
||||||
"defect_class": "safety",
|
|
||||||
"inline_tier": "warning",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/wildcard-import.yml"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "yield-return-outside-function",
|
|
||||||
"name": "Yield/Return Outside Function",
|
|
||||||
"severity": "error",
|
|
||||||
"language": "python",
|
|
||||||
"message": "{{STATEMENT}} used outside function — syntax error",
|
|
||||||
"query": " (module\n (expression_statement\n (yield) @STATEMENT))\n (module\n (expression_statement\n (yield_expression) @STATEMENT))\n (module\n (return_statement) @STATEMENT)",
|
|
||||||
"metavars": [
|
|
||||||
"STATEMENT"
|
|
||||||
],
|
|
||||||
"defect_class": "correctness",
|
|
||||||
"inline_tier": "blocking",
|
|
||||||
"filePath": "/home/alex/.npm-global/lib/node_modules/pi-lens/rules/tree-sitter-queries/python/yield-return-outside-function.yml"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# apps/api (index)
|
|
||||||
dir: apps/api
|
|
||||||
|
|
||||||
## role
|
|
||||||
Self-hosted FastAPI backend API that manages projects, git repositories, and development tools via Docker instances.
|
|
||||||
## parent
|
|
||||||
index: apps/.pi-map.index.md
|
|
||||||
map: apps/.pi-map.md
|
|
||||||
## children
|
|
||||||
- apps/api/.pi-lens
|
|
||||||
index: apps/api/.pi-lens/.pi-map.index.md
|
|
||||||
map: apps/api/.pi-lens/.pi-map.md
|
|
||||||
- apps/api/.pytest_cache
|
|
||||||
index: apps/api/.pytest_cache/.pi-map.index.md
|
|
||||||
map: apps/api/.pytest_cache/.pi-map.md
|
|
||||||
- apps/api/.venv-test
|
|
||||||
index: apps/api/.venv-test/.pi-map.index.md
|
|
||||||
map: apps/api/.venv-test/.pi-map.md
|
|
||||||
- apps/api/alembic
|
|
||||||
index: apps/api/alembic/.pi-map.index.md
|
|
||||||
map: apps/api/alembic/.pi-map.md
|
|
||||||
- apps/api/src
|
|
||||||
index: apps/api/src/.pi-map.index.md
|
|
||||||
map: apps/api/src/.pi-map.md
|
|
||||||
- apps/api/tests
|
|
||||||
index: apps/api/tests/.pi-map.index.md
|
|
||||||
map: apps/api/tests/.pi-map.md
|
|
||||||
- apps/api/uploads
|
|
||||||
index: apps/api/uploads/.pi-map.index.md
|
|
||||||
map: apps/api/uploads/.pi-map.md
|
|
||||||
## files
|
|
||||||
- .dockerignore
|
|
||||||
- Dockerfile
|
|
||||||
- README.md
|
|
||||||
- alembic.ini
|
|
||||||
- pyproject.toml
|
|
||||||
- uv.lock
|
|
||||||
- wait-for-db.sh
|
|
||||||
## links
|
|
||||||
index: apps/api/.pi-map.index.md
|
|
||||||
map: apps/api/.pi-map.md
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# apps/api
|
|
||||||
dir: apps/api
|
|
||||||
|
|
||||||
index: apps/api/.pi-map.index.md
|
|
||||||
|
|
||||||
## role
|
|
||||||
Self-hosted FastAPI backend API that manages projects, git repositories, and development tools via Docker instances.
|
|
||||||
## files
|
|
||||||
- .dockerignore | Specifies files and directories to exclude from Docker build context to reduce image size and avoid copying unnecessary files into containers. | dep: Docker
|
|
||||||
- Dockerfile | Multi-stage Docker build for a Python application with Docker socket access, Cloudflare tunneling, and database dependency waiting | dep: python:3.11-slim, gcc, libpq-dev, docker-ce-cli, docker-compose-plugin, cloudflared, uvicorn, pyproject.toml dependencies
|
|
||||||
- README.md | Documentation for a self-hosted FastAPI backend API that manages projects, git repositories, and development tools via Docker instances. | dep: FastAPI, SQLAlchemy, PostgreSQL, asyncpg, Alembic, Docker, Docker Compose, Authentik, uvicorn, pytest, ruff, mypy
|
|
||||||
- alembic.ini | Configuration file for Alembic database migration tool connecting to a PostgreSQL database with async driver | dep: alembic, sqlalchemy, asyncpg, PostgreSQL
|
|
||||||
- pyproject.toml | Defines Python project metadata, dependencies, and tool configurations for a FastAPI-based backend API called "headquarter-api" | dep: fastapi, uvicorn, sqlalchemy, asyncpg, alembic, pydantic, pydantic-settings, python-multipart, httpx, structlog, cryptography, pytest, pytest-asyncio, mypy, ruff, aiosqlite
|
|
||||||
- uv.lock | Lock file for the uv Python package manager that pins exact dependency versions and their artifact hashes for reproducible installations | dep: uv, Python 3.11+, aiosqlite, alembic, annotated-doc, annotated-types, anyio, ast-serialize, asyncpg, and many other PyPI packages
|
|
||||||
- wait-for-db.sh | Wait for a PostgreSQL database to become available before executing a command, with configurable retry logic. | dep: nc (netcat), sh (POSIX shell), sleep
|
|
||||||
## arch
|
|
||||||
Async Python/FastAPI with PostgreSQL (Alembic migrations), multi-stage Docker deployment with Cloudflare tunneling, uv package management, and containerized service orchestration.
|
|
||||||
## tags
|
|
||||||
docker, alembic, python, database, fastapi, postgresql, asyncpg, uvicorn
|
|
||||||
## symbols
|
|
||||||
-
|
|
||||||
## workflows
|
|
||||||
-
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.10.18
|
||||||
+9
-65
@@ -1,74 +1,18 @@
|
|||||||
# Build stage
|
FROM python:3.12-slim
|
||||||
FROM python:3.11-slim as builder
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
# Install build dependencies
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
gcc \
|
|
||||||
libpq-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install Python dependencies
|
|
||||||
COPY pyproject.toml .
|
|
||||||
RUN pip install --no-cache-dir --user -e ".[dev]"
|
|
||||||
|
|
||||||
# Production stage
|
|
||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
# Create non-root user and add to docker group
|
|
||||||
RUN groupadd -r appgroup && useradd -r -g appgroup appuser \
|
|
||||||
&& groupadd -r docker || true \
|
|
||||||
&& usermod -aG docker appuser
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install runtime dependencies including Docker CLI
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
ENV PYTHONUNBUFFERED=1
|
||||||
libpq5 \
|
|
||||||
git \
|
|
||||||
openssh-client \
|
|
||||||
netcat-openbsd \
|
|
||||||
ca-certificates \
|
|
||||||
curl \
|
|
||||||
gnupg \
|
|
||||||
&& install -m 0755 -d /etc/apt/keyrings \
|
|
||||||
&& curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
|
|
||||||
&& chmod a+r /etc/apt/keyrings/docker.gpg \
|
|
||||||
&& echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
|
|
||||||
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" > /etc/apt/sources.list.d/docker.list \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends docker-ce-cli docker-compose-plugin \
|
|
||||||
&& curl -L --output /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 \
|
|
||||||
&& chmod +x /usr/local/bin/cloudflared \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Copy dependencies from builder
|
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
|
||||||
COPY --from=builder /root/.local /root/.local
|
|
||||||
ENV PATH=/root/.local/bin:$PATH
|
|
||||||
|
|
||||||
# Copy application code
|
COPY app/ ./app/
|
||||||
COPY --chown=appuser:appgroup . .
|
COPY pyproject.toml ./
|
||||||
|
RUN pip install --no-cache-dir -e "."
|
||||||
|
|
||||||
# Create directories for repo, instance, and workspace storage
|
USER appuser
|
||||||
RUN mkdir -p /data/repos /data/instances /data/working-copies && chown -R appuser:appgroup /data
|
|
||||||
|
|
||||||
# Copy wait-for-db script
|
|
||||||
COPY wait-for-db.sh /usr/local/bin/wait-for-db.sh
|
|
||||||
RUN chmod +x /usr/local/bin/wait-for-db.sh
|
|
||||||
|
|
||||||
# NOTE: Running as root to access Docker socket for managing tool instances
|
|
||||||
# This is required because Docker socket permissions require root or docker group membership
|
|
||||||
# which doesn't work well across container boundaries.
|
|
||||||
# Consider using Docker-in-Docker or rootless Docker for production hardening.
|
|
||||||
|
|
||||||
# Expose port
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
# Health check
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
|
||||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
|
|
||||||
|
|
||||||
# Run the application (with database wait)
|
|
||||||
ENTRYPOINT ["/usr/local/bin/wait-for-db.sh"]
|
|
||||||
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
.PHONY: revision upgrade downgrade lint test typecheck
|
||||||
|
|
||||||
|
revision:
|
||||||
|
.venv/bin/alembic revision --autogenerate -m "$(msg)"
|
||||||
|
|
||||||
|
upgrade:
|
||||||
|
.venv/bin/alembic upgrade head
|
||||||
|
|
||||||
|
downgrade:
|
||||||
|
.venv/bin/alembic downgrade -1
|
||||||
|
|
||||||
|
lint:
|
||||||
|
.venv/bin/ruff check app tests
|
||||||
|
|
||||||
|
test:
|
||||||
|
.venv/bin/pytest
|
||||||
|
|
||||||
|
typecheck:
|
||||||
|
.venv/bin/mypy app tests
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
# Headquarter API
|
|
||||||
|
|
||||||
The backend API for Headquarter - a self-hosted platform for managing projects, git repositories, and development tools.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Built with **FastAPI** and **SQLAlchemy** (async), using **PostgreSQL** for data storage and **Docker** for tool instance management.
|
|
||||||
|
|
||||||
### Tech Stack
|
|
||||||
|
|
||||||
- **Framework**: FastAPI (Python 3.12+)
|
|
||||||
- **Database**: PostgreSQL 15+ with asyncpg
|
|
||||||
- **ORM**: SQLAlchemy 2.0 (async)
|
|
||||||
- **Auth**: OAuth2 via Authentik with session cookies
|
|
||||||
- **Migrations**: Alembic
|
|
||||||
- **Tools**: Docker Compose for instance management
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- Python 3.12+
|
|
||||||
- PostgreSQL 15+ running locally
|
|
||||||
- Docker (for tool instances)
|
|
||||||
|
|
||||||
### Setup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/api
|
|
||||||
|
|
||||||
# Create virtual environment
|
|
||||||
python -m venv .venv
|
|
||||||
source .venv/bin/activate
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
pip install -e ".[dev]"
|
|
||||||
|
|
||||||
# Set up database
|
|
||||||
# Ensure PostgreSQL is running with a 'headquarter' database
|
|
||||||
|
|
||||||
# Run migrations
|
|
||||||
alembic upgrade head
|
|
||||||
|
|
||||||
# Start development server
|
|
||||||
uvicorn src.main:app --reload --port 8000
|
|
||||||
```
|
|
||||||
|
|
||||||
The API will be available at `http://localhost:8000`.
|
|
||||||
|
|
||||||
### Interactive Documentation
|
|
||||||
|
|
||||||
Once running, visit:
|
|
||||||
- **Swagger UI**: http://localhost:8000/docs
|
|
||||||
- **ReDoc**: http://localhost:8000/redoc
|
|
||||||
- **OpenAPI JSON**: http://localhost:8000/openapi.json
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
| Variable | Required | Default | Description |
|
|
||||||
|----------|----------|---------|-------------|
|
|
||||||
| `DATABASE_URL` | Yes | - | PostgreSQL connection string |
|
|
||||||
| `API_BASE_URL` | Yes | - | Public API URL (e.g., `https://api.example.com`) |
|
|
||||||
| `AUTHENTIK_DOMAIN` | Yes | - | Authentik server domain |
|
|
||||||
| `AUTHENTIK_CLIENT_ID` | Yes | - | OAuth2 client ID |
|
|
||||||
| `AUTHENTIK_CLIENT_SECRET` | Yes | - | OAuth2 client secret |
|
|
||||||
| `AUTHENTIK_APPLICATION_SLUG` | Yes | - | Authentik application slug |
|
|
||||||
| `WEB_BASE_URL` | Yes | - | Public frontend URL |
|
|
||||||
| `SESSION_SECRET` | Yes | - | Secret for session cookie signing |
|
|
||||||
| `COOKIE_DOMAIN` | No | - | Cookie domain (e.g., `.example.com`) |
|
|
||||||
| `UPLOAD_DIR` | No | `./uploads` | Directory for file uploads |
|
|
||||||
| `REPO_BASE_PATH` | No | `./repositories` | Base path for git repositories |
|
|
||||||
| `INSTANCES_BASE_PATH` | No | `./instances` | Base path for tool instances |
|
|
||||||
| `LOG_LEVEL` | No | `INFO` | Logging level |
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Running Tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run all tests
|
|
||||||
pytest
|
|
||||||
|
|
||||||
# Run specific test category
|
|
||||||
pytest -m unit # Unit tests (no DB)
|
|
||||||
pytest -m integration # Integration tests (requires DB)
|
|
||||||
|
|
||||||
# Run with coverage
|
|
||||||
pytest --cov=src --cov-report=html
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code Quality
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Format code
|
|
||||||
ruff format src tests
|
|
||||||
|
|
||||||
# Lint
|
|
||||||
ruff check src tests
|
|
||||||
|
|
||||||
# Type check
|
|
||||||
mypy src
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Migrations
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create new migration
|
|
||||||
alembic revision --autogenerate -m "description"
|
|
||||||
|
|
||||||
# Apply migrations
|
|
||||||
alembic upgrade head
|
|
||||||
|
|
||||||
# Rollback one migration
|
|
||||||
alembic downgrade -1
|
|
||||||
|
|
||||||
# Show current revision
|
|
||||||
alembic current
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Directory Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── api/ # API endpoint routers
|
|
||||||
│ ├── auth.py # OAuth2 authentication
|
|
||||||
│ ├── dashboard.py # Dashboard summary
|
|
||||||
│ ├── git_repositories.py # Git repo management
|
|
||||||
│ ├── health.py # Health checks
|
|
||||||
│ ├── projects.py # Project CRUD
|
|
||||||
│ ├── ssh_keys.py # SSH key management
|
|
||||||
│ ├── terminal.py # WebSocket terminal
|
|
||||||
│ ├── tool_instances.py # Tool instance management
|
|
||||||
│ ├── tool_types.py # Tool type definitions
|
|
||||||
│ ├── user_config.py # User preferences
|
|
||||||
│ └── users.py # User profile
|
|
||||||
├── auth/ # Authentication logic
|
|
||||||
│ ├── cookies.py # Cookie utilities
|
|
||||||
│ ├── dependencies.py # Auth dependencies
|
|
||||||
│ ├── oidc.py # OpenID Connect
|
|
||||||
│ └── session.py # Session management
|
|
||||||
├── config.py # Application settings
|
|
||||||
├── database.py # Database setup
|
|
||||||
├── main.py # FastAPI application
|
|
||||||
├── models/ # SQLAlchemy models
|
|
||||||
├── schemas/ # Pydantic schemas
|
|
||||||
├── services/ # Business logic
|
|
||||||
│ ├── docker.py # Docker Compose management
|
|
||||||
│ ├── terminal_manager.py # Terminal sessions
|
|
||||||
│ └── terminal_session.py # Terminal I/O
|
|
||||||
└── utils/ # Utilities
|
|
||||||
├── git_control.py # Git operations
|
|
||||||
├── git_files.py # File operations
|
|
||||||
├── git_history.py # History extraction
|
|
||||||
└── git_url_parser.py # URL parsing
|
|
||||||
```
|
|
||||||
|
|
||||||
### Authentication Flow
|
|
||||||
|
|
||||||
1. User clicks "Login" → redirects to Authentik OAuth
|
|
||||||
2. Authentik redirects back with authorization code
|
|
||||||
3. API exchanges code for tokens and fetches user info
|
|
||||||
4. API creates session cookie (HMAC-signed, httpOnly)
|
|
||||||
5. Frontend stores nothing - cookie sent automatically
|
|
||||||
6. Subsequent requests include cookie for authentication
|
|
||||||
|
|
||||||
### Data Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
Client → FastAPI Router → Auth Dependency → Service Layer → Database
|
|
||||||
↓
|
|
||||||
Pydantic Models (validation)
|
|
||||||
↓
|
|
||||||
SQLAlchemy Models (ORM)
|
|
||||||
↓
|
|
||||||
PostgreSQL (storage)
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
- `GET /auth/login` - Initiate OAuth login
|
|
||||||
- `GET /auth/callback` - OAuth callback
|
|
||||||
- `GET /auth/me` - Get current user
|
|
||||||
- `POST /auth/logout` - Logout
|
|
||||||
|
|
||||||
### Projects
|
|
||||||
- `GET /projects` - List projects
|
|
||||||
- `POST /projects` - Create project
|
|
||||||
- `GET /projects/{id}` - Get project
|
|
||||||
- `PUT /projects/{id}` - Update project
|
|
||||||
- `DELETE /projects/{id}` - Delete project
|
|
||||||
|
|
||||||
### Git Repositories
|
|
||||||
- `GET /projects/{id}/repositories` - List repositories
|
|
||||||
- `POST /projects/{id}/repositories` - Create repository
|
|
||||||
- `GET /projects/{id}/repositories/{id}` - Get repository
|
|
||||||
- `DELETE /projects/{id}/repositories/{id}` - Delete repository
|
|
||||||
- `GET /projects/{id}/repositories/{id}/files` - List files
|
|
||||||
- `GET /projects/{id}/repositories/{id}/files/content` - Get file content
|
|
||||||
- `POST /projects/{id}/repositories/{id}/files/content` - Update file
|
|
||||||
- `GET /projects/{id}/repositories/{id}/branches` - List branches
|
|
||||||
- `GET /projects/{id}/repositories/{id}/history` - Commit history
|
|
||||||
- `GET /projects/{id}/repositories/{id}/commits/{hash}` - Commit detail
|
|
||||||
|
|
||||||
### Tool Types
|
|
||||||
- `GET /tool-types` - List tool types
|
|
||||||
- `POST /tool-types` - Create tool type
|
|
||||||
- `GET /tool-types/{id}` - Get tool type
|
|
||||||
- `PUT /tool-types/{id}` - Update tool type
|
|
||||||
- `DELETE /tool-types/{id}` - Delete tool type
|
|
||||||
|
|
||||||
### Tool Instances
|
|
||||||
- `GET /tool-instances` - List instances
|
|
||||||
- `POST /tool-instances` - Create instance
|
|
||||||
- `GET /tool-instances/{id}` - Get instance
|
|
||||||
- `POST /tool-instances/{id}/start` - Start instance
|
|
||||||
- `POST /tool-instances/{id}/stop` - Stop instance
|
|
||||||
- `POST /tool-instances/{id}/restart` - Restart instance
|
|
||||||
- `DELETE /tool-instances/{id}` - Delete instance
|
|
||||||
- `GET /tool-instances/{id}/logs` - Get logs
|
|
||||||
|
|
||||||
### Terminal
|
|
||||||
- `WS /ws/tool-instances/{id}/terminal` - WebSocket terminal
|
|
||||||
|
|
||||||
### Users
|
|
||||||
- `GET /users/me` - Get profile
|
|
||||||
- `PUT /users/me` - Update profile
|
|
||||||
- `POST /users/me/avatar` - Upload avatar
|
|
||||||
- `GET /users/me/config` - Get config
|
|
||||||
- `PATCH /users/me/config` - Update config
|
|
||||||
|
|
||||||
### SSH Keys
|
|
||||||
- `GET /ssh-keys` - List keys
|
|
||||||
- `POST /ssh-keys` - Create key
|
|
||||||
- `DELETE /ssh-keys/{id}` - Delete key
|
|
||||||
|
|
||||||
### Health
|
|
||||||
- `GET /health` - System health
|
|
||||||
- `GET /health/db` - Database health
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
See the [deployment documentation](../../docs/deployment/) for Docker and Traefik setup.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
1. Follow PEP 8 style guide
|
|
||||||
2. Add tests for new endpoints
|
|
||||||
3. Update documentation
|
|
||||||
4. Run quality gates before committing
|
|
||||||
+119
-6
@@ -1,8 +1,119 @@
|
|||||||
[alembic]
|
# A generic, single database configuration.
|
||||||
script_location = alembic
|
|
||||||
prepend_sys_path = .
|
|
||||||
sqlalchemy.url = postgresql+asyncpg://headquarter:headquarter@postgres:5432/headquarter
|
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# path to migration scripts.
|
||||||
|
# this is typically a path given in POSIX (e.g. forward slashes)
|
||||||
|
# format, relative to the token %(here)s which refers to the location of this
|
||||||
|
# ini file
|
||||||
|
script_location = alembic
|
||||||
|
|
||||||
|
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||||
|
# Uncomment the line below if you want the files to be prepended with date and time
|
||||||
|
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||||
|
# for all available tokens
|
||||||
|
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||||
|
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
|
||||||
|
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# sys.path path, will be prepended to sys.path if present.
|
||||||
|
# defaults to the current working directory. for multiple paths, the path separator
|
||||||
|
# is defined by "path_separator" below.
|
||||||
|
prepend_sys_path = .
|
||||||
|
|
||||||
|
|
||||||
|
# timezone to use when rendering the date within the migration file
|
||||||
|
# as well as the filename.
|
||||||
|
# If specified, requires the tzdata library which can be installed by adding
|
||||||
|
# `alembic[tz]` to the pip requirements.
|
||||||
|
# string value is passed to ZoneInfo()
|
||||||
|
# leave blank for localtime
|
||||||
|
# timezone =
|
||||||
|
|
||||||
|
# max length of characters to apply to the "slug" field
|
||||||
|
# truncate_slug_length = 40
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
# set to 'true' to allow .pyc and .pyo files without
|
||||||
|
# a source .py file to be detected as revisions in the
|
||||||
|
# versions/ directory
|
||||||
|
# sourceless = false
|
||||||
|
|
||||||
|
# version location specification; This defaults
|
||||||
|
# to <script_location>/versions. When using multiple version
|
||||||
|
# directories, initial revisions must be specified with --version-path.
|
||||||
|
# The path separator used here should be the separator specified by "path_separator"
|
||||||
|
# below.
|
||||||
|
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
|
||||||
|
|
||||||
|
# path_separator; This indicates what character is used to split lists of file
|
||||||
|
# paths, including version_locations and prepend_sys_path within configparser
|
||||||
|
# files such as alembic.ini.
|
||||||
|
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
|
||||||
|
# to provide os-dependent path splitting.
|
||||||
|
#
|
||||||
|
# Note that in order to support legacy alembic.ini files, this default does NOT
|
||||||
|
# take place if path_separator is not present in alembic.ini. If this
|
||||||
|
# option is omitted entirely, fallback logic is as follows:
|
||||||
|
#
|
||||||
|
# 1. Parsing of the version_locations option falls back to using the legacy
|
||||||
|
# "version_path_separator" key, which if absent then falls back to the legacy
|
||||||
|
# behavior of splitting on spaces and/or commas.
|
||||||
|
# 2. Parsing of the prepend_sys_path option falls back to the legacy
|
||||||
|
# behavior of splitting on spaces, commas, or colons.
|
||||||
|
#
|
||||||
|
# Valid values for path_separator are:
|
||||||
|
#
|
||||||
|
# path_separator = :
|
||||||
|
# path_separator = ;
|
||||||
|
# path_separator = space
|
||||||
|
# path_separator = newline
|
||||||
|
#
|
||||||
|
# Use os.pathsep. Default configuration used for new projects.
|
||||||
|
path_separator = os
|
||||||
|
|
||||||
|
# set to 'true' to search source files recursively
|
||||||
|
# in each "version_locations" directory
|
||||||
|
# new in Alembic version 1.10
|
||||||
|
# recursive_version_locations = false
|
||||||
|
|
||||||
|
# the output encoding used when revision files
|
||||||
|
# are written from script.py.mako
|
||||||
|
# output_encoding = utf-8
|
||||||
|
|
||||||
|
# database URL. This is consumed by the user-maintained env.py script only.
|
||||||
|
# other means of configuring database URLs may be customized within the env.py
|
||||||
|
# file.
|
||||||
|
sqlalchemy.url = postgresql+asyncpg://
|
||||||
|
|
||||||
|
|
||||||
|
[post_write_hooks]
|
||||||
|
# post_write_hooks defines scripts or Python functions that are run
|
||||||
|
# on newly generated revision scripts. See the documentation for further
|
||||||
|
# detail and examples
|
||||||
|
|
||||||
|
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||||
|
# hooks = black
|
||||||
|
# black.type = console_scripts
|
||||||
|
# black.entrypoint = black
|
||||||
|
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||||
|
|
||||||
|
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
|
||||||
|
# hooks = ruff
|
||||||
|
# ruff.type = module
|
||||||
|
# ruff.module = ruff
|
||||||
|
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||||
|
|
||||||
|
# Alternatively, use the exec runner to execute a binary found on your PATH
|
||||||
|
# hooks = ruff
|
||||||
|
# ruff.type = exec
|
||||||
|
# ruff.executable = ruff
|
||||||
|
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||||
|
|
||||||
|
# Logging configuration. This is also consumed by the user-maintained
|
||||||
|
# env.py script only.
|
||||||
[loggers]
|
[loggers]
|
||||||
keys = root,sqlalchemy,alembic
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
@@ -13,11 +124,12 @@ keys = console
|
|||||||
keys = generic
|
keys = generic
|
||||||
|
|
||||||
[logger_root]
|
[logger_root]
|
||||||
level = WARN
|
level = WARNING
|
||||||
handlers = console
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
[logger_sqlalchemy]
|
[logger_sqlalchemy]
|
||||||
level = WARN
|
level = WARNING
|
||||||
handlers =
|
handlers =
|
||||||
qualname = sqlalchemy.engine
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
@@ -34,3 +146,4 @@ formatter = generic
|
|||||||
|
|
||||||
[formatter_generic]
|
[formatter_generic]
|
||||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
# apps/api/alembic (index)
|
|
||||||
dir: apps/api/alembic
|
|
||||||
|
|
||||||
## role
|
|
||||||
Database migration infrastructure for the API application, providing version-controlled schema evolution with async SQLAlchemy support.
|
|
||||||
## parent
|
|
||||||
index: apps/api/.pi-map.index.md
|
|
||||||
map: apps/api/.pi-map.md
|
|
||||||
## children
|
|
||||||
- apps/api/alembic/versions
|
|
||||||
index: apps/api/alembic/versions/.pi-map.index.md
|
|
||||||
map: apps/api/alembic/versions/.pi-map.md
|
|
||||||
## files
|
|
||||||
- env.py
|
|
||||||
- script.py.mako
|
|
||||||
## links
|
|
||||||
index: apps/api/alembic/.pi-map.index.md
|
|
||||||
map: apps/api/alembic/.pi-map.md
|
|
||||||
## workflows
|
|
||||||
- change alembic behavior
|
|
||||||
read: env.py, script.py.mako
|
|
||||||
- explore alembic subdirectories
|
|
||||||
index: apps/api/alembic/versions/.pi-map.index.md
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# apps/api/alembic
|
|
||||||
dir: apps/api/alembic
|
|
||||||
|
|
||||||
index: apps/api/alembic/.pi-map.index.md
|
|
||||||
|
|
||||||
## role
|
|
||||||
Database migration infrastructure for the API application, providing version-controlled schema evolution with async SQLAlchemy support.
|
|
||||||
## files
|
|
||||||
- env.py | Configures Alembic database migration environment with async SQLAlchemy support for a project. | exp: func:run_migrations_offline() → None, call:context.configure, call:context.begin_transaction, call:context.run_migrations, func:do_run_migrations(connection: Connection) → None, call:context.configure, call:context.begin_transaction, call:context.run_migrations, func:run_async_migrations() → None, call:async_engine_from_config, call:config.get_section, call:connectable.connect, call:connection.run_sync, call:connectable.dispose, func:run_migrations_online() → None, call:asyncio.run, call:run_async_migrations | dep: logging.config, alembic, sqlalchemy, sqlalchemy.engine, sqlalchemy.ext.asyncio, src.config, src.models, asyncio
|
|
||||||
- script.py.mako | Alembic database migration script template that generates upgrade/downgrade functions for SQLAlchemy schema migrations | dep: alembic, sqlalchemy
|
|
||||||
## arch
|
|
||||||
Alembic migration framework with Mako templating for generating revision scripts, async SQLAlchemy engine configuration, and autogenerate capabilities for schema change tracking.
|
|
||||||
## tags
|
|
||||||
migrations, run, sqlalchemy, async, alembic, call:context.configure, call:context.begin, transaction
|
|
||||||
## symbols
|
|
||||||
- run_migrations_offline
|
|
||||||
- do_run_migrations
|
|
||||||
- run_async_migrations
|
|
||||||
- run_migrations_online
|
|
||||||
- call:context.configure
|
|
||||||
- call:context.begin_transaction
|
|
||||||
- call:context.run_migrations
|
|
||||||
- call:async_engine_from_config
|
|
||||||
## workflows
|
|
||||||
- change alembic behavior
|
|
||||||
read: env.py, script.py.mako
|
|
||||||
- explore alembic subdirectories
|
|
||||||
index: apps/api/alembic/versions/.pi-map.index.md
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Generic single-database configuration.
|
||||||
+29
-18
@@ -1,32 +1,44 @@
|
|||||||
from __future__ import annotations
|
import asyncio
|
||||||
|
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
|
||||||
from alembic import context
|
|
||||||
from sqlalchemy import pool
|
from sqlalchemy import pool
|
||||||
from sqlalchemy.engine import Connection
|
from sqlalchemy.engine import Connection
|
||||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||||
|
|
||||||
from src.config import Settings
|
from alembic import context
|
||||||
from src.models import Base
|
from app.config import settings
|
||||||
|
from app.models import Base
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
config = context.config
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
if config.config_file_name is not None:
|
if config.config_file_name is not None:
|
||||||
fileConfig(config.config_file_name)
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
settings = Settings()
|
# add your model's MetaData object here
|
||||||
config.set_main_option("sqlalchemy.url", settings.database_url)
|
# for 'autogenerate' support
|
||||||
|
|
||||||
target_metadata = Base.metadata
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
# Build async URL from settings
|
||||||
|
database_url = settings.database_url
|
||||||
|
if database_url.startswith("postgresql://"):
|
||||||
|
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
|
||||||
|
|
||||||
|
config.set_main_option("sqlalchemy.url", database_url)
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_offline() -> None:
|
def run_migrations_offline() -> None:
|
||||||
|
"""Run migrations in 'offline' mode."""
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
context.configure(
|
context.configure(
|
||||||
url=settings.database_url,
|
url=url,
|
||||||
target_metadata=target_metadata,
|
target_metadata=target_metadata,
|
||||||
literal_binds=True,
|
literal_binds=True,
|
||||||
dialect_opts={"paramstyle": "named"},
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
compare_type=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
with context.begin_transaction():
|
with context.begin_transaction():
|
||||||
@@ -34,13 +46,18 @@ def run_migrations_offline() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def do_run_migrations(connection: Connection) -> None:
|
def do_run_migrations(connection: Connection) -> None:
|
||||||
context.configure(connection=connection, target_metadata=target_metadata)
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
compare_type=True,
|
||||||
|
)
|
||||||
|
|
||||||
with context.begin_transaction():
|
with context.begin_transaction():
|
||||||
context.run_migrations()
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
async def run_async_migrations() -> None:
|
async def run_migrations_online() -> None:
|
||||||
|
"""Run migrations in 'online' mode."""
|
||||||
connectable = async_engine_from_config(
|
connectable = async_engine_from_config(
|
||||||
config.get_section(config.config_ini_section, {}),
|
config.get_section(config.config_ini_section, {}),
|
||||||
prefix="sqlalchemy.",
|
prefix="sqlalchemy.",
|
||||||
@@ -53,13 +70,7 @@ async def run_async_migrations() -> None:
|
|||||||
await connectable.dispose()
|
await connectable.dispose()
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_online() -> None:
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
asyncio.run(run_async_migrations())
|
|
||||||
|
|
||||||
|
|
||||||
if context.is_offline_mode():
|
if context.is_offline_mode():
|
||||||
run_migrations_offline()
|
run_migrations_offline()
|
||||||
else:
|
else:
|
||||||
run_migrations_online()
|
asyncio.run(run_migrations_online())
|
||||||
|
|||||||
@@ -3,23 +3,26 @@
|
|||||||
Revision ID: ${up_revision}
|
Revision ID: ${up_revision}
|
||||||
Revises: ${down_revision | comma,n}
|
Revises: ${down_revision | comma,n}
|
||||||
Create Date: ${create_date}
|
Create Date: ${create_date}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
${imports if imports else ""}
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = ${repr(up_revision)}
|
revision: str = ${repr(up_revision)}
|
||||||
down_revision = ${repr(down_revision)}
|
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
||||||
branch_labels = ${repr(branch_labels)}
|
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||||
depends_on = ${repr(depends_on)}
|
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
${upgrades if upgrades else "pass"}
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
${downgrades if downgrades else "pass"}
|
${downgrades if downgrades else "pass"}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
# apps/api/alembic/versions (index)
|
|
||||||
dir: apps/api/alembic/versions
|
|
||||||
|
|
||||||
## role
|
|
||||||
Manages incremental database schema evolution for the API application using Alembic migrations, tracking all table creations, column additions, relationship changes, and data transformations over the project's lifecycle.
|
|
||||||
## parent
|
|
||||||
index: apps/api/alembic/.pi-map.index.md
|
|
||||||
map: apps/api/alembic/.pi-map.md
|
|
||||||
## children
|
|
||||||
-
|
|
||||||
## files
|
|
||||||
- 0001_initial_schema.py
|
|
||||||
- 0002_refresh_tokens.py
|
|
||||||
- 0003_user_configs.py
|
|
||||||
- 0004_tool_types.py
|
|
||||||
- 0005_ssh_keys_timestamps.py
|
|
||||||
- 0006_tool_instances.py
|
|
||||||
- 0007_instance_container_name.py
|
|
||||||
- 0008_tool_type_category.py
|
|
||||||
- 0009_tool_configs.py
|
|
||||||
- 0010_tool_type_default_port.py
|
|
||||||
- 0011_tool_instance_tunnel_fields.py
|
|
||||||
- 0012_default_port_req.py
|
|
||||||
- 0013_add_config_profiles.py
|
|
||||||
- 0013_add_probe_result.py
|
|
||||||
- 0014_add_profile_resolver_fields.py
|
|
||||||
- 0014_merge_heads.py
|
|
||||||
- 0015_single_interface.py
|
|
||||||
- 069d3da4dc9b_add_ssh_key_id_to_config_profiles.py
|
|
||||||
- 20260527160017_add_pi_agent_tool_type.py
|
|
||||||
- 2026_05_22_add_clone_mode.py
|
|
||||||
- 2026_05_23_remove_is_builtin.py
|
|
||||||
- 2026_05_24_220141_add_startup_command.py
|
|
||||||
- 2026_05_24_add_config_profiles.py
|
|
||||||
- 2026_05_26_add_git_mounts.py
|
|
||||||
- 2026_05_27_external_repos.py
|
|
||||||
- 2026_05_28_add_monitoring_tables.py
|
|
||||||
- 2026_05_28_add_terminal_sessions_table.py
|
|
||||||
- 2026_05_28_add_tool_definition_manifests.py
|
|
||||||
- 2026_05_28_drop_tool_configs_and_config_folders.py
|
|
||||||
- 2026_05_29_add_notifications_table.py
|
|
||||||
- 2026_05_29_add_ssh_key_ids_to_tool_instances.py
|
|
||||||
- 2026_05_29_drop_ssh_key_id_from_config_profiles.py
|
|
||||||
- 2026_05_29_fix_code_server_bind_addr.py
|
|
||||||
- 2026_05_29_fix_code_server_bind_addr_port.py
|
|
||||||
- 2026_05_29_fix_web_tool_bind_address.py
|
|
||||||
- 2026_05_29_remove_lsio_command_override.py
|
|
||||||
- 2026_05_29_remove_ssh_keys_mount_from_manifest.py
|
|
||||||
- 2026_06_01_add_workspaces.py
|
|
||||||
- 2026_06_13_make_clone_mode_nullable.py
|
|
||||||
- 398082499c30_add_tool_config_fields.py
|
|
||||||
- 6fc7bfcf199f_merge_remove_is_builtin_and_add_config_.py
|
|
||||||
- 86cec91fdb00_merge_profile_resolver_and_workspaces_.py
|
|
||||||
- 8c6d1dbd4798_remove_pi_config_and_state_mounts_from_.py
|
|
||||||
- 8ed7dd80973d_create_config_folders_table.py
|
|
||||||
- af8512103d67_add_tool_type_fields.py
|
|
||||||
- f3d2dc90ba3a_merge_single_interface_and_clone_mode.py
|
|
||||||
## links
|
|
||||||
index: apps/api/alembic/versions/.pi-map.index.md
|
|
||||||
map: apps/api/alembic/versions/.pi-map.md
|
|
||||||
## workflows
|
|
||||||
- change versions behavior
|
|
||||||
read: 0001_initial_schema.py, 0002_refresh_tokens.py, 0003_user_configs.py
|
|
||||||
- change versions CLI
|
|
||||||
read: 2026_05_24_220141_add_startup_command.py, 2026_05_29_remove_lsio_command_override.py
|
|
||||||
- change versions config
|
|
||||||
read: 0003_user_configs.py, 0009_tool_configs.py, 0013_add_config_profiles.py
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# apps/api/alembic/versions
|
|
||||||
dir: apps/api/alembic/versions
|
|
||||||
|
|
||||||
index: apps/api/alembic/versions/.pi-map.index.md
|
|
||||||
|
|
||||||
## role
|
|
||||||
Manages incremental database schema evolution for the API application using Alembic migrations, tracking all table creations, column additions, relationship changes, and data transformations over the project's lifecycle.
|
|
||||||
## files
|
|
||||||
- 0001_initial_schema.py | Defines the initial database schema migration creating five tables (users, ssh_keys, projects, git_repositories, user_configs) with relationships, indexes, and constraints using Alembic. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.String, call:postgresql.UUID, call:sa.DateTime, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:op.create_index, call:op.f, call:sa.Text, call:sa.ForeignKeyConstraint, call:sa.Boolean, call:postgresql.JSONB, func:downgrade() → None, call:op.drop_table, call:op.drop_index, call:op.f | dep: alembic, sqlalchemy.dialects, sqlalchemy, postgresql dialect
|
|
||||||
- 0002_refresh_tokens.py | Alembic database migration that creates a refresh_tokens table with indexes for user authentication token management | exp: func:upgrade() → None, call:op.get_bind, call:sa.inspect, call:inspector.has_table, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.String, call:sa.DateTime, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:inspector.get_indexes, call:op.f, call:op.create_index, func:downgrade() → None, call:op.get_bind, call:sa.inspect, call:inspector.has_table, call:inspector.get_indexes, call:op.f, call:op.drop_index, call:op.drop_table | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
|
||||||
- 0003_user_configs.py | Alembic database migration that creates a user_configs table with JSON configuration storage linked to users | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.JSON, call:sa.DateTime, call:sa.text, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, func:downgrade() → None, call:op.drop_table | dep: typing, alembic, sqlalchemy
|
|
||||||
- 0004_tool_types.py | Alembic database migration that creates a tool_types table with metadata, templates, and versioning columns for a tool management system. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.Boolean, call:sa.ForeignKey, call:sa.DateTime, call:sa.text, func:downgrade() → None, call:op.drop_table | dep: typing, alembic, sqlalchemy
|
|
||||||
- 0005_ssh_keys_timestamps.py | Alembic database migration that adds created_at and updated_at timestamp columns to the ssh_keys table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.DateTime, call:sa.text, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
|
||||||
- 0006_tool_instances.py | Alembic database migration that creates a tool_instances table with columns for tracking deployed tool instances, their status, container info, and foreign key relationships to tool_types, git_repositories, projects, and users. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.String, call:sa.Integer, call:sa.DateTime, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
|
||||||
- 0007_instance_container_name.py | Alembic database migration that adds a nullable container_name column to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
|
||||||
- 0008_tool_type_category.py | Alembic database migration that adds `category` and `interfaces` columns to the `tool_types` table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:sa.JSON, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
|
||||||
- 0009_tool_configs.py | Alembic database migration that creates a tool_configs table with UUID keys, foreign key relationships, and indexes for storing user/project tool configuration settings. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.String, call:sa.Text, call:sa.DateTime, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, postgresql dialect
|
|
||||||
- 0010_tool_type_default_port.py | Alembic database migration that adds a nullable default_port column to the tool_types table. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Integer, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
|
||||||
- 0011_tool_instance_tunnel_fields.py | Alembic database migration that adds tunnel-related fields (public_url and tunnel_id) to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
|
||||||
- 0012_default_port_req.py | Alembic database migration that populates null default_port values for existing tool types and then makes the column non-nullable | exp: func:upgrade() → None, call:op.execute, call:op.alter_column, call:sa.Integer, func:downgrade() → None, call:op.alter_column, call:sa.Integer | dep: typing, alembic, sqlalchemy
|
|
||||||
- 0013_add_config_profiles.py | Alembic database migration that adds config profiles, includes, mounts tables and links tool instances to profiles with defensive idempotent checks | exp: func:_table_exists(table_name: str) → bool, call:sa.inspect(op.get_bind()).has_table, call:op.get_bind, func:_column_exists(table_name: str, column_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_columns, call:op.get_bind, func:_index_exists(table_name: str, index_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_indexes, call:op.get_bind, func:_foreign_key_exists(table_name: str, constrained_columns: list[str], referred_table: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_foreign_keys, call:op.get_bind, call:foreign_key.get, func:upgrade() → None, call:_table_exists, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.String, call:sa.Text, call:sa.DateTime, call:sa.text, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:_index_exists, call:op.create_index, call:sa.Integer, call:_column_exists, call:op.add_column, call:_foreign_key_exists, call:op.create_foreign_key, func:downgrade() → None, call:op.drop_index, call:op.drop_constraint, call:op.drop_column, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
|
||||||
- 0013_add_probe_result.py | Alembic database migration that adds a JSON probe_result column to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
|
||||||
- 0014_add_profile_resolver_fields.py | Alembic database migration that adds profile resolver fields (project_id, tool_type_id, environment_variables, etc.) to config_profiles table and restructures config_mounts table (renaming mount_path to target_path, adding mode/files, removing content/source_profile_id). | exp: func:_table_exists(table_name: str) → bool, call:sa.inspect(op.get_bind()).has_table, call:op.get_bind, func:_column_exists(table_name: str, column_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_columns, call:op.get_bind, func:_index_exists(table_name: str, index_name: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_indexes, call:op.get_bind, func:_foreign_key_exists(table_name: str, constrained_columns: list[str], referred_table: str) → bool, call:_table_exists, call:sa.inspect(op.get_bind()).get_foreign_keys, call:op.get_bind, call:foreign_key.get, func:_foreign_key_names_for_column(table_name: str, column_name: str) → list[str], call:_table_exists, call:sa.inspect(op.get_bind()).get_foreign_keys, call:op.get_bind, call:foreign_key.get, call:names.append, func:upgrade() → None, call:_column_exists, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:sa.JSON, call:sa.Text, call:sa.Integer, call:sa.Boolean, call:_foreign_key_exists, call:op.create_foreign_key, call:_index_exists, call:op.create_index, call:op.alter_column, call:sa.String, call:_foreign_key_names_for_column, call:op.drop_constraint, call:op.drop_column, func:downgrade() → None, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:sa.Text, call:op.drop_column, call:op.alter_column, call:op.drop_index, call:op.drop_constraint | dep: collections.abc, alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
|
||||||
- 0014_merge_heads.py | Alembic merge migration that reconciles two divergent migration branches into a single history line | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
|
|
||||||
- 0015_single_interface.py | Alembic database migration that replaces a JSON array `interfaces` column with `interface_type` string and `requires_port` boolean columns in the `tool_types` table, with dialect-specific data migration for PostgreSQL and SQLite. | exp: func:_get_dialect() → str, call:op.get_bind, func:upgrade() → None, call:_get_dialect, call:op.add_column, call:sa.Column, call:sa.String, call:sa.Boolean, call:op.execute, call:op.alter_column, call:op.drop_column, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:_get_dialect, call:op.drop_constraint, call:op.add_column, call:sa.Column, call:postgresql.JSONB, call:sa.Text, call:op.execute, call:sa.JSON, call:op.drop_column | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, postgresql (dialect)
|
|
||||||
- 069d3da4dc9b_add_ssh_key_id_to_config_profiles.py | Alembic database migration that adds a nullable UUID foreign key column `ssh_key_id` to the `config_profiles` table referencing `ssh_keys.id` with SET NULL on delete | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Uuid, call:sa.ForeignKey, func:downgrade() → None, call:op.drop_column | dep: alembic, sqlalchemy
|
|
||||||
- 20260527160017_add_pi_agent_tool_type.py | Alembic database migration that adds a "pi-agent" terminal-based coding tool type to a tool_types table with Docker configuration templates | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text("SELECT id FROM tool_types WHERE name = 'pi-agent'") ).fetchone, call:sa.text, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text | dep: json, typing, alembic, uuid, sqlalchemy
|
|
||||||
- 2026_05_22_add_clone_mode.py | Alembic database migration that adds ssh_key_id foreign key to git_repositories table and clone_mode/branch columns to tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:postgresql.UUID, call:op.create_foreign_key, call:sa.String, func:downgrade() → None, call:op.drop_column, call:op.drop_constraint | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
|
||||||
- 2026_05_23_remove_is_builtin.py | Alembic database migration to remove the `is_builtin` column from the `tool_types` table | exp: func:upgrade() → None, call:op.execute, func:downgrade() → None, call:op.add_column, call:sa.Column, call:sa.Boolean | dep: alembic, sqlalchemy
|
|
||||||
- 2026_05_24_220141_add_startup_command.py | Alembic database migration that adds a nullable `startup_command` text column to the `tool_types` table. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Text, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
|
||||||
- 2026_05_24_add_config_profiles.py | Alembic database migration that creates config_profiles and config_profile_includes tables with indexes, and adds a foreign key column to tool_instances for managing user configuration profiles. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.ForeignKey, call:sa.String, call:sa.Text, call:postgresql.JSONB, call:sa.Boolean, call:sa.DateTime, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:op.create_index, call:sa.Integer, call:op.add_column, func:downgrade() → None, call:op.drop_index, call:op.drop_column, call:op.drop_table | dep: typing, alembic, sqlalchemy.dialects, sqlalchemy, postgresql dialect
|
|
||||||
- 2026_05_26_add_git_mounts.py | Alembic database migration that adds a git_mounts JSON column to the config_profiles table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.JSON, func:downgrade() → None, call:op.drop_column | dep: typing, alembic, sqlalchemy
|
|
||||||
- 2026_05_27_external_repos.py | Alembic database migration that makes project_id nullable in git_repositories table to support external repositories and expands alembic_version version_num column to 64 characters. | exp: func:upgrade() → None, call:op.execute, call:op.alter_column, call:sa.UUID, func:downgrade() → None, call:op.alter_column, call:sa.UUID, call:op.execute | dep: typing, alembic, sqlalchemy
|
|
||||||
- 2026_05_28_add_monitoring_tables.py | Alembic database migration that creates monitoring tables (instance_events and health_checks) with indexes for tracking tool instance events and health checks | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.DateTime, call:sa.func.now, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:sa.Boolean, call:sa.Integer, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
|
||||||
- 2026_05_28_add_terminal_sessions_table.py | Alembic database migration that creates a terminal_sessions table with tracking columns and foreign key to tool_instances | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.DateTime, call:sa.text, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:op.f, func:downgrade() → None, call:op.drop_index, call:op.f, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
|
||||||
- 2026_05_28_add_tool_definition_manifests.py | Alembic database migration that creates a tool_definition_manifests table, adds manifest-related columns to tool_types and tool_instances, and migrates the pi-agent tool from Dockerfile-based to manifest-based definitions with seed data. | exp: func:upgrade() → None, call:op.get_bind, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.Boolean, call:sa.TIMESTAMP, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.UniqueConstraint, call:sa.ForeignKeyConstraint, call:sa.CheckConstraint, call:conn.execute, call:sa.text, call:result.fetchone, call:op.add_column, call:op.create_foreign_key, call:op.drop_constraint, call:op.execute, call:json.dumps, call:str, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:op.drop_column, call:op.drop_constraint, call:op.drop_table | dep: json, uuid, typing, alembic, sqlalchemy
|
|
||||||
- 2026_05_28_drop_tool_configs_and_config_folders.py | Alembic database migration that drops `tool_configs` and `config_folders` tables with conditional existence checks and full downgrade recreation | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:op.drop_table, func:downgrade() → None, call:op.create_table, call:sa.Column, call:sa.UUID, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.Boolean, call:sa.TIMESTAMP, call:sa.func.now, call:sa.PrimaryKeyConstraint, call:sa.Integer | dep: typing, alembic, sqlalchemy
|
|
||||||
- 2026_05_29_add_notifications_table.py | Alembic database migration that creates a notifications table with user-linked, categorized, severity-graded messages supporting read/dismissed tracking and optimized querying indexes. | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.Text, call:sa.JSON, call:sa.DateTime, call:sa.func.now, call:sa.ForeignKeyConstraint, call:sa.PrimaryKeyConstraint, call:op.create_index, call:sa.text, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
|
||||||
- 2026_05_29_add_ssh_key_ids_to_tool_instances.py | Alembic database migration that adds a JSON column named ssh_key_ids to the tool_instances table | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.JSON, func:downgrade() → None, call:op.drop_column | dep: alembic, sqlalchemy
|
|
||||||
- 2026_05_29_drop_ssh_key_id_from_config_profiles.py | Alembic database migration that removes the ssh_key_id column from the config_profiles table | exp: func:upgrade() → None, call:op.drop_column, func:downgrade() → None, call:op.add_column, call:sa.Column, call:sa.Uuid, call:sa.ForeignKey | dep: alembic, sqlalchemy
|
|
||||||
- 2026_05_29_fix_code_server_bind_addr.py | Alembic database migration that fixes code-server tool type compose templates by replacing deprecated `--bind-addr` flag with `--host` flag | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text(""" SELECT id, compose_template FROM tool_types WHERE name = 'code-server' AND compose_template LIKE '%--bind-addr%' """) ).fetchall, call:sa.text, call:compose_template.replace( "--bind-addr 0.0.0.0:8443", "--host 0.0.0.0" ).replace, call:print, func:downgrade() → None | dep: typing, alembic, sqlalchemy
|
|
||||||
- 2026_05_29_fix_code_server_bind_addr_port.py | Alembic database migration that fixes code-server Docker compose templates and instance files by replacing broken `--host` flags with correct `--bind-addr 0.0.0.0:port` configurations | exp: func:_fix_tool_type_templates(conn) → None, call:conn.execute( sa.text(""" SELECT id, compose_template, default_port FROM tool_types WHERE name = 'code-server' AND compose_template LIKE '%--host%' """) ).fetchall, call:sa.text, call:compose_template.split, call:len, call:line.lstrip, call:new_lines.append, call:"\n".join, call:print, func:_fix_instance_compose_files(conn) → None, call:conn.execute( sa.text(""" SELECT column_name FROM information_schema.columns WHERE table_name = 'tool_instances' AND column_name = 'compose_path' """) ).fetchone, call:sa.text, call:print, call:conn.execute( sa.text(""" SELECT id, compose_path, tool_type_id FROM tool_instances WHERE compose_path IS NOT NULL """) ).fetchall, call:Path, call:path.exists, call:path.read_text, call:conn.execute( sa.text(""" SELECT default_port FROM tool_types WHERE id = :id """), {"id": tool_type_id}, ).fetchone, call:yaml.safe_load, call:data["services"].values, call:path.write_text, call:yaml.dump, func:upgrade() → None, call:op.get_bind, call:_fix_tool_type_templates, call:_fix_instance_compose_files, func:downgrade() → None | dep: typing, alembic, yaml, pathlib, sqlalchemy
|
|
||||||
- 2026_05_29_fix_web_tool_bind_address.py | Alembic database migration that updates code-server and jupyter-notebook tool type compose templates to bind to 0.0.0.0 | exp: func:_fix_code_server_compose(conn) → None, call:conn.execute( sa.text(""" SELECT id, compose_template, definition_type FROM tool_types WHERE name = 'code-server' """) ).fetchone, call:sa.text, call:compose_template.split, call:enumerate, call:len, call:line.lstrip, call:new_lines.append, call:image_line.lstrip, call:new_lines.index, call:new_lines.insert, call:"\n".join, call:print, func:_fix_jupyter_compose(conn) → None, call:conn.execute( sa.text(""" SELECT id, compose_template, definition_type FROM tool_types WHERE name = 'jupyter-notebook' """) ).fetchone, call:sa.text, call:compose_template.split, call:enumerate, call:new_lines.append, call:len, call:line.lstrip, call:"\n".join, call:print, func:upgrade() → None, call:op.get_bind, call:_fix_code_server_compose, call:_fix_jupyter_compose, func:downgrade() → None | dep: typing, alembic, sqlalchemy
|
|
||||||
- 2026_05_29_remove_lsio_command_override.py | Alembic database migration that removes broken command overrides containing --bind-addr or --host flags from LinuxServer.io code-server Docker Compose templates in both database tool_types records and on-disk instance compose files. | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute( sa.text(""" SELECT id, compose_template FROM tool_types WHERE name = 'code-server' """) ).fetchall, call:sa.text, call:yaml.safe_load, call:data["services"].values, call:svc.get, call:yaml.dump, call:print, call:conn.execute( sa.text(""" SELECT column_name FROM information_schema.columns WHERE table_name = 'tool_instances' AND column_name = 'compose_path' """) ).fetchone, call:conn.execute( sa.text(""" SELECT id, compose_path FROM tool_instances WHERE compose_path IS NOT NULL """) ).fetchall, call:Path, call:path.exists, call:path.read_text, call:path.write_text, func:downgrade() → None | dep: collections.abc, alembic, yaml, pathlib, sqlalchemy, pathlib.Path, information_schema
|
|
||||||
- 2026_05_29_remove_ssh_keys_mount_from_manifest.py | Alembic database migration that removes (or restores) the ssh_keys mount from a JSON manifest stored in the tool_definition_manifests table for the pi-agent tool definition. | exp: func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:isinstance, call:json.loads, call:manifest.get, call:len, call:m.get, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:isinstance, call:json.loads, call:manifest.get, call:any, call:m.get, call:mounts.append, call:json.dumps | dep: json, typing, alembic, sqlalchemy
|
|
||||||
- 2026_06_01_add_workspaces.py | Alembic database migration that creates a workspaces table with foreign keys to git_repositories and users, adds indexes, and adds a workspace_id column to tool_instances | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:sa.Uuid, call:sa.String, call:sa.ForeignKey, call:sa.DateTime, call:sa.text, call:sa.UniqueConstraint, call:op.create_index, call:op.add_column, func:downgrade() → None, call:op.drop_index, call:op.drop_column, call:op.drop_table | dep: collections.abc, alembic, sqlalchemy
|
|
||||||
- 2026_06_13_make_clone_mode_nullable.py | Alembic database migration that makes the `clone_mode` column in `tool_instances` table nullable to allow NULL values for new rows | exp: func:upgrade() → None, call:op.alter_column, call:sa.String, func:downgrade() → None, call:op.alter_column, call:sa.String | dep: alembic, sqlalchemy
|
|
||||||
- 398082499c30_add_tool_config_fields.py | Alembic database migration that adds five new columns (port_override, start_command, working_directory, environment_variables, volumes) to the tool_configs table with a port range check constraint. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.Integer, call:sa.Text, call:postgresql.JSONB, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:op.drop_constraint, call:op.drop_column | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
|
||||||
- 6fc7bfcf199f_merge_remove_is_builtin_and_add_config_.py | Alembic database migration that merges two parallel revision branches (removing is_builtin and adding config_profiles) into a single history line | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
|
||||||
- 86cec91fdb00_merge_profile_resolver_and_workspaces_.py | Alembic database migration that merges two divergent migration branches (profile resolver and workspaces) into a single head | exp: func:upgrade() → None, func:downgrade() → None | dep: alembic
|
|
||||||
- 8c6d1dbd4798_remove_pi_config_and_state_mounts_from_.py | Alembic database migration that removes pi_state and pi_config mounts from the pi-agent manifest in upgrade, and restores them in downgrade | exp: func:_load_manifest(manifest_json), call:isinstance, call:json.loads, func:upgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:_load_manifest, call:manifest.get, call:len, call:m.get, call:json.dumps, func:downgrade() → None, call:op.get_bind, call:conn.execute, call:sa.text, call:result.fetchone, call:_load_manifest, call:manifest.get, call:m.get, call:mounts.append, call:json.dumps | dep: json, alembic, sqlalchemy
|
|
||||||
- 8ed7dd80973d_create_config_folders_table.py | Alembic database migration that creates a config_folders table with user-owned configuration folders supporting JSONB file storage and project overrides | exp: func:upgrade() → None, call:op.create_table, call:sa.Column, call:postgresql.UUID, call:sa.text, call:sa.ForeignKey, call:sa.String, call:sa.Text, call:postgresql.JSONB, call:sa.Boolean, call:sa.DateTime, call:sa.UniqueConstraint, call:op.create_index, func:downgrade() → None, call:op.drop_index, call:op.drop_table | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
|
||||||
- af8512103d67_add_tool_type_fields.py | Alembic database migration that adds new columns (definition_type, dockerfile_template, build_context, readiness_probe) to the tool_types table with a CHECK constraint on definition_type. | exp: func:upgrade() → None, call:op.add_column, call:sa.Column, call:sa.String, call:sa.Text, call:postgresql.JSONB, call:op.create_check_constraint, call:sa.text, func:downgrade() → None, call:op.drop_constraint, call:op.drop_column | dep: alembic, sqlalchemy.dialects, sqlalchemy, sqlalchemy.dialects.postgresql
|
|
||||||
- f3d2dc90ba3a_merge_single_interface_and_clone_mode.py | Alembic database migration that merges two prior revisions (single_interface and clone_mode) into a single migration path | exp: func:upgrade() → None, func:downgrade() → None | dep: typing, alembic
|
|
||||||
## arch
|
|
||||||
Linear and branched migration pattern using Alembic's revision system with merge migrations to reconcile divergent branches; each migration is an imperative upgrade/downgrade script containing raw SQL/DDL operations, with some migrations including data seeding and dialect-specific logic (PostgreSQL/SQLite), but lacks consistent naming convention (mixed timestamp and numeric prefixes) indicating organic evolution rather than planned schema design.
|
|
||||||
## tags
|
|
||||||
column, table, call:op.drop, downgrade, alembic, upgrade, key, call:sa.text
|
|
||||||
## symbols
|
|
||||||
- upgrade
|
|
||||||
- downgrade
|
|
||||||
- _table_exists
|
|
||||||
- _column_exists
|
|
||||||
- _index_exists
|
|
||||||
- _foreign_key_exists
|
|
||||||
- _foreign_key_names_for_column
|
|
||||||
- _get_dialect
|
|
||||||
## workflows
|
|
||||||
- change versions behavior
|
|
||||||
read: 0001_initial_schema.py, 0002_refresh_tokens.py, 0003_user_configs.py
|
|
||||||
- change versions CLI
|
|
||||||
read: 2026_05_24_220141_add_startup_command.py, 2026_05_29_remove_lsio_command_override.py
|
|
||||||
- change versions config
|
|
||||||
read: 0003_user_configs.py, 0009_tool_configs.py, 0013_add_config_profiles.py
|
|
||||||
## dirty
|
|
||||||
-
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
"""initial schema
|
|
||||||
|
|
||||||
Revision ID: 0001_initial_schema
|
|
||||||
Revises:
|
|
||||||
Create Date: 2026-05-17 00:00:00.000000
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from alembic import op
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
revision = "0001_initial_schema"
|
|
||||||
down_revision = None
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
TABLE_NAMES = [
|
|
||||||
"users",
|
|
||||||
"ssh_keys",
|
|
||||||
"projects",
|
|
||||||
"git_repositories",
|
|
||||||
"user_configs",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.create_table(
|
|
||||||
"users",
|
|
||||||
sa.Column("email", sa.String(length=255), nullable=False),
|
|
||||||
sa.Column("name", sa.String(length=255), nullable=False),
|
|
||||||
sa.Column("authentik_id", sa.String(length=255), nullable=False),
|
|
||||||
sa.Column("avatar_url", sa.String(length=1024), nullable=True),
|
|
||||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
||||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.UniqueConstraint("authentik_id"),
|
|
||||||
sa.UniqueConstraint("email"),
|
|
||||||
)
|
|
||||||
op.create_index(op.f("ix_users_authentik_id"), "users", ["authentik_id"], unique=True)
|
|
||||||
op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True)
|
|
||||||
|
|
||||||
op.create_table(
|
|
||||||
"ssh_keys",
|
|
||||||
sa.Column("name", sa.String(length=255), nullable=False),
|
|
||||||
sa.Column("public_key", sa.Text(), nullable=False),
|
|
||||||
sa.Column("private_key_encrypted", sa.Text(), nullable=False),
|
|
||||||
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=True),
|
|
||||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
op.create_table(
|
|
||||||
"projects",
|
|
||||||
sa.Column("name", sa.String(length=255), nullable=False),
|
|
||||||
sa.Column("description", sa.Text(), nullable=True),
|
|
||||||
sa.Column("owner_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("default_ssh_key_id", postgresql.UUID(as_uuid=True), nullable=True),
|
|
||||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
||||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.ForeignKeyConstraint(["default_ssh_key_id"], ["ssh_keys.id"]),
|
|
||||||
sa.ForeignKeyConstraint(["owner_id"], ["users.id"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
op.create_table(
|
|
||||||
"git_repositories",
|
|
||||||
sa.Column("name", sa.String(length=255), nullable=False),
|
|
||||||
sa.Column("path", sa.String(length=1024), nullable=False),
|
|
||||||
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("owner_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("is_mirror", sa.Boolean(), nullable=False),
|
|
||||||
sa.Column("remote_url", sa.String(length=1024), nullable=True),
|
|
||||||
sa.Column("last_push", sa.DateTime(timezone=True), nullable=True),
|
|
||||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
||||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.ForeignKeyConstraint(["owner_id"], ["users.id"]),
|
|
||||||
sa.ForeignKeyConstraint(["project_id"], ["projects.id"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
op.create_table(
|
|
||||||
"user_configs",
|
|
||||||
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("config", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
|
||||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
||||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.UniqueConstraint("user_id"),
|
|
||||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_table("user_configs")
|
|
||||||
op.drop_table("git_repositories")
|
|
||||||
op.drop_table("projects")
|
|
||||||
op.drop_table("ssh_keys")
|
|
||||||
op.drop_index(op.f("ix_users_email"), table_name="users")
|
|
||||||
op.drop_index(op.f("ix_users_authentik_id"), table_name="users")
|
|
||||||
op.drop_table("users")
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
"""add refresh tokens table
|
|
||||||
|
|
||||||
Revision ID: 0002_refresh_tokens
|
|
||||||
Revises: 0001_initial_schema
|
|
||||||
Create Date: 2026-05-17 00:00:01.000000
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from alembic import op
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
revision = "0002_refresh_tokens"
|
|
||||||
down_revision = "0001_initial_schema"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
connection = op.get_bind()
|
|
||||||
inspector = sa.inspect(connection)
|
|
||||||
|
|
||||||
if not inspector.has_table("refresh_tokens"):
|
|
||||||
op.create_table(
|
|
||||||
"refresh_tokens",
|
|
||||||
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("token_hash", sa.String(length=255), nullable=False),
|
|
||||||
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
|
|
||||||
sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True),
|
|
||||||
sa.Column("user_agent", sa.String(length=512), nullable=True),
|
|
||||||
sa.Column("ip_address", sa.String(length=64), nullable=True),
|
|
||||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
|
||||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.UniqueConstraint("token_hash"),
|
|
||||||
)
|
|
||||||
|
|
||||||
existing_indexes = {index["name"] for index in inspector.get_indexes("refresh_tokens")}
|
|
||||||
user_index = op.f("ix_refresh_tokens_user_id")
|
|
||||||
expires_index = op.f("ix_refresh_tokens_expires_at")
|
|
||||||
if user_index not in existing_indexes:
|
|
||||||
op.create_index(user_index, "refresh_tokens", ["user_id"], unique=False)
|
|
||||||
if expires_index not in existing_indexes:
|
|
||||||
op.create_index(expires_index, "refresh_tokens", ["expires_at"], unique=False)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
connection = op.get_bind()
|
|
||||||
inspector = sa.inspect(connection)
|
|
||||||
if inspector.has_table("refresh_tokens"):
|
|
||||||
existing_indexes = {index["name"] for index in inspector.get_indexes("refresh_tokens")}
|
|
||||||
expires_index = op.f("ix_refresh_tokens_expires_at")
|
|
||||||
user_index = op.f("ix_refresh_tokens_user_id")
|
|
||||||
if expires_index in existing_indexes:
|
|
||||||
op.drop_index(expires_index, table_name="refresh_tokens")
|
|
||||||
if user_index in existing_indexes:
|
|
||||||
op.drop_index(user_index, table_name="refresh_tokens")
|
|
||||||
op.drop_table("refresh_tokens")
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
"""add user_configs table
|
|
||||||
|
|
||||||
Revision ID: 0003
|
|
||||||
Revises: 0002
|
|
||||||
Create Date: 2025-05-18
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = '0003_user_configs'
|
|
||||||
down_revision: Union[str, None] = '0002_refresh_tokens'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.create_table(
|
|
||||||
'user_configs',
|
|
||||||
sa.Column('id', sa.UUID(), nullable=False),
|
|
||||||
sa.Column('user_id', sa.UUID(), nullable=False),
|
|
||||||
sa.Column('config', sa.JSON(), nullable=False),
|
|
||||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id'),
|
|
||||||
sa.UniqueConstraint('user_id'),
|
|
||||||
if_not_exists=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_table('user_configs')
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
"""add tool_types table
|
|
||||||
|
|
||||||
Revision ID: 0004_tool_types
|
|
||||||
Revises: 0003_user_configs
|
|
||||||
Create Date: 2026-05-18 15:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "0004_tool_types"
|
|
||||||
down_revision: Union[str, None] = "0003_user_configs"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.create_table(
|
|
||||||
"tool_types",
|
|
||||||
sa.Column("id", sa.Uuid(as_uuid=True), primary_key=True),
|
|
||||||
sa.Column("name", sa.String(255), nullable=False, unique=True),
|
|
||||||
sa.Column("display_name", sa.String(255), nullable=False),
|
|
||||||
sa.Column("description", sa.Text(), nullable=True),
|
|
||||||
sa.Column("compose_template", sa.Text(), nullable=False),
|
|
||||||
sa.Column("required_variables", sa.JSON(), nullable=False, default=list),
|
|
||||||
sa.Column("is_builtin", sa.Boolean(), nullable=False, default=False),
|
|
||||||
sa.Column("created_by_id", sa.Uuid(as_uuid=True), sa.ForeignKey("users.id"), nullable=True),
|
|
||||||
sa.Column(
|
|
||||||
"created_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.text("now()"),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
sa.Column(
|
|
||||||
"updated_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.text("now()"),
|
|
||||||
onupdate=sa.text("now()"),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
if_not_exists=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_table("tool_types")
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
"""add timestamps to ssh_keys table
|
|
||||||
|
|
||||||
Revision ID: 0005_ssh_keys_timestamps
|
|
||||||
Revises: 0004_tool_types
|
|
||||||
Create Date: 2026-05-19 09:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "0005_ssh_keys_timestamps"
|
|
||||||
down_revision: Union[str, None] = "0004_tool_types"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.add_column(
|
|
||||||
"ssh_keys",
|
|
||||||
sa.Column(
|
|
||||||
"created_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.text("now()"),
|
|
||||||
nullable=True,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
op.add_column(
|
|
||||||
"ssh_keys",
|
|
||||||
sa.Column(
|
|
||||||
"updated_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.text("now()"),
|
|
||||||
nullable=True,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_column("ssh_keys", "updated_at")
|
|
||||||
op.drop_column("ssh_keys", "created_at")
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
"""add tool_instances table
|
|
||||||
|
|
||||||
Revision ID: 0006_tool_instances
|
|
||||||
Revises: 0005_ssh_keys_timestamps
|
|
||||||
Create Date: 2026-05-19 10:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "0006_tool_instances"
|
|
||||||
down_revision: Union[str, None] = "0005_ssh_keys_timestamps"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.create_table(
|
|
||||||
"tool_instances",
|
|
||||||
sa.Column("id", postgresql.UUID(as_uuid=True), server_default=sa.text("gen_random_uuid()"), nullable=False),
|
|
||||||
sa.Column("name", sa.String(255), nullable=False),
|
|
||||||
sa.Column("display_name", sa.String(255), nullable=False),
|
|
||||||
sa.Column("tool_type_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("repository_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("owner_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("status", sa.String(50), nullable=False, server_default="pending"),
|
|
||||||
sa.Column("container_id", sa.String(255), nullable=True),
|
|
||||||
sa.Column("compose_path", sa.String(1024), nullable=True),
|
|
||||||
sa.Column("url", sa.String(1024), nullable=True),
|
|
||||||
sa.Column("port", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("last_started_at", sa.DateTime(timezone=True), nullable=True),
|
|
||||||
sa.Column("last_stopped_at", sa.DateTime(timezone=True), nullable=True),
|
|
||||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
|
||||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(["tool_type_id"], ["tool_types.id"]),
|
|
||||||
sa.ForeignKeyConstraint(["repository_id"], ["git_repositories.id"]),
|
|
||||||
sa.ForeignKeyConstraint(["project_id"], ["projects.id"]),
|
|
||||||
sa.ForeignKeyConstraint(["owner_id"], ["users.id"]),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_index("idx_tool_instances_owner", "tool_instances", ["owner_id"])
|
|
||||||
op.create_index("idx_tool_instances_repo", "tool_instances", ["repository_id"])
|
|
||||||
op.create_index("idx_tool_instances_status", "tool_instances", ["status"])
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_index("idx_tool_instances_status", table_name="tool_instances")
|
|
||||||
op.drop_index("idx_tool_instances_repo", table_name="tool_instances")
|
|
||||||
op.drop_index("idx_tool_instances_owner", table_name="tool_instances")
|
|
||||||
op.drop_table("tool_instances")
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
"""add container_name to tool_instances
|
|
||||||
|
|
||||||
Revision ID: 0007_instance_container_name
|
|
||||||
Revises: 0006_tool_instances
|
|
||||||
Create Date: 2026-05-20 08:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "0007_instance_container_name"
|
|
||||||
down_revision: Union[str, None] = "0006_tool_instances"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.add_column(
|
|
||||||
"tool_instances",
|
|
||||||
sa.Column("container_name", sa.String(255), nullable=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_column("tool_instances", "container_name")
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
"""add category and interfaces to tool_types
|
|
||||||
|
|
||||||
Revision ID: 0008_tool_type_category
|
|
||||||
Revises: 0007_instance_container_name
|
|
||||||
Create Date: 2026-05-20 09:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "0008_tool_type_category"
|
|
||||||
down_revision: Union[str, None] = "0007_instance_container_name"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.add_column(
|
|
||||||
"tool_types",
|
|
||||||
sa.Column("category", sa.String(50), nullable=False, server_default="other")
|
|
||||||
)
|
|
||||||
op.add_column(
|
|
||||||
"tool_types",
|
|
||||||
sa.Column("interfaces", sa.JSON(), nullable=False, server_default='["web"]')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_column("tool_types", "interfaces")
|
|
||||||
op.drop_column("tool_types", "category")
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
"""add tool_configs table
|
|
||||||
|
|
||||||
Revision ID: 0009_tool_configs
|
|
||||||
Revises: 0008_tool_type_category
|
|
||||||
Create Date: 2026-05-20 09:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "0009_tool_configs"
|
|
||||||
down_revision: Union[str, None] = "0008_tool_type_category"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.create_table(
|
|
||||||
"tool_configs",
|
|
||||||
sa.Column("id", postgresql.UUID(as_uuid=True), server_default=sa.text("gen_random_uuid()"), nullable=False),
|
|
||||||
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("tool_type_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=True),
|
|
||||||
sa.Column("key", sa.String(255), nullable=False),
|
|
||||||
sa.Column("value", sa.Text(), nullable=False),
|
|
||||||
sa.Column("config_type", sa.String(20), nullable=False, server_default="env"),
|
|
||||||
sa.Column("file_path", sa.String(1024), nullable=True),
|
|
||||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
|
||||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
|
|
||||||
sa.ForeignKeyConstraint(["tool_type_id"], ["tool_types.id"]),
|
|
||||||
sa.ForeignKeyConstraint(["project_id"], ["projects.id"]),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_index("idx_tool_configs_user_tool", "tool_configs", ["user_id", "tool_type_id"])
|
|
||||||
op.create_index("idx_tool_configs_project", "tool_configs", ["project_id"])
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_index("idx_tool_configs_project", table_name="tool_configs")
|
|
||||||
op.drop_index("idx_tool_configs_user_tool", table_name="tool_configs")
|
|
||||||
op.drop_table("tool_configs")
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
"""add default_port to tool_types
|
|
||||||
|
|
||||||
Revision ID: 0010_tool_type_default_port
|
|
||||||
Revises: 0009_tool_configs
|
|
||||||
Create Date: 2026-05-20 10:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "0010_tool_type_default_port"
|
|
||||||
down_revision: Union[str, None] = "0009_tool_configs"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.add_column(
|
|
||||||
"tool_types",
|
|
||||||
sa.Column("default_port", sa.Integer(), nullable=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_column("tool_types", "default_port")
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
"""add tunnel fields to tool_instances
|
|
||||||
|
|
||||||
Revision ID: 0011_tool_instance_tunnel_fields
|
|
||||||
Revises: 0010_tool_type_default_port
|
|
||||||
Create Date: 2026-05-20 12:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "0011_tool_instance_tunnel_fields"
|
|
||||||
down_revision: Union[str, None] = "0010_tool_type_default_port"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.add_column(
|
|
||||||
"tool_instances",
|
|
||||||
sa.Column("public_url", sa.String(1024), nullable=True)
|
|
||||||
)
|
|
||||||
op.add_column(
|
|
||||||
"tool_instances",
|
|
||||||
sa.Column("tunnel_id", sa.String(255), nullable=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_column("tool_instances", "tunnel_id")
|
|
||||||
op.drop_column("tool_instances", "public_url")
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
"""make default_port non-nullable and set values
|
|
||||||
|
|
||||||
Revision ID: 0012_default_port_req
|
|
||||||
Revises: 0011_tool_instance_tunnel_fields
|
|
||||||
Create Date: 2026-05-20 15:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "0012_default_port_req"
|
|
||||||
down_revision: Union[str, None] = "0011_tool_instance_tunnel_fields"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
# Set default_port for existing built-in tool types
|
|
||||||
op.execute("""
|
|
||||||
UPDATE tool_types
|
|
||||||
SET default_port = CASE
|
|
||||||
WHEN name = 'code-server' THEN 8443
|
|
||||||
WHEN name = 'jupyter-notebook' THEN 8888
|
|
||||||
WHEN name = 'opencode' THEN 3000
|
|
||||||
ELSE 8080
|
|
||||||
END
|
|
||||||
WHERE default_port IS NULL
|
|
||||||
""")
|
|
||||||
|
|
||||||
# Make default_port non-nullable
|
|
||||||
op.alter_column(
|
|
||||||
"tool_types",
|
|
||||||
"default_port",
|
|
||||||
existing_type=sa.Integer(),
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.alter_column(
|
|
||||||
"tool_types",
|
|
||||||
"default_port",
|
|
||||||
existing_type=sa.Integer(),
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
"""add config profiles, includes, mounts, and tool instance profile selection
|
|
||||||
|
|
||||||
Revision ID: 0013_add_config_profiles
|
|
||||||
Revises: 0012_default_port_req
|
|
||||||
Create Date: 2026-05-24 12:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from collections.abc import Sequence
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "0013_add_config_profiles"
|
|
||||||
down_revision: str | None = "0012_default_port_req"
|
|
||||||
branch_labels: str | Sequence[str] | None = None
|
|
||||||
depends_on: str | Sequence[str] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def _table_exists(table_name: str) -> bool:
|
|
||||||
return sa.inspect(op.get_bind()).has_table(table_name)
|
|
||||||
|
|
||||||
|
|
||||||
def _column_exists(table_name: str, column_name: str) -> bool:
|
|
||||||
if not _table_exists(table_name):
|
|
||||||
return False
|
|
||||||
return column_name in {
|
|
||||||
column["name"] for column in sa.inspect(op.get_bind()).get_columns(table_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _index_exists(table_name: str, index_name: str) -> bool:
|
|
||||||
if not _table_exists(table_name):
|
|
||||||
return False
|
|
||||||
return index_name in {
|
|
||||||
index["name"] for index in sa.inspect(op.get_bind()).get_indexes(table_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _foreign_key_exists(
|
|
||||||
table_name: str,
|
|
||||||
constrained_columns: list[str],
|
|
||||||
referred_table: str,
|
|
||||||
) -> bool:
|
|
||||||
if not _table_exists(table_name):
|
|
||||||
return False
|
|
||||||
for foreign_key in sa.inspect(op.get_bind()).get_foreign_keys(table_name):
|
|
||||||
if (
|
|
||||||
foreign_key.get("constrained_columns") == constrained_columns
|
|
||||||
and foreign_key.get("referred_table") == referred_table
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
# Earlier branches may already have created config_profiles. Keep this
|
|
||||||
# migration defensive so databases can converge onto the current graph.
|
|
||||||
if not _table_exists("config_profiles"):
|
|
||||||
op.create_table(
|
|
||||||
"config_profiles",
|
|
||||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("name", sa.String(length=255), nullable=False),
|
|
||||||
sa.Column("description", sa.Text(), nullable=True),
|
|
||||||
sa.Column(
|
|
||||||
"created_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.text("NOW()"),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
sa.Column(
|
|
||||||
"updated_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.text("NOW()"),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.UniqueConstraint(
|
|
||||||
"user_id", "name", name="uq_config_profiles_user_name"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if not _index_exists("config_profiles", "idx_config_profiles_user"):
|
|
||||||
op.create_index("idx_config_profiles_user", "config_profiles", ["user_id"])
|
|
||||||
|
|
||||||
if not _table_exists("config_includes"):
|
|
||||||
op.create_table(
|
|
||||||
"config_includes",
|
|
||||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("profile_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column(
|
|
||||||
"included_profile_id", postgresql.UUID(as_uuid=True), nullable=False
|
|
||||||
),
|
|
||||||
sa.Column("order_index", sa.Integer(), nullable=False, server_default="0"),
|
|
||||||
sa.Column(
|
|
||||||
"created_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.text("NOW()"),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
sa.Column(
|
|
||||||
"updated_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.text("NOW()"),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["profile_id"], ["config_profiles.id"], ondelete="CASCADE"
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["included_profile_id"],
|
|
||||||
["config_profiles.id"],
|
|
||||||
ondelete="CASCADE",
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.UniqueConstraint(
|
|
||||||
"profile_id", "included_profile_id", name="uq_config_includes_pair"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if not _index_exists("config_includes", "idx_config_includes_profile"):
|
|
||||||
op.create_index("idx_config_includes_profile", "config_includes", ["profile_id"])
|
|
||||||
if not _index_exists("config_includes", "idx_config_includes_included"):
|
|
||||||
op.create_index(
|
|
||||||
"idx_config_includes_included", "config_includes", ["included_profile_id"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not _table_exists("config_mounts"):
|
|
||||||
op.create_table(
|
|
||||||
"config_mounts",
|
|
||||||
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("profile_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
||||||
sa.Column("mount_path", sa.String(length=1024), nullable=False),
|
|
||||||
sa.Column("content", sa.Text(), nullable=True),
|
|
||||||
sa.Column("source_profile_id", postgresql.UUID(as_uuid=True), nullable=True),
|
|
||||||
sa.Column("order_index", sa.Integer(), nullable=False, server_default="0"),
|
|
||||||
sa.Column(
|
|
||||||
"created_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.text("NOW()"),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
sa.Column(
|
|
||||||
"updated_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.text("NOW()"),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["profile_id"], ["config_profiles.id"], ondelete="CASCADE"
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["source_profile_id"], ["config_profiles.id"], ondelete="SET NULL"
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
if not _index_exists("config_mounts", "idx_config_mounts_profile"):
|
|
||||||
op.create_index("idx_config_mounts_profile", "config_mounts", ["profile_id"])
|
|
||||||
|
|
||||||
if not _column_exists("tool_instances", "selected_profile_id"):
|
|
||||||
op.add_column(
|
|
||||||
"tool_instances",
|
|
||||||
sa.Column("selected_profile_id", postgresql.UUID(as_uuid=True), nullable=True),
|
|
||||||
)
|
|
||||||
if not _foreign_key_exists(
|
|
||||||
"tool_instances", ["selected_profile_id"], "config_profiles"
|
|
||||||
):
|
|
||||||
op.create_foreign_key(
|
|
||||||
"fk_tool_instances_selected_profile",
|
|
||||||
"tool_instances",
|
|
||||||
"config_profiles",
|
|
||||||
["selected_profile_id"],
|
|
||||||
["id"],
|
|
||||||
ondelete="SET NULL",
|
|
||||||
)
|
|
||||||
if not _index_exists("tool_instances", "idx_tool_instances_selected_profile"):
|
|
||||||
op.create_index(
|
|
||||||
"idx_tool_instances_selected_profile",
|
|
||||||
"tool_instances",
|
|
||||||
["selected_profile_id"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
# Remove selected_profile_id from tool_instances
|
|
||||||
op.drop_index("idx_tool_instances_selected_profile", table_name="tool_instances")
|
|
||||||
op.drop_constraint(
|
|
||||||
"fk_tool_instances_selected_profile", "tool_instances", type_="foreignkey"
|
|
||||||
)
|
|
||||||
op.drop_column("tool_instances", "selected_profile_id")
|
|
||||||
|
|
||||||
# Drop config_mounts
|
|
||||||
op.drop_index("idx_config_mounts_profile", table_name="config_mounts")
|
|
||||||
op.drop_table("config_mounts")
|
|
||||||
|
|
||||||
# Drop config_includes
|
|
||||||
op.drop_index("idx_config_includes_included", table_name="config_includes")
|
|
||||||
op.drop_index("idx_config_includes_profile", table_name="config_includes")
|
|
||||||
op.drop_table("config_includes")
|
|
||||||
|
|
||||||
# Drop config_profiles
|
|
||||||
op.drop_index("idx_config_profiles_user", table_name="config_profiles")
|
|
||||||
op.drop_table("config_profiles")
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
"""add probe_result to tool_instances
|
|
||||||
|
|
||||||
Revision ID: 0013_add_probe_result
|
|
||||||
Revises: 0012_default_port_req
|
|
||||||
Create Date: 2026-05-22 21:45:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "0013_add_probe_result"
|
|
||||||
down_revision: Union[str, None] = "0012_default_port_req"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.add_column(
|
|
||||||
"tool_instances",
|
|
||||||
sa.Column("probe_result", postgresql.JSON, nullable=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_column("tool_instances", "probe_result")
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
"""add profile resolver fields to config profiles and mounts
|
|
||||||
|
|
||||||
Revision ID: 0014_add_profile_resolver_fields
|
|
||||||
Revises: 0013_add_config_profiles
|
|
||||||
Create Date: 2026-05-24 14:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from collections.abc import Sequence
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "0014_add_profile_resolver_fields"
|
|
||||||
down_revision: str | None = "0013_add_config_profiles"
|
|
||||||
branch_labels: str | Sequence[str] | None = None
|
|
||||||
depends_on: str | Sequence[str] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def _table_exists(table_name: str) -> bool:
|
|
||||||
return sa.inspect(op.get_bind()).has_table(table_name)
|
|
||||||
|
|
||||||
|
|
||||||
def _column_exists(table_name: str, column_name: str) -> bool:
|
|
||||||
if not _table_exists(table_name):
|
|
||||||
return False
|
|
||||||
return column_name in {
|
|
||||||
column["name"] for column in sa.inspect(op.get_bind()).get_columns(table_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _index_exists(table_name: str, index_name: str) -> bool:
|
|
||||||
if not _table_exists(table_name):
|
|
||||||
return False
|
|
||||||
return index_name in {
|
|
||||||
index["name"] for index in sa.inspect(op.get_bind()).get_indexes(table_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _foreign_key_exists(
|
|
||||||
table_name: str,
|
|
||||||
constrained_columns: list[str],
|
|
||||||
referred_table: str,
|
|
||||||
) -> bool:
|
|
||||||
if not _table_exists(table_name):
|
|
||||||
return False
|
|
||||||
for foreign_key in sa.inspect(op.get_bind()).get_foreign_keys(table_name):
|
|
||||||
if (
|
|
||||||
foreign_key.get("constrained_columns") == constrained_columns
|
|
||||||
and foreign_key.get("referred_table") == referred_table
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _foreign_key_names_for_column(table_name: str, column_name: str) -> list[str]:
|
|
||||||
if not _table_exists(table_name):
|
|
||||||
return []
|
|
||||||
names: list[str] = []
|
|
||||||
for foreign_key in sa.inspect(op.get_bind()).get_foreign_keys(table_name):
|
|
||||||
if column_name in foreign_key.get("constrained_columns", []):
|
|
||||||
name = foreign_key.get("name")
|
|
||||||
if name:
|
|
||||||
names.append(name)
|
|
||||||
return names
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
if not _column_exists("config_profiles", "project_id"):
|
|
||||||
op.add_column(
|
|
||||||
"config_profiles",
|
|
||||||
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=True),
|
|
||||||
)
|
|
||||||
if not _column_exists("config_profiles", "tool_type_id"):
|
|
||||||
op.add_column(
|
|
||||||
"config_profiles",
|
|
||||||
sa.Column("tool_type_id", postgresql.UUID(as_uuid=True), nullable=True),
|
|
||||||
)
|
|
||||||
if not _column_exists("config_profiles", "environment_variables"):
|
|
||||||
op.add_column(
|
|
||||||
"config_profiles",
|
|
||||||
sa.Column("environment_variables", sa.JSON(), nullable=True),
|
|
||||||
)
|
|
||||||
if not _column_exists("config_profiles", "start_command"):
|
|
||||||
op.add_column(
|
|
||||||
"config_profiles",
|
|
||||||
sa.Column("start_command", sa.Text(), nullable=True),
|
|
||||||
)
|
|
||||||
if not _column_exists("config_profiles", "working_directory"):
|
|
||||||
op.add_column(
|
|
||||||
"config_profiles",
|
|
||||||
sa.Column("working_directory", sa.Text(), nullable=True),
|
|
||||||
)
|
|
||||||
if not _column_exists("config_profiles", "port"):
|
|
||||||
op.add_column("config_profiles", sa.Column("port", sa.Integer(), nullable=True))
|
|
||||||
if not _column_exists("config_profiles", "is_default"):
|
|
||||||
op.add_column(
|
|
||||||
"config_profiles",
|
|
||||||
sa.Column("is_default", sa.Boolean(), nullable=False, server_default="false"),
|
|
||||||
)
|
|
||||||
|
|
||||||
if not _foreign_key_exists("config_profiles", ["project_id"], "projects"):
|
|
||||||
op.create_foreign_key(
|
|
||||||
"fk_config_profiles_project",
|
|
||||||
"config_profiles",
|
|
||||||
"projects",
|
|
||||||
["project_id"],
|
|
||||||
["id"],
|
|
||||||
ondelete="CASCADE",
|
|
||||||
)
|
|
||||||
if not _foreign_key_exists("config_profiles", ["tool_type_id"], "tool_types"):
|
|
||||||
op.create_foreign_key(
|
|
||||||
"fk_config_profiles_tool_type",
|
|
||||||
"config_profiles",
|
|
||||||
"tool_types",
|
|
||||||
["tool_type_id"],
|
|
||||||
["id"],
|
|
||||||
ondelete="CASCADE",
|
|
||||||
)
|
|
||||||
|
|
||||||
if not _index_exists("config_profiles", "idx_config_profiles_project"):
|
|
||||||
op.create_index("idx_config_profiles_project", "config_profiles", ["project_id"])
|
|
||||||
if not _index_exists("config_profiles", "idx_config_profiles_tool_type"):
|
|
||||||
op.create_index(
|
|
||||||
"idx_config_profiles_tool_type", "config_profiles", ["tool_type_id"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if _column_exists("config_mounts", "mount_path") and not _column_exists(
|
|
||||||
"config_mounts", "target_path"
|
|
||||||
):
|
|
||||||
op.alter_column("config_mounts", "mount_path", new_column_name="target_path")
|
|
||||||
if not _column_exists("config_mounts", "mode"):
|
|
||||||
op.add_column(
|
|
||||||
"config_mounts",
|
|
||||||
sa.Column("mode", sa.String(length=10), nullable=False, server_default="rw"),
|
|
||||||
)
|
|
||||||
if not _column_exists("config_mounts", "files"):
|
|
||||||
op.add_column(
|
|
||||||
"config_mounts",
|
|
||||||
sa.Column("files", sa.JSON(), nullable=True),
|
|
||||||
)
|
|
||||||
for constraint_name in _foreign_key_names_for_column(
|
|
||||||
"config_mounts", "source_profile_id"
|
|
||||||
):
|
|
||||||
op.drop_constraint(constraint_name, "config_mounts", type_="foreignkey")
|
|
||||||
if _column_exists("config_mounts", "content"):
|
|
||||||
op.drop_column("config_mounts", "content")
|
|
||||||
if _column_exists("config_mounts", "source_profile_id"):
|
|
||||||
op.drop_column("config_mounts", "source_profile_id")
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
# Restore config_mounts
|
|
||||||
op.add_column(
|
|
||||||
"config_mounts",
|
|
||||||
sa.Column("source_profile_id", postgresql.UUID(as_uuid=True), nullable=True),
|
|
||||||
)
|
|
||||||
op.add_column(
|
|
||||||
"config_mounts",
|
|
||||||
sa.Column("content", sa.Text(), nullable=True),
|
|
||||||
)
|
|
||||||
op.drop_column("config_mounts", "files")
|
|
||||||
op.drop_column("config_mounts", "mode")
|
|
||||||
op.alter_column("config_mounts", "target_path", new_column_name="mount_path")
|
|
||||||
|
|
||||||
# Restore config_profiles
|
|
||||||
op.drop_index("idx_config_profiles_tool_type", table_name="config_profiles")
|
|
||||||
op.drop_index("idx_config_profiles_project", table_name="config_profiles")
|
|
||||||
op.drop_constraint(
|
|
||||||
"fk_config_profiles_tool_type", "config_profiles", type_="foreignkey"
|
|
||||||
)
|
|
||||||
op.drop_constraint("fk_config_profiles_project", "config_profiles", type_="foreignkey")
|
|
||||||
op.drop_column("config_profiles", "is_default")
|
|
||||||
op.drop_column("config_profiles", "port")
|
|
||||||
op.drop_column("config_profiles", "working_directory")
|
|
||||||
op.drop_column("config_profiles", "start_command")
|
|
||||||
op.drop_column("config_profiles", "environment_variables")
|
|
||||||
op.drop_column("config_profiles", "tool_type_id")
|
|
||||||
op.drop_column("config_profiles", "project_id")
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
"""merge migration heads
|
|
||||||
|
|
||||||
Revision ID: 0014_merge_heads
|
|
||||||
Revises: 0013_add_probe_result, 8ed7dd80973d
|
|
||||||
Create Date: 2026-05-22 21:50:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "0014_merge_heads"
|
|
||||||
down_revision: Union[str, Sequence[str], None] = ("0013_add_probe_result", "8ed7dd80973d")
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
pass
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
"""replace interfaces with interface_type and add requires_port
|
|
||||||
|
|
||||||
Revision ID: 0015_single_interface
|
|
||||||
Revises: 0014_merge_heads
|
|
||||||
Create Date: 2026-05-22 22:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "0015_single_interface"
|
|
||||||
down_revision: Union[str, Sequence[str], None] = "0014_merge_heads"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_dialect() -> str:
|
|
||||||
"""Get the current database dialect name."""
|
|
||||||
conn = op.get_bind()
|
|
||||||
return conn.dialect.name
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
dialect = _get_dialect()
|
|
||||||
|
|
||||||
# Add new columns
|
|
||||||
op.add_column('tool_types', sa.Column('interface_type', sa.String(20), nullable=True))
|
|
||||||
op.add_column('tool_types', sa.Column('requires_port', sa.Boolean(), nullable=False, server_default='true'))
|
|
||||||
|
|
||||||
# Migrate data: take first element from interfaces JSON array
|
|
||||||
if dialect == 'postgresql':
|
|
||||||
op.execute("""
|
|
||||||
UPDATE tool_types
|
|
||||||
SET interface_type = COALESCE(
|
|
||||||
(SELECT elem FROM jsonb_array_elements_text(interfaces::jsonb) AS elem LIMIT 1),
|
|
||||||
'web'
|
|
||||||
),
|
|
||||||
requires_port = CASE
|
|
||||||
WHEN COALESCE(
|
|
||||||
(SELECT elem FROM jsonb_array_elements_text(interfaces::jsonb) AS elem LIMIT 1),
|
|
||||||
'web'
|
|
||||||
) = 'web' THEN true
|
|
||||||
ELSE false
|
|
||||||
END
|
|
||||||
""")
|
|
||||||
else:
|
|
||||||
# SQLite: interfaces is stored as JSON text, extract first array element
|
|
||||||
op.execute("""
|
|
||||||
UPDATE tool_types
|
|
||||||
SET interface_type = COALESCE(
|
|
||||||
(SELECT json_extract(value, '$[0]')
|
|
||||||
FROM json_each(interfaces) AS value
|
|
||||||
WHERE json_valid(interfaces)
|
|
||||||
LIMIT 1),
|
|
||||||
'web'
|
|
||||||
),
|
|
||||||
requires_port = CASE
|
|
||||||
WHEN COALESCE(
|
|
||||||
(SELECT json_extract(value, '$[0]')
|
|
||||||
FROM json_each(interfaces) AS value
|
|
||||||
WHERE json_valid(interfaces)
|
|
||||||
LIMIT 1),
|
|
||||||
'web'
|
|
||||||
) = 'web' THEN true
|
|
||||||
ELSE false
|
|
||||||
END
|
|
||||||
""")
|
|
||||||
|
|
||||||
# Make interface_type non-nullable after data migration
|
|
||||||
op.alter_column('tool_types', 'interface_type', nullable=False)
|
|
||||||
|
|
||||||
# Drop old interfaces column
|
|
||||||
op.drop_column('tool_types', 'interfaces')
|
|
||||||
|
|
||||||
# Add CHECK constraint for interface_type (only on PostgreSQL; SQLite supports it too)
|
|
||||||
op.create_check_constraint('chk_interface_type', 'tool_types', sa.text("interface_type IN ('web', 'terminal')"))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
dialect = _get_dialect()
|
|
||||||
|
|
||||||
# Drop CHECK constraint
|
|
||||||
op.drop_constraint('chk_interface_type', 'tool_types', type_='check')
|
|
||||||
|
|
||||||
# Add back interfaces column
|
|
||||||
if dialect == 'postgresql':
|
|
||||||
op.add_column('tool_types', sa.Column('interfaces', postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default='["web"]'))
|
|
||||||
|
|
||||||
# Migrate data back: wrap interface_type in array
|
|
||||||
op.execute("""
|
|
||||||
UPDATE tool_types
|
|
||||||
SET interfaces = jsonb_build_array(interface_type)
|
|
||||||
""")
|
|
||||||
else:
|
|
||||||
op.add_column('tool_types', sa.Column('interfaces', sa.JSON(), nullable=False, server_default='["web"]'))
|
|
||||||
|
|
||||||
# Migrate data back: wrap interface_type in array for SQLite
|
|
||||||
op.execute("""
|
|
||||||
UPDATE tool_types
|
|
||||||
SET interfaces = json_array(interface_type)
|
|
||||||
""")
|
|
||||||
|
|
||||||
# Drop new columns
|
|
||||||
op.drop_column('tool_types', 'requires_port')
|
|
||||||
op.drop_column('tool_types', 'interface_type')
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"""add_ssh_key_id_to_config_profiles
|
|
||||||
|
|
||||||
Revision ID: 069d3da4dc9b
|
|
||||||
Revises: 2026_05_29_add_notifications_table
|
|
||||||
Create Date: 2026-05-29 12:30:16.580532
|
|
||||||
"""
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "069d3da4dc9b"
|
|
||||||
down_revision = "2026_05_29_add_notifications_table"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.add_column(
|
|
||||||
"config_profiles",
|
|
||||||
sa.Column(
|
|
||||||
"ssh_key_id",
|
|
||||||
sa.Uuid(),
|
|
||||||
sa.ForeignKey("ssh_keys.id", ondelete="SET NULL"),
|
|
||||||
nullable=True,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_column("config_profiles", "ssh_key_id")
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
"""add pi agent tool type
|
|
||||||
|
|
||||||
Revision ID: 20260527_160017_add_pi_agent
|
|
||||||
Revises: f3d2dc90ba3a
|
|
||||||
Create Date: 2026-05-27T16:00:17
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "20260527_160017_add_pi_agent"
|
|
||||||
down_revision: Union[str, None] = "2026_05_27_external_repos"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
PI_AGENT_ID = uuid.UUID("d07b8376-2151-4119-8c1d-27f792aae9a3")
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
# Check if pi-agent already exists
|
|
||||||
conn = op.get_bind()
|
|
||||||
result = conn.execute(
|
|
||||||
sa.text("SELECT id FROM tool_types WHERE name = 'pi-agent'")
|
|
||||||
).fetchone()
|
|
||||||
|
|
||||||
if result is None:
|
|
||||||
conn.execute(
|
|
||||||
sa.text("""
|
|
||||||
INSERT INTO tool_types (
|
|
||||||
id, name, display_name, description, category,
|
|
||||||
interface_type, requires_port, default_port,
|
|
||||||
definition_type, compose_template, dockerfile_template, required_variables,
|
|
||||||
created_at, updated_at
|
|
||||||
) VALUES (
|
|
||||||
:id, :name, :display_name, :description, :category,
|
|
||||||
:interface_type, :requires_port, :default_port,
|
|
||||||
:definition_type, :compose_template, :dockerfile_template, :required_variables,
|
|
||||||
now(), now()
|
|
||||||
)
|
|
||||||
"""),
|
|
||||||
{
|
|
||||||
"id": PI_AGENT_ID,
|
|
||||||
"name": "pi-agent",
|
|
||||||
"display_name": "Pi Agent",
|
|
||||||
"description": "Pi coding agent terminal environment with nvim, ranger, and tmux",
|
|
||||||
"category": "development",
|
|
||||||
"interface_type": "terminal",
|
|
||||||
"requires_port": False,
|
|
||||||
"default_port": 0,
|
|
||||||
"definition_type": "dockerfile",
|
|
||||||
"compose_template": """services:
|
|
||||||
app:
|
|
||||||
build: .
|
|
||||||
stdin_open: true
|
|
||||||
tty: true
|
|
||||||
volumes:
|
|
||||||
- ${REPO_PATH}:/workspace
|
|
||||||
working_dir: /workspace
|
|
||||||
command: /bin/bash""",
|
|
||||||
"dockerfile_template": """# Pi Coding Agent - Terminal-based coding harness
|
|
||||||
FROM ubuntu:24.04
|
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
# Install base dependencies
|
|
||||||
RUN apt-get update && apt-get install -y \\
|
|
||||||
curl \\
|
|
||||||
wget \\
|
|
||||||
git \\
|
|
||||||
neovim \\
|
|
||||||
ranger \\
|
|
||||||
tmux \\
|
|
||||||
htop \\
|
|
||||||
tree \\
|
|
||||||
jq \\
|
|
||||||
ca-certificates \\
|
|
||||||
python3 \\
|
|
||||||
python3-pip \\
|
|
||||||
build-essential \\
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install Node.js (required for Pi)
|
|
||||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \\
|
|
||||||
&& apt-get install -y nodejs \\
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install Pi Coding Agent globally
|
|
||||||
RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent
|
|
||||||
|
|
||||||
# Create non-root user
|
|
||||||
RUN useradd -m -s /bin/bash user
|
|
||||||
WORKDIR /home/user
|
|
||||||
|
|
||||||
# Set up git
|
|
||||||
RUN git config --global init.defaultBranch main \\
|
|
||||||
&& git config --global user.email "dev@headquarter.local" \\
|
|
||||||
&& git config --global user.name "Developer"
|
|
||||||
|
|
||||||
# Create default tmux config
|
|
||||||
RUN echo 'set -g mouse on\\nset -g default-terminal "screen-256color"' > /home/user/.tmux.conf
|
|
||||||
|
|
||||||
# Create default ranger config
|
|
||||||
RUN mkdir -p /home/user/.config/ranger \\
|
|
||||||
&& echo 'set preview_files true\\nset use_preview_script true' > /home/user/.config/ranger/rc.conf
|
|
||||||
|
|
||||||
# Set up Pi config directory
|
|
||||||
RUN mkdir -p /home/user/.pi/agent
|
|
||||||
|
|
||||||
USER user
|
|
||||||
|
|
||||||
# Default to bash (Pi is invoked manually via `pi` command)
|
|
||||||
CMD ["/bin/bash"]""",
|
|
||||||
"required_variables": json.dumps(["REPO_PATH"]),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
conn = op.get_bind()
|
|
||||||
conn.execute(
|
|
||||||
sa.text("DELETE FROM tool_types WHERE name = 'pi-agent'")
|
|
||||||
)
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
"""add_clone_mode_and_ssh_key_id
|
|
||||||
|
|
||||||
Revision ID: 2026_05_22_add_clone_mode
|
|
||||||
Revises: 0014_merge_heads
|
|
||||||
Create Date: 2026-05-22 20:30:00.000000
|
|
||||||
"""
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '2026_05_22_add_clone_mode'
|
|
||||||
down_revision = '0015_single_interface'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
# Add ssh_key_id to git_repositories
|
|
||||||
op.add_column('git_repositories', sa.Column('ssh_key_id', postgresql.UUID(), nullable=True))
|
|
||||||
op.create_foreign_key('fk_git_repositories_ssh_key', 'git_repositories', 'ssh_keys', ['ssh_key_id'], ['id'])
|
|
||||||
|
|
||||||
# Add clone_mode and branch to tool_instances
|
|
||||||
op.add_column('tool_instances', sa.Column('clone_mode', sa.String(20), nullable=False, server_default='mount'))
|
|
||||||
op.add_column('tool_instances', sa.Column('branch', sa.String(255), nullable=True, server_default='main'))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
# Drop columns from tool_instances
|
|
||||||
op.drop_column('tool_instances', 'branch')
|
|
||||||
op.drop_column('tool_instances', 'clone_mode')
|
|
||||||
|
|
||||||
# Drop ssh_key_id from git_repositories
|
|
||||||
op.drop_constraint('fk_git_repositories_ssh_key', 'git_repositories', type_='foreignkey')
|
|
||||||
op.drop_column('git_repositories', 'ssh_key_id')
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
"""remove_is_builtin_from_tool_types
|
|
||||||
|
|
||||||
Revision ID: 2026_05_23_remove_is_builtin
|
|
||||||
Revises: 2026_05_22_add_clone_mode
|
|
||||||
Create Date: 2026-05-23 14:30:00.000000
|
|
||||||
"""
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '2026_05_23_remove_is_builtin'
|
|
||||||
down_revision = 'f3d2dc90ba3a'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
# Drop the is_builtin column from tool_types
|
|
||||||
op.execute("ALTER TABLE tool_types DROP COLUMN IF EXISTS is_builtin")
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
# Add the is_builtin column back to tool_types
|
|
||||||
op.add_column('tool_types', sa.Column('is_builtin', sa.Boolean(), nullable=False, server_default='false'))
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
"""add startup_command to tool_types
|
|
||||||
|
|
||||||
Revision ID: 2026_05_24_220141
|
|
||||||
Revises: 6fc7bfcf199f
|
|
||||||
Create Date: 2026-05-24 22:01:41.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "2026_05_24_220141"
|
|
||||||
down_revision: Union[str, Sequence[str], None] = "6fc7bfcf199f"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.add_column(
|
|
||||||
"tool_types",
|
|
||||||
sa.Column("startup_command", sa.Text(), nullable=True),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_column("tool_types", "startup_command")
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
"""add_config_profiles
|
|
||||||
|
|
||||||
Revision ID: 2026_05_24_add_config_profiles
|
|
||||||
Revises: f3d2dc90ba3a
|
|
||||||
Create Date: 2026-05-24 14:00:00.000000
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "2026_05_24_add_config_profiles"
|
|
||||||
down_revision: Union[str, Sequence[str], None] = "f3d2dc90ba3a"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
# Create config_profiles table
|
|
||||||
op.create_table(
|
|
||||||
"config_profiles",
|
|
||||||
sa.Column("id", postgresql.UUID(as_uuid=True), server_default=sa.text("gen_random_uuid()"), nullable=False),
|
|
||||||
sa.Column("user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
|
|
||||||
sa.Column("name", sa.String(255), nullable=False),
|
|
||||||
sa.Column("description", sa.Text(), nullable=True),
|
|
||||||
sa.Column("project_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("projects.id", ondelete="CASCADE"), nullable=True),
|
|
||||||
sa.Column("tool_type_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("tool_types.id", ondelete="CASCADE"), nullable=True),
|
|
||||||
sa.Column("env_vars", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="{}"),
|
|
||||||
sa.Column("runtime_hints", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="{}"),
|
|
||||||
sa.Column("mounts", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="[]"),
|
|
||||||
sa.Column("files", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="{}"),
|
|
||||||
sa.Column("is_default", sa.Boolean(), nullable=False, server_default="false"),
|
|
||||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False),
|
|
||||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.UniqueConstraint("user_id", "name", name="uq_config_profiles_user_name"),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create indexes for config_profiles
|
|
||||||
op.create_index("idx_config_profiles_user", "config_profiles", ["user_id"])
|
|
||||||
op.create_index("idx_config_profiles_project", "config_profiles", ["project_id"])
|
|
||||||
op.create_index("idx_config_profiles_tool_type", "config_profiles", ["tool_type_id"])
|
|
||||||
|
|
||||||
# Create config_profile_includes table
|
|
||||||
op.create_table(
|
|
||||||
"config_profile_includes",
|
|
||||||
sa.Column("id", postgresql.UUID(as_uuid=True), server_default=sa.text("gen_random_uuid()"), nullable=False),
|
|
||||||
sa.Column("profile_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("config_profiles.id", ondelete="CASCADE"), nullable=False),
|
|
||||||
sa.Column("included_profile_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("config_profiles.id", ondelete="CASCADE"), nullable=False),
|
|
||||||
sa.Column("order_index", sa.Integer(), nullable=False, server_default="0"),
|
|
||||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False),
|
|
||||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.UniqueConstraint("profile_id", "included_profile_id", name="uq_config_profile_includes"),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create indexes for config_profile_includes
|
|
||||||
op.create_index("idx_config_profile_includes_profile", "config_profile_includes", ["profile_id"])
|
|
||||||
op.create_index("idx_config_profile_includes_included", "config_profile_includes", ["included_profile_id"])
|
|
||||||
|
|
||||||
# Add selected_config_profile_id to tool_instances
|
|
||||||
op.add_column(
|
|
||||||
"tool_instances",
|
|
||||||
sa.Column("selected_config_profile_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("config_profiles.id", ondelete="SET NULL"), nullable=True),
|
|
||||||
)
|
|
||||||
op.create_index("idx_tool_instances_config_profile", "tool_instances", ["selected_config_profile_id"])
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
# Remove selected_config_profile_id from tool_instances
|
|
||||||
op.drop_index("idx_tool_instances_config_profile", table_name="tool_instances")
|
|
||||||
op.drop_column("tool_instances", "selected_config_profile_id")
|
|
||||||
|
|
||||||
# Drop config_profile_includes table
|
|
||||||
op.drop_index("idx_config_profile_includes_included", table_name="config_profile_includes")
|
|
||||||
op.drop_index("idx_config_profile_includes_profile", table_name="config_profile_includes")
|
|
||||||
op.drop_table("config_profile_includes")
|
|
||||||
|
|
||||||
# Drop config_profiles table
|
|
||||||
op.drop_index("idx_config_profiles_tool_type", table_name="config_profiles")
|
|
||||||
op.drop_index("idx_config_profiles_project", table_name="config_profiles")
|
|
||||||
op.drop_index("idx_config_profiles_user", table_name="config_profiles")
|
|
||||||
op.drop_table("config_profiles")
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
"""add_git_mounts_to_config_profiles
|
|
||||||
|
|
||||||
Revision ID: 2026_05_26_add_git_mounts
|
|
||||||
Revises: f3d2dc90ba3a
|
|
||||||
Create Date: 2026-05-26 12:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "2026_05_26_add_git_mounts"
|
|
||||||
down_revision: Union[str, Sequence[str], None] = "2026_05_24_220141"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.add_column(
|
|
||||||
"config_profiles",
|
|
||||||
sa.Column("git_mounts", sa.JSON(), nullable=True, default=list),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_column("config_profiles", "git_mounts")
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
"""make_project_id_nullable_in_git_repositories
|
|
||||||
|
|
||||||
Revision ID: 2026_05_27_external_repos
|
|
||||||
Revises: 2026_05_26_add_git_mounts
|
|
||||||
Create Date: 2026-05-27 08:30:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "2026_05_27_external_repos"
|
|
||||||
down_revision: Union[str, Sequence[str], None] = "2026_05_26_add_git_mounts"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
# Expand alembic_version version_num to avoid truncation errors
|
|
||||||
op.execute("ALTER TABLE alembic_version ALTER COLUMN version_num TYPE VARCHAR(64)")
|
|
||||||
|
|
||||||
# Make project_id nullable to allow external repositories
|
|
||||||
op.alter_column(
|
|
||||||
"git_repositories",
|
|
||||||
"project_id",
|
|
||||||
existing_type=sa.UUID(),
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.alter_column(
|
|
||||||
"git_repositories",
|
|
||||||
"project_id",
|
|
||||||
existing_type=sa.UUID(),
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
op.execute("ALTER TABLE alembic_version ALTER COLUMN version_num TYPE VARCHAR(32)")
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
"""add monitoring tables
|
|
||||||
|
|
||||||
Revision ID: 2026_05_28_add_monitoring_tables
|
|
||||||
Revises: 2026_05_28_drop_tool_configs_and_config_folders
|
|
||||||
Create Date: 2026-05-28
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from collections.abc import Sequence
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "2026_05_28_add_monitoring_tables"
|
|
||||||
down_revision: str | None = "2026_05_28_drop_tool_configs_and_config_folders"
|
|
||||||
branch_labels: str | Sequence[str] | None = None
|
|
||||||
depends_on: str | Sequence[str] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.create_table(
|
|
||||||
"instance_events",
|
|
||||||
sa.Column("id", sa.Uuid(), nullable=False),
|
|
||||||
sa.Column(
|
|
||||||
"instance_id",
|
|
||||||
sa.Uuid(),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
sa.Column("event_type", sa.String(length=50), nullable=False),
|
|
||||||
sa.Column("status", sa.String(length=50), nullable=True),
|
|
||||||
sa.Column("message", sa.Text(), nullable=True),
|
|
||||||
sa.Column("created_by", sa.Uuid(), nullable=True),
|
|
||||||
sa.Column(
|
|
||||||
"metadata",
|
|
||||||
sa.JSON(),
|
|
||||||
nullable=False,
|
|
||||||
server_default="{}",
|
|
||||||
),
|
|
||||||
sa.Column(
|
|
||||||
"created_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.func.now(),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["instance_id"],
|
|
||||||
["tool_instances.id"],
|
|
||||||
ondelete="CASCADE",
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["created_by"],
|
|
||||||
["users.id"],
|
|
||||||
ondelete="SET NULL",
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"idx_instance_events_instance_id",
|
|
||||||
"instance_events",
|
|
||||||
["instance_id"],
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"idx_instance_events_created_at",
|
|
||||||
"instance_events",
|
|
||||||
["created_at"],
|
|
||||||
postgresql_using="btree",
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"idx_instance_events_event_type",
|
|
||||||
"instance_events",
|
|
||||||
["event_type"],
|
|
||||||
)
|
|
||||||
|
|
||||||
op.create_table(
|
|
||||||
"health_checks",
|
|
||||||
sa.Column("id", sa.Uuid(), nullable=False),
|
|
||||||
sa.Column(
|
|
||||||
"instance_id",
|
|
||||||
sa.Uuid(),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
sa.Column("container_status", sa.String(length=50), nullable=True),
|
|
||||||
sa.Column("container_healthy", sa.Boolean(), nullable=True),
|
|
||||||
sa.Column("tunnel_healthy", sa.Boolean(), nullable=True),
|
|
||||||
sa.Column("exit_code", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("probe_status", sa.String(length=50), nullable=True),
|
|
||||||
sa.Column("probe_output", sa.Text(), nullable=True),
|
|
||||||
sa.Column(
|
|
||||||
"checked_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.func.now(),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["instance_id"],
|
|
||||||
["tool_instances.id"],
|
|
||||||
ondelete="CASCADE",
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"idx_health_checks_instance_id",
|
|
||||||
"health_checks",
|
|
||||||
["instance_id"],
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"idx_health_checks_checked_at",
|
|
||||||
"health_checks",
|
|
||||||
["checked_at"],
|
|
||||||
postgresql_using="btree",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_index("idx_health_checks_checked_at", table_name="health_checks")
|
|
||||||
op.drop_index("idx_health_checks_instance_id", table_name="health_checks")
|
|
||||||
op.drop_table("health_checks")
|
|
||||||
op.drop_index("idx_instance_events_event_type", table_name="instance_events")
|
|
||||||
op.drop_index("idx_instance_events_created_at", table_name="instance_events")
|
|
||||||
op.drop_index("idx_instance_events_instance_id", table_name="instance_events")
|
|
||||||
op.drop_table("instance_events")
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
"""add terminal_sessions table
|
|
||||||
|
|
||||||
Revision ID: 2026_05_28_add_terminal_sessions
|
|
||||||
Revises: 20260527_160017_add_pi_agent
|
|
||||||
Create Date: 2026-05-28
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from collections.abc import Sequence
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "2026_05_28_add_terminal_sessions"
|
|
||||||
down_revision: str | None = "2026_05_28_add_tool_definition_manifests"
|
|
||||||
branch_labels: str | Sequence[str] | None = None
|
|
||||||
depends_on: str | Sequence[str] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.create_table(
|
|
||||||
"terminal_sessions",
|
|
||||||
sa.Column("id", sa.UUID(), nullable=False),
|
|
||||||
sa.Column("instance_id", sa.UUID(), nullable=False),
|
|
||||||
sa.Column("name", sa.String(length=255), nullable=True),
|
|
||||||
sa.Column("status", sa.String(length=50), nullable=False),
|
|
||||||
sa.Column("last_activity_at", sa.DateTime(timezone=True), nullable=True),
|
|
||||||
sa.Column("closed_at", sa.DateTime(timezone=True), nullable=True),
|
|
||||||
sa.Column(
|
|
||||||
"created_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.text("now()"),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
sa.Column(
|
|
||||||
"updated_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.text("now()"),
|
|
||||||
onupdate=sa.text("now()"),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["instance_id"], ["tool_instances.id"], ondelete="CASCADE"
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
op.f("ix_terminal_sessions_instance_id"),
|
|
||||||
"terminal_sessions",
|
|
||||||
["instance_id"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_index(
|
|
||||||
op.f("ix_terminal_sessions_instance_id"),
|
|
||||||
table_name="terminal_sessions",
|
|
||||||
)
|
|
||||||
op.drop_table("terminal_sessions")
|
|
||||||
@@ -1,359 +0,0 @@
|
|||||||
"""add tool definition manifests
|
|
||||||
|
|
||||||
Revision ID: 2026_05_28_add_tool_definition_manifests
|
|
||||||
Revises: 20260527_160017_add_pi_agent
|
|
||||||
Create Date: 2026-05-28T11:00:00
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import uuid
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "2026_05_28_add_tool_definition_manifests"
|
|
||||||
down_revision: Union[str, None] = "20260527_160017_add_pi_agent"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
BASE_UBUNTU_ID = uuid.UUID("a1b2c3d4-e5f6-7890-abcd-ef1234567890")
|
|
||||||
PI_AGENT_MANIFEST_ID = uuid.UUID("d07b8376-2151-4119-8c1d-27f792aae9a3")
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
conn = op.get_bind()
|
|
||||||
|
|
||||||
# ── Create tool_definition_manifests table ───────────────────────
|
|
||||||
op.create_table(
|
|
||||||
"tool_definition_manifests",
|
|
||||||
sa.Column("id", sa.UUID(), nullable=False),
|
|
||||||
sa.Column("name", sa.String(64), nullable=False),
|
|
||||||
sa.Column("display_name", sa.String(128), nullable=False),
|
|
||||||
sa.Column("description", sa.Text(), nullable=True),
|
|
||||||
sa.Column("category", sa.String(64), nullable=True),
|
|
||||||
sa.Column("interface_type", sa.String(16), nullable=False),
|
|
||||||
sa.Column("base_image", sa.String(256), nullable=True),
|
|
||||||
sa.Column("base_definition_id", sa.UUID(), nullable=True),
|
|
||||||
sa.Column(
|
|
||||||
"base_version", sa.String(32), nullable=False, server_default="latest"
|
|
||||||
),
|
|
||||||
sa.Column("manifest", sa.JSON(), nullable=False),
|
|
||||||
sa.Column("dockerfile_cache", sa.Text(), nullable=True),
|
|
||||||
sa.Column("compose_cache", sa.Text(), nullable=True),
|
|
||||||
sa.Column("version", sa.String(32), nullable=False, server_default="v1"),
|
|
||||||
sa.Column("is_base", sa.Boolean(), nullable=False, server_default="false"),
|
|
||||||
sa.Column("created_by_id", sa.UUID(), nullable=True),
|
|
||||||
sa.Column(
|
|
||||||
"created_at", sa.TIMESTAMP(timezone=True), server_default=sa.func.now()
|
|
||||||
),
|
|
||||||
sa.Column(
|
|
||||||
"updated_at", sa.TIMESTAMP(timezone=True), server_default=sa.func.now()
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.UniqueConstraint("name"),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["base_definition_id"], ["tool_definition_manifests.id"]
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(["created_by_id"], ["users.id"]),
|
|
||||||
sa.CheckConstraint(
|
|
||||||
"(base_image IS NOT NULL) OR (base_definition_id IS NOT NULL)",
|
|
||||||
name="ck_tool_definition_manifests_base_required",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# ── Add columns to tool_types ────────────────────────────────────
|
|
||||||
# Check if manifest_id exists before adding
|
|
||||||
conn = op.get_bind()
|
|
||||||
result = conn.execute(
|
|
||||||
sa.text("""
|
|
||||||
SELECT column_name FROM information_schema.columns
|
|
||||||
WHERE table_name = 'tool_types' AND column_name = 'manifest_id'
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
if not result.fetchone():
|
|
||||||
op.add_column("tool_types", sa.Column("manifest_id", sa.UUID(), nullable=True))
|
|
||||||
op.create_foreign_key(
|
|
||||||
"fk_tool_types_manifest_id",
|
|
||||||
"tool_types",
|
|
||||||
"tool_definition_manifests",
|
|
||||||
["manifest_id"],
|
|
||||||
["id"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update definition_type to allow 'legacy' and 'manifest'
|
|
||||||
result = conn.execute(
|
|
||||||
sa.text("""
|
|
||||||
SELECT constraint_name FROM information_schema.check_constraints
|
|
||||||
WHERE constraint_name = 'chk_definition_type'
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
if result.fetchone():
|
|
||||||
op.drop_constraint("chk_definition_type", "tool_types", type_="check")
|
|
||||||
|
|
||||||
op.execute("ALTER TABLE tool_types ALTER COLUMN definition_type TYPE VARCHAR(16)")
|
|
||||||
op.execute(
|
|
||||||
"ALTER TABLE tool_types ALTER COLUMN definition_type SET DEFAULT 'legacy'"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ── Add columns to tool_instances ────────────────────────────────
|
|
||||||
result = conn.execute(
|
|
||||||
sa.text("""
|
|
||||||
SELECT column_name FROM information_schema.columns
|
|
||||||
WHERE table_name = 'tool_instances' AND column_name = 'manifest_compiled_at'
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
if not result.fetchone():
|
|
||||||
op.add_column(
|
|
||||||
"tool_instances",
|
|
||||||
sa.Column(
|
|
||||||
"manifest_compiled_at", sa.TIMESTAMP(timezone=True), nullable=True
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
result = conn.execute(
|
|
||||||
sa.text("""
|
|
||||||
SELECT column_name FROM information_schema.columns
|
|
||||||
WHERE table_name = 'tool_instances' AND column_name = 'image_tag'
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
if not result.fetchone():
|
|
||||||
op.add_column(
|
|
||||||
"tool_instances",
|
|
||||||
sa.Column("image_tag", sa.String(256), nullable=True),
|
|
||||||
)
|
|
||||||
|
|
||||||
# ── Data migration: create base definition + pi-agent manifest ───
|
|
||||||
conn.execute(
|
|
||||||
sa.text(
|
|
||||||
"""
|
|
||||||
INSERT INTO tool_definition_manifests
|
|
||||||
(id, name, display_name, description, interface_type, base_image,
|
|
||||||
manifest, is_base, version, created_at, updated_at)
|
|
||||||
VALUES
|
|
||||||
(:base_id, 'ubuntu-24.04-dev', 'Ubuntu 24.04 Dev Base',
|
|
||||||
'Base development environment with build tools', 'terminal',
|
|
||||||
'ubuntu:24.04', :base_manifest, true, 'v1', now(), now())
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
{
|
|
||||||
"base_id": BASE_UBUNTU_ID,
|
|
||||||
"base_manifest": json.dumps(
|
|
||||||
{
|
|
||||||
"name": "ubuntu-24.04-dev",
|
|
||||||
"display_name": "Ubuntu 24.04 Dev Base",
|
|
||||||
"interface_type": "terminal",
|
|
||||||
"base_image": "ubuntu:24.04",
|
|
||||||
"packages": {
|
|
||||||
"apt": [
|
|
||||||
"curl",
|
|
||||||
"wget",
|
|
||||||
"git",
|
|
||||||
"build-essential",
|
|
||||||
"ca-certificates",
|
|
||||||
"python3",
|
|
||||||
"python3-pip",
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"name": "user",
|
|
||||||
"uid": 1000,
|
|
||||||
"gid": 1000,
|
|
||||||
"create_home": True,
|
|
||||||
"shell": "/bin/bash",
|
|
||||||
},
|
|
||||||
"env": {"DEBIAN_FRONTEND": "noninteractive"},
|
|
||||||
}
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
conn.execute(
|
|
||||||
sa.text(
|
|
||||||
"""
|
|
||||||
INSERT INTO tool_definition_manifests
|
|
||||||
(id, name, display_name, description, category, interface_type,
|
|
||||||
base_definition_id, base_version, manifest, version, created_at, updated_at)
|
|
||||||
VALUES
|
|
||||||
(:manifest_id, 'pi-agent', 'Pi Agent',
|
|
||||||
'Terminal-based coding harness with nvim, ranger, tmux',
|
|
||||||
'development', 'terminal', :base_id, 'v1', :manifest, 'v1',
|
|
||||||
now(), now())
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
{
|
|
||||||
"manifest_id": PI_AGENT_MANIFEST_ID,
|
|
||||||
"base_id": BASE_UBUNTU_ID,
|
|
||||||
"manifest": json.dumps(
|
|
||||||
{
|
|
||||||
"name": "pi-agent",
|
|
||||||
"display_name": "Pi Agent",
|
|
||||||
"description": "Terminal-based coding harness",
|
|
||||||
"category": "development",
|
|
||||||
"interface_type": "terminal",
|
|
||||||
"base_definition_id": str(BASE_UBUNTU_ID),
|
|
||||||
"base_version": "v1",
|
|
||||||
"packages": {
|
|
||||||
"apt": [
|
|
||||||
"neovim",
|
|
||||||
"ranger",
|
|
||||||
"tmux",
|
|
||||||
"htop",
|
|
||||||
"tree",
|
|
||||||
"jq",
|
|
||||||
],
|
|
||||||
"node": {"version": "20"},
|
|
||||||
"npm_global": ["@earendil-works/pi-coding-agent"],
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"name": "user",
|
|
||||||
"uid": 1001,
|
|
||||||
"gid": 1001,
|
|
||||||
"create_home": True,
|
|
||||||
"shell": "/bin/bash",
|
|
||||||
},
|
|
||||||
"env": {"DEBIAN_FRONTEND": "noninteractive"},
|
|
||||||
"scripts": {
|
|
||||||
"build": [
|
|
||||||
"git config --global init.defaultBranch main && git config --global user.email 'dev@headquarter.local' && git config --global user.name 'Developer'",
|
|
||||||
"mkdir -p /home/user/.config/ranger && echo 'set preview_files true' > /home/user/.config/ranger/rc.conf",
|
|
||||||
],
|
|
||||||
"startup": [
|
|
||||||
"if [ -d /workspace ]; then sudo chown -R user:user /workspace 2>/dev/null || true; fi",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"mounts": [
|
|
||||||
{
|
|
||||||
"name": "workspace",
|
|
||||||
"target": "/workspace",
|
|
||||||
"source_type": "repo",
|
|
||||||
"writable": True,
|
|
||||||
"owner": "user",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"runtime": {
|
|
||||||
"command": ["/bin/bash"],
|
|
||||||
"stdin_open": True,
|
|
||||||
"tty": True,
|
|
||||||
"working_dir": "/workspace",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# ── Update existing pi-agent tool_type ───────────────────────────
|
|
||||||
conn.execute(
|
|
||||||
sa.text(
|
|
||||||
"""
|
|
||||||
UPDATE tool_types
|
|
||||||
SET manifest_id = :manifest_id,
|
|
||||||
definition_type = 'manifest',
|
|
||||||
dockerfile_template = NULL,
|
|
||||||
compose_template = NULL
|
|
||||||
WHERE name = 'pi-agent'
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
{"manifest_id": PI_AGENT_MANIFEST_ID},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
conn = op.get_bind()
|
|
||||||
|
|
||||||
# Restore pi-agent templates if manifest_id column exists
|
|
||||||
result = conn.execute(
|
|
||||||
sa.text("""
|
|
||||||
SELECT column_name FROM information_schema.columns
|
|
||||||
WHERE table_name = 'tool_types' AND column_name = 'manifest_id'
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
has_manifest_id = result.fetchone() is not None
|
|
||||||
|
|
||||||
if has_manifest_id:
|
|
||||||
conn.execute(
|
|
||||||
sa.text(
|
|
||||||
"""
|
|
||||||
UPDATE tool_types
|
|
||||||
SET manifest_id = NULL,
|
|
||||||
definition_type = 'dockerfile',
|
|
||||||
dockerfile_template = :dockerfile,
|
|
||||||
compose_template = :compose
|
|
||||||
WHERE name = 'pi-agent'
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
{
|
|
||||||
"dockerfile": """# Pi Coding Agent - Terminal-based coding harness
|
|
||||||
FROM ubuntu:24.04
|
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \\
|
|
||||||
curl wget git neovim ranger tmux htop tree jq \\
|
|
||||||
ca-certificates python3 python3-pip build-essential \\
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \\
|
|
||||||
&& apt-get install -y nodejs \\
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent
|
|
||||||
|
|
||||||
RUN useradd -m -s /bin/bash user
|
|
||||||
WORKDIR /home/user
|
|
||||||
|
|
||||||
RUN git config --global init.defaultBranch main \\
|
|
||||||
&& git config --global user.email "dev@headquarter.local" \\
|
|
||||||
&& git config --global user.name "Developer"
|
|
||||||
|
|
||||||
RUN echo 'set -g mouse on\\nset -g default-terminal "screen-256color"' > /home/user/.tmux.conf
|
|
||||||
|
|
||||||
RUN mkdir -p /home/user/.config/ranger \\
|
|
||||||
&& echo 'set preview_files true\\nset use_preview_script true' > /home/user/.config/ranger/rc.conf
|
|
||||||
|
|
||||||
RUN mkdir -p /home/user/.pi/agent
|
|
||||||
|
|
||||||
USER user
|
|
||||||
|
|
||||||
CMD ["/bin/bash"]
|
|
||||||
""",
|
|
||||||
"compose": """services:
|
|
||||||
app:
|
|
||||||
build: .
|
|
||||||
stdin_open: true
|
|
||||||
tty: true
|
|
||||||
volumes:
|
|
||||||
- ${REPO_PATH}:/workspace
|
|
||||||
working_dir: /workspace
|
|
||||||
command: /bin/bash""",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Drop columns conditionally
|
|
||||||
result = conn.execute(
|
|
||||||
sa.text("""
|
|
||||||
SELECT column_name FROM information_schema.columns
|
|
||||||
WHERE table_name = 'tool_instances' AND column_name = 'image_tag'
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
if result.fetchone():
|
|
||||||
op.drop_column("tool_instances", "image_tag")
|
|
||||||
|
|
||||||
result = conn.execute(
|
|
||||||
sa.text("""
|
|
||||||
SELECT column_name FROM information_schema.columns
|
|
||||||
WHERE table_name = 'tool_instances' AND column_name = 'manifest_compiled_at'
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
if result.fetchone():
|
|
||||||
op.drop_column("tool_instances", "manifest_compiled_at")
|
|
||||||
|
|
||||||
if has_manifest_id:
|
|
||||||
op.drop_constraint(
|
|
||||||
"fk_tool_types_manifest_id", "tool_types", type_="foreignkey"
|
|
||||||
)
|
|
||||||
op.drop_column("tool_types", "manifest_id")
|
|
||||||
|
|
||||||
op.drop_table("tool_definition_manifests")
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
"""drop tool_configs and config_folders tables
|
|
||||||
|
|
||||||
Revision ID: 2026_05_28_drop_tool_configs_and_config_folders
|
|
||||||
Revises: 2026_05_28_add_tool_definition_manifests
|
|
||||||
Create Date: 2026-05-28
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "2026_05_28_drop_tool_configs_and_config_folders"
|
|
||||||
down_revision: Union[str, None] = "2026_05_28_add_terminal_sessions"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
conn = op.get_bind()
|
|
||||||
|
|
||||||
# Drop tool_configs table if it exists
|
|
||||||
result = conn.execute(
|
|
||||||
sa.text("""
|
|
||||||
SELECT table_name FROM information_schema.tables
|
|
||||||
WHERE table_name = 'tool_configs'
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
if result.fetchone():
|
|
||||||
op.drop_table("tool_configs")
|
|
||||||
|
|
||||||
# Drop config_folders table if it exists
|
|
||||||
result = conn.execute(
|
|
||||||
sa.text("""
|
|
||||||
SELECT table_name FROM information_schema.tables
|
|
||||||
WHERE table_name = 'config_folders'
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
if result.fetchone():
|
|
||||||
op.drop_table("config_folders")
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
# Recreate config_folders table
|
|
||||||
op.create_table(
|
|
||||||
"config_folders",
|
|
||||||
sa.Column("id", sa.UUID(), nullable=False),
|
|
||||||
sa.Column("user_id", sa.UUID(), nullable=False),
|
|
||||||
sa.Column("name", sa.String(255), nullable=False),
|
|
||||||
sa.Column("description", sa.Text(), nullable=True),
|
|
||||||
sa.Column("mount_path", sa.String(1024), nullable=False),
|
|
||||||
sa.Column("files", sa.JSON(), default=dict, nullable=False),
|
|
||||||
sa.Column("project_overrides", sa.JSON(), default=dict, nullable=True),
|
|
||||||
sa.Column("is_active", sa.Boolean(), default=True, nullable=False),
|
|
||||||
sa.Column(
|
|
||||||
"created_at", sa.TIMESTAMP(timezone=True), server_default=sa.func.now()
|
|
||||||
),
|
|
||||||
sa.Column(
|
|
||||||
"updated_at", sa.TIMESTAMP(timezone=True), server_default=sa.func.now()
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Recreate tool_configs table
|
|
||||||
op.create_table(
|
|
||||||
"tool_configs",
|
|
||||||
sa.Column("id", sa.UUID(), nullable=False),
|
|
||||||
sa.Column("user_id", sa.UUID(), nullable=False),
|
|
||||||
sa.Column("tool_type_id", sa.UUID(), nullable=False),
|
|
||||||
sa.Column("project_id", sa.UUID(), nullable=True),
|
|
||||||
sa.Column("key", sa.String(255), nullable=False),
|
|
||||||
sa.Column("value", sa.Text(), nullable=False),
|
|
||||||
sa.Column("config_type", sa.String(20), default="env", nullable=False),
|
|
||||||
sa.Column("file_path", sa.String(1024), nullable=True),
|
|
||||||
sa.Column("port_override", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("start_command", sa.Text(), nullable=True),
|
|
||||||
sa.Column("working_directory", sa.Text(), nullable=True),
|
|
||||||
sa.Column("environment_variables", sa.JSON(), default=dict, nullable=True),
|
|
||||||
sa.Column("volumes", sa.JSON(), default=list, nullable=True),
|
|
||||||
sa.Column(
|
|
||||||
"created_at", sa.TIMESTAMP(timezone=True), server_default=sa.func.now()
|
|
||||||
),
|
|
||||||
sa.Column(
|
|
||||||
"updated_at", sa.TIMESTAMP(timezone=True), server_default=sa.func.now()
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
"""add notifications table
|
|
||||||
|
|
||||||
Revision ID: 2026_05_29_add_notifications_table
|
|
||||||
Revises: 2026_05_28_add_monitoring_tables
|
|
||||||
Create Date: 2026-05-29
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from collections.abc import Sequence
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from alembic import op
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "2026_05_29_add_notifications_table"
|
|
||||||
down_revision: str | None = "2026_05_28_add_monitoring_tables"
|
|
||||||
branch_labels: str | Sequence[str] | None = None
|
|
||||||
depends_on: str | Sequence[str] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
op.create_table(
|
|
||||||
"notifications",
|
|
||||||
sa.Column("id", sa.Uuid(), nullable=False),
|
|
||||||
sa.Column("user_id", sa.Uuid(), nullable=False),
|
|
||||||
sa.Column("category", sa.String(length=32), nullable=False),
|
|
||||||
sa.Column("severity", sa.String(length=16), nullable=False),
|
|
||||||
sa.Column("title", sa.String(length=255), nullable=False),
|
|
||||||
sa.Column("message", sa.Text(), nullable=True),
|
|
||||||
sa.Column("source_type", sa.String(length=64), nullable=True),
|
|
||||||
sa.Column("source_id", sa.Uuid(), nullable=True),
|
|
||||||
sa.Column(
|
|
||||||
"metadata",
|
|
||||||
sa.JSON(),
|
|
||||||
nullable=False,
|
|
||||||
server_default="{}",
|
|
||||||
),
|
|
||||||
sa.Column("read_at", sa.DateTime(timezone=True), nullable=True),
|
|
||||||
sa.Column("dismissed_at", sa.DateTime(timezone=True), nullable=True),
|
|
||||||
sa.Column(
|
|
||||||
"created_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.func.now(),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["user_id"],
|
|
||||||
["users.id"],
|
|
||||||
ondelete="CASCADE",
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"idx_notifications_user_created_at",
|
|
||||||
"notifications",
|
|
||||||
["user_id", sa.text("created_at DESC")],
|
|
||||||
)
|
|
||||||
op.create_index(
|
|
||||||
"idx_notifications_user_unread",
|
|
||||||
"notifications",
|
|
||||||
["user_id", "read_at"],
|
|
||||||
postgresql_where=sa.text("read_at IS NULL"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
op.drop_index("idx_notifications_user_unread", table_name="notifications")
|
|
||||||
op.drop_index("idx_notifications_user_created_at", table_name="notifications")
|
|
||||||
op.drop_table("notifications")
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user