Compare commits

..

2 Commits

Author SHA1 Message Date
GitHub Action
ad6c28834d chore: bump relay-tunnel version to 0.1.7 2026-03-23 16:26:27 +00:00
GitHub Action
1bfdc29471 chore: bump version to 0.1.36 2026-03-23 15:43:11 +00:00
578 changed files with 15681 additions and 26774 deletions

View File

@@ -1,163 +0,0 @@
name: container
# Build the self-hosted images from THIS repo and publish them to the Gitea
# registry:
#
# git.lair.cafe/lair/vibe-kanban-remote crates/remote/Dockerfile :8081
# git.lair.cafe/lair/vibe-kanban-relay crates/relay-tunnel/Dockerfile :8082
#
# This build used to live in lair/containers, which is for *third-party* images
# built from someone else's source. We own this fork now, so the build belongs
# next to the code it builds — a source change and its image are one commit.
#
# Both crates ship their own Dockerfile (context = repo root), so there is no
# vendored Containerfile here. Nothing in this workflow reaches out to GitHub:
# upstream is sunset and may disappear, and a build that asks github.com what to
# build would disappear with it.
on:
push:
branches: [main]
paths:
- "crates/**"
- "packages/**"
- "shared/**"
- "assets/**"
- "patches/**"
- "Cargo.toml"
- "Cargo.lock"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "rust-toolchain.toml"
- ".gitea/workflows/container.yml"
workflow_dispatch:
# Never let two builds race: they would fight over the :latest tags and the
# loser's digest would win at random.
concurrency:
group: container
cancel-in-progress: false
jobs:
remote:
runs-on:
- metal
- podman
# The Rust workspace builds in release mode; from a cold cache the default
# job timeout is not enough headroom.
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: derive image tags
id: meta
run: |
set -euo pipefail
version=$(jq -r .version package.json)
short=$(git rev-parse --short HEAD)
# Immutable tag first. :latest is a convenience for humans, never a
# deployment target -- a floating tag decides which build you are
# running based on whenever something last pulled.
echo "immutable=${version}-g${short}" >> "$GITHUB_OUTPUT"
- name: login to registry
run: podman login -u ${{ gitea.actor }} -p ${{ secrets.REGISTRY_TOKEN }} git.lair.cafe
- name: build
env:
IMMUTABLE: ${{ steps.meta.outputs.immutable }}
run: |
set -euo pipefail
IMAGE=git.lair.cafe/lair/vibe-kanban-remote
# FEATURES is deliberately unset: the Dockerfile strips the private
# billing crate only when it is empty (the self-host path), and we have
# no access to BloopAI/vibe-kanban-private.
#
# VITE_RELAY_API_BASE_URL is baked into the SPA at build time and is
# ORIGIN-RELATIVE on purpose. It is not a switch that disables the
# relay -- leaving it empty makes Bootstrap.tsx fall back to
# window.location.origin, which aims relay calls at the remote API,
# which does not serve them. "/relay-api" instead means the SPA calls
# whichever host served it, so one build works unchanged behind both
# kanban.internal and a future public name, with no CORS and no second
# certificate. nginx strips the prefix and forwards to the relay.
podman build --pull=newer \
-f crates/remote/Dockerfile \
--build-arg VITE_RELAY_API_BASE_URL=/relay-api \
-t "${IMAGE}:${IMMUTABLE}" \
-t "${IMAGE}:latest" \
.
- name: verify the sunset really is gone
env:
IMMUTABLE: ${{ steps.meta.outputs.immutable }}
run: |
set -euo pipefail
IMAGE=git.lair.cafe/lair/vibe-kanban-remote
# This fork exists because upstream replaced the board with an
# export-only page. If a merge or a bad rebase ever reinstates it, fail
# here rather than publish an image that greets us with a dead product.
if podman run --rm --entrypoint sh "${IMAGE}:${IMMUTABLE}" -c \
'grep -rlq "Project functionality has been retired" /srv/static' 2>/dev/null; then
echo "ERROR: the sunset page is present in the built bundle."
echo "The revert of #3387 has been lost -- refusing to publish."
exit 1
fi
echo "ok: no sunset page in the bundle"
- name: push
env:
IMMUTABLE: ${{ steps.meta.outputs.immutable }}
run: |
set -euo pipefail
IMAGE=git.lair.cafe/lair/vibe-kanban-remote
podman push "${IMAGE}:${IMMUTABLE}"
podman push "${IMAGE}:latest"
echo "published ${IMAGE}:${IMMUTABLE} (and :latest)"
echo
echo "pin the quadlet to the immutable tag:"
echo " Image=${IMAGE}:${IMMUTABLE}"
relay:
runs-on:
- metal
- podman
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: derive image tags
id: meta
run: |
set -euo pipefail
version=$(jq -r .version package.json)
short=$(git rev-parse --short HEAD)
echo "immutable=${version}-g${short}" >> "$GITHUB_OUTPUT"
- name: login to registry
run: podman login -u ${{ gitea.actor }} -p ${{ secrets.REGISTRY_TOKEN }} git.lair.cafe
- name: build
env:
IMMUTABLE: ${{ steps.meta.outputs.immutable }}
run: |
set -euo pipefail
IMAGE=git.lair.cafe/lair/vibe-kanban-relay
# No build args: the relay takes all of its configuration from the
# environment at runtime (DATABASE_URL, RELAY_LISTEN_ADDR and the JWT
# secret it shares with the remote server). It has no notion of its own
# public address, which is what makes mounting it under a path safe.
podman build --pull=newer \
-f crates/relay-tunnel/Dockerfile \
-t "${IMAGE}:${IMMUTABLE}" \
-t "${IMAGE}:latest" \
.
- name: push
env:
IMMUTABLE: ${{ steps.meta.outputs.immutable }}
run: |
set -euo pipefail
IMAGE=git.lair.cafe/lair/vibe-kanban-relay
podman push "${IMAGE}:${IMMUTABLE}"
podman push "${IMAGE}:latest"
echo "published ${IMAGE}:${IMMUTABLE} (and :latest)"

View File

@@ -1,198 +0,0 @@
name: host-release
# Build the LOCAL vibe-kanban server and publish it as a signed RPM to
# rpm.lair.cafe, so a host installs it with dnf instead of `npx vibe-kanban`.
#
# npx is not an option here: npx-cli resolves its download from R2_BASE_URL, a
# placeholder substituted at npm-publish time to BloopAI's bucket, with no
# runtime override (npx-cli/src/download.ts). That serves upstream's binary --
# which still carries the product sunset -- from infrastructure that disappears
# when they do.
#
# Convention mirrors lair/claude-desktop-package: package and publish on the
# `rpm` runner, sign with RPM_SIGNING_KEY, rsync to oolon, createrepo under
# flock. The compile happens first on a podman runner rather than on the rpm
# runner, so this never depends on a host toolchain matching the pinned
# nightly in rust-toolchain.toml.
on:
push:
branches: [main]
paths:
- "crates/**"
- "packages/local-web/**"
- "packages/web-core/**"
- "packages/ui/**"
- "shared/**"
- "Cargo.toml"
- "Cargo.lock"
- "rust-toolchain.toml"
- "rpm/**"
- ".gitea/workflows/host-release.yml"
workflow_dispatch:
concurrency:
group: host-release
cancel-in-progress: false
jobs:
build:
runs-on:
- metal
- podman
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: derive version
id: meta
run: |
set -euo pipefail
echo "version=$(jq -r .version package.json)" >> "$GITHUB_OUTPUT"
echo "commit=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
- name: build the host binary
run: |
set -euo pipefail
# --target export writes the binary straight out of the scratch stage,
# so nothing has to be extracted from an image afterwards.
mkdir -p out
podman build --pull=newer \
-f rpm/Containerfile \
--target export \
--output type=local,dest=out \
.
test -s out/vibe-kanban
chmod 0755 out/vibe-kanban
file out/vibe-kanban || true
- name: sanity-check the binary
run: |
set -euo pipefail
# The whole point of building our own: upstream's carries the sunset.
if grep -qa "Project functionality has been retired" out/vibe-kanban; then
echo "ERROR: the sunset page is embedded in the host binary."
echo "The revert of #3387 has been lost -- refusing to publish."
exit 1
fi
if grep -qa "Cloud is shutting down" out/vibe-kanban; then
echo "ERROR: the cloud-shutdown banner is embedded in the host binary."
exit 1
fi
# And it must point at us, not at api.vibekanban.com.
grep -qa "kanban.internal" out/vibe-kanban \
|| { echo "ERROR: baked API base missing from the binary"; exit 1; }
echo "ok: no sunset UI, lair endpoints baked in"
- uses: actions/upload-artifact@v3
with:
name: host-binary
path: out/vibe-kanban
retention-days: 7
outputs:
version: ${{ steps.meta.outputs.version }}
commit: ${{ steps.meta.outputs.commit }}
package:
needs: build
runs-on: rpm
strategy:
fail-fast: false
matrix:
fedora_version: ["43", "44"]
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
with:
name: host-binary
path: bin/
- name: rpmbuild
run: |
set -euo pipefail
find bin/ -name vibe-kanban -exec mv --target-directory=bin/ {} + 2>/dev/null || true
mkdir -p ~/rpmbuild/SOURCES
install -m 0755 bin/vibe-kanban ~/rpmbuild/SOURCES/vibe-kanban
rpmbuild -bb rpm/vibe-kanban.spec \
--define "vk_version ${{ needs.build.outputs.version }}" \
--define "vk_commit ${{ needs.build.outputs.commit }}" \
--define "dist .fc${{ matrix.fedora_version }}"
ls -l ~/rpmbuild/RPMS/x86_64/
- uses: actions/upload-artifact@v3
with:
name: rpm-fc${{ matrix.fedora_version }}
path: ~/rpmbuild/RPMS/x86_64/*.rpm
retention-days: 7
publish:
needs: package
runs-on: rpm
env:
RPM_REPO_HOST: oolon.kosherinata.internal
strategy:
fail-fast: false
matrix:
fedora_version: ["43", "44"]
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
with:
name: rpm-fc${{ matrix.fedora_version }}
path: rpms/
- name: Flatten RPM artifacts
run: |
find rpms/ -name '*.rpm' -exec mv --target-directory=rpms/ {} +
find rpms/ -mindepth 1 -type d -empty -delete
- name: Import signing key
run: |
echo "${{ secrets.RPM_SIGNING_KEY }}" | gpg --batch --import
fpr=$(gpg --batch --with-colons --list-keys "${{ secrets.RPM_SIGNING_KEY_ID }}" | awk -F: '/^fpr:/ { print $10; exit }')
echo "${fpr}:6:" | gpg --batch --import-ownertrust
sed "s/@GPG_NAME@/${{ secrets.RPM_SIGNING_KEY_ID }}/" rpm/rpmmacros > ~/.rpmmacros
- name: Sign RPMs
run: |
for rpm in rpms/*.rpm; do
echo "signing ${rpm}..."
rpm --addsign "${rpm}"
done
- name: Set up SSH
run: |
install --directory --mode 700 ~/.ssh
echo "${RSYNC_SSH_KEY}" | install --mode 600 /dev/stdin ~/.ssh/id_ed25519
env:
RSYNC_SSH_KEY: ${{ secrets.RSYNC_SSH_KEY }}
- name: Test SSH connectivity
run: |
ssh -o StrictHostKeyChecking=accept-new "gitea_ci@${RPM_REPO_HOST}" exit
- name: Sync RPMs to repo
run: |
rsync \
--archive \
--verbose \
--chmod D755,F644 \
rpms/*.rpm \
"gitea_ci@${RPM_REPO_HOST}:/var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/"
- name: Update repo metadata
run: |
# flock guards createrepo against concurrent publishes into the
# shared repo tree (other package repos publish here too).
ssh "gitea_ci@${RPM_REPO_HOST}" \
"flock /var/www/rpm/.publish.lock -c 'cd /var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64 && createrepo_c --update .'"
- name: Generate packages.json
run: |
scp rpm/generate-packages-json.py "gitea_ci@${RPM_REPO_HOST}:/tmp/"
ssh "gitea_ci@${RPM_REPO_HOST}" \
"flock /var/www/rpm/.publish.lock -c 'python3 /tmp/generate-packages-json.py \
--repodata-dir /var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/repodata \
--output /var/www/rpm/fedora/${{ matrix.fedora_version }}/x86_64/packages.json \
--base-url https://rpm.lair.cafe/fedora/${{ matrix.fedora_version }}/x86_64'"

View File

@@ -33,19 +33,19 @@ runs:
- name: Install Rust toolchain
if: ${{ inputs.components == '' }}
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ inputs.toolchain }}
- name: Install Rust toolchain with components
if: ${{ inputs.components != '' }}
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ inputs.toolchain }}
components: ${{ inputs.components }}
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
uses: Swatinem/rust-cache@v2
env:
RUST_CACHE_DEBUG: true
with:

View File

@@ -11,7 +11,7 @@ runs:
registry-url: 'https://registry.npmjs.org'
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}

1149
.github/workflows/pre-release.yml vendored Normal file

File diff suppressed because it is too large Load Diff

169
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,169 @@
name: Publish to npm
on:
release:
types: [released]
workflow_dispatch:
inputs:
tag_name:
description: "Release tag (e.g., v1.2.3)"
required: true
release_id:
description: "GitHub release ID"
required: true
concurrency:
group: publish
cancel-in-progress: true
permissions:
contents: write
packages: write
id-token: write # Required for OIDC trusted publishing
env:
NODE_VERSION: 22
PNPM_VERSION: 10.13.1
jobs:
publish:
runs-on: ubuntu-latest
# Only run for main app releases (not remote-v* tags) that were converted from pre-release
if: github.event.release.prerelease == false && !startsWith(github.event.release.tag_name, 'remote-')
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.release.tag_name || inputs.tag_name }}
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Upgrade npm for OIDC support
run: npm install -g npm@latest
- name: Download release assets
uses: actions/github-script@v8
env:
RELEASE_ID: ${{ inputs.release_id }}
with:
script: |
const fs = require('fs');
const path = require('path');
const releaseId = context.payload.release?.id || process.env.RELEASE_ID;
console.log("releaseId:", releaseId);
if (!releaseId) {
core.setFailed('No release ID found.');
return;
}
const release = await github.rest.repos.getRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: releaseId
});
// Find the .tgz file
const tgzAsset = release.data.assets.find(asset => asset.name.endsWith('.tgz'));
if (!tgzAsset) {
core.setFailed('No .tgz file found in release assets');
return;
}
// Download the asset
const response = await github.rest.repos.getReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
asset_id: tgzAsset.id,
headers: {
Accept: 'application/octet-stream'
}
});
// Save to npx-cli directory
const filePath = path.join('npx-cli', tgzAsset.name);
fs.writeFileSync(filePath, Buffer.from(response.data));
console.log(`Downloaded ${tgzAsset.name} to ${filePath}`);
// Set output for next step
core.setOutput('package-file', filePath);
core.setOutput('package-name', tgzAsset.name);
- name: Verify package integrity
id: verify
run: |
cd npx-cli
# List files to confirm download
ls -la *.tgz
# Verify the package can be read
npm pack --dry-run || echo "Note: This is expected to show differences since we're using the pre-built package"
# Extract package name from the downloaded file
PACKAGE_FILE=$(ls *.tgz | head -n1)
echo "package-file=$PACKAGE_FILE" >> $GITHUB_OUTPUT
- name: Publish to npm
run: |
cd npx-cli
# Publish the exact same package that was tested
PACKAGE_FILE="${{ steps.verify.outputs.package-file }}"
echo "Publishing $PACKAGE_FILE to npm..."
npm publish "$PACKAGE_FILE" --provenance --access public
echo "✅ Successfully published to npm!"
- name: Update release description
uses: actions/github-script@v8
env:
RELEASE_ID: ${{ inputs.release_id }}
with:
script: |
const releaseId = context.payload.release?.id || process.env.RELEASE_ID;;
// Fetch the release to get the current body
const release = await github.rest.repos.getRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: releaseId
});
const currentBody = release.data.body || '';
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: releaseId,
body: currentBody + '\n\n✅ **Published to npm registry**'
});
# Promote the tag-specific Tauri update manifest to the fixed endpoint
# so existing desktop app users receive the update notification.
promote-tauri-update:
runs-on: ubuntu-latest
if: github.event.release.prerelease == false && !startsWith(github.event.release.tag_name, 'remote-')
steps:
- name: Configure AWS CLI for R2
run: |
aws configure set aws_access_key_id ${{ secrets.R2_BINARIES_ACCESS_KEY_ID }}
aws configure set aws_secret_access_key ${{ secrets.R2_BINARIES_SECRET_ACCESS_KEY }}
aws configure set default.region auto
- name: Copy update manifest to live endpoint
run: |
TAG="${{ github.event.release.tag_name || inputs.tag_name }}"
ENDPOINT="${{ secrets.R2_BINARIES_ENDPOINT }}"
BUCKET="${{ secrets.R2_BINARIES_BUCKET }}"
echo "Promoting update manifest for $TAG to live endpoint..."
aws s3 cp \
"s3://$BUCKET/binaries/$TAG/tauri/latest.json" \
"s3://$BUCKET/binaries/tauri-update/latest.json" \
--endpoint-url "$ENDPOINT" --content-type "application/json"
echo "Update manifest promoted: binaries/tauri-update/latest.json"

28
.github/workflows/relay-deploy-dev.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Relay Deploy Dev
on:
push:
branches:
- main
paths:
- crates/relay-tunnel/**
workflow_dispatch:
jobs:
run-relay-deploy:
name: Deploy Relay Dev
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Dispatch dev relay deployment workflow
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
with:
token: ${{ secrets.REMOTE_DEPLOYMENT_TOKEN }}
repository: BloopAI/vibe-kanban-remote-deployment
event-type: vibe-kanban-relay-deploy-dev
client-payload: |
{
"ref": "${{ github.ref_name }}",
"sha": "${{ github.sha }}"
}

108
.github/workflows/relay-deploy-prod.yml vendored Normal file
View File

@@ -0,0 +1,108 @@
name: Deploy Relay Prod
on:
workflow_dispatch:
inputs:
version_type:
description: "Version bump type"
required: true
default: "patch"
type: choice
options:
- patch
- minor
- major
concurrency:
group: relay-deploy-${{ github.ref_name }}
cancel-in-progress: true
permissions:
contents: write
env:
RUST_TOOLCHAIN: nightly-2025-12-04
jobs:
release-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Cache cargo-edit
uses: actions/cache@v5
id: cache-cargo-edit
with:
path: ~/.cargo/bin/cargo-set-version
key: cargo-edit-${{ runner.os }}-${{ env.RUST_TOOLCHAIN }}
- name: Install cargo-edit
if: steps.cache-cargo-edit.outputs.cache-hit != 'true'
run: cargo install cargo-edit
- uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
ssh-key: ${{ secrets.DEPLOY_KEY }}
- name: Determine and update version
id: version
run: |
# Get current version from crates/relay-tunnel/Cargo.toml
current_version=$(grep '^version = ' crates/relay-tunnel/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
echo "Current relay-tunnel version: $current_version"
# Parse version components
IFS='.' read -r major minor patch <<< "$current_version"
# Bump based on type
case "${{ github.event.inputs.version_type }}" in
major)
major=$((major + 1))
minor=0
patch=0
;;
minor)
minor=$((minor + 1))
patch=0
;;
patch)
patch=$((patch + 1))
;;
esac
new_version="${major}.${minor}.${patch}"
new_tag="relay-v${new_version}"
# Update version in crates/relay-tunnel/Cargo.toml
cd crates/relay-tunnel
cargo set-version "$new_version"
cargo update relay-tunnel
cd ../..
echo "New version: $new_version"
echo "New tag: $new_tag"
echo "new_version=$new_version" >> $GITHUB_OUTPUT
echo "new_tag=$new_tag" >> $GITHUB_OUTPUT
echo "new_ref=$new_tag" >> $GITHUB_OUTPUT
- name: Commit changes and create tag
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add crates/relay-tunnel/Cargo.toml crates/relay-tunnel/Cargo.lock
git commit -m "chore: bump relay-tunnel version to ${{ steps.version.outputs.new_version }}"
git tag -a ${{ steps.version.outputs.new_tag }} -m "Relay release ${{ steps.version.outputs.new_tag }}"
git push
git push --tags
- name: Dispatch relay deployment workflow
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
with:
token: ${{ secrets.REMOTE_DEPLOYMENT_TOKEN }}
repository: BloopAI/vibe-kanban-remote-deployment
event-type: vibe-kanban-relay-deploy-prod
client-payload: |
{
"ref": "${{ steps.version.outputs.new_ref }}",
"sha": "${{ github.sha }}",
"version": "${{ steps.version.outputs.new_version }}"
}

22
.github/workflows/relay-release.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Create Relay Release
on:
repository_dispatch:
types: [relay-deploy-success]
jobs:
create-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.client_payload.tag }}
name: ${{ github.event.client_payload.tag }}
generate_release_notes: true
body: |
## Relay Tunnel Service Release
Deployed to ${{ github.event.client_payload.environment }}

29
.github/workflows/remote-deploy-dev.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Remote Deploy Dev
on:
push:
branches:
- main
paths:
- crates/remote/**
- packages/remote-web/**
workflow_dispatch:
jobs:
run-remote-deploy:
name: Deploy Remote Dev
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Dispatch dev remote deployment workflow
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
with:
token: ${{ secrets.REMOTE_DEPLOYMENT_TOKEN }}
repository: BloopAI/vibe-kanban-remote-deployment
event-type: vibe-kanban-remote-deploy-dev
client-payload: |
{
"ref": "${{ github.ref_name }}",
"sha": "${{ github.sha }}"
}

116
.github/workflows/remote-deploy-prod.yml vendored Normal file
View File

@@ -0,0 +1,116 @@
name: Deploy Remote Prod
on:
workflow_dispatch:
inputs:
version_type:
description: "Version bump type"
required: true
default: "patch"
type: choice
options:
- patch
- minor
- major
concurrency:
group: remote-deploy-${{ github.ref_name }}
cancel-in-progress: true
permissions:
contents: write
env:
RUST_TOOLCHAIN: nightly-2025-12-04
jobs:
release-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Cache cargo-edit
uses: actions/cache@v5
id: cache-cargo-edit
with:
path: ~/.cargo/bin/cargo-set-version
key: cargo-edit-${{ runner.os }}-${{ env.RUST_TOOLCHAIN }}
- name: Install cargo-edit
if: steps.cache-cargo-edit.outputs.cache-hit != 'true'
run: cargo install cargo-edit
- uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
ssh-key: ${{ secrets.DEPLOY_KEY }}
- name: Setup SSH Agent for private dependencies
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.VK_PRIVATE_DEPLOY_KEY }}
- name: Determine and update version
id: version
run: |
# Get current version from crates/remote/Cargo.toml
current_version=$(grep '^version = ' crates/remote/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
echo "Current remote version: $current_version"
# Parse version components
IFS='.' read -r major minor patch <<< "$current_version"
# Bump based on type
case "${{ github.event.inputs.version_type }}" in
major)
major=$((major + 1))
minor=0
patch=0
;;
minor)
minor=$((minor + 1))
patch=0
;;
patch)
patch=$((patch + 1))
;;
esac
new_version="${major}.${minor}.${patch}"
new_tag="remote-v${new_version}"
# Update version in crates/remote/Cargo.toml
cd crates/remote
cargo set-version "$new_version"
cargo update remote
cd ../..
echo "New version: $new_version"
echo "New tag: $new_tag"
echo "new_version=$new_version" >> $GITHUB_OUTPUT
echo "new_tag=$new_tag" >> $GITHUB_OUTPUT
echo "new_ref=$new_tag" >> $GITHUB_OUTPUT
- name: Stop SSH agent
run: ssh-agent -k
- name: Commit changes and create tag
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add crates/remote/Cargo.toml crates/remote/Cargo.lock
git commit -m "chore: bump remote version to ${{ steps.version.outputs.new_version }}"
git tag -a ${{ steps.version.outputs.new_tag }} -m "Remote release ${{ steps.version.outputs.new_tag }}"
git push
git push --tags
- name: Dispatch remote deployment workflow
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
with:
token: ${{ secrets.REMOTE_DEPLOYMENT_TOKEN }}
repository: BloopAI/vibe-kanban-remote-deployment
event-type: vibe-kanban-remote-deploy-prod
client-payload: |
{
"ref": "${{ steps.version.outputs.new_ref }}",
"sha": "${{ github.sha }}",
"version": "${{ steps.version.outputs.new_version }}"
}

22
.github/workflows/remote-release.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Create Remote Release
on:
repository_dispatch:
types: [remote-deploy-success]
jobs:
create-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.client_payload.tag }}
name: ${{ github.event.client_payload.tag }}
generate_release_notes: true
body: |
## Remote Service Release
✅ Deployed to ${{ github.event.client_payload.environment }}

304
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,304 @@
name: Test
on:
pull_request:
branches:
- main
paths-ignore:
- .github/workflows/**
- '!.github/workflows/test.yml'
push:
branches:
- main
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
CARGO_TERM_COLOR: always
NODE_VERSION: 22
PNPM_VERSION: 10.13.1
RUST_TOOLCHAIN: nightly-2025-12-04
RUNNER_LABEL: &runner_label blacksmith-16vcpu-ubuntu-2404
jobs:
changes:
runs-on: *runner_label
if: github.event_name == 'pull_request'
permissions:
contents: read
pull-requests: read
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
backend: ${{ steps.filter.outputs.backend }}
backend-remote: ${{ steps.filter.outputs['backend-remote'] }}
tauri: ${{ steps.filter.outputs.tauri }}
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@0bc4621a3135347011ad047f9ecf449bf72ce2bd # v3.0.0
id: filter
with:
filters: |
frontend:
- 'packages/local-web/**'
- 'packages/web-core/**'
- 'packages/remote-web/**'
- 'packages/ui/**'
- 'shared/**'
- 'scripts/check-i18n.sh'
- 'scripts/check-unused-i18n-keys.mjs'
- 'scripts/check-legacy-frontend-paths.sh'
- 'scripts/legacy-frontend-paths-allowlist.txt'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'package.json'
- '.npmrc'
- '.github/workflows/test.yml'
- '.github/actions/**'
backend:
- 'crates/api-types/**'
- 'crates/db/**'
- 'crates/deployment/**'
- 'crates/executors/**'
- 'crates/git/**'
- 'crates/local-deployment/**'
- 'crates/mcp/**'
- 'crates/review/**'
- 'crates/server/**'
- 'crates/services/**'
- 'crates/utils/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'shared/**'
- 'scripts/prepare-db.js'
- 'pnpm-lock.yaml'
- 'package.json'
- 'rustfmt.toml'
- 'rust-toolchain.toml'
- '.cargo/**'
- '.github/workflows/test.yml'
- '.github/actions/**'
backend-remote:
- 'crates/remote/**'
- 'crates/relay-tunnel/**'
- 'crates/api-types/**'
- 'crates/utils/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'shared/**'
- 'rustfmt.toml'
- 'rust-toolchain.toml'
- '.cargo/**'
- 'pnpm-lock.yaml'
- 'package.json'
- '.github/workflows/test.yml'
- '.github/actions/**'
tauri:
- 'crates/tauri-app/**'
- 'crates/api-types/**'
- 'crates/db/**'
- 'crates/deployment/**'
- 'crates/executors/**'
- 'crates/git/**'
- 'crates/local-deployment/**'
- 'crates/mcp/**'
- 'crates/review/**'
- 'crates/server/**'
- 'crates/services/**'
- 'crates/utils/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'rustfmt.toml'
- 'rust-toolchain.toml'
- '.cargo/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- '.npmrc'
- '.github/workflows/test.yml'
- '.github/actions/**'
frontend-checks:
needs: changes
if: always() && (needs.changes.outputs.frontend == 'true' || needs.changes.result == 'skipped')
runs-on: *runner_label
steps:
- uses: actions/checkout@v6
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Install dependencies
run: pnpm install
- name: Run frontend checks
env:
NODE_OPTIONS: --max-old-space-size=8192
run: |
npx concurrently \
--kill-others-on-fail \
--names "local:lint,local:fmt,local:build,remote:fmt,remote:build,ui:check,ui:lint,ui:fmt,core:check,core:fmt,i18n,i18n:unused,legacy" \
--timings \
"cd packages/local-web && npm run lint" \
"cd packages/local-web && npm run format:check" \
"cd packages/local-web && npm run build" \
"cd packages/remote-web && npm run format:check" \
"cd packages/remote-web && npm run build" \
"cd packages/ui && npm run check" \
"cd packages/ui && npm run lint" \
"cd packages/ui && npm run format:check" \
"cd packages/web-core && npm run check" \
"cd packages/web-core && npm run format:check" \
"GITHUB_BASE_REF=${{ github.base_ref || 'main' }} ./scripts/check-i18n.sh" \
"node scripts/check-unused-i18n-keys.mjs" \
"./scripts/check-legacy-frontend-paths.sh"
backend-schema-checks:
needs: changes
if: always() && (needs.changes.outputs.backend == 'true' || needs.changes.result == 'skipped')
runs-on: *runner_label
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
steps:
- uses: actions/checkout@v6
- name: Setup Rust
uses: ./.github/actions/cargo-checks-common-setup
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
cache-key: backend-schema-checks-${{ runner.os }}-${{ runner.arch }}-${{ env.RUNNER_LABEL }}
setup-node: 'true'
setup-sqlx-cli: 'true'
- name: Check generated types
run: npm run generate-types:check
- name: Sqlx checks
run: npm run prepare-db:check
backend-remote-checks:
needs: changes
if: >-
always()
&& (needs.changes.outputs['backend-remote'] == 'true' || needs.changes.result == 'skipped')
&& (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
runs-on: *runner_label
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
steps:
- uses: actions/checkout@v6
- name: Setup Rust
uses: ./.github/actions/cargo-checks-common-setup
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: rustfmt, clippy
cache-key: backend-remote-checks-${{ runner.os }}-${{ runner.arch }}-${{ env.RUNNER_LABEL }}
setup-node: 'true'
setup-sqlx-cli: 'true'
- name: Setup SSH Agent for private dependencies
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.VK_PRIVATE_DEPLOY_KEY }}
- name: Check remote Cargo.lock is consistent
run: |
cargo metadata --locked --format-version 1 --manifest-path crates/remote/Cargo.toml > /dev/null
cargo metadata --locked --format-version 1 --manifest-path crates/relay-tunnel/Cargo.toml > /dev/null
- name: Check formatting
run: |
cargo fmt --all --manifest-path crates/remote/Cargo.toml -- --check
cargo fmt --all --manifest-path crates/relay-tunnel/Cargo.toml -- --check
- name: Run Clippy
run: |
cargo clippy --all-targets --manifest-path crates/remote/Cargo.toml -- -D warnings
cargo clippy --all-targets --manifest-path crates/relay-tunnel/Cargo.toml -- -D warnings
- name: Check generated types
run: npm run remote:generate-types:check
- name: Sqlx checks
run: npm run remote:prepare-db:check
backend-clippy:
needs: changes
if: always() && (needs.changes.outputs.backend == 'true' || needs.changes.result == 'skipped')
runs-on: *runner_label
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
steps:
- uses: actions/checkout@v6
- name: Setup Rust
uses: ./.github/actions/cargo-checks-common-setup
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: rustfmt, clippy
cache-key: backend-clippy-${{ runner.os }}-${{ runner.arch }}-${{ env.RUNNER_LABEL }}
- name: Run Clippy and fmt checks
run: |
set -x
cargo fmt --all -- --check & pid_fmt=$!
cargo clippy --workspace --all-targets --exclude vibe-kanban-tauri -- -D warnings
wait $pid_fmt
backend-test:
needs: changes
if: always() && (needs.changes.outputs.backend == 'true' || needs.changes.result == 'skipped')
runs-on: *runner_label
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
steps:
- uses: actions/checkout@v6
- name: Setup Rust
uses: ./.github/actions/cargo-checks-common-setup
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
cache-key: backend-test-${{ runner.os }}-${{ runner.arch }}-${{ env.RUNNER_LABEL }}
- name: Install cargo-nextest
uses: taiki-e/install-action@nextest
- name: Run Cargo tests
run: cargo nextest run --workspace --exclude vibe-kanban-tauri
tauri-checks:
needs: changes
if: always() && (needs.changes.outputs.tauri == 'true' || needs.changes.result == 'skipped')
runs-on: *runner_label
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
steps:
- uses: actions/checkout@v6
- name: Setup Rust
uses: ./.github/actions/cargo-checks-common-setup
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: rustfmt, clippy
cache-key: tauri-checks-${{ runner.os }}-${{ runner.arch }}-${{ env.RUNNER_LABEL }}
- name: Install Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential \
libssl-dev libayatana-appindicator3-dev librsvg2-dev \
libxdo-dev file pkg-config xdg-utils
- name: Run Tauri fmt, clippy, and compile checks
run: |
cargo fmt --all --manifest-path crates/tauri-app/Cargo.toml -- --check
cargo clippy --all-targets --manifest-path crates/tauri-app/Cargo.toml -- -D warnings
cargo check -p vibe-kanban-tauri

View File

@@ -55,4 +55,3 @@ Do not manually edit shared/remote-types.ts, instead edit crates/remote/src/bin/
- Use `.env` for local overrides; never commit secrets. Key envs: `FRONTEND_PORT`, `BACKEND_PORT`, `HOST`
- Dev ports and assets are managed by `scripts/setup-dev-environment.js`.

View File

@@ -1,15 +0,0 @@
localhost:3001, relay.localhost:3001, *.relay.localhost:3001 {
tls internal
@relay host relay.localhost *.relay.localhost
handle @relay {
reverse_proxy 127.0.0.1:8082
}
@app expression `{http.request.host} == "localhost:3001" || {http.request.host} == "localhost"`
handle @app {
reverse_proxy 127.0.0.1:3000
}
respond "not found" 404
}

2395
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,9 +13,7 @@ members = [
"crates/worktree-manager",
"crates/workspace-manager",
"crates/relay-control",
"crates/relay-protocol",
"crates/relay-ws",
"crates/ws-bridge",
"crates/relay-hosts",
"crates/client-info",
"crates/remote-info",
@@ -30,7 +28,6 @@ members = [
"crates/tauri-app",
"crates/preview-proxy",
"crates/relay-tunnel-core",
"crates/relay-webrtc",
]
exclude = ["crates/remote", "crates/relay-tunnel"]

View File

@@ -72,11 +72,7 @@ COPY crates/git-host/Cargo.toml crates/git-host/Cargo.toml
COPY crates/local-deployment/Cargo.toml crates/local-deployment/Cargo.toml
COPY crates/mcp/Cargo.toml crates/mcp/Cargo.toml
COPY crates/relay-control/Cargo.toml crates/relay-control/Cargo.toml
COPY crates/relay-hosts/Cargo.toml crates/relay-hosts/Cargo.toml
COPY crates/relay-protocol/Cargo.toml crates/relay-protocol/Cargo.toml
COPY crates/relay-tunnel-core/Cargo.toml crates/relay-tunnel-core/Cargo.toml
COPY crates/relay-webrtc/Cargo.toml crates/relay-webrtc/Cargo.toml
COPY crates/relay-ws/Cargo.toml crates/relay-ws/Cargo.toml
COPY crates/review/Cargo.toml crates/review/Cargo.toml
COPY crates/server/Cargo.toml crates/server/Cargo.toml
COPY crates/server-info/Cargo.toml crates/server-info/Cargo.toml
@@ -86,7 +82,6 @@ COPY crates/trusted-key-auth/Cargo.toml crates/trusted-key-auth/Cargo.toml
COPY crates/utils/Cargo.toml crates/utils/Cargo.toml
COPY crates/workspace-manager/Cargo.toml crates/workspace-manager/Cargo.toml
COPY crates/worktree-manager/Cargo.toml crates/worktree-manager/Cargo.toml
COPY crates/ws-bridge/Cargo.toml crates/ws-bridge/Cargo.toml
COPY crates/api-types/ crates/api-types/
COPY crates/db/ crates/db/
@@ -97,11 +92,7 @@ COPY crates/git-host/ crates/git-host/
COPY crates/local-deployment/ crates/local-deployment/
COPY crates/mcp/ crates/mcp/
COPY crates/relay-control/ crates/relay-control/
COPY crates/relay-hosts/ crates/relay-hosts/
COPY crates/relay-protocol/ crates/relay-protocol/
COPY crates/relay-tunnel-core/ crates/relay-tunnel-core/
COPY crates/relay-webrtc/ crates/relay-webrtc/
COPY crates/relay-ws/ crates/relay-ws/
COPY crates/review/ crates/review/
COPY crates/server/ crates/server/
COPY crates/server-info/ crates/server-info/
@@ -110,7 +101,6 @@ COPY crates/trusted-key-auth/ crates/trusted-key-auth/
COPY crates/utils/ crates/utils/
COPY crates/workspace-manager/ crates/workspace-manager/
COPY crates/worktree-manager/ crates/worktree-manager/
COPY crates/ws-bridge/ crates/ws-bridge/
COPY assets/ assets/
COPY --from=fe-builder /app/packages/local-web/dist packages/local-web/dist

View File

@@ -1,6 +1,6 @@
[package]
name = "api-types"
version = "0.1.44"
version = "0.1.36"
edition = "2024"
[dependencies]

View File

@@ -11,8 +11,6 @@ pub struct AuthSession {
pub revoked_at: Option<DateTime<Utc>>,
pub refresh_token_id: Option<Uuid>,
pub refresh_token_issued_at: Option<DateTime<Utc>>,
pub previous_refresh_token_id: Option<Uuid>,
pub previous_refresh_token_grace_expires_at: Option<DateTime<Utc>>,
}
impl AuthSession {

View File

@@ -1,11 +0,0 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
pub struct ExportRequest {
pub organization_id: Uuid,
/// If empty, exports all projects in the organization.
pub project_ids: Vec<Uuid>,
pub include_attachments: bool,
}

View File

@@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize};
use ts_rs::TS;
use uuid::Uuid;
use crate::some_if_present;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
pub struct IssueAssignee {
pub id: Uuid,
@@ -21,6 +23,12 @@ pub struct CreateIssueAssigneeRequest {
pub user_id: Uuid,
}
#[derive(Debug, Clone, Deserialize, TS)]
pub struct UpdateIssueAssigneeRequest {
#[serde(default, deserialize_with = "some_if_present")]
pub user_id: Option<Uuid>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ListIssueAssigneesQuery {
pub issue_id: Uuid,

View File

@@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize};
use ts_rs::TS;
use uuid::Uuid;
use crate::some_if_present;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
pub struct IssueFollower {
pub id: Uuid,
@@ -19,6 +21,12 @@ pub struct CreateIssueFollowerRequest {
pub user_id: Uuid,
}
#[derive(Debug, Clone, Deserialize, TS)]
pub struct UpdateIssueFollowerRequest {
#[serde(default, deserialize_with = "some_if_present")]
pub user_id: Option<Uuid>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ListIssueFollowersQuery {
pub issue_id: Uuid,

View File

@@ -5,6 +5,8 @@ use sqlx::Type;
use ts_rs::TS;
use uuid::Uuid;
use crate::some_if_present;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type, TS, JsonSchema)]
#[sqlx(type_name = "issue_relationship_type", rename_all = "snake_case")]
#[serde(rename_all = "snake_case")]
@@ -34,6 +36,14 @@ pub struct CreateIssueRelationshipRequest {
pub relationship_type: IssueRelationshipType,
}
#[derive(Debug, Clone, Deserialize, TS)]
pub struct UpdateIssueRelationshipRequest {
#[serde(default, deserialize_with = "some_if_present")]
pub related_issue_id: Option<Uuid>,
#[serde(default, deserialize_with = "some_if_present")]
pub relationship_type: Option<IssueRelationshipType>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ListIssueRelationshipsQuery {
pub issue_id: Uuid,

View File

@@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize};
use ts_rs::TS;
use uuid::Uuid;
use crate::some_if_present;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
pub struct IssueTag {
pub id: Uuid,
@@ -19,6 +21,12 @@ pub struct CreateIssueTagRequest {
pub tag_id: Uuid,
}
#[derive(Debug, Clone, Deserialize, TS)]
pub struct UpdateIssueTagRequest {
#[serde(default, deserialize_with = "some_if_present")]
pub tag_id: Option<Uuid>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ListIssueTagsQuery {
pub issue_id: Uuid,

View File

@@ -10,7 +10,6 @@ use serde::{Deserialize, Deserializer};
pub mod attachment;
pub mod auth;
pub mod blob;
pub mod export;
pub mod issue;
pub mod issue_assignee;
pub mod issue_comment;
@@ -18,6 +17,7 @@ pub mod issue_comment_reaction;
pub mod issue_follower;
pub mod issue_relationship;
pub mod issue_tag;
pub mod migration;
pub mod notification;
pub mod oauth;
pub mod organization_member;
@@ -35,7 +35,6 @@ pub mod workspaces;
pub use attachment::*;
pub use auth::*;
pub use blob::*;
pub use export::*;
pub use issue::*;
pub use issue_assignee::*;
pub use issue_comment::*;
@@ -43,6 +42,7 @@ pub use issue_comment_reaction::*;
pub use issue_follower::*;
pub use issue_relationship::*;
pub use issue_tag::*;
pub use migration::*;
pub use notification::*;
pub use oauth::*;
pub use organization_member::*;

View File

@@ -0,0 +1,50 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrateProjectRequest {
pub organization_id: Uuid,
pub name: String,
pub color: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrateIssueRequest {
pub project_id: Uuid,
pub status_name: String,
pub title: String,
pub description: Option<String>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigratePullRequestRequest {
pub url: String,
pub number: i32,
pub status: String,
pub merged_at: Option<DateTime<Utc>>,
pub merge_commit_sha: Option<String>,
pub target_branch_name: String,
pub issue_id: Uuid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrateWorkspaceRequest {
pub project_id: Uuid,
pub issue_id: Option<Uuid>,
pub local_workspace_id: Uuid,
pub archived: bool,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BulkMigrateRequest<T> {
pub items: Vec<T>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BulkMigrateResponse {
pub ids: Vec<Uuid>,
}

View File

@@ -28,24 +28,6 @@ pub struct HandoffRedeemResponse {
pub refresh_token: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, TS)]
pub struct LocalLoginRequest {
pub email: String,
pub password: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, TS)]
pub struct LocalLoginResponse {
pub access_token: String,
pub refresh_token: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, TS)]
pub struct AuthMethodsResponse {
pub local_auth_enabled: bool,
pub oauth_providers: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, TS)]
pub struct TokenRefreshRequest {
pub refresh_token: String,
@@ -78,10 +60,7 @@ pub struct ProfileResponse {
#[serde(tag = "status", rename_all = "lowercase")]
pub enum LoginStatus {
LoggedOut,
LoggedIn {
#[serde(skip_serializing_if = "Option::is_none")]
profile: Option<ProfileResponse>,
},
LoggedIn { profile: ProfileResponse },
}
#[derive(Debug, Serialize, Deserialize, Clone, TS)]

View File

@@ -23,21 +23,12 @@ pub struct PullRequest {
pub merged_at: Option<DateTime<Utc>>,
pub merge_commit_sha: Option<String>,
pub target_branch_name: String,
pub project_id: Uuid,
#[deprecated(note = "use pull_request_issues join table instead")]
pub issue_id: Uuid,
pub workspace_id: Option<Uuid>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
pub struct PullRequestIssue {
pub id: Uuid,
pub pull_request_id: Uuid,
pub issue_id: Uuid,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ListPullRequestsQuery {
pub issue_id: Uuid,
@@ -47,35 +38,3 @@ pub struct ListPullRequestsQuery {
pub struct ListPullRequestsResponse {
pub pull_requests: Vec<PullRequest>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
pub struct ListPullRequestIssuesResponse {
pub pull_request_issues: Vec<PullRequestIssue>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
pub struct CreatePullRequestIssueRequest {
/// Optional client-generated ID. If not provided, server generates one.
/// Using client-generated IDs enables stable optimistic updates.
#[ts(optional)]
pub id: Option<Uuid>,
pub issue_id: Uuid,
pub url: String,
pub number: i32,
pub status: PullRequestStatus,
pub merged_at: Option<DateTime<Utc>>,
pub merge_commit_sha: Option<String>,
pub target_branch_name: String,
}
/// Request to update a PR status on the remote server.
#[derive(Debug, Deserialize, Serialize)]
pub struct UpdatePullRequestApiRequest {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<PullRequestStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub merged_at: Option<Option<DateTime<Utc>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub merge_commit_sha: Option<Option<String>>,
}

View File

@@ -1,4 +1,4 @@
[package]
name = "client-info"
version = "0.1.44"
version = "0.1.36"
edition = "2024"

View File

@@ -0,0 +1,68 @@
{
"db_name": "SQLite",
"query": "SELECT\n id as \"id!: Uuid\",\n execution_process_id as \"execution_process_id!: Uuid\",\n agent_session_id,\n agent_message_id,\n prompt,\n summary,\n seen as \"seen!: bool\",\n created_at as \"created_at!: DateTime<Utc>\",\n updated_at as \"updated_at!: DateTime<Utc>\"\n FROM coding_agent_turns\n WHERE agent_session_id = ?\n ORDER BY updated_at DESC\n LIMIT 1",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "execution_process_id!: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "agent_session_id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "agent_message_id",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "prompt",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "summary",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "seen!: bool",
"ordinal": 6,
"type_info": "Integer"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 8,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
true,
true,
true,
true,
false,
false,
false
]
},
"hash": "04e5a05c7cad438d39c4c8590410889ab1eefa7376d474a10c119d3f4d9143c7"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE migration_state\n SET status = 'migrated',\n remote_id = $3,\n error_message = NULL,\n updated_at = datetime('now', 'subsec')\n WHERE entity_type = $1 AND local_id = $2",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "0a805c219c9028b2677bd94ccabd47916e60d26c1cede27e467f0ae91f6639ab"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE migration_state\n SET status = 'pending',\n error_message = NULL,\n updated_at = datetime('now', 'subsec')\n WHERE status = 'failed'",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "0ac0d0f3826330836e3fd1bf57c42777eb489ac41a650f9361e6b563fc69bf35"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "DELETE FROM pull_requests WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "0ee24d51e7557d9d6c9418a06156c03d7dd49dfe938194cd6951942dfd8f179d"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE migration_state\n SET status = 'skipped',\n error_message = $3,\n updated_at = datetime('now', 'subsec')\n WHERE entity_type = $1 AND local_id = $2",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "0f90844fc62261ed140e02515ae464b940743113814507313c9fdc176000d1bf"
}

View File

@@ -0,0 +1,86 @@
{
"db_name": "SQLite",
"query": "SELECT\n id as \"id!: Uuid\",\n workspace_id as \"workspace_id!: Uuid\",\n repo_id as \"repo_id!: Uuid\",\n merge_type as \"merge_type!: MergeType\",\n merge_commit,\n pr_number,\n pr_url,\n pr_status as \"pr_status?: MergeStatus\",\n pr_merged_at as \"pr_merged_at?: DateTime<Utc>\",\n pr_merge_commit_sha,\n target_branch_name as \"target_branch_name!: String\",\n created_at as \"created_at!: DateTime<Utc>\"\n FROM merges\n WHERE workspace_id = $1 AND repo_id = $2\n ORDER BY created_at DESC",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "workspace_id!: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "repo_id!: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "merge_type!: MergeType",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "merge_commit",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "pr_number",
"ordinal": 5,
"type_info": "Integer"
},
{
"name": "pr_url",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "pr_status?: MergeStatus",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "pr_merged_at?: DateTime<Utc>",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "pr_merge_commit_sha",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "target_branch_name!: String",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 11,
"type_info": "Text"
}
],
"parameters": {
"Right": 2
},
"nullable": [
true,
false,
false,
false,
true,
true,
true,
true,
true,
true,
false,
false
]
},
"hash": "1085d1f8107c7e16fc2058ef610918760d8d420f0fca97adecd76d698f6f3a51"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "SELECT COUNT(1) as \"count!: i64\"\n FROM merges\n WHERE workspace_id = $1\n AND merge_type = 'pr'\n AND pr_status = 'open'",
"describe": {
"columns": [
{
"name": "count!: i64",
"ordinal": 0,
"type_info": "Integer"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "167c1d13ee37ebe62cd2316feaf6b5354eb26d0a8fc16efb22827a5cde59a60e"
}

View File

@@ -1,86 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT\n id,\n workspace_id AS \"workspace_id: Uuid\",\n repo_id AS \"repo_id: Uuid\",\n pr_url,\n pr_number,\n pr_status AS \"pr_status: MergeStatus\",\n target_branch_name,\n merged_at AS \"merged_at: DateTime<Utc>\",\n merge_commit_sha,\n created_at AS \"created_at!: DateTime<Utc>\",\n updated_at AS \"updated_at!: DateTime<Utc>\",\n synced_at AS \"synced_at: DateTime<Utc>\"\n FROM pull_requests\n WHERE synced_at IS NULL OR synced_at < updated_at",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "workspace_id: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "repo_id: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "pr_url",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "pr_number",
"ordinal": 4,
"type_info": "Integer"
},
{
"name": "pr_status: MergeStatus",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "target_branch_name",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "merged_at: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "merge_commit_sha",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 9,
"type_info": "Datetime"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 10,
"type_info": "Datetime"
},
{
"name": "synced_at: DateTime<Utc>",
"ordinal": 11,
"type_info": "Datetime"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
true,
true,
false,
false,
false,
false,
true,
true,
false,
false,
true
]
},
"hash": "1af7a606a64d9662d6edc4fbeaae0d53a4d657a6e29b2561e820085a2d9e3348"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE pull_requests SET pr_status = ?, merged_at = ?, merge_commit_sha = ?, updated_at = ?, synced_at = NULL WHERE pr_url = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "2948ec0606a39ee67bdbaf4f2cf82592448a1d91a66d7d89cf7204f71f706274"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "SELECT w.id as \"workspace_id!: Uuid\"\n FROM workspaces w\n WHERE w.container_ref = ?",
"describe": {
"columns": [
{
"name": "workspace_id!: Uuid",
"ordinal": 0,
"type_info": "Blob"
}
],
"parameters": {
"Right": 1
},
"nullable": [
true
]
},
"hash": "29cc3aa8d0ad5deda94494402500a4125e29381d63f18ef083cc4da95e2c5db5"
}

View File

@@ -1,86 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT\n id,\n workspace_id AS \"workspace_id: Uuid\",\n repo_id AS \"repo_id: Uuid\",\n pr_url,\n pr_number,\n pr_status AS \"pr_status: MergeStatus\",\n target_branch_name,\n merged_at AS \"merged_at: DateTime<Utc>\",\n merge_commit_sha,\n created_at AS \"created_at!: DateTime<Utc>\",\n updated_at AS \"updated_at!: DateTime<Utc>\",\n synced_at AS \"synced_at: DateTime<Utc>\"\n FROM pull_requests\n WHERE workspace_id = $1 AND repo_id = $2\n ORDER BY created_at DESC",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "workspace_id: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "repo_id: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "pr_url",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "pr_number",
"ordinal": 4,
"type_info": "Integer"
},
{
"name": "pr_status: MergeStatus",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "target_branch_name",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "merged_at: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "merge_commit_sha",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 9,
"type_info": "Datetime"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 10,
"type_info": "Datetime"
},
{
"name": "synced_at: DateTime<Utc>",
"ordinal": 11,
"type_info": "Datetime"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
true,
true,
false,
false,
false,
false,
true,
true,
false,
false,
true
]
},
"hash": "32c74a54fdd72a6b167583228f2c30944837b7cc29c9cc4acfbb4f047cea7118"
}

View File

@@ -0,0 +1,68 @@
{
"db_name": "SQLite",
"query": "SELECT\n id as \"id!: Uuid\",\n entity_type as \"entity_type!: EntityType\",\n local_id as \"local_id!: Uuid\",\n remote_id as \"remote_id: Uuid\",\n status as \"status!: MigrationStatus\",\n error_message,\n attempt_count as \"attempt_count!\",\n created_at as \"created_at!: DateTime<Utc>\",\n updated_at as \"updated_at!: DateTime<Utc>\"\n FROM migration_state\n WHERE entity_type = $1 AND status = 'pending'\n ORDER BY created_at ASC",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "entity_type!: EntityType",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "local_id!: Uuid",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "remote_id: Uuid",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "status!: MigrationStatus",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "error_message",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "attempt_count!",
"ordinal": 6,
"type_info": "Integer"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 8,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
false,
true,
false,
true,
false,
false,
false
]
},
"hash": "33f23656ba343bd75a88b0fadf2a4ba01eda330f9b549e625e27701e3b0b5a31"
}

View File

@@ -0,0 +1,86 @@
{
"db_name": "SQLite",
"query": "SELECT\n m.id as \"id!: Uuid\",\n m.workspace_id as \"workspace_id!: Uuid\",\n m.repo_id as \"repo_id!: Uuid\",\n m.merge_type as \"merge_type!: MergeType\",\n m.merge_commit,\n m.pr_number,\n m.pr_url,\n m.pr_status as \"pr_status?: MergeStatus\",\n m.pr_merged_at as \"pr_merged_at?: DateTime<Utc>\",\n m.pr_merge_commit_sha,\n m.target_branch_name as \"target_branch_name!: String\",\n m.created_at as \"created_at!: DateTime<Utc>\"\n FROM merges m\n INNER JOIN (\n SELECT workspace_id, MAX(created_at) as max_created_at\n FROM merges\n WHERE merge_type = 'pr'\n GROUP BY workspace_id\n ) latest ON m.workspace_id = latest.workspace_id\n AND m.created_at = latest.max_created_at\n INNER JOIN workspaces w ON m.workspace_id = w.id\n WHERE m.merge_type = 'pr' AND w.archived = $1",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "workspace_id!: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "repo_id!: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "merge_type!: MergeType",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "merge_commit",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "pr_number",
"ordinal": 5,
"type_info": "Integer"
},
{
"name": "pr_url",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "pr_status?: MergeStatus",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "pr_merged_at?: DateTime<Utc>",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "pr_merge_commit_sha",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "target_branch_name!: String",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 11,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
false,
false,
true,
true,
true,
true,
true,
true,
false,
false
]
},
"hash": "3ace1ee8dba0669400d69891912b86823e41ca643092d990b12c1a6160112427"
}

View File

@@ -0,0 +1,86 @@
{
"db_name": "SQLite",
"query": "INSERT INTO merges (\n id, workspace_id, repo_id, merge_type, pr_number, pr_url, pr_status, created_at, target_branch_name\n ) VALUES ($1, $2, $3, 'pr', $4, $5, 'open', $6, $7)\n RETURNING\n id as \"id!: Uuid\",\n workspace_id as \"workspace_id!: Uuid\",\n repo_id as \"repo_id!: Uuid\",\n merge_type as \"merge_type!: MergeType\",\n merge_commit,\n pr_number,\n pr_url,\n pr_status as \"pr_status?: MergeStatus\",\n pr_merged_at as \"pr_merged_at?: DateTime<Utc>\",\n pr_merge_commit_sha,\n created_at as \"created_at!: DateTime<Utc>\",\n target_branch_name as \"target_branch_name!: String\"\n ",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "workspace_id!: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "repo_id!: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "merge_type!: MergeType",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "merge_commit",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "pr_number",
"ordinal": 5,
"type_info": "Integer"
},
{
"name": "pr_url",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "pr_status?: MergeStatus",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "pr_merged_at?: DateTime<Utc>",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "pr_merge_commit_sha",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "target_branch_name!: String",
"ordinal": 11,
"type_info": "Text"
}
],
"parameters": {
"Right": 7
},
"nullable": [
true,
false,
false,
false,
true,
true,
true,
true,
true,
true,
false,
false
]
},
"hash": "3d85256618729c1c0bf2758ffab9cdb4ec2af0751e3a37db4009c02f95f6f556"
}

View File

@@ -0,0 +1,86 @@
{
"db_name": "SQLite",
"query": "SELECT\n id as \"id!: Uuid\",\n workspace_id as \"workspace_id!: Uuid\",\n repo_id as \"repo_id!: Uuid\",\n merge_type as \"merge_type!: MergeType\",\n merge_commit,\n pr_number,\n pr_url,\n pr_status as \"pr_status?: MergeStatus\",\n pr_merged_at as \"pr_merged_at?: DateTime<Utc>\",\n pr_merge_commit_sha,\n target_branch_name as \"target_branch_name!: String\",\n created_at as \"created_at!: DateTime<Utc>\"\n FROM merges\n WHERE workspace_id = $1\n ORDER BY created_at DESC",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "workspace_id!: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "repo_id!: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "merge_type!: MergeType",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "merge_commit",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "pr_number",
"ordinal": 5,
"type_info": "Integer"
},
{
"name": "pr_url",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "pr_status?: MergeStatus",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "pr_merged_at?: DateTime<Utc>",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "pr_merge_commit_sha",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "target_branch_name!: String",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 11,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
false,
false,
true,
true,
true,
true,
true,
true,
false,
false
]
},
"hash": "4b59b958807be54c7c9949d96ced96e4ab1498f1056e7d0d7956aff46352d90f"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE execution_process_repo_states\n SET merge_commit = $1, updated_at = $2\n WHERE execution_process_id = $3\n AND repo_id = $4",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "4d84b308a2cc7677da65111d080bf02e5e35c052048360d3dbea656bbbcd3edb"
}

View File

@@ -0,0 +1,68 @@
{
"db_name": "SQLite",
"query": "SELECT\n id as \"id!: Uuid\",\n entity_type as \"entity_type!: EntityType\",\n local_id as \"local_id!: Uuid\",\n remote_id as \"remote_id: Uuid\",\n status as \"status!: MigrationStatus\",\n error_message,\n attempt_count as \"attempt_count!\",\n created_at as \"created_at!: DateTime<Utc>\",\n updated_at as \"updated_at!: DateTime<Utc>\"\n FROM migration_state\n WHERE entity_type = $1\n ORDER BY created_at ASC",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "entity_type!: EntityType",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "local_id!: Uuid",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "remote_id: Uuid",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "status!: MigrationStatus",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "error_message",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "attempt_count!",
"ordinal": 6,
"type_info": "Integer"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 8,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
false,
true,
false,
true,
false,
false,
false
]
},
"hash": "517fb82570b9624e166696af53963ec499966562b23b5833fc4ca4cf43bcaccc"
}

View File

@@ -0,0 +1,86 @@
{
"db_name": "SQLite",
"query": "SELECT\n id as \"id!: Uuid\",\n workspace_id as \"workspace_id!: Uuid\",\n repo_id as \"repo_id!: Uuid\",\n merge_type as \"merge_type!: MergeType\",\n merge_commit,\n pr_number,\n pr_url,\n pr_status as \"pr_status?: MergeStatus\",\n pr_merged_at as \"pr_merged_at?: DateTime<Utc>\",\n pr_merge_commit_sha,\n created_at as \"created_at!: DateTime<Utc>\",\n target_branch_name as \"target_branch_name!: String\"\n FROM merges\n WHERE merge_type = 'pr'\n ORDER BY created_at ASC",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "workspace_id!: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "repo_id!: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "merge_type!: MergeType",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "merge_commit",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "pr_number",
"ordinal": 5,
"type_info": "Integer"
},
{
"name": "pr_url",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "pr_status?: MergeStatus",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "pr_merged_at?: DateTime<Utc>",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "pr_merge_commit_sha",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "target_branch_name!: String",
"ordinal": 11,
"type_info": "Text"
}
],
"parameters": {
"Right": 0
},
"nullable": [
true,
false,
false,
false,
true,
true,
true,
true,
true,
true,
false,
false
]
},
"hash": "570f62a32921c4a9a7e4e1006e9b31c4c58e69ab8681d76dfe9b184ff1e0bc65"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE merges\n SET pr_status = $1,\n pr_merge_commit_sha = $2,\n pr_merged_at = $3\n WHERE id = $4",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "5785a10c3d51ff9b001aa455f6296a4dcba61cec700a4b72031c5c643b273938"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "SELECT EXISTS(\n SELECT 1 FROM coding_agent_turns cat\n JOIN execution_processes ep ON cat.execution_process_id = ep.id\n JOIN sessions s ON ep.session_id = s.id\n WHERE s.workspace_id = $1 AND cat.seen = 0\n ) as \"has_unseen!: bool\"",
"describe": {
"columns": [
{
"name": "has_unseen!: bool",
"ordinal": 0,
"type_info": "Integer"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "606016bf31cf0abd87d7eb95ea99d7c8c014523cb02e482d608299ceefaeffc5"
}

View File

@@ -1,86 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT\n t.id,\n t.workspace_id AS \"workspace_id: Uuid\",\n t.repo_id AS \"repo_id: Uuid\",\n t.pr_url,\n t.pr_number,\n t.pr_status AS \"pr_status: MergeStatus\",\n t.target_branch_name,\n t.merged_at AS \"merged_at: DateTime<Utc>\",\n t.merge_commit_sha,\n t.created_at AS \"created_at!: DateTime<Utc>\",\n t.updated_at AS \"updated_at!: DateTime<Utc>\",\n t.synced_at AS \"synced_at: DateTime<Utc>\"\n FROM pull_requests t\n INNER JOIN (\n SELECT workspace_id, MAX(created_at) as max_created_at\n FROM pull_requests\n WHERE workspace_id IS NOT NULL\n GROUP BY workspace_id\n ) latest ON t.workspace_id = latest.workspace_id AND t.created_at = latest.max_created_at\n INNER JOIN workspaces w ON t.workspace_id = w.id\n WHERE t.workspace_id IS NOT NULL AND w.archived = $1",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "workspace_id: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "repo_id: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "pr_url",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "pr_number",
"ordinal": 4,
"type_info": "Integer"
},
{
"name": "pr_status: MergeStatus",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "target_branch_name",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "merged_at: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "merge_commit_sha",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 9,
"type_info": "Datetime"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 10,
"type_info": "Datetime"
},
{
"name": "synced_at: DateTime<Utc>",
"ordinal": 11,
"type_info": "Datetime"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true,
true,
false,
false,
false,
false,
true,
true,
false,
false,
true
]
},
"hash": "64cdb811a6d15a483fc0413dc49ba8aa093b1892831699c456afe0507ac51aa3"
}

View File

@@ -1,50 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT\n id AS \"id!: Uuid\",\n workspace_id AS \"workspace_id!: Uuid\",\n repo_id AS \"repo_id!: Uuid\",\n merge_commit,\n target_branch_name,\n created_at AS \"created_at!: DateTime<Utc>\"\n FROM merges\n WHERE workspace_id = ? AND merge_type = 'direct'\n ORDER BY created_at DESC",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "workspace_id!: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "repo_id!: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "merge_commit",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "target_branch_name",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 5,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
false,
true,
false,
false
]
},
"hash": "67a7192158ad406cf8b97165c46a602330e807ed2757497e483295ea21952f1f"
}

View File

@@ -0,0 +1,68 @@
{
"db_name": "SQLite",
"query": "SELECT\n id as \"id!: Uuid\",\n entity_type as \"entity_type!: EntityType\",\n local_id as \"local_id!: Uuid\",\n remote_id as \"remote_id: Uuid\",\n status as \"status!: MigrationStatus\",\n error_message,\n attempt_count as \"attempt_count!\",\n created_at as \"created_at!: DateTime<Utc>\",\n updated_at as \"updated_at!: DateTime<Utc>\"\n FROM migration_state\n WHERE entity_type = $1 AND local_id = $2",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "entity_type!: EntityType",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "local_id!: Uuid",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "remote_id: Uuid",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "status!: MigrationStatus",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "error_message",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "attempt_count!",
"ordinal": 6,
"type_info": "Integer"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 8,
"type_info": "Text"
}
],
"parameters": {
"Right": 2
},
"nullable": [
true,
false,
false,
true,
false,
true,
false,
false,
false
]
},
"hash": "686810c5271e1d44042b6ea2c6cc434eb2e3f5d3540c97d703c34dd4e978c690"
}

View File

@@ -1,50 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT\n id AS \"id!: Uuid\",\n workspace_id AS \"workspace_id!: Uuid\",\n repo_id AS \"repo_id!: Uuid\",\n merge_commit,\n target_branch_name,\n created_at AS \"created_at!: DateTime<Utc>\"\n FROM merges\n WHERE workspace_id = ? AND repo_id = ? AND merge_type = 'direct'\n ORDER BY created_at DESC",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "workspace_id!: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "repo_id!: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "merge_commit",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "target_branch_name",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 5,
"type_info": "Text"
}
],
"parameters": {
"Right": 2
},
"nullable": [
true,
false,
false,
true,
false,
false
]
},
"hash": "687a386ec0c65676bcd9d1ced30549e4c2a3d2bdb63a0a5623027e50aa6e4389"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "SELECT COUNT(1) AS \"count!: i64\" FROM pull_requests WHERE workspace_id = ? AND pr_status = 'open'",
"query": "SELECT COUNT(*) as \"count!: i64\" FROM workspaces",
"describe": {
"columns": [
{
@@ -10,11 +10,11 @@
}
],
"parameters": {
"Right": 1
"Right": 0
},
"nullable": [
false
]
},
"hash": "21c7a9f62322212a5f6b923bb410c25ac06e6a0c857d910d93fb80ab77fc9b35"
"hash": "70b21c5c0a2ba5c21c9c1132f14a68c02a8a2cd555caea74e57a0aeb206770d3"
}

View File

@@ -0,0 +1,68 @@
{
"db_name": "SQLite",
"query": "INSERT INTO migration_state (id, entity_type, local_id)\n VALUES ($1, $2, $3)\n ON CONFLICT (entity_type, local_id) DO UPDATE SET\n updated_at = datetime('now', 'subsec')\n RETURNING\n id as \"id!: Uuid\",\n entity_type as \"entity_type!: EntityType\",\n local_id as \"local_id!: Uuid\",\n remote_id as \"remote_id: Uuid\",\n status as \"status!: MigrationStatus\",\n error_message,\n attempt_count as \"attempt_count!\",\n created_at as \"created_at!: DateTime<Utc>\",\n updated_at as \"updated_at!: DateTime<Utc>\"",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "entity_type!: EntityType",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "local_id!: Uuid",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "remote_id: Uuid",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "status!: MigrationStatus",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "error_message",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "attempt_count!",
"ordinal": 6,
"type_info": "Integer"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 8,
"type_info": "Text"
}
],
"parameters": {
"Right": 3
},
"nullable": [
true,
false,
false,
true,
false,
true,
false,
false,
false
]
},
"hash": "7364150098bec681451c43762117a1f5c5b4e27f5f65186c3cc16092b3491c37"
}

View File

@@ -0,0 +1,68 @@
{
"db_name": "SQLite",
"query": "SELECT\n id as \"id!: Uuid\",\n entity_type as \"entity_type!: EntityType\",\n local_id as \"local_id!: Uuid\",\n remote_id as \"remote_id: Uuid\",\n status as \"status!: MigrationStatus\",\n error_message,\n attempt_count as \"attempt_count!\",\n created_at as \"created_at!: DateTime<Utc>\",\n updated_at as \"updated_at!: DateTime<Utc>\"\n FROM migration_state\n ORDER BY created_at ASC",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "entity_type!: EntityType",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "local_id!: Uuid",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "remote_id: Uuid",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "status!: MigrationStatus",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "error_message",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "attempt_count!",
"ordinal": 6,
"type_info": "Integer"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 8,
"type_info": "Text"
}
],
"parameters": {
"Right": 0
},
"nullable": [
true,
false,
false,
true,
false,
true,
false,
false,
false
]
},
"hash": "7807cb09da72c5a1e35bf4f4da1bea1743a578588e72444ede98f5f969af08c1"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE pull_requests SET synced_at = ? WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "8076c3d62eba1cdf2a050c9d61dc20ef2fc3e24b0830d1e20099a42ae78333d0"
}

View File

@@ -0,0 +1,68 @@
{
"db_name": "SQLite",
"query": "SELECT\n id as \"id!: Uuid\",\n entity_type as \"entity_type!: EntityType\",\n local_id as \"local_id!: Uuid\",\n remote_id as \"remote_id: Uuid\",\n status as \"status!: MigrationStatus\",\n error_message,\n attempt_count as \"attempt_count!\",\n created_at as \"created_at!: DateTime<Utc>\",\n updated_at as \"updated_at!: DateTime<Utc>\"\n FROM migration_state\n WHERE status = $1\n ORDER BY created_at ASC",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "entity_type!: EntityType",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "local_id!: Uuid",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "remote_id: Uuid",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "status!: MigrationStatus",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "error_message",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "attempt_count!",
"ordinal": 6,
"type_info": "Integer"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 8,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
false,
true,
false,
true,
false,
false,
false
]
},
"hash": "84ee994f0aad005cf62ca318eb20ae29d218a72cdd1fadf2a5ae399b0719ca19"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO execution_process_logs (execution_id, logs, byte_size, inserted_at)\n VALUES ($1, $2, $3, datetime('now', 'subsec'))",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "9747ebaebd562d65f0c333b0f5efc74fa63ab9fcb35a43f75f57da3fcb9a2588"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "INSERT INTO merges (id, workspace_id, repo_id, merge_type, merge_commit, created_at, target_branch_name)\n VALUES (?, ?, ?, 'direct', ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "9ab0fedd6e06a0372797e66c9353dd807e85e059520f20db4b03d9a27bb3efb7"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "DELETE FROM migration_state",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "a4a50fcfb903e6d0a315676f4f760e5bb7718e10ea550aedf990c9da84834416"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE migration_state\n SET status = 'failed',\n error_message = $3,\n attempt_count = attempt_count + 1,\n updated_at = datetime('now', 'subsec')\n WHERE entity_type = $1 AND local_id = $2",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "a915c22f5ed0bb86c3a242ca38cbc1bfca40ebfe9096058c27e94479b67c7c02"
}

View File

@@ -1,86 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT\n id,\n workspace_id AS \"workspace_id: Uuid\",\n repo_id AS \"repo_id: Uuid\",\n pr_url,\n pr_number,\n pr_status AS \"pr_status: MergeStatus\",\n target_branch_name,\n merged_at AS \"merged_at: DateTime<Utc>\",\n merge_commit_sha,\n created_at AS \"created_at!: DateTime<Utc>\",\n updated_at AS \"updated_at!: DateTime<Utc>\",\n synced_at AS \"synced_at: DateTime<Utc>\"\n FROM pull_requests\n WHERE workspace_id = $1\n ORDER BY created_at DESC",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "workspace_id: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "repo_id: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "pr_url",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "pr_number",
"ordinal": 4,
"type_info": "Integer"
},
{
"name": "pr_status: MergeStatus",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "target_branch_name",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "merged_at: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "merge_commit_sha",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 9,
"type_info": "Datetime"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 10,
"type_info": "Datetime"
},
{
"name": "synced_at: DateTime<Utc>",
"ordinal": 11,
"type_info": "Datetime"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true,
true,
false,
false,
false,
false,
true,
true,
false,
false,
true
]
},
"hash": "b21ce1663242be3d16c21a076d7b4b584692eb6ae19a3a5f19caba8ad16edce6"
}

View File

@@ -0,0 +1,86 @@
{
"db_name": "SQLite",
"query": "INSERT INTO merges (\n id, workspace_id, repo_id, merge_type, merge_commit, created_at, target_branch_name\n ) VALUES ($1, $2, $3, 'direct', $4, $5, $6)\n RETURNING\n id as \"id!: Uuid\",\n workspace_id as \"workspace_id!: Uuid\",\n repo_id as \"repo_id!: Uuid\",\n merge_type as \"merge_type!: MergeType\",\n merge_commit,\n pr_number,\n pr_url,\n pr_status as \"pr_status?: MergeStatus\",\n pr_merged_at as \"pr_merged_at?: DateTime<Utc>\",\n pr_merge_commit_sha,\n created_at as \"created_at!: DateTime<Utc>\",\n target_branch_name as \"target_branch_name!: String\"\n ",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "workspace_id!: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "repo_id!: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "merge_type!: MergeType",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "merge_commit",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "pr_number",
"ordinal": 5,
"type_info": "Integer"
},
{
"name": "pr_url",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "pr_status?: MergeStatus",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "pr_merged_at?: DateTime<Utc>",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "pr_merge_commit_sha",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "target_branch_name!: String",
"ordinal": 11,
"type_info": "Text"
}
],
"parameters": {
"Right": 6
},
"nullable": [
true,
false,
false,
false,
true,
true,
true,
true,
true,
true,
false,
false
]
},
"hash": "b4476bedb8e5c0f7bc21654dc62fb00fbd8a41e24efaba55be8278031d71cc59"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "DELETE FROM repos\n WHERE id NOT IN (SELECT repo_id FROM project_repos)\n AND id NOT IN (SELECT repo_id FROM workspace_repos)",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "c27f2fd6b3696cb5a8ec54226608440786a6cec601783f797be3a8c515080d62"
}

View File

@@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "SELECT remote_id as \"remote_id: Uuid\"\n FROM migration_state\n WHERE entity_type = $1 AND local_id = $2 AND status = 'migrated'",
"describe": {
"columns": [
{
"name": "remote_id: Uuid",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 2
},
"nullable": [
true
]
},
"hash": "c793ee8493c54ea295a62a51650d00894fdad2f2cadc5665ae1e16a605626cb2"
}

View File

@@ -0,0 +1,44 @@
{
"db_name": "SQLite",
"query": "SELECT\n COALESCE(SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END), 0) as \"pending!: i64\",\n COALESCE(SUM(CASE WHEN status = 'migrated' THEN 1 ELSE 0 END), 0) as \"migrated!: i64\",\n COALESCE(SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END), 0) as \"failed!: i64\",\n COALESCE(SUM(CASE WHEN status = 'skipped' THEN 1 ELSE 0 END), 0) as \"skipped!: i64\",\n COUNT(*) as \"total!: i64\"\n FROM migration_state",
"describe": {
"columns": [
{
"name": "pending!: i64",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "migrated!: i64",
"ordinal": 1,
"type_info": "Integer"
},
{
"name": "failed!: i64",
"ordinal": 2,
"type_info": "Integer"
},
{
"name": "skipped!: i64",
"ordinal": 3,
"type_info": "Integer"
},
{
"name": "total!: i64",
"ordinal": 4,
"type_info": "Integer"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "cd6d7ca74442a100d9caf170ac43118795226f50b8392069b47abd4f7564c135"
}

View File

@@ -1,86 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT\n id,\n workspace_id AS \"workspace_id: Uuid\",\n repo_id AS \"repo_id: Uuid\",\n pr_url,\n pr_number,\n pr_status AS \"pr_status: MergeStatus\",\n target_branch_name,\n merged_at AS \"merged_at: DateTime<Utc>\",\n merge_commit_sha,\n created_at AS \"created_at!: DateTime<Utc>\",\n updated_at AS \"updated_at!: DateTime<Utc>\",\n synced_at AS \"synced_at: DateTime<Utc>\"\n FROM pull_requests\n WHERE pr_url = $1",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "workspace_id: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "repo_id: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "pr_url",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "pr_number",
"ordinal": 4,
"type_info": "Integer"
},
{
"name": "pr_status: MergeStatus",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "target_branch_name",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "merged_at: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "merge_commit_sha",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 9,
"type_info": "Datetime"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 10,
"type_info": "Datetime"
},
{
"name": "synced_at: DateTime<Utc>",
"ordinal": 11,
"type_info": "Datetime"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true,
true,
false,
false,
false,
false,
true,
true,
false,
false,
true
]
},
"hash": "cf93a57f1be9e5944ad328cddfdaf648073c9ce886079d5cc57007abb91459df"
}

View File

@@ -0,0 +1,68 @@
{
"db_name": "SQLite",
"query": "INSERT INTO migration_state (id, entity_type, local_id)\n VALUES ($1, $2, $3)\n RETURNING\n id as \"id!: Uuid\",\n entity_type as \"entity_type!: EntityType\",\n local_id as \"local_id!: Uuid\",\n remote_id as \"remote_id: Uuid\",\n status as \"status!: MigrationStatus\",\n error_message,\n attempt_count as \"attempt_count!\",\n created_at as \"created_at!: DateTime<Utc>\",\n updated_at as \"updated_at!: DateTime<Utc>\"",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "entity_type!: EntityType",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "local_id!: Uuid",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "remote_id: Uuid",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "status!: MigrationStatus",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "error_message",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "attempt_count!",
"ordinal": 6,
"type_info": "Integer"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 8,
"type_info": "Text"
}
],
"parameters": {
"Right": 3
},
"nullable": [
true,
false,
false,
true,
false,
true,
false,
false,
false
]
},
"hash": "ee06dfd8dc7fc2ffc239db9635a3a5cac2e603992392a632bff7d450c6bca061"
}

View File

@@ -1,86 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT\n id,\n workspace_id AS \"workspace_id: Uuid\",\n repo_id AS \"repo_id: Uuid\",\n pr_url,\n pr_number,\n pr_status AS \"pr_status: MergeStatus\",\n target_branch_name,\n merged_at AS \"merged_at: DateTime<Utc>\",\n merge_commit_sha,\n created_at AS \"created_at!: DateTime<Utc>\",\n updated_at AS \"updated_at!: DateTime<Utc>\",\n synced_at AS \"synced_at: DateTime<Utc>\"\n FROM pull_requests\n WHERE workspace_id IS NOT NULL\n ORDER BY created_at ASC",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "workspace_id: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "repo_id: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "pr_url",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "pr_number",
"ordinal": 4,
"type_info": "Integer"
},
{
"name": "pr_status: MergeStatus",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "target_branch_name",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "merged_at: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "merge_commit_sha",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 9,
"type_info": "Datetime"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 10,
"type_info": "Datetime"
},
{
"name": "synced_at: DateTime<Utc>",
"ordinal": 11,
"type_info": "Datetime"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
true,
true,
false,
false,
false,
false,
true,
true,
false,
false,
true
]
},
"hash": "ef0020050eca66193646fc9193159715625820e6fc5b314631f762ccaba6ac74"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "INSERT INTO pull_requests (id, workspace_id, repo_id, pr_url, pr_number, pr_status, target_branch_name, created_at)\n VALUES (?, ?, ?, ?, ?, 'open', ?, ?)\n ON CONFLICT(pr_url) DO UPDATE SET\n workspace_id = COALESCE(pull_requests.workspace_id, excluded.workspace_id),\n repo_id = COALESCE(pull_requests.repo_id, excluded.repo_id),\n updated_at = CURRENT_TIMESTAMP",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "f393e611063de7c8a3f1e32f5b3cd6ca859139a79aa94ca8669470ba61e691e4"
}

View File

@@ -1,86 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT\n id,\n workspace_id AS \"workspace_id: Uuid\",\n repo_id AS \"repo_id: Uuid\",\n pr_url,\n pr_number,\n pr_status AS \"pr_status: MergeStatus\",\n target_branch_name,\n merged_at AS \"merged_at: DateTime<Utc>\",\n merge_commit_sha,\n created_at AS \"created_at!: DateTime<Utc>\",\n updated_at AS \"updated_at!: DateTime<Utc>\",\n synced_at AS \"synced_at: DateTime<Utc>\"\n FROM pull_requests\n WHERE pr_status = 'open'",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "workspace_id: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "repo_id: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "pr_url",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "pr_number",
"ordinal": 4,
"type_info": "Integer"
},
{
"name": "pr_status: MergeStatus",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "target_branch_name",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "merged_at: DateTime<Utc>",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "merge_commit_sha",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 9,
"type_info": "Datetime"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 10,
"type_info": "Datetime"
},
{
"name": "synced_at: DateTime<Utc>",
"ordinal": 11,
"type_info": "Datetime"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
true,
true,
false,
false,
false,
false,
true,
true,
false,
false,
true
]
},
"hash": "f81f1b377d63ffb204e2860289c5dc3001a68404e016c75ef6427bdd0909862e"
}

View File

@@ -0,0 +1,98 @@
{
"db_name": "SQLite",
"query": "SELECT DISTINCT r.id as \"id!: Uuid\",\n r.path,\n r.name,\n r.display_name,\n r.setup_script,\n r.cleanup_script,\n r.archive_script,\n r.copy_files,\n r.parallel_setup_script as \"parallel_setup_script!: bool\",\n r.dev_server_script,\n r.default_target_branch,\n r.default_working_dir,\n r.created_at as \"created_at!: DateTime<Utc>\",\n r.updated_at as \"updated_at!: DateTime<Utc>\"\n FROM repos r\n JOIN workspace_repos wr ON r.id = wr.repo_id\n JOIN workspaces w ON wr.workspace_id = w.id\n WHERE w.task_id = $1\n ORDER BY r.display_name ASC",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "path",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "name",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "display_name",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "setup_script",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "cleanup_script",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "archive_script",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "copy_files",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "parallel_setup_script!: bool",
"ordinal": 8,
"type_info": "Integer"
},
{
"name": "dev_server_script",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "default_target_branch",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "default_working_dir",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 12,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 13,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
false,
false,
true,
true,
true,
true,
false,
true,
true,
true,
false,
false
]
},
"hash": "f9e8640c28fae8aebf3d8b0d3984804fdb3f197c8cc2d5750fd267c82e3e68a1"
}

View File

@@ -0,0 +1,86 @@
{
"db_name": "SQLite",
"query": "SELECT\n id as \"id!: Uuid\",\n workspace_id as \"workspace_id!: Uuid\",\n repo_id as \"repo_id!: Uuid\",\n merge_type as \"merge_type!: MergeType\",\n merge_commit,\n pr_number,\n pr_url,\n pr_status as \"pr_status?: MergeStatus\",\n pr_merged_at as \"pr_merged_at?: DateTime<Utc>\",\n pr_merge_commit_sha,\n created_at as \"created_at!: DateTime<Utc>\",\n target_branch_name as \"target_branch_name!: String\"\n FROM merges\n WHERE merge_type = 'pr' AND pr_status = 'open'\n ORDER BY created_at DESC",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "workspace_id!: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "repo_id!: Uuid",
"ordinal": 2,
"type_info": "Blob"
},
{
"name": "merge_type!: MergeType",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "merge_commit",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "pr_number",
"ordinal": 5,
"type_info": "Integer"
},
{
"name": "pr_url",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "pr_status?: MergeStatus",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "pr_merged_at?: DateTime<Utc>",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "pr_merge_commit_sha",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "target_branch_name!: String",
"ordinal": 11,
"type_info": "Text"
}
],
"parameters": {
"Right": 0
},
"nullable": [
true,
false,
false,
false,
true,
true,
true,
true,
true,
true,
false,
false
]
},
"hash": "faae305f6ac9dc7d04d21c76531cde3912647430195267ffa5b99bb9a7df1feb"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "db"
version = "0.1.44"
version = "0.1.36"
edition = "2024"
[dependencies]

View File

@@ -1,27 +0,0 @@
CREATE TABLE pull_requests (
id TEXT PRIMARY KEY NOT NULL,
workspace_id BLOB,
repo_id BLOB,
pr_url TEXT NOT NULL UNIQUE,
pr_number INTEGER NOT NULL,
pr_status TEXT NOT NULL DEFAULT 'open',
target_branch_name TEXT NOT NULL,
merged_at TEXT,
merge_commit_sha TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
synced_at DATETIME
);
-- Migrate workspace PR data from merges into pull_requests
INSERT OR IGNORE INTO pull_requests (id, workspace_id, repo_id, pr_url, pr_number, pr_status, target_branch_name, merged_at, merge_commit_sha, created_at, updated_at, synced_at)
SELECT hex(id), workspace_id, repo_id, pr_url, pr_number, COALESCE(pr_status, 'open'), target_branch_name, pr_merged_at, pr_merge_commit_sha, created_at, created_at, created_at
FROM merges WHERE merge_type = 'pr' AND pr_url IS NOT NULL;
-- Remove PR rows from merges (now in pull_requests)
DELETE FROM merges WHERE merge_type = 'pr';
CREATE INDEX idx_pull_requests_status ON pull_requests(pr_status);
CREATE INDEX idx_pull_requests_workspace_id ON pull_requests(workspace_id);
PRAGMA optimize;

View File

@@ -81,6 +81,32 @@ impl CodingAgentTurn {
.await
}
pub async fn find_by_agent_session_id(
pool: &SqlitePool,
agent_session_id: &str,
) -> Result<Option<Self>, sqlx::Error> {
sqlx::query_as!(
CodingAgentTurn,
r#"SELECT
id as "id!: Uuid",
execution_process_id as "execution_process_id!: Uuid",
agent_session_id,
agent_message_id,
prompt,
summary,
seen as "seen!: bool",
created_at as "created_at!: DateTime<Utc>",
updated_at as "updated_at!: DateTime<Utc>"
FROM coding_agent_turns
WHERE agent_session_id = ?
ORDER BY updated_at DESC
LIMIT 1"#,
agent_session_id
)
.fetch_optional(pool)
.await
}
/// Create a new coding agent turn
pub async fn create(
pool: &SqlitePool,
@@ -233,6 +259,25 @@ impl CodingAgentTurn {
}
/// Check if a workspace has any unseen coding agent turns
pub async fn has_unseen_by_workspace_id(
pool: &SqlitePool,
workspace_id: Uuid,
) -> Result<bool, sqlx::Error> {
let result = sqlx::query_scalar!(
r#"SELECT EXISTS(
SELECT 1 FROM coding_agent_turns cat
JOIN execution_processes ep ON cat.execution_process_id = ep.id
JOIN sessions s ON ep.session_id = s.id
WHERE s.workspace_id = $1 AND cat.seen = 0
) as "has_unseen!: bool""#,
workspace_id
)
.fetch_one(pool)
.await?;
Ok(result)
}
/// Find all workspaces that have unseen coding agent turns, filtered by archived status
pub async fn find_workspaces_with_unseen(
pool: &SqlitePool,

View File

@@ -84,6 +84,14 @@ pub struct CreateExecutionProcess {
pub run_reason: ExecutionProcessRunReason,
}
#[derive(Debug, Deserialize, TS)]
#[allow(dead_code)]
pub struct UpdateExecutionProcess {
pub status: Option<ExecutionProcessStatus>,
pub exit_code: Option<i64>,
pub completed_at: Option<DateTime<Utc>>,
}
#[derive(Debug)]
pub struct ExecutionContext {
pub execution_process: ExecutionProcess,
@@ -342,6 +350,35 @@ impl ExecutionProcess {
}
/// Find latest execution process by session and run reason
pub async fn find_latest_by_session_and_run_reason(
pool: &SqlitePool,
session_id: Uuid,
run_reason: &ExecutionProcessRunReason,
) -> Result<Option<Self>, sqlx::Error> {
sqlx::query_as!(
ExecutionProcess,
r#"SELECT
ep.id as "id!: Uuid",
ep.session_id as "session_id!: Uuid",
ep.run_reason as "run_reason!: ExecutionProcessRunReason",
ep.executor_action as "executor_action!: sqlx::types::Json<ExecutorActionField>",
ep.status as "status!: ExecutionProcessStatus",
ep.exit_code,
ep.dropped as "dropped!: bool",
ep.started_at as "started_at!: DateTime<Utc>",
ep.completed_at as "completed_at?: DateTime<Utc>",
ep.created_at as "created_at!: DateTime<Utc>",
ep.updated_at as "updated_at!: DateTime<Utc>"
FROM execution_processes ep
WHERE ep.session_id = ? AND ep.run_reason = ? AND ep.dropped = FALSE
ORDER BY ep.created_at DESC LIMIT 1"#,
session_id,
run_reason
)
.fetch_optional(pool)
.await
}
/// Find latest execution process by workspace and run reason (across all sessions)
pub async fn find_latest_by_workspace_and_run_reason(
pool: &SqlitePool,
@@ -510,6 +547,11 @@ impl ExecutionProcess {
Ok(result.flatten())
}
/// Get the parent Session for this execution process
pub async fn parent_session(&self, pool: &SqlitePool) -> Result<Option<Session>, sqlx::Error> {
Session::find_by_id(pool, self.session_id).await
}
/// Get both the parent Workspace and Session for this execution process
pub async fn parent_workspace_and_session(
&self,

View File

@@ -126,4 +126,24 @@ impl ExecutionProcessLogs {
}
Ok(messages)
}
/// Append a JSONL line to the logs for an execution process
pub async fn append_log_line(
pool: &SqlitePool,
execution_id: Uuid,
jsonl_line: &str,
) -> Result<(), sqlx::Error> {
let byte_size = jsonl_line.len() as i64;
sqlx::query!(
r#"INSERT INTO execution_process_logs (execution_id, logs, byte_size, inserted_at)
VALUES ($1, $2, $3, datetime('now', 'subsec'))"#,
execution_id,
jsonl_line,
byte_size
)
.execute(pool)
.await?;
Ok(())
}
}

View File

@@ -111,6 +111,28 @@ impl ExecutionProcessRepoState {
Ok(())
}
pub async fn set_merge_commit(
pool: &SqlitePool,
execution_process_id: Uuid,
repo_id: Uuid,
merge_commit: &str,
) -> Result<(), sqlx::Error> {
let now = Utc::now();
sqlx::query!(
r#"UPDATE execution_process_repo_states
SET merge_commit = $1, updated_at = $2
WHERE execution_process_id = $3
AND repo_id = $4"#,
merge_commit,
now,
execution_process_id,
repo_id
)
.execute(pool)
.await?;
Ok(())
}
pub async fn find_by_execution_process_id(
pool: &SqlitePool,
execution_process_id: Uuid,

View File

@@ -1,11 +1,11 @@
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{SqlitePool, Type};
use sqlx::{FromRow, SqlitePool, Type};
use ts_rs::TS;
use uuid::Uuid;
use super::pull_request::PullRequest;
#[derive(Debug, Clone, Serialize, Deserialize, TS, Type)]
#[sqlx(type_name = "merge_status", rename_all = "snake_case")]
#[serde(rename_all = "snake_case")]
@@ -53,13 +53,26 @@ pub struct PullRequestInfo {
pub merge_commit_sha: Option<String>,
}
/// Row type for direct merges only (PR data now lives in pull_requests).
struct DirectMergeRow {
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
#[sqlx(type_name = "TEXT", rename_all = "snake_case")]
pub enum MergeType {
Direct,
Pr,
}
#[derive(FromRow)]
struct MergeRow {
id: Uuid,
workspace_id: Uuid,
repo_id: Uuid,
merge_type: MergeType,
merge_commit: Option<String>,
target_branch_name: String,
pr_number: Option<i64>,
pr_url: Option<String>,
pr_status: Option<MergeStatus>,
pr_merged_at: Option<DateTime<Utc>>,
pr_merge_commit_sha: Option<String>,
created_at: DateTime<Utc>,
}
@@ -82,71 +95,210 @@ impl Merge {
let id = Uuid::new_v4();
let now = Utc::now();
sqlx::query!(
"INSERT INTO merges (id, workspace_id, repo_id, merge_type, merge_commit, created_at, target_branch_name)
VALUES (?, ?, ?, 'direct', ?, ?, ?)",
sqlx::query_as!(
MergeRow,
r#"INSERT INTO merges (
id, workspace_id, repo_id, merge_type, merge_commit, created_at, target_branch_name
) VALUES ($1, $2, $3, 'direct', $4, $5, $6)
RETURNING
id as "id!: Uuid",
workspace_id as "workspace_id!: Uuid",
repo_id as "repo_id!: Uuid",
merge_type as "merge_type!: MergeType",
merge_commit,
pr_number,
pr_url,
pr_status as "pr_status?: MergeStatus",
pr_merged_at as "pr_merged_at?: DateTime<Utc>",
pr_merge_commit_sha,
created_at as "created_at!: DateTime<Utc>",
target_branch_name as "target_branch_name!: String"
"#,
id,
workspace_id,
repo_id,
merge_commit,
now,
target_branch_name,
target_branch_name
)
.execute(pool)
.await?;
.fetch_one(pool)
.await
.map(Into::into)
}
/// Create a new PR record (when PR is opened)
pub async fn create_pr(
pool: &SqlitePool,
workspace_id: Uuid,
repo_id: Uuid,
target_branch_name: &str,
pr_number: i64,
pr_url: &str,
) -> Result<PrMerge, sqlx::Error> {
let id = Uuid::new_v4();
let now = Utc::now();
Ok(DirectMerge {
sqlx::query_as!(
MergeRow,
r#"INSERT INTO merges (
id, workspace_id, repo_id, merge_type, pr_number, pr_url, pr_status, created_at, target_branch_name
) VALUES ($1, $2, $3, 'pr', $4, $5, 'open', $6, $7)
RETURNING
id as "id!: Uuid",
workspace_id as "workspace_id!: Uuid",
repo_id as "repo_id!: Uuid",
merge_type as "merge_type!: MergeType",
merge_commit,
pr_number,
pr_url,
pr_status as "pr_status?: MergeStatus",
pr_merged_at as "pr_merged_at?: DateTime<Utc>",
pr_merge_commit_sha,
created_at as "created_at!: DateTime<Utc>",
target_branch_name as "target_branch_name!: String"
"#,
id,
workspace_id,
repo_id,
merge_commit: merge_commit.to_string(),
target_branch_name: target_branch_name.to_string(),
created_at: now,
})
pr_number,
pr_url,
now,
target_branch_name
)
.fetch_one(pool)
.await
.map(Into::into)
}
/// Find all merges for a workspace (returns both direct merges and PRs).
/// Direct merges come from the `merges` table, PRs from `pull_requests`.
pub async fn find_by_workspace_id(
pool: &SqlitePool,
workspace_id: Uuid,
) -> Result<Vec<Self>, sqlx::Error> {
let direct_rows = sqlx::query_as!(
DirectMergeRow,
pub async fn find_all_pr(pool: &SqlitePool) -> Result<Vec<PrMerge>, sqlx::Error> {
let rows = sqlx::query_as!(
MergeRow,
r#"SELECT
id AS "id!: Uuid",
workspace_id AS "workspace_id!: Uuid",
repo_id AS "repo_id!: Uuid",
id as "id!: Uuid",
workspace_id as "workspace_id!: Uuid",
repo_id as "repo_id!: Uuid",
merge_type as "merge_type!: MergeType",
merge_commit,
target_branch_name,
created_at AS "created_at!: DateTime<Utc>"
FROM merges
WHERE workspace_id = ? AND merge_type = 'direct'
ORDER BY created_at DESC"#,
workspace_id,
pr_number,
pr_url,
pr_status as "pr_status?: MergeStatus",
pr_merged_at as "pr_merged_at?: DateTime<Utc>",
pr_merge_commit_sha,
created_at as "created_at!: DateTime<Utc>",
target_branch_name as "target_branch_name!: String"
FROM merges
WHERE merge_type = 'pr'
ORDER BY created_at ASC"#,
)
.fetch_all(pool)
.await?;
let pull_requests = PullRequest::find_by_workspace_id(pool, workspace_id).await?;
Ok(rows.into_iter().map(Into::into).collect())
}
let mut merges: Vec<Merge> = direct_rows.into_iter().map(|row| row.into()).collect();
merges.extend(pull_requests.iter().map(|pr| pr.to_merge()));
pub async fn get_open_prs(pool: &SqlitePool) -> Result<Vec<PrMerge>, sqlx::Error> {
let rows = sqlx::query_as!(
MergeRow,
r#"SELECT
id as "id!: Uuid",
workspace_id as "workspace_id!: Uuid",
repo_id as "repo_id!: Uuid",
merge_type as "merge_type!: MergeType",
merge_commit,
pr_number,
pr_url,
pr_status as "pr_status?: MergeStatus",
pr_merged_at as "pr_merged_at?: DateTime<Utc>",
pr_merge_commit_sha,
created_at as "created_at!: DateTime<Utc>",
target_branch_name as "target_branch_name!: String"
FROM merges
WHERE merge_type = 'pr' AND pr_status = 'open'
ORDER BY created_at DESC"#,
)
.fetch_all(pool)
.await?;
// Sort by created_at descending (matching previous behavior)
merges.sort_by(|a, b| {
let a_time = match a {
Merge::Direct(d) => d.created_at,
Merge::Pr(p) => p.created_at,
};
let b_time = match b {
Merge::Direct(d) => d.created_at,
Merge::Pr(p) => p.created_at,
};
b_time.cmp(&a_time)
});
Ok(rows.into_iter().map(Into::into).collect())
}
Ok(merges)
pub async fn count_open_prs_for_workspace(
pool: &SqlitePool,
workspace_id: Uuid,
) -> Result<i64, sqlx::Error> {
let count = sqlx::query_scalar!(
r#"SELECT COUNT(1) as "count!: i64"
FROM merges
WHERE workspace_id = $1
AND merge_type = 'pr'
AND pr_status = 'open'"#,
workspace_id
)
.fetch_one(pool)
.await?;
Ok(count)
}
/// Update PR status for a workspace
pub async fn update_status(
pool: &SqlitePool,
merge_id: Uuid,
pr_status: MergeStatus,
merge_commit_sha: Option<String>,
) -> Result<(), sqlx::Error> {
let merged_at = if matches!(pr_status, MergeStatus::Merged) {
Some(Utc::now())
} else {
None
};
sqlx::query!(
r#"UPDATE merges
SET pr_status = $1,
pr_merge_commit_sha = $2,
pr_merged_at = $3
WHERE id = $4"#,
pr_status,
merge_commit_sha,
merged_at,
merge_id
)
.execute(pool)
.await?;
Ok(())
}
/// Find all merges for a workspace (returns both direct and PR merges)
pub async fn find_by_workspace_id(
pool: &SqlitePool,
workspace_id: Uuid,
) -> Result<Vec<Self>, sqlx::Error> {
// Get raw data from database
let rows = sqlx::query_as!(
MergeRow,
r#"SELECT
id as "id!: Uuid",
workspace_id as "workspace_id!: Uuid",
repo_id as "repo_id!: Uuid",
merge_type as "merge_type!: MergeType",
merge_commit,
pr_number,
pr_url,
pr_status as "pr_status?: MergeStatus",
pr_merged_at as "pr_merged_at?: DateTime<Utc>",
pr_merge_commit_sha,
target_branch_name as "target_branch_name!: String",
created_at as "created_at!: DateTime<Utc>"
FROM merges
WHERE workspace_id = $1
ORDER BY created_at DESC"#,
workspace_id
)
.fetch_all(pool)
.await?;
// Convert to appropriate types based on merge_type
Ok(rows.into_iter().map(Into::into).collect())
}
/// Find all merges for a workspace and specific repo
@@ -155,48 +307,84 @@ impl Merge {
workspace_id: Uuid,
repo_id: Uuid,
) -> Result<Vec<Self>, sqlx::Error> {
let direct_rows = sqlx::query_as!(
DirectMergeRow,
let rows = sqlx::query_as!(
MergeRow,
r#"SELECT
id AS "id!: Uuid",
workspace_id AS "workspace_id!: Uuid",
repo_id AS "repo_id!: Uuid",
id as "id!: Uuid",
workspace_id as "workspace_id!: Uuid",
repo_id as "repo_id!: Uuid",
merge_type as "merge_type!: MergeType",
merge_commit,
target_branch_name,
created_at AS "created_at!: DateTime<Utc>"
pr_number,
pr_url,
pr_status as "pr_status?: MergeStatus",
pr_merged_at as "pr_merged_at?: DateTime<Utc>",
pr_merge_commit_sha,
target_branch_name as "target_branch_name!: String",
created_at as "created_at!: DateTime<Utc>"
FROM merges
WHERE workspace_id = ? AND repo_id = ? AND merge_type = 'direct'
WHERE workspace_id = $1 AND repo_id = $2
ORDER BY created_at DESC"#,
workspace_id,
repo_id,
repo_id
)
.fetch_all(pool)
.await?;
let pull_requests =
PullRequest::find_by_workspace_and_repo_id(pool, workspace_id, repo_id).await?;
Ok(rows.into_iter().map(Into::into).collect())
}
let mut merges: Vec<Merge> = direct_rows.into_iter().map(|row| row.into()).collect();
merges.extend(pull_requests.iter().map(|pr| pr.to_merge()));
/// Get the latest PR for each workspace (for workspace summaries)
/// Returns a map of workspace_id -> PrMerge for workspaces that have PRs
pub async fn get_latest_pr_status_for_workspaces(
pool: &SqlitePool,
archived: bool,
) -> Result<HashMap<Uuid, PrMerge>, sqlx::Error> {
// Get the latest PR for each workspace by using a subquery to find the max created_at
// Only consider PR merges (not direct merges)
let rows = sqlx::query_as!(
MergeRow,
r#"SELECT
m.id as "id!: Uuid",
m.workspace_id as "workspace_id!: Uuid",
m.repo_id as "repo_id!: Uuid",
m.merge_type as "merge_type!: MergeType",
m.merge_commit,
m.pr_number,
m.pr_url,
m.pr_status as "pr_status?: MergeStatus",
m.pr_merged_at as "pr_merged_at?: DateTime<Utc>",
m.pr_merge_commit_sha,
m.target_branch_name as "target_branch_name!: String",
m.created_at as "created_at!: DateTime<Utc>"
FROM merges m
INNER JOIN (
SELECT workspace_id, MAX(created_at) as max_created_at
FROM merges
WHERE merge_type = 'pr'
GROUP BY workspace_id
) latest ON m.workspace_id = latest.workspace_id
AND m.created_at = latest.max_created_at
INNER JOIN workspaces w ON m.workspace_id = w.id
WHERE m.merge_type = 'pr' AND w.archived = $1"#,
archived
)
.fetch_all(pool)
.await?;
merges.sort_by(|a, b| {
let a_time = match a {
Merge::Direct(d) => d.created_at,
Merge::Pr(p) => p.created_at,
};
let b_time = match b {
Merge::Direct(d) => d.created_at,
Merge::Pr(p) => p.created_at,
};
b_time.cmp(&a_time)
});
Ok(merges)
Ok(rows
.into_iter()
.map(|row| {
let workspace_id = row.workspace_id;
(workspace_id, PrMerge::from(row))
})
.collect())
}
}
impl From<DirectMergeRow> for DirectMerge {
fn from(row: DirectMergeRow) -> Self {
// Conversion implementations
impl From<MergeRow> for DirectMerge {
fn from(row: MergeRow) -> Self {
DirectMerge {
id: row.id,
workspace_id: row.workspace_id,
@@ -210,8 +398,30 @@ impl From<DirectMergeRow> for DirectMerge {
}
}
impl From<DirectMergeRow> for Merge {
fn from(row: DirectMergeRow) -> Self {
Merge::Direct(DirectMerge::from(row))
impl From<MergeRow> for PrMerge {
fn from(row: MergeRow) -> Self {
PrMerge {
id: row.id,
workspace_id: row.workspace_id,
repo_id: row.repo_id,
target_branch_name: row.target_branch_name,
pr_info: PullRequestInfo {
number: row.pr_number.expect("pr merge must have pr_number"),
url: row.pr_url.expect("pr merge must have pr_url"),
status: row.pr_status.expect("pr merge must have status"),
merged_at: row.pr_merged_at,
merge_commit_sha: row.pr_merge_commit_sha,
},
created_at: row.created_at,
}
}
}
impl From<MergeRow> for Merge {
fn from(row: MergeRow) -> Self {
match row.merge_type {
MergeType::Direct => Merge::Direct(DirectMerge::from(row)),
MergeType::Pr => Merge::Pr(PrMerge::from(row)),
}
}
}

View File

@@ -0,0 +1,392 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{Executor, FromRow, Sqlite, SqlitePool, Type};
use strum_macros::{Display, EnumString};
use thiserror::Error;
use uuid::Uuid;
#[derive(Debug, Error)]
pub enum MigrationStateError {
#[error(transparent)]
Database(#[from] sqlx::Error),
}
#[derive(Debug, Clone, Type, Serialize, Deserialize, PartialEq, EnumString, Display)]
#[sqlx(type_name = "text", rename_all = "lowercase")]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum EntityType {
Project,
Task,
PrMerge,
Workspace,
}
#[derive(Debug, Clone, Type, Serialize, Deserialize, PartialEq, EnumString, Display, Default)]
#[sqlx(type_name = "text", rename_all = "lowercase")]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum MigrationStatus {
#[default]
Pending,
Migrated,
Failed,
Skipped,
}
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
pub struct MigrationState {
pub id: Uuid,
pub entity_type: EntityType,
pub local_id: Uuid,
pub remote_id: Option<Uuid>,
pub status: MigrationStatus,
pub error_message: Option<String>,
pub attempt_count: i64,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateMigrationState {
pub entity_type: EntityType,
pub local_id: Uuid,
}
impl MigrationState {
pub async fn find_all(pool: &SqlitePool) -> Result<Vec<Self>, MigrationStateError> {
let records = sqlx::query_as!(
MigrationState,
r#"SELECT
id as "id!: Uuid",
entity_type as "entity_type!: EntityType",
local_id as "local_id!: Uuid",
remote_id as "remote_id: Uuid",
status as "status!: MigrationStatus",
error_message,
attempt_count as "attempt_count!",
created_at as "created_at!: DateTime<Utc>",
updated_at as "updated_at!: DateTime<Utc>"
FROM migration_state
ORDER BY created_at ASC"#
)
.fetch_all(pool)
.await?;
Ok(records)
}
pub async fn find_by_entity_type(
pool: &SqlitePool,
entity_type: EntityType,
) -> Result<Vec<Self>, MigrationStateError> {
let entity_type_str = entity_type.to_string();
let records = sqlx::query_as!(
MigrationState,
r#"SELECT
id as "id!: Uuid",
entity_type as "entity_type!: EntityType",
local_id as "local_id!: Uuid",
remote_id as "remote_id: Uuid",
status as "status!: MigrationStatus",
error_message,
attempt_count as "attempt_count!",
created_at as "created_at!: DateTime<Utc>",
updated_at as "updated_at!: DateTime<Utc>"
FROM migration_state
WHERE entity_type = $1
ORDER BY created_at ASC"#,
entity_type_str
)
.fetch_all(pool)
.await?;
Ok(records)
}
pub async fn find_by_status(
pool: &SqlitePool,
status: MigrationStatus,
) -> Result<Vec<Self>, MigrationStateError> {
let status_str = status.to_string();
let records = sqlx::query_as!(
MigrationState,
r#"SELECT
id as "id!: Uuid",
entity_type as "entity_type!: EntityType",
local_id as "local_id!: Uuid",
remote_id as "remote_id: Uuid",
status as "status!: MigrationStatus",
error_message,
attempt_count as "attempt_count!",
created_at as "created_at!: DateTime<Utc>",
updated_at as "updated_at!: DateTime<Utc>"
FROM migration_state
WHERE status = $1
ORDER BY created_at ASC"#,
status_str
)
.fetch_all(pool)
.await?;
Ok(records)
}
pub async fn find_pending_by_type(
pool: &SqlitePool,
entity_type: EntityType,
) -> Result<Vec<Self>, MigrationStateError> {
let entity_type_str = entity_type.to_string();
let records = sqlx::query_as!(
MigrationState,
r#"SELECT
id as "id!: Uuid",
entity_type as "entity_type!: EntityType",
local_id as "local_id!: Uuid",
remote_id as "remote_id: Uuid",
status as "status!: MigrationStatus",
error_message,
attempt_count as "attempt_count!",
created_at as "created_at!: DateTime<Utc>",
updated_at as "updated_at!: DateTime<Utc>"
FROM migration_state
WHERE entity_type = $1 AND status = 'pending'
ORDER BY created_at ASC"#,
entity_type_str
)
.fetch_all(pool)
.await?;
Ok(records)
}
pub async fn find_by_entity(
pool: &SqlitePool,
entity_type: EntityType,
local_id: Uuid,
) -> Result<Option<Self>, MigrationStateError> {
let entity_type_str = entity_type.to_string();
let record = sqlx::query_as!(
MigrationState,
r#"SELECT
id as "id!: Uuid",
entity_type as "entity_type!: EntityType",
local_id as "local_id!: Uuid",
remote_id as "remote_id: Uuid",
status as "status!: MigrationStatus",
error_message,
attempt_count as "attempt_count!",
created_at as "created_at!: DateTime<Utc>",
updated_at as "updated_at!: DateTime<Utc>"
FROM migration_state
WHERE entity_type = $1 AND local_id = $2"#,
entity_type_str,
local_id
)
.fetch_optional(pool)
.await?;
Ok(record)
}
pub async fn get_remote_id(
pool: &SqlitePool,
entity_type: EntityType,
local_id: Uuid,
) -> Result<Option<Uuid>, MigrationStateError> {
let entity_type_str = entity_type.to_string();
let record = sqlx::query_scalar!(
r#"SELECT remote_id as "remote_id: Uuid"
FROM migration_state
WHERE entity_type = $1 AND local_id = $2 AND status = 'migrated'"#,
entity_type_str,
local_id
)
.fetch_optional(pool)
.await?;
Ok(record.flatten())
}
pub async fn create<'e, E>(
executor: E,
data: &CreateMigrationState,
) -> Result<Self, MigrationStateError>
where
E: Executor<'e, Database = Sqlite>,
{
let id = Uuid::new_v4();
let entity_type_str = data.entity_type.to_string();
let record = sqlx::query_as!(
MigrationState,
r#"INSERT INTO migration_state (id, entity_type, local_id)
VALUES ($1, $2, $3)
RETURNING
id as "id!: Uuid",
entity_type as "entity_type!: EntityType",
local_id as "local_id!: Uuid",
remote_id as "remote_id: Uuid",
status as "status!: MigrationStatus",
error_message,
attempt_count as "attempt_count!",
created_at as "created_at!: DateTime<Utc>",
updated_at as "updated_at!: DateTime<Utc>""#,
id,
entity_type_str,
data.local_id
)
.fetch_one(executor)
.await?;
Ok(record)
}
pub async fn upsert<'e, E>(
executor: E,
data: &CreateMigrationState,
) -> Result<Self, MigrationStateError>
where
E: Executor<'e, Database = Sqlite>,
{
let id = Uuid::new_v4();
let entity_type_str = data.entity_type.to_string();
let record = sqlx::query_as!(
MigrationState,
r#"INSERT INTO migration_state (id, entity_type, local_id)
VALUES ($1, $2, $3)
ON CONFLICT (entity_type, local_id) DO UPDATE SET
updated_at = datetime('now', 'subsec')
RETURNING
id as "id!: Uuid",
entity_type as "entity_type!: EntityType",
local_id as "local_id!: Uuid",
remote_id as "remote_id: Uuid",
status as "status!: MigrationStatus",
error_message,
attempt_count as "attempt_count!",
created_at as "created_at!: DateTime<Utc>",
updated_at as "updated_at!: DateTime<Utc>""#,
id,
entity_type_str,
data.local_id
)
.fetch_one(executor)
.await?;
Ok(record)
}
pub async fn mark_migrated<'e, E>(
executor: E,
entity_type: EntityType,
local_id: Uuid,
remote_id: Uuid,
) -> Result<(), MigrationStateError>
where
E: Executor<'e, Database = Sqlite>,
{
let entity_type_str = entity_type.to_string();
sqlx::query!(
r#"UPDATE migration_state
SET status = 'migrated',
remote_id = $3,
error_message = NULL,
updated_at = datetime('now', 'subsec')
WHERE entity_type = $1 AND local_id = $2"#,
entity_type_str,
local_id,
remote_id
)
.execute(executor)
.await?;
Ok(())
}
pub async fn mark_failed<'e, E>(
executor: E,
entity_type: EntityType,
local_id: Uuid,
error_message: &str,
) -> Result<(), MigrationStateError>
where
E: Executor<'e, Database = Sqlite>,
{
let entity_type_str = entity_type.to_string();
sqlx::query!(
r#"UPDATE migration_state
SET status = 'failed',
error_message = $3,
attempt_count = attempt_count + 1,
updated_at = datetime('now', 'subsec')
WHERE entity_type = $1 AND local_id = $2"#,
entity_type_str,
local_id,
error_message
)
.execute(executor)
.await?;
Ok(())
}
pub async fn mark_skipped<'e, E>(
executor: E,
entity_type: EntityType,
local_id: Uuid,
reason: &str,
) -> Result<(), MigrationStateError>
where
E: Executor<'e, Database = Sqlite>,
{
let entity_type_str = entity_type.to_string();
sqlx::query!(
r#"UPDATE migration_state
SET status = 'skipped',
error_message = $3,
updated_at = datetime('now', 'subsec')
WHERE entity_type = $1 AND local_id = $2"#,
entity_type_str,
local_id,
reason
)
.execute(executor)
.await?;
Ok(())
}
pub async fn reset_failed(pool: &SqlitePool) -> Result<u64, MigrationStateError> {
let result = sqlx::query!(
r#"UPDATE migration_state
SET status = 'pending',
error_message = NULL,
updated_at = datetime('now', 'subsec')
WHERE status = 'failed'"#
)
.execute(pool)
.await?;
Ok(result.rows_affected())
}
pub async fn get_stats(pool: &SqlitePool) -> Result<MigrationStats, MigrationStateError> {
let stats = sqlx::query_as!(
MigrationStats,
r#"SELECT
COALESCE(SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END), 0) as "pending!: i64",
COALESCE(SUM(CASE WHEN status = 'migrated' THEN 1 ELSE 0 END), 0) as "migrated!: i64",
COALESCE(SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END), 0) as "failed!: i64",
COALESCE(SUM(CASE WHEN status = 'skipped' THEN 1 ELSE 0 END), 0) as "skipped!: i64",
COUNT(*) as "total!: i64"
FROM migration_state"#
)
.fetch_one(pool)
.await?;
Ok(stats)
}
pub async fn clear_all(pool: &SqlitePool) -> Result<u64, MigrationStateError> {
let result = sqlx::query!("DELETE FROM migration_state")
.execute(pool)
.await?;
Ok(result.rows_affected())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct MigrationStats {
pub pending: i64,
pub migrated: i64,
pub failed: i64,
pub skipped: i64,
pub total: i64,
}

View File

@@ -4,8 +4,8 @@ pub mod execution_process_logs;
pub mod execution_process_repo_state;
pub mod file;
pub mod merge;
pub mod migration_state;
pub mod project;
pub mod pull_request;
pub mod repo;
pub mod requests;
pub mod scratch;

View File

@@ -1,354 +0,0 @@
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use sqlx::{FromRow, SqlitePool};
use uuid::Uuid;
use super::merge::{Merge, MergeStatus, PrMerge, PullRequestInfo};
#[derive(Debug, Clone, FromRow)]
pub struct PullRequest {
pub id: String,
pub workspace_id: Option<Uuid>,
pub repo_id: Option<Uuid>,
pub pr_url: String,
pub pr_number: i64,
pub pr_status: MergeStatus,
pub target_branch_name: String,
pub merged_at: Option<DateTime<Utc>>,
pub merge_commit_sha: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub synced_at: Option<DateTime<Utc>>,
}
impl PullRequest {
pub async fn create(
pool: &SqlitePool,
workspace_id: Option<Uuid>,
repo_id: Option<Uuid>,
pr_url: &str,
pr_number: i64,
target_branch_name: &str,
) -> Result<PullRequest, sqlx::Error> {
let id = Uuid::new_v4().to_string();
let now = Utc::now();
sqlx::query!(
"INSERT INTO pull_requests (id, workspace_id, repo_id, pr_url, pr_number, pr_status, target_branch_name, created_at)
VALUES (?, ?, ?, ?, ?, 'open', ?, ?)
ON CONFLICT(pr_url) DO UPDATE SET
workspace_id = COALESCE(pull_requests.workspace_id, excluded.workspace_id),
repo_id = COALESCE(pull_requests.repo_id, excluded.repo_id),
updated_at = CURRENT_TIMESTAMP",
id,
workspace_id,
repo_id,
pr_url,
pr_number,
target_branch_name,
now,
)
.execute(pool)
.await?;
let pr = Self::find_by_url(pool, pr_url)
.await?
.ok_or(sqlx::Error::RowNotFound)?;
Ok(pr)
}
pub async fn create_for_workspace(
pool: &SqlitePool,
workspace_id: Uuid,
repo_id: Uuid,
target_branch_name: &str,
pr_number: i64,
pr_url: &str,
) -> Result<PullRequest, sqlx::Error> {
Self::create(
pool,
Some(workspace_id),
Some(repo_id),
pr_url,
pr_number,
target_branch_name,
)
.await
}
pub async fn get_open(pool: &SqlitePool) -> Result<Vec<PullRequest>, sqlx::Error> {
sqlx::query_as!(
PullRequest,
r#"SELECT
id,
workspace_id AS "workspace_id: Uuid",
repo_id AS "repo_id: Uuid",
pr_url,
pr_number,
pr_status AS "pr_status: MergeStatus",
target_branch_name,
merged_at AS "merged_at: DateTime<Utc>",
merge_commit_sha,
created_at AS "created_at!: DateTime<Utc>",
updated_at AS "updated_at!: DateTime<Utc>",
synced_at AS "synced_at: DateTime<Utc>"
FROM pull_requests
WHERE pr_status = 'open'"#,
)
.fetch_all(pool)
.await
}
pub async fn update_status(
pool: &SqlitePool,
pr_url: &str,
status: &MergeStatus,
merged_at: Option<DateTime<Utc>>,
merge_commit_sha: Option<String>,
) -> Result<(), sqlx::Error> {
let status_str = match status {
MergeStatus::Open => "open",
MergeStatus::Merged => "merged",
MergeStatus::Closed => "closed",
MergeStatus::Unknown => "unknown",
};
let now = Utc::now();
sqlx::query!(
"UPDATE pull_requests SET pr_status = ?, merged_at = ?, merge_commit_sha = ?, updated_at = ?, synced_at = NULL WHERE pr_url = ?",
status_str,
merged_at,
merge_commit_sha,
now,
pr_url,
)
.execute(pool)
.await?;
Ok(())
}
pub async fn find_by_url(
pool: &SqlitePool,
pr_url: &str,
) -> Result<Option<PullRequest>, sqlx::Error> {
sqlx::query_as!(
PullRequest,
r#"SELECT
id,
workspace_id AS "workspace_id: Uuid",
repo_id AS "repo_id: Uuid",
pr_url,
pr_number,
pr_status AS "pr_status: MergeStatus",
target_branch_name,
merged_at AS "merged_at: DateTime<Utc>",
merge_commit_sha,
created_at AS "created_at!: DateTime<Utc>",
updated_at AS "updated_at!: DateTime<Utc>",
synced_at AS "synced_at: DateTime<Utc>"
FROM pull_requests
WHERE pr_url = $1"#,
pr_url,
)
.fetch_optional(pool)
.await
}
pub async fn find_by_workspace_id(
pool: &SqlitePool,
workspace_id: Uuid,
) -> Result<Vec<PullRequest>, sqlx::Error> {
sqlx::query_as!(
PullRequest,
r#"SELECT
id,
workspace_id AS "workspace_id: Uuid",
repo_id AS "repo_id: Uuid",
pr_url,
pr_number,
pr_status AS "pr_status: MergeStatus",
target_branch_name,
merged_at AS "merged_at: DateTime<Utc>",
merge_commit_sha,
created_at AS "created_at!: DateTime<Utc>",
updated_at AS "updated_at!: DateTime<Utc>",
synced_at AS "synced_at: DateTime<Utc>"
FROM pull_requests
WHERE workspace_id = $1
ORDER BY created_at DESC"#,
workspace_id,
)
.fetch_all(pool)
.await
}
pub async fn find_by_workspace_and_repo_id(
pool: &SqlitePool,
workspace_id: Uuid,
repo_id: Uuid,
) -> Result<Vec<PullRequest>, sqlx::Error> {
sqlx::query_as!(
PullRequest,
r#"SELECT
id,
workspace_id AS "workspace_id: Uuid",
repo_id AS "repo_id: Uuid",
pr_url,
pr_number,
pr_status AS "pr_status: MergeStatus",
target_branch_name,
merged_at AS "merged_at: DateTime<Utc>",
merge_commit_sha,
created_at AS "created_at!: DateTime<Utc>",
updated_at AS "updated_at!: DateTime<Utc>",
synced_at AS "synced_at: DateTime<Utc>"
FROM pull_requests
WHERE workspace_id = $1 AND repo_id = $2
ORDER BY created_at DESC"#,
workspace_id,
repo_id,
)
.fetch_all(pool)
.await
}
pub async fn count_open_for_workspace(
pool: &SqlitePool,
workspace_id: Uuid,
) -> Result<i64, sqlx::Error> {
let row = sqlx::query!(
r#"SELECT COUNT(1) AS "count!: i64" FROM pull_requests WHERE workspace_id = ? AND pr_status = 'open'"#,
workspace_id,
)
.fetch_one(pool)
.await?;
Ok(row.count)
}
pub async fn get_latest_for_workspaces(
pool: &SqlitePool,
archived: bool,
) -> Result<HashMap<Uuid, PullRequest>, sqlx::Error> {
let rows = sqlx::query_as!(
PullRequest,
r#"SELECT
t.id,
t.workspace_id AS "workspace_id: Uuid",
t.repo_id AS "repo_id: Uuid",
t.pr_url,
t.pr_number,
t.pr_status AS "pr_status: MergeStatus",
t.target_branch_name,
t.merged_at AS "merged_at: DateTime<Utc>",
t.merge_commit_sha,
t.created_at AS "created_at!: DateTime<Utc>",
t.updated_at AS "updated_at!: DateTime<Utc>",
t.synced_at AS "synced_at: DateTime<Utc>"
FROM pull_requests t
INNER JOIN (
SELECT workspace_id, MAX(created_at) as max_created_at
FROM pull_requests
WHERE workspace_id IS NOT NULL
GROUP BY workspace_id
) latest ON t.workspace_id = latest.workspace_id AND t.created_at = latest.max_created_at
INNER JOIN workspaces w ON t.workspace_id = w.id
WHERE t.workspace_id IS NOT NULL AND w.archived = $1"#,
archived,
)
.fetch_all(pool)
.await?;
Ok(rows
.into_iter()
.filter_map(|pr| pr.workspace_id.map(|ws_id| (ws_id, pr)))
.collect())
}
pub async fn find_all_with_workspace(
pool: &SqlitePool,
) -> Result<Vec<PullRequest>, sqlx::Error> {
sqlx::query_as!(
PullRequest,
r#"SELECT
id,
workspace_id AS "workspace_id: Uuid",
repo_id AS "repo_id: Uuid",
pr_url,
pr_number,
pr_status AS "pr_status: MergeStatus",
target_branch_name,
merged_at AS "merged_at: DateTime<Utc>",
merge_commit_sha,
created_at AS "created_at!: DateTime<Utc>",
updated_at AS "updated_at!: DateTime<Utc>",
synced_at AS "synced_at: DateTime<Utc>"
FROM pull_requests
WHERE workspace_id IS NOT NULL
ORDER BY created_at ASC"#,
)
.fetch_all(pool)
.await
}
pub async fn get_pending_sync(pool: &SqlitePool) -> Result<Vec<PullRequest>, sqlx::Error> {
sqlx::query_as!(
PullRequest,
r#"SELECT
id,
workspace_id AS "workspace_id: Uuid",
repo_id AS "repo_id: Uuid",
pr_url,
pr_number,
pr_status AS "pr_status: MergeStatus",
target_branch_name,
merged_at AS "merged_at: DateTime<Utc>",
merge_commit_sha,
created_at AS "created_at!: DateTime<Utc>",
updated_at AS "updated_at!: DateTime<Utc>",
synced_at AS "synced_at: DateTime<Utc>"
FROM pull_requests
WHERE synced_at IS NULL OR synced_at < updated_at"#,
)
.fetch_all(pool)
.await
}
pub async fn mark_synced(pool: &SqlitePool, id: &str) -> Result<(), sqlx::Error> {
let now = Utc::now();
sqlx::query!(
"UPDATE pull_requests SET synced_at = ? WHERE id = ?",
now,
id,
)
.execute(pool)
.await?;
Ok(())
}
pub fn to_pr_merge(&self) -> PrMerge {
PrMerge {
id: Uuid::parse_str(&self.id).unwrap_or_else(|_| Uuid::nil()),
workspace_id: self.workspace_id.unwrap_or_else(Uuid::nil),
repo_id: self.repo_id.unwrap_or_else(Uuid::nil),
created_at: self.created_at,
target_branch_name: self.target_branch_name.clone(),
pr_info: PullRequestInfo {
number: self.pr_number,
url: self.pr_url.clone(),
status: self.pr_status.clone(),
merged_at: self.merged_at,
merge_commit_sha: self.merge_commit_sha.clone(),
},
}
}
pub async fn delete(pool: &SqlitePool, id: &str) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM pull_requests WHERE id = ?", id)
.execute(pool)
.await?;
Ok(())
}
pub fn to_merge(&self) -> Merge {
Merge::Pr(self.to_pr_merge())
}
}

View File

@@ -256,6 +256,17 @@ impl Repo {
.await
}
pub async fn delete_orphaned(pool: &SqlitePool) -> Result<u64, sqlx::Error> {
let result = sqlx::query!(
r#"DELETE FROM repos
WHERE id NOT IN (SELECT repo_id FROM project_repos)
AND id NOT IN (SELECT repo_id FROM workspace_repos)"#
)
.execute(pool)
.await?;
Ok(result.rows_affected())
}
pub async fn list_all(pool: &SqlitePool) -> Result<Vec<Self>, sqlx::Error> {
sqlx::query_as!(
Repo,

View File

@@ -144,12 +144,6 @@ pub struct UiPreferencesData {
/// Default setting for creating a draft workspace from new issues
#[serde(default)]
pub create_draft_workspace_by_default: Option<bool>,
/// Kanban project view selections (active view per project)
#[serde(default)]
pub kanban_project_view_selections: std::collections::HashMap<String, serde_json::Value>,
/// Kanban project view preferences (filters, toggles per project per view)
#[serde(default)]
pub kanban_project_view_preferences: std::collections::HashMap<String, serde_json::Value>,
}
/// Linked issue data for draft workspace scratch

View File

@@ -74,6 +74,13 @@ pub struct CreateFollowUpAttempt {
pub prompt: String,
}
/// Context data for resume operations (simplified)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttemptResumeContext {
pub execution_history: String,
pub cumulative_diffs: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceContext {
pub workspace: Workspace,
@@ -345,6 +352,25 @@ impl Workspace {
Ok(())
}
pub async fn resolve_container_ref(
pool: &SqlitePool,
container_ref: &str,
) -> Result<ContainerInfo, sqlx::Error> {
let result = sqlx::query!(
r#"SELECT w.id as "workspace_id!: Uuid"
FROM workspaces w
WHERE w.container_ref = ?"#,
container_ref
)
.fetch_optional(pool)
.await?
.ok_or(sqlx::Error::RowNotFound)?;
Ok(ContainerInfo {
workspace_id: result.workspace_id,
})
}
/// Find workspace by path using container-ref path containment.
/// Used by clients that may open a repo subfolder rather than the workspace root.
pub async fn resolve_container_ref_by_prefix(
@@ -591,6 +617,13 @@ impl Workspace {
}
/// Count total workspaces across all projects
pub async fn count_all(pool: &SqlitePool) -> Result<i64, WorkspaceError> {
sqlx::query_scalar!(r#"SELECT COUNT(*) as "count!: i64" FROM workspaces"#)
.fetch_one(pool)
.await
.map_err(WorkspaceError::Database)
}
pub async fn find_by_id_with_status(
pool: &SqlitePool,
id: Uuid,

View File

@@ -250,6 +250,37 @@ impl WorkspaceRepo {
Ok(result.rows_affected())
}
pub async fn find_unique_repos_for_task(
pool: &SqlitePool,
task_id: Uuid,
) -> Result<Vec<Repo>, sqlx::Error> {
sqlx::query_as!(
Repo,
r#"SELECT DISTINCT r.id as "id!: Uuid",
r.path,
r.name,
r.display_name,
r.setup_script,
r.cleanup_script,
r.archive_script,
r.copy_files,
r.parallel_setup_script as "parallel_setup_script!: bool",
r.dev_server_script,
r.default_target_branch,
r.default_working_dir,
r.created_at as "created_at!: DateTime<Utc>",
r.updated_at as "updated_at!: DateTime<Utc>"
FROM repos r
JOIN workspace_repos wr ON r.id = wr.repo_id
JOIN workspaces w ON wr.workspace_id = w.id
WHERE w.task_id = $1
ORDER BY r.display_name ASC"#,
task_id
)
.fetch_all(pool)
.await
}
/// Find repos for a workspace with their copy_files configuration.
pub async fn find_repos_with_copy_files(
pool: &SqlitePool,

View File

@@ -1,10 +1,11 @@
[package]
name = "deployment"
version = "0.1.44"
version = "0.1.36"
edition = "2024"
[dependencies]
db = { path = "../db" }
desktop-bridge = { path = "../desktop-bridge" }
utils = { path = "../utils" }
git = { path = "../git" }
services = { path = "../services" }
@@ -22,6 +23,6 @@ preview-proxy = { path = "../preview-proxy" }
tokio = { workspace = true }
sqlx = "0.8.6"
serde_json = { workspace = true }
git2 = { workspace = true }
futures = "0.3.31"
axum = { workspace = true }
tokio-util = { version = "0.7" }

View File

@@ -5,9 +5,11 @@ use async_trait::async_trait;
use axum::response::sse::Event;
use client_info::ClientInfo;
use db::{DBService, models::workspace::WorkspaceError};
use desktop_bridge::tunnel::TunnelManager;
use executors::executors::ExecutorError;
use futures::{StreamExt, TryStreamExt};
use git::{GitService, GitServiceError};
use git2::Error as Git2Error;
use preview_proxy::PreviewProxyService;
use relay_control::{RelayControl, signing::RelaySigningService};
use relay_hosts::RelayHosts;
@@ -31,7 +33,6 @@ use services::services::{
use sqlx::Error as SqlxError;
use thiserror::Error;
use tokio::sync::RwLock;
use tokio_util::sync::CancellationToken;
use trusted_key_auth::runtime::TrustedKeyAuthRuntime;
use utils::sentry as sentry_utils;
use worktree_manager::WorktreeError;
@@ -51,6 +52,8 @@ pub enum DeploymentError {
#[error(transparent)]
Sqlx(#[from] SqlxError),
#[error(transparent)]
Git2(#[from] Git2Error),
#[error(transparent)]
GitServiceError(#[from] GitServiceError),
#[error(transparent)]
FilesystemWatcherError(#[from] FilesystemWatcherError),
@@ -78,7 +81,7 @@ pub enum DeploymentError {
#[async_trait]
pub trait Deployment: Clone + Send + Sync + 'static {
async fn new(shutdown: CancellationToken) -> Result<Self, DeploymentError>;
async fn new() -> Result<Self, DeploymentError>;
fn user_id(&self) -> &str;
@@ -118,6 +121,8 @@ pub trait Deployment: Clone + Send + Sync + 'static {
fn preview_proxy(&self) -> &PreviewProxyService;
fn tunnel_manager(&self) -> &Arc<TunnelManager>;
fn relay_hosts(&self) -> Result<&Arc<RelayHosts>, RelayHostsNotConfigured> {
Err(RelayHostsNotConfigured)
}

View File

@@ -1,15 +1,24 @@
[package]
name = "desktop-bridge"
version = "0.1.44"
version = "0.1.36"
edition = "2024"
publish = false
[dependencies]
anyhow = "1.0"
base64 = "0.22"
dirs = "5"
ssh-key = { version = "0.6", features = ["ed25519"] }
relay-control = { path = "../relay-control" }
relay-ws = { path = "../relay-ws" }
relay-tunnel-core = { path = "../relay-tunnel-core" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha2 = "0.10"
tokio = { version = "1.0", features = ["full"] }
tokio-tungstenite = { version = "0.26", features = ["rustls-tls-native-roots"] }
tokio-util = "0.7"
thiserror = "2.0"
tracing = "0.1"
ts-rs = { git = "https://github.com/xazukx/ts-rs.git", branch = "use-ts-enum", features = ["uuid-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] }
uuid = { version = "1", features = ["serde", "v4"] }

View File

@@ -1,5 +1,6 @@
pub mod service;
pub mod ssh_config;
pub mod tunnel;
#[derive(Debug, thiserror::Error)]
pub enum DesktopBridgeError {

View File

@@ -21,7 +21,7 @@ use crate::DesktopBridgeError;
///
/// Writes the OpenSSH PEM private key to `~/.vk-ssh/keys/{hash}` and returns
/// the path and the host alias (`vk-{host_id}`).
pub(crate) fn provision_ssh_key(
pub fn provision_ssh_key(
signing: &RelaySigningService,
host_id: &str,
) -> Result<(PathBuf, String), DesktopBridgeError> {
@@ -63,7 +63,7 @@ pub(crate) fn provision_ssh_key(
/// Write (or update) an SSH config entry for the given host alias.
///
/// The config is written to `~/.vk-ssh/config` and points at the local tunnel port.
pub(crate) fn update_ssh_config(
pub fn update_ssh_config(
alias: &str,
port: u16,
key_path: &std::path::Path,
@@ -86,7 +86,7 @@ pub(crate) fn update_ssh_config(
}
/// Ensure `~/.ssh/config` includes our `~/.vk-ssh/config`.
pub(crate) fn ensure_ssh_include() -> Result<(), DesktopBridgeError> {
pub fn ensure_ssh_include() -> Result<(), DesktopBridgeError> {
let ssh_dir = dirs::home_dir()
.ok_or(DesktopBridgeError::NoHomeDirectory)?
.join(".ssh");

View File

@@ -0,0 +1,267 @@
//! TCP port tunnel manager.
//!
//! Creates local TCP listeners that tunnel to remote relay hosts via WebSocket.
//! Each tunnel bridges `localhost:{local_port}` → WS → relay proxy → host backend → `localhost:{remote_port}`.
use std::{collections::HashMap, sync::Arc};
use anyhow::Context as _;
use relay_control::signing::{self, RelaySigningService};
use relay_tunnel_core::{tls::ws_connector, ws_io::tungstenite_ws_stream_io};
use relay_ws::signed_tungstenite_websocket;
use tokio::{net::TcpListener, sync::Mutex};
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
/// Key for deduplicating tunnels.
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
struct TunnelKey {
host_id: Uuid,
api_path: String,
}
struct ActiveTunnel {
id: Uuid,
local_port: u16,
relay_session_base_url: String,
signing_session_id: Uuid,
cancel: CancellationToken,
}
pub struct TunnelManager {
tunnels: Arc<Mutex<HashMap<TunnelKey, ActiveTunnel>>>,
signing: RelaySigningService,
}
impl TunnelManager {
pub fn new(signing: RelaySigningService) -> Self {
Self {
tunnels: Arc::new(Mutex::new(HashMap::new())),
signing,
}
}
/// Get or create a tunnel to the embedded SSH session endpoint.
/// Returns the local port to connect to.
pub async fn get_or_create_ssh_tunnel(
&self,
host_id: Uuid,
relay_session_base_url: &str,
signing_session_id: Uuid,
) -> std::io::Result<u16> {
let local_port = self
.get_or_create_tunnel_for_path(
host_id,
relay_session_base_url,
signing_session_id,
"/api/ssh-session",
)
.await?;
tracing::debug!(local_port, "SSH session tunnel created");
Ok(local_port)
}
async fn get_or_create_tunnel_for_path(
&self,
host_id: Uuid,
relay_session_base_url: &str,
signing_session_id: Uuid,
api_path: &str,
) -> std::io::Result<u16> {
let key = TunnelKey {
host_id,
api_path: api_path.to_string(),
};
// Check for existing healthy tunnel.
// If signing session or relay session rotated, replace the existing tunnel.
{
let mut tunnels = self.tunnels.lock().await;
if let Some(tunnel) = tunnels.get(&key)
&& !tunnel.cancel.is_cancelled()
{
if tunnel.signing_session_id == signing_session_id
&& tunnel.relay_session_base_url == relay_session_base_url
{
return Ok(tunnel.local_port);
}
tracing::debug!(
previous_relay_session_base_url = %tunnel.relay_session_base_url,
new_relay_session_base_url = %relay_session_base_url,
previous_signing_session_id = %tunnel.signing_session_id,
new_signing_session_id = %signing_session_id,
local_port = tunnel.local_port,
"Replacing tunnel after session rotation"
);
tunnel.cancel.cancel();
tunnels.remove(&key);
}
}
// Create new tunnel
let listener = TcpListener::bind("127.0.0.1:0").await?;
let local_port = listener.local_addr()?.port();
let tunnel_id = Uuid::new_v4();
let cancel = CancellationToken::new();
let relay_session_base_url_owned = relay_session_base_url.to_string();
{
let mut tunnels = self.tunnels.lock().await;
if let Some(tunnel) = tunnels.get(&key)
&& !tunnel.cancel.is_cancelled()
{
if tunnel.signing_session_id == signing_session_id
&& tunnel.relay_session_base_url == relay_session_base_url
{
return Ok(tunnel.local_port);
}
tracing::debug!(
previous_relay_session_base_url = %tunnel.relay_session_base_url,
new_relay_session_base_url = %relay_session_base_url,
previous_signing_session_id = %tunnel.signing_session_id,
new_signing_session_id = %signing_session_id,
local_port = tunnel.local_port,
"Replacing tunnel after session rotation"
);
tunnel.cancel.cancel();
tunnels.remove(&key);
}
tunnels.insert(
key.clone(),
ActiveTunnel {
id: tunnel_id,
local_port,
relay_session_base_url: relay_session_base_url_owned.clone(),
signing_session_id,
cancel: cancel.clone(),
},
);
}
let cancel_clone = cancel.clone();
let relay_session_base_url_for_task = relay_session_base_url_owned;
let signing = self.signing.clone();
let tunnels = self.tunnels.clone();
let key_clone = key;
let tunnel_id_clone = tunnel_id;
let api_path = api_path.to_string();
tokio::spawn(async move {
run_tunnel_listener(
listener,
&relay_session_base_url_for_task,
&signing,
signing_session_id,
&api_path,
cancel_clone,
)
.await;
// Clean up on exit, but only if this task still owns the key.
let mut tunnels = tunnels.lock().await;
if tunnels
.get(&key_clone)
.is_some_and(|active| active.id == tunnel_id_clone)
{
tunnels.remove(&key_clone);
}
});
Ok(local_port)
}
}
async fn run_tunnel_listener(
listener: TcpListener,
relay_session_base_url: &str,
signing: &RelaySigningService,
signing_session_id: Uuid,
api_path: &str,
cancel: CancellationToken,
) {
loop {
tokio::select! {
_ = cancel.cancelled() => break,
result = listener.accept() => {
match result {
Ok((tcp_stream, _addr)) => {
let relay_session_base_url = relay_session_base_url.to_string();
let signing = signing.clone();
let api_path = api_path.to_string();
tokio::spawn(async move {
if let Err(error) = bridge_tcp_to_relay(
tcp_stream,
&relay_session_base_url,
&signing,
signing_session_id,
&api_path,
)
.await
{
tracing::warn!(?error, "Tunnel bridge failed");
}
});
}
Err(error) => {
tracing::warn!(?error, "Tunnel accept failed");
break;
}
}
}
}
}
}
/// Bridge a single TCP connection to the relay via signed WebSocket.
async fn bridge_tcp_to_relay(
mut tcp_stream: tokio::net::TcpStream,
relay_session_base_url: &str,
signing: &RelaySigningService,
signing_session_id: Uuid,
api_path: &str,
) -> anyhow::Result<()> {
let base = relay_session_base_url.trim_end_matches('/');
let ws_url = relay_tunnel_core::http_to_ws_url(&format!("{base}{api_path}"))?;
let sig = signing.sign_request(signing_session_id, "GET", api_path, &[]);
let mut request = ws_url
.into_client_request()
.context("Failed to build WS request")?;
request.headers_mut().insert(
signing::SIGNING_SESSION_HEADER,
sig.signing_session_id.to_string().parse()?,
);
request.headers_mut().insert(
signing::TIMESTAMP_HEADER,
sig.timestamp.to_string().parse()?,
);
request
.headers_mut()
.insert(signing::NONCE_HEADER, sig.nonce.to_string().parse()?);
request.headers_mut().insert(
signing::REQUEST_SIGNATURE_HEADER,
sig.signature_b64.parse()?,
);
let (ws_stream, _response) =
tokio_tungstenite::connect_async_tls_with_config(request, None, false, ws_connector())
.await
.context("Failed to connect relay tunnel WS")?;
let signed_ws = signed_tungstenite_websocket(signing, &sig, ws_stream)
.await
.context("Failed to create signed WebSocket")?;
let mut ws_io = tungstenite_ws_stream_io(signed_ws);
tokio::io::copy_bidirectional(&mut tcp_stream, &mut ws_io)
.await
.context("Tunnel copy ended")?;
Ok(())
}

View File

@@ -1,6 +1,6 @@
[package]
name = "embedded-ssh"
version = "0.1.44"
version = "0.1.36"
edition = "2024"
[dependencies]

View File

@@ -1,6 +1,6 @@
[package]
name = "executors"
version = "0.1.44"
version = "0.1.36"
edition = "2024"
[dependencies]
@@ -36,8 +36,8 @@ convert_case = "0.6"
sqlx = "0.8.6"
shlex = "1.3.0"
agent-client-protocol = { version = "0.8", features = ["unstable"] }
codex-protocol = { git = "https://github.com/openai/codex.git", package = "codex-protocol", tag = "rust-v0.124.0" }
codex-app-server-protocol = { git = "https://github.com/openai/codex.git", package = "codex-app-server-protocol", tag = "rust-v0.124.0" }
codex-protocol = { git = "https://github.com/openai/codex.git", package = "codex-protocol", tag = "rust-v0.114.0" }
codex-app-server-protocol = { git = "https://github.com/openai/codex.git", package = "codex-app-server-protocol", tag = "rust-v0.114.0" }
sha2 = "0.10"
derivative = "2.2.0"
reqwest = { workspace = true }
@@ -52,13 +52,6 @@ async-stream = "0.3"
[target.'cfg(windows)'.dependencies]
winsplit = "0.1.0"
# On Linux, codex-protocol → reqwest → hyper-tls → native-tls pulls in
# openssl-sys. Vendor it so musl cross-compile (no system OpenSSL) builds.
# macOS uses Security.framework and Windows uses schannel via native-tls,
# so neither pulls in openssl-sys — keep this Linux-only.
[target.'cfg(target_os = "linux")'.dependencies]
openssl = { version = "0.10", features = ["vendored"] }
[features]
default = []
qa-mode = []

View File

@@ -779,7 +779,7 @@ struct AcpEventParser;
impl AcpEventParser {
/// Parse a line that may contain an ACP event
fn parse_line(line: &str) -> Option<AcpEvent> {
pub fn parse_line(line: &str) -> Option<AcpEvent> {
let trimmed = line.trim();
if let Ok(acp_event) = serde_json::from_str::<AcpEvent>(trimmed) {
@@ -792,7 +792,7 @@ impl AcpEventParser {
}
/// Parse command from tool title (for execute tools)
fn parse_execute_command(tc: &PartialToolCallData) -> String {
pub fn parse_execute_command(tc: &PartialToolCallData) -> String {
if let Some(command) = tc.raw_input.as_ref().and_then(|value| {
value
.as_object()
@@ -811,6 +811,16 @@ impl AcpEventParser {
}
}
/// Result of parsing a line
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum ParsedLine {
SessionId(String),
Event(AcpEvent),
Error(String),
Done,
}
impl TryFrom<SessionNotification> for AcpEvent {
type Error = ();

View File

@@ -5,6 +5,8 @@ use std::{
str::FromStr,
};
use serde::{Deserialize, Serialize};
use crate::executors::acp::AcpEvent;
/// Manages session persistence and state for ACP interactions
@@ -138,6 +140,15 @@ impl SessionManager {
Ok(())
}
/// Delete a session
pub fn delete_session(&self, session_id: &str) -> Result<()> {
let path = self.session_file_path(session_id);
if path.exists() {
fs::remove_file(path)?;
}
Ok(())
}
/// Generate a resume prompt from session history
pub fn generate_resume_prompt(&self, session_id: &str, current_prompt: &str) -> Result<String> {
let session_context = self.read_session_raw(session_id)?;
@@ -159,3 +170,13 @@ impl SessionManager {
))
}
}
/// Session metadata stored separately from events
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionMetadata {
pub session_id: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub parent_session: Option<String>,
pub tags: Vec<String>,
}

View File

@@ -62,7 +62,7 @@ fn base_command(claude_code_router: bool) -> &'static str {
if claude_code_router {
"npx -y @musistudio/claude-code-router@1.0.66 code"
} else {
"npx -y @anthropic-ai/claude-code@2.1.119"
"npx -y @anthropic-ai/claude-code@2.1.62"
}
}
@@ -111,7 +111,6 @@ pub enum ClaudeEffort {
Low,
Medium,
High,
XHigh,
Max,
}
@@ -270,7 +269,7 @@ fn default_discovered_options() -> crate::executor_discovery::ExecutorDiscovered
};
let effort_options =
ReasoningOption::from_names(["low", "medium", "high", "xhigh", "max"].map(String::from));
ReasoningOption::from_names(["low", "medium", "high", "max"].map(String::from));
let supports_effort = |id: &str| -> bool { id.contains("opus") || id.contains("sonnet") };
@@ -580,12 +579,7 @@ impl StandardCodingAgentExecutor for ClaudeCode {
ExecutorConfig {
executor: BaseCodingAgent::ClaudeCode,
variant: None,
model_id: self.model.clone().or_else(|| {
default_discovered_options()
.model_selector
.default_model
.clone()
}),
model_id: self.model.clone(),
agent_id: None,
reasoning_id: self.effort.as_ref().map(|e| e.as_ref().to_owned()),
permission_policy: Some(permission_policy),

Some files were not shown because too many files have changed in this diff Show More