Compare commits
80 commits
dev
...
core/merge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48d385279b | ||
|
|
4dcd168cd4 | ||
|
|
1b23082e25 | ||
|
|
da81534897 | ||
|
|
a290ab31e9 | ||
|
|
d2bb19c6bd | ||
|
|
298c8d9cd8 | ||
|
|
a27a31faad | ||
|
|
478bbdd44c | ||
|
|
b6fbb88bfb | ||
|
|
70c32135d0 | ||
|
|
ef22946f35 | ||
|
|
c8e66918c3 | ||
|
|
1ce4f6b251 | ||
|
|
f8d8bd6556 | ||
|
|
6f2d9f8de4 | ||
|
|
c8ec0f9e49 | ||
|
|
290e3416ce | ||
|
|
f89e80732a | ||
|
|
065b42a0be | ||
|
|
004c5c9eb9 | ||
|
|
9d664c055a | ||
|
|
ca8c155d85 | ||
|
|
01d9aa1b73 | ||
| b8b144bec0 | |||
|
|
f2272e4f6f | ||
|
|
5fd7705580 | ||
|
|
3dbb5988a8 | ||
|
|
fcd1758b7d | ||
|
|
3fdc3f3086 | ||
|
|
6f52e4e3ae | ||
|
|
1f43073f57 | ||
|
|
1facdd602f | ||
|
|
c72f35bd3f | ||
|
|
2a8b5c207f | ||
|
|
5d0b6c3a71 | ||
|
|
d583a074f7 | ||
|
|
f963a45d9f | ||
|
|
74bb62fda8 | ||
|
|
f85bba5332 | ||
|
|
0af6407666 | ||
|
|
1f3e6ba4ab | ||
|
|
39d6dccbf8 | ||
|
|
2979816d83 | ||
|
|
f0595f6858 | ||
|
|
bdbfc5e59e | ||
|
|
b779c5ece0 | ||
|
|
1e0bff0a2e | ||
|
|
e9df62b04e | ||
|
|
03d7a7dc4e | ||
|
|
6abe90c8cb | ||
|
|
b0ef3fb215 | ||
|
|
bcb559630e | ||
|
|
bde00e40f4 | ||
|
|
45d8b5b7d4 | ||
|
|
6702d56edb | ||
|
|
4bc43939a6 | ||
|
|
d2dd23697f | ||
|
|
65c138e126 | ||
|
|
740cf115b2 | ||
|
|
3bfaf37ab1 | ||
|
|
72529a8281 | ||
| 7ce8ca717c | |||
| 37b04695d1 | |||
| 9fe4d5f063 | |||
| 16a5ba70ef | |||
| 88e5560086 | |||
| b57e30ea06 | |||
| 8d3f9a73ee | |||
|
|
c4d59f9850 | ||
| 9f9e8cc044 | |||
| 74e2614e41 | |||
|
|
149dc3de14 | ||
|
|
9319015219 | ||
|
|
25985af53c | ||
|
|
796ec563ed | ||
|
|
e83e416854 | ||
|
|
3fc04f809b | ||
|
|
169428a945 | ||
|
|
440086b83a |
684 changed files with 28691 additions and 5828 deletions
|
|
@ -30,7 +30,7 @@ core/
|
|||
package domain
|
||||
|
||||
import (
|
||||
"github.com/host-uk/core/pkg/cli"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ func NewNameCmd() *cobra.Command {
|
|||
## CLI Output Helpers
|
||||
|
||||
```go
|
||||
import "github.com/host-uk/core/pkg/cli"
|
||||
import "forge.lthn.ai/core/cli/pkg/cli"
|
||||
|
||||
cli.Success("Operation completed") // Green check
|
||||
cli.Warning("Something to note") // Yellow warning
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Issue 258: Smart Test Detection
|
||||
|
||||
## Original Issue
|
||||
<https://github.com/host-uk/core/issues/258>
|
||||
<https://forge.lthn.ai/core/cli/issues/258>
|
||||
|
||||
## Summary
|
||||
Make `core test` smart — detect changed Go files and run only relevant tests.
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
name: Bug Report
|
||||
description: Report a problem with the core CLI
|
||||
title: "[Bug]: "
|
||||
labels: ["bug", "triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for reporting! Please fill out the details below.
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating System
|
||||
options:
|
||||
- macOS
|
||||
- Windows
|
||||
- Linux (Ubuntu/Debian)
|
||||
- Linux (Other)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: command
|
||||
attributes:
|
||||
label: Command
|
||||
description: Which command failed?
|
||||
placeholder: "e.g., core dev work, core php test"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: Output of `core version`
|
||||
placeholder: "e.g., core v0.1.0"
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Describe the issue
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behaviour
|
||||
description: What should have happened?
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Error output
|
||||
description: Paste any error messages
|
||||
render: shell
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Host UK Documentation
|
||||
url: https://github.com/host-uk/core-devops
|
||||
about: Setup guides and workspace documentation
|
||||
- name: Discussions
|
||||
url: https://github.com/orgs/host-uk/discussions
|
||||
about: Ask questions and share ideas
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
name: Feature Request
|
||||
description: Suggest a new feature or enhancement
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for the suggestion! Please describe your idea below.
|
||||
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area
|
||||
options:
|
||||
- dev commands (work, commit, push, pull)
|
||||
- php commands (test, lint, stan)
|
||||
- GitHub integration (issues, reviews, ci)
|
||||
- New command
|
||||
- Documentation
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem or use case
|
||||
description: What problem does this solve?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: How would you like it to work?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
description: Any other approaches you've thought about?
|
||||
|
||||
- type: dropdown
|
||||
id: complexity
|
||||
attributes:
|
||||
label: Estimated complexity
|
||||
description: How much work do you think this requires?
|
||||
options:
|
||||
- "Small - Quick fix, single file, < 1 hour"
|
||||
- "Medium - Multiple files, few hours to a day"
|
||||
- "Large - Significant changes, multiple days"
|
||||
- "Unknown - Not sure"
|
||||
validations:
|
||||
required: false
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
labels:
|
||||
- "type:dependencies"
|
||||
- "priority:low"
|
||||
commit-message:
|
||||
prefix: "deps(go):"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
labels:
|
||||
- "type:dependencies"
|
||||
- "priority:low"
|
||||
commit-message:
|
||||
prefix: "deps(actions):"
|
||||
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
name: Agent Verification
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
uses: host-uk/.github/.github/workflows/agent-verify.yml@main
|
||||
secrets: inherit
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch
|
||||
name: "Alpha Release: Manual"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
env:
|
||||
NEXT_VERSION: "0.0.4"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
platform: linux/amd64
|
||||
- os: ubuntu-latest
|
||||
platform: linux/arm64
|
||||
- os: macos-latest
|
||||
platform: darwin/universal
|
||||
- os: windows-latest
|
||||
platform: windows/amd64
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Build
|
||||
uses: host-uk/build@v3
|
||||
with:
|
||||
build-name: core
|
||||
build-platform: ${{ matrix.platform }}
|
||||
build: true
|
||||
package: true
|
||||
sign: false
|
||||
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Prepare release files
|
||||
run: |
|
||||
mkdir -p release
|
||||
cp dist/* release/ 2>/dev/null || true
|
||||
ls -la release/
|
||||
|
||||
- name: Create alpha release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
VERSION="v${{ env.NEXT_VERSION }}-alpha.${{ github.run_number }}"
|
||||
|
||||
gh release create "$VERSION" \
|
||||
--title "Alpha: $VERSION" \
|
||||
--notes "Canary build from dev branch.
|
||||
|
||||
**Version:** $VERSION
|
||||
**Commit:** ${{ github.sha }}
|
||||
**Built:** $(date -u +'%Y-%m-%d %H:%M:%S UTC')
|
||||
**Run:** ${{ github.run_id }}
|
||||
|
||||
## Channel: Alpha (Canary)
|
||||
|
||||
This is an automated pre-release for early testing.
|
||||
|
||||
- Systems and early adopters can test breaking changes
|
||||
- Quality scoring determines promotion to beta
|
||||
- Use stable releases for production
|
||||
|
||||
## Installation
|
||||
|
||||
\`\`\`bash
|
||||
# macOS/Linux
|
||||
curl -fsSL https://github.com/host-uk/core/releases/download/$VERSION/core-linux-amd64 -o core
|
||||
chmod +x core && sudo mv core /usr/local/bin/
|
||||
\`\`\`
|
||||
" \
|
||||
--prerelease \
|
||||
--target dev \
|
||||
release/*
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push
|
||||
name: "Alpha Release: Push"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
env:
|
||||
NEXT_VERSION: "0.0.4"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
platform: linux/amd64
|
||||
- os: ubuntu-latest
|
||||
platform: linux/arm64
|
||||
- os: macos-latest
|
||||
platform: darwin/universal
|
||||
- os: windows-latest
|
||||
platform: windows/amd64
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Build
|
||||
uses: host-uk/build@v3
|
||||
with:
|
||||
build-name: core
|
||||
build-platform: ${{ matrix.platform }}
|
||||
build: true
|
||||
package: true
|
||||
sign: false
|
||||
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Prepare release files
|
||||
run: |
|
||||
mkdir -p release
|
||||
cp dist/* release/ 2>/dev/null || true
|
||||
ls -la release/
|
||||
|
||||
- name: Create alpha release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
VERSION="v${{ env.NEXT_VERSION }}-alpha.${{ github.run_number }}"
|
||||
|
||||
gh release create "$VERSION" \
|
||||
--title "Alpha: $VERSION" \
|
||||
--notes "Canary build from dev branch.
|
||||
|
||||
**Version:** $VERSION
|
||||
**Commit:** ${{ github.sha }}
|
||||
**Built:** $(date -u +'%Y-%m-%d %H:%M:%S UTC')
|
||||
**Run:** ${{ github.run_id }}
|
||||
|
||||
## Channel: Alpha (Canary)
|
||||
|
||||
This is an automated pre-release for early testing.
|
||||
|
||||
- Systems and early adopters can test breaking changes
|
||||
- Quality scoring determines promotion to beta
|
||||
- Use stable releases for production
|
||||
|
||||
## Installation
|
||||
|
||||
\`\`\`bash
|
||||
# macOS/Linux
|
||||
curl -fsSL https://github.com/host-uk/core/releases/download/$VERSION/core-linux-amd64 -o core
|
||||
chmod +x core && sudo mv core /usr/local/bin/
|
||||
\`\`\`
|
||||
" \
|
||||
--prerelease \
|
||||
--target dev \
|
||||
release/*
|
||||
|
|
@ -1,500 +0,0 @@
|
|||
name: Alpha Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
env:
|
||||
# Next version - update when releasing
|
||||
NEXT_VERSION: "0.0.4"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
- os: ubuntu-latest
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
- os: macos-latest
|
||||
goos: darwin
|
||||
goarch: arm64
|
||||
- os: windows-latest
|
||||
goos: windows
|
||||
goarch: amd64
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
# GUI build disabled until build action supports Wails v3
|
||||
# - name: Wails Build Action
|
||||
# uses: host-uk/build@v4.0.0
|
||||
# with:
|
||||
# build-name: core
|
||||
# build-platform: ${{ matrix.goos }}/${{ matrix.goarch }}
|
||||
# build: true
|
||||
# package: true
|
||||
# sign: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: host-uk/build/actions/setup/go@v4.0.0
|
||||
with:
|
||||
go-version: "1.25"
|
||||
|
||||
- name: Build CLI
|
||||
shell: bash
|
||||
run: |
|
||||
EXT=""
|
||||
if [ "$GOOS" = "windows" ]; then EXT=".exe"; fi
|
||||
BINARY="core${EXT}"
|
||||
ARCHIVE_PREFIX="core-${GOOS}-${GOARCH}"
|
||||
|
||||
APP_VERSION="${{ env.NEXT_VERSION }}-alpha.${{ github.run_number }}"
|
||||
go build -ldflags "-s -w -X github.com/host-uk/core/pkg/cli.AppVersion=${APP_VERSION}" -o "./bin/${BINARY}" .
|
||||
|
||||
# Create tar.gz for Homebrew (non-Windows)
|
||||
if [ "$GOOS" != "windows" ]; then
|
||||
tar czf "./bin/${ARCHIVE_PREFIX}.tar.gz" -C ./bin "${BINARY}"
|
||||
fi
|
||||
|
||||
# Create zip for Scoop (Windows)
|
||||
if [ "$GOOS" = "windows" ]; then
|
||||
cd ./bin && zip "${ARCHIVE_PREFIX}.zip" "${BINARY}" && cd ..
|
||||
fi
|
||||
|
||||
# Rename raw binary to platform-specific name for release
|
||||
mv "./bin/${BINARY}" "./bin/${ARCHIVE_PREFIX}${EXT}"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: core-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: ./bin/core-*
|
||||
|
||||
build-ide:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-latest
|
||||
goos: darwin
|
||||
goarch: arm64
|
||||
- os: ubuntu-latest
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
- os: windows-latest
|
||||
goos: windows
|
||||
goarch: amd64
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: internal/core-ide
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Go
|
||||
uses: host-uk/build/actions/setup/go@v4.0.0
|
||||
with:
|
||||
go-version: "1.25"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Install Wails CLI
|
||||
run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: internal/core-ide/frontend
|
||||
run: npm ci
|
||||
|
||||
- name: Generate bindings
|
||||
run: wails3 generate bindings -f '-tags production' -clean=false -ts -i
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: internal/core-ide/frontend
|
||||
run: npm run build
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: matrix.goos == 'linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev
|
||||
|
||||
- name: Build IDE
|
||||
shell: bash
|
||||
run: |
|
||||
EXT=""
|
||||
if [ "$GOOS" = "windows" ]; then EXT=".exe"; fi
|
||||
BINARY="core-ide${EXT}"
|
||||
ARCHIVE_PREFIX="core-ide-${GOOS}-${GOARCH}"
|
||||
|
||||
BUILD_FLAGS="-tags production -trimpath -buildvcs=false"
|
||||
|
||||
if [ "$GOOS" = "windows" ]; then
|
||||
# Windows: no CGO, use windowsgui linker flag
|
||||
export CGO_ENABLED=0
|
||||
LDFLAGS="-w -s -H windowsgui"
|
||||
|
||||
# Generate Windows syso resource
|
||||
cd build
|
||||
wails3 generate syso -arch ${GOARCH} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_${GOARCH}.syso
|
||||
cd ..
|
||||
elif [ "$GOOS" = "darwin" ]; then
|
||||
export CGO_ENABLED=1
|
||||
export CGO_CFLAGS="-mmacosx-version-min=10.15"
|
||||
export CGO_LDFLAGS="-mmacosx-version-min=10.15"
|
||||
export MACOSX_DEPLOYMENT_TARGET="10.15"
|
||||
LDFLAGS="-w -s"
|
||||
else
|
||||
export CGO_ENABLED=1
|
||||
LDFLAGS="-w -s"
|
||||
fi
|
||||
|
||||
go build ${BUILD_FLAGS} -ldflags="${LDFLAGS}" -o "./bin/${BINARY}"
|
||||
|
||||
# Clean up syso files
|
||||
rm -f *.syso
|
||||
|
||||
# Package
|
||||
if [ "$GOOS" = "darwin" ]; then
|
||||
# Create .app bundle
|
||||
mkdir -p "./bin/Core IDE.app/Contents/"{MacOS,Resources}
|
||||
cp build/darwin/icons.icns "./bin/Core IDE.app/Contents/Resources/"
|
||||
cp "./bin/${BINARY}" "./bin/Core IDE.app/Contents/MacOS/"
|
||||
cp build/darwin/Info.plist "./bin/Core IDE.app/Contents/"
|
||||
codesign --force --deep --sign - "./bin/Core IDE.app"
|
||||
tar czf "./bin/${ARCHIVE_PREFIX}.tar.gz" -C ./bin "Core IDE.app"
|
||||
elif [ "$GOOS" = "windows" ]; then
|
||||
cd ./bin && zip "${ARCHIVE_PREFIX}.zip" "${BINARY}" && cd ..
|
||||
else
|
||||
tar czf "./bin/${ARCHIVE_PREFIX}.tar.gz" -C ./bin "${BINARY}"
|
||||
fi
|
||||
|
||||
# Rename raw binary
|
||||
mv "./bin/${BINARY}" "./bin/${ARCHIVE_PREFIX}${EXT}"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: core-ide-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: internal/core-ide/bin/core-ide-*
|
||||
|
||||
release:
|
||||
needs: [build, build-ide]
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set version
|
||||
id: version
|
||||
run: echo "version=v${{ env.NEXT_VERSION }}-alpha.${{ github.run_number }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Prepare release files
|
||||
run: |
|
||||
mkdir -p release
|
||||
cp dist/* release/ 2>/dev/null || true
|
||||
ls -la release/
|
||||
|
||||
- name: Create alpha release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
gh release create "$VERSION" \
|
||||
--title "Alpha: $VERSION" \
|
||||
--notes "Canary build from dev branch.
|
||||
|
||||
**Version:** $VERSION
|
||||
**Commit:** ${{ github.sha }}
|
||||
**Built:** $(date -u +'%Y-%m-%d %H:%M:%S UTC')
|
||||
**Run:** ${{ github.run_id }}
|
||||
|
||||
## Channel: Alpha (Canary)
|
||||
|
||||
This is an automated pre-release for early testing.
|
||||
|
||||
- Systems and early adopters can test breaking changes
|
||||
- Quality scoring determines promotion to beta
|
||||
- Use stable releases for production
|
||||
|
||||
## Installation
|
||||
|
||||
\`\`\`bash
|
||||
# Homebrew (macOS/Linux)
|
||||
brew install host-uk/tap/core
|
||||
|
||||
# Scoop (Windows)
|
||||
scoop bucket add host-uk https://github.com/host-uk/scoop-bucket
|
||||
scoop install core
|
||||
|
||||
# Direct download (example: Linux amd64)
|
||||
curl -fsSL https://github.com/host-uk/core/releases/download/$VERSION/core-linux-amd64 -o core
|
||||
chmod +x core && sudo mv core /usr/local/bin/
|
||||
\`\`\`
|
||||
" \
|
||||
--prerelease \
|
||||
--target dev \
|
||||
release/*
|
||||
|
||||
update-tap:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
cd dist
|
||||
for f in *.tar.gz; do
|
||||
sha256sum "$f" | awk '{print $1}' > "${f}.sha256"
|
||||
done
|
||||
echo "=== Checksums ==="
|
||||
cat *.sha256
|
||||
|
||||
- name: Update Homebrew formula
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
VERSION: ${{ needs.release.outputs.version }}
|
||||
run: |
|
||||
# Strip leading 'v' for formula version
|
||||
FORMULA_VERSION="${VERSION#v}"
|
||||
|
||||
# Read checksums
|
||||
DARWIN_ARM64=$(cat dist/core-darwin-arm64.tar.gz.sha256)
|
||||
LINUX_AMD64=$(cat dist/core-linux-amd64.tar.gz.sha256)
|
||||
LINUX_ARM64=$(cat dist/core-linux-arm64.tar.gz.sha256)
|
||||
|
||||
# Clone tap repo (configure auth for push)
|
||||
gh repo clone host-uk/homebrew-tap /tmp/tap -- --depth=1
|
||||
cd /tmp/tap
|
||||
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/host-uk/homebrew-tap.git"
|
||||
cd -
|
||||
mkdir -p /tmp/tap/Formula
|
||||
|
||||
# Write formula
|
||||
cat > /tmp/tap/Formula/core.rb << FORMULA
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Core < Formula
|
||||
desc "Host UK development CLI"
|
||||
homepage "https://github.com/host-uk/core"
|
||||
version "${FORMULA_VERSION}"
|
||||
license "EUPL-1.2"
|
||||
|
||||
on_macos do
|
||||
url "https://github.com/host-uk/core/releases/download/${VERSION}/core-darwin-arm64.tar.gz"
|
||||
sha256 "${DARWIN_ARM64}"
|
||||
end
|
||||
|
||||
on_linux do
|
||||
if Hardware::CPU.arm?
|
||||
url "https://github.com/host-uk/core/releases/download/${VERSION}/core-linux-arm64.tar.gz"
|
||||
sha256 "${LINUX_ARM64}"
|
||||
else
|
||||
url "https://github.com/host-uk/core/releases/download/${VERSION}/core-linux-amd64.tar.gz"
|
||||
sha256 "${LINUX_AMD64}"
|
||||
end
|
||||
end
|
||||
|
||||
def install
|
||||
bin.install "core"
|
||||
end
|
||||
|
||||
test do
|
||||
system "\#{bin}/core", "--version"
|
||||
end
|
||||
end
|
||||
FORMULA
|
||||
|
||||
# Remove leading whitespace from heredoc
|
||||
sed -i 's/^ //' /tmp/tap/Formula/core.rb
|
||||
|
||||
# Read IDE checksums (may not exist if build-ide failed)
|
||||
IDE_DARWIN_ARM64=$(cat dist/core-ide-darwin-arm64.tar.gz.sha256 2>/dev/null || echo "")
|
||||
IDE_LINUX_AMD64=$(cat dist/core-ide-linux-amd64.tar.gz.sha256 2>/dev/null || echo "")
|
||||
|
||||
# Write core-ide Formula (Linux binary)
|
||||
if [ -n "${IDE_LINUX_AMD64}" ]; then
|
||||
cat > /tmp/tap/Formula/core-ide.rb << FORMULA
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CoreIde < Formula
|
||||
desc "Host UK desktop development environment"
|
||||
homepage "https://github.com/host-uk/core"
|
||||
version "${FORMULA_VERSION}"
|
||||
license "EUPL-1.2"
|
||||
|
||||
on_linux do
|
||||
url "https://github.com/host-uk/core/releases/download/${VERSION}/core-ide-linux-amd64.tar.gz"
|
||||
sha256 "${IDE_LINUX_AMD64}"
|
||||
end
|
||||
|
||||
def install
|
||||
bin.install "core-ide"
|
||||
end
|
||||
end
|
||||
FORMULA
|
||||
sed -i 's/^ //' /tmp/tap/Formula/core-ide.rb
|
||||
fi
|
||||
|
||||
# Write core-ide Cask (macOS .app bundle)
|
||||
if [ -n "${IDE_DARWIN_ARM64}" ]; then
|
||||
mkdir -p /tmp/tap/Casks
|
||||
cat > /tmp/tap/Casks/core-ide.rb << CASK
|
||||
cask "core-ide" do
|
||||
version "${FORMULA_VERSION}"
|
||||
sha256 "${IDE_DARWIN_ARM64}"
|
||||
|
||||
url "https://github.com/host-uk/core/releases/download/${VERSION}/core-ide-darwin-arm64.tar.gz"
|
||||
name "Core IDE"
|
||||
desc "Host UK desktop development environment"
|
||||
homepage "https://github.com/host-uk/core"
|
||||
|
||||
app "Core IDE.app"
|
||||
end
|
||||
CASK
|
||||
sed -i 's/^ //' /tmp/tap/Casks/core-ide.rb
|
||||
fi
|
||||
|
||||
cd /tmp/tap
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add .
|
||||
git diff --cached --quiet && echo "No changes to tap" && exit 0
|
||||
git commit -m "Update core to ${FORMULA_VERSION}"
|
||||
git push
|
||||
|
||||
update-scoop:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
cd dist
|
||||
for f in *.zip; do
|
||||
[ -f "$f" ] || continue
|
||||
sha256sum "$f" | awk '{print $1}' > "${f}.sha256"
|
||||
done
|
||||
echo "=== Checksums ==="
|
||||
cat *.sha256 2>/dev/null || echo "No zip checksums"
|
||||
|
||||
- name: Update Scoop manifests
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
VERSION: ${{ needs.release.outputs.version }}
|
||||
run: |
|
||||
# Strip leading 'v' for manifest version
|
||||
MANIFEST_VERSION="${VERSION#v}"
|
||||
|
||||
# Read checksums
|
||||
WIN_AMD64=$(cat dist/core-windows-amd64.zip.sha256 2>/dev/null || echo "")
|
||||
IDE_WIN_AMD64=$(cat dist/core-ide-windows-amd64.zip.sha256 2>/dev/null || echo "")
|
||||
|
||||
# Clone scoop bucket
|
||||
gh repo clone host-uk/scoop-bucket /tmp/scoop -- --depth=1
|
||||
cd /tmp/scoop
|
||||
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/host-uk/scoop-bucket.git"
|
||||
|
||||
# Write core.json manifest
|
||||
cat > core.json << 'MANIFEST'
|
||||
{
|
||||
"version": "VERSION_PLACEHOLDER",
|
||||
"description": "Host UK development CLI",
|
||||
"homepage": "https://github.com/host-uk/core",
|
||||
"license": "EUPL-1.2",
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": "URL_PLACEHOLDER",
|
||||
"hash": "HASH_PLACEHOLDER",
|
||||
"bin": "core.exe"
|
||||
}
|
||||
},
|
||||
"checkver": "github",
|
||||
"autoupdate": {
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": "https://github.com/host-uk/core/releases/download/v$version/core-windows-amd64.zip"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MANIFEST
|
||||
|
||||
sed -i "s|VERSION_PLACEHOLDER|${MANIFEST_VERSION}|g" core.json
|
||||
sed -i "s|URL_PLACEHOLDER|https://github.com/host-uk/core/releases/download/${VERSION}/core-windows-amd64.zip|g" core.json
|
||||
sed -i "s|HASH_PLACEHOLDER|${WIN_AMD64}|g" core.json
|
||||
sed -i 's/^ //' core.json
|
||||
|
||||
# Write core-ide.json manifest
|
||||
if [ -n "${IDE_WIN_AMD64}" ]; then
|
||||
cat > core-ide.json << 'MANIFEST'
|
||||
{
|
||||
"version": "VERSION_PLACEHOLDER",
|
||||
"description": "Host UK desktop development environment",
|
||||
"homepage": "https://github.com/host-uk/core",
|
||||
"license": "EUPL-1.2",
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": "URL_PLACEHOLDER",
|
||||
"hash": "HASH_PLACEHOLDER",
|
||||
"bin": "core-ide.exe"
|
||||
}
|
||||
},
|
||||
"checkver": "github",
|
||||
"autoupdate": {
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": "https://github.com/host-uk/core/releases/download/v$version/core-ide-windows-amd64.zip"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MANIFEST
|
||||
sed -i "s|VERSION_PLACEHOLDER|${MANIFEST_VERSION}|g" core-ide.json
|
||||
sed -i "s|URL_PLACEHOLDER|https://github.com/host-uk/core/releases/download/${VERSION}/core-ide-windows-amd64.zip|g" core-ide.json
|
||||
sed -i "s|HASH_PLACEHOLDER|${IDE_WIN_AMD64}|g" core-ide.json
|
||||
sed -i 's/^ //' core-ide.json
|
||||
fi
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add .
|
||||
git diff --cached --quiet && echo "No changes to scoop bucket" && exit 0
|
||||
git commit -m "Update core to ${MANIFEST_VERSION}"
|
||||
git push
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues
|
||||
name: "Auto Label: Issue Created/Edited"
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Auto-label based on content
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const title = issue.title.toLowerCase();
|
||||
const body = (issue.body || '').toLowerCase();
|
||||
const content = title + ' ' + body;
|
||||
|
||||
const labelsToAdd = [];
|
||||
|
||||
// Type labels based on title prefix
|
||||
if (title.includes('[bug]')) {
|
||||
labelsToAdd.push('bug');
|
||||
} else if (title.includes('[feature]') || title.includes('feat(') || title.includes('feat:')) {
|
||||
labelsToAdd.push('enhancement');
|
||||
} else if (title.includes('[docs]') || title.includes('docs(') || title.includes('docs:')) {
|
||||
labelsToAdd.push('documentation');
|
||||
}
|
||||
|
||||
// Project labels based on content
|
||||
if (content.includes('core dev') || content.includes('core work') || content.includes('core commit') || content.includes('core push')) {
|
||||
labelsToAdd.push('project:core-cli');
|
||||
}
|
||||
if (content.includes('core php') || content.includes('composer') || content.includes('pest') || content.includes('phpstan')) {
|
||||
labelsToAdd.push('project:core-php');
|
||||
}
|
||||
|
||||
// Language labels
|
||||
if (content.includes('.go') || content.includes('golang') || content.includes('go mod')) {
|
||||
labelsToAdd.push('go');
|
||||
}
|
||||
|
||||
// Priority detection
|
||||
if (content.includes('critical') || content.includes('urgent') || content.includes('breaking')) {
|
||||
labelsToAdd.push('priority:high');
|
||||
}
|
||||
|
||||
// Agent labels
|
||||
if (content.includes('agent') || content.includes('ai ') || content.includes('claude') || content.includes('agentic')) {
|
||||
labelsToAdd.push('agentic');
|
||||
}
|
||||
|
||||
// Complexity - from template dropdown or heuristics
|
||||
if (body.includes('small - quick fix')) {
|
||||
labelsToAdd.push('complexity:small');
|
||||
labelsToAdd.push('good first issue');
|
||||
} else if (body.includes('medium - multiple files')) {
|
||||
labelsToAdd.push('complexity:medium');
|
||||
} else if (body.includes('large - significant')) {
|
||||
labelsToAdd.push('complexity:large');
|
||||
} else if (!body.includes('unknown - not sure')) {
|
||||
// Heuristic complexity detection
|
||||
const checklistCount = (body.match(/- \[ \]/g) || []).length;
|
||||
const codeBlocks = (body.match(/```/g) || []).length / 2;
|
||||
const sections = (body.match(/^##/gm) || []).length;
|
||||
const fileRefs = (body.match(/\.(go|php|js|ts|yml|yaml|json|md)\b/g) || []).length;
|
||||
|
||||
const complexKeywords = ['refactor', 'rewrite', 'migration', 'breaking change', 'across repos', 'architecture'];
|
||||
const simpleKeywords = ['simple', 'quick fix', 'typo', 'minor', 'trivial'];
|
||||
|
||||
const hasComplexKeyword = complexKeywords.some(k => content.includes(k));
|
||||
const hasSimpleKeyword = simpleKeywords.some(k => content.includes(k));
|
||||
|
||||
let score = checklistCount * 2 + codeBlocks + sections + fileRefs;
|
||||
score += hasComplexKeyword ? 5 : 0;
|
||||
score -= hasSimpleKeyword ? 3 : 0;
|
||||
|
||||
if (hasSimpleKeyword || score <= 2) {
|
||||
labelsToAdd.push('complexity:small');
|
||||
labelsToAdd.push('good first issue');
|
||||
} else if (score <= 6) {
|
||||
labelsToAdd.push('complexity:medium');
|
||||
} else {
|
||||
labelsToAdd.push('complexity:large');
|
||||
}
|
||||
}
|
||||
|
||||
// Apply labels if any detected
|
||||
if (labelsToAdd.length > 0) {
|
||||
// Filter to only existing labels
|
||||
const existingLabels = await github.rest.issues.listLabelsForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
per_page: 100
|
||||
});
|
||||
const validLabels = existingLabels.data.map(l => l.name);
|
||||
const filteredLabels = labelsToAdd.filter(l => validLabels.includes(l));
|
||||
|
||||
if (filteredLabels.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: filteredLabels
|
||||
});
|
||||
console.log(`Added labels: ${filteredLabels.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
name: Auto Merge
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.draft == false
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Enable auto-merge
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const author = context.payload.pull_request.user.login;
|
||||
const association = context.payload.pull_request.author_association;
|
||||
|
||||
// Trusted bot accounts (act as org members)
|
||||
const trustedBots = ['google-labs-jules[bot]'];
|
||||
const isTrustedBot = trustedBots.includes(author);
|
||||
|
||||
// Check author association from webhook payload
|
||||
const trusted = ['MEMBER', 'OWNER', 'COLLABORATOR'];
|
||||
if (!isTrustedBot && !trusted.includes(association)) {
|
||||
core.info(`${author} is ${association} — skipping auto-merge`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await exec.exec('gh', [
|
||||
'pr', 'merge', process.env.PR_NUMBER,
|
||||
'--auto',
|
||||
'--merge',
|
||||
'-R', `${context.repo.owner}/${context.repo.repo}`
|
||||
]);
|
||||
core.info(`Auto-merge enabled for #${process.env.PR_NUMBER}`);
|
||||
} catch (error) {
|
||||
core.error(`Failed to enable auto-merge: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
name: Auto Project
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, labeled]
|
||||
|
||||
jobs:
|
||||
project:
|
||||
uses: host-uk/.github/.github/workflows/auto-project.yml@main
|
||||
secrets: inherit
|
||||
|
|
@ -1,309 +0,0 @@
|
|||
# BugSETI Release Workflow
|
||||
# Builds for all platforms and creates GitHub releases
|
||||
name: "BugSETI Release"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'bugseti-v*.*.*' # Stable: bugseti-v1.0.0
|
||||
- 'bugseti-v*.*.*-beta.*' # Beta: bugseti-v1.0.0-beta.1
|
||||
- 'bugseti-nightly-*' # Nightly: bugseti-nightly-20260205
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
APP_NAME: bugseti
|
||||
WAILS_VERSION: "3"
|
||||
|
||||
jobs:
|
||||
# Determine release channel from tag
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
channel: ${{ steps.version.outputs.channel }}
|
||||
prerelease: ${{ steps.version.outputs.prerelease }}
|
||||
steps:
|
||||
- name: Determine version and channel
|
||||
id: version
|
||||
env:
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
if [[ "$TAG" == bugseti-nightly-* ]]; then
|
||||
VERSION="${TAG#bugseti-}"
|
||||
CHANNEL="nightly"
|
||||
PRERELEASE="true"
|
||||
elif [[ "$TAG" == *-beta.* ]]; then
|
||||
VERSION="${TAG#bugseti-v}"
|
||||
CHANNEL="beta"
|
||||
PRERELEASE="true"
|
||||
else
|
||||
VERSION="${TAG#bugseti-v}"
|
||||
CHANNEL="stable"
|
||||
PRERELEASE="false"
|
||||
fi
|
||||
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "channel=${CHANNEL}" >> "$GITHUB_OUTPUT"
|
||||
echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "Tag: $TAG"
|
||||
echo "Version: $VERSION"
|
||||
echo "Channel: $CHANNEL"
|
||||
echo "Prerelease: $PRERELEASE"
|
||||
|
||||
build:
|
||||
needs: prepare
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# macOS ARM64 (Apple Silicon)
|
||||
- os: macos-latest
|
||||
goos: darwin
|
||||
goarch: arm64
|
||||
ext: ""
|
||||
archive: tar.gz
|
||||
# macOS AMD64 (Intel)
|
||||
- os: macos-13
|
||||
goos: darwin
|
||||
goarch: amd64
|
||||
ext: ""
|
||||
archive: tar.gz
|
||||
# Linux AMD64
|
||||
- os: ubuntu-latest
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
ext: ""
|
||||
archive: tar.gz
|
||||
# Linux ARM64
|
||||
- os: ubuntu-24.04-arm
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
ext: ""
|
||||
archive: tar.gz
|
||||
# Windows AMD64
|
||||
- os: windows-latest
|
||||
goos: windows
|
||||
goarch: amd64
|
||||
ext: ".exe"
|
||||
archive: zip
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
CHANNEL: ${{ needs.prepare.outputs.channel }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: cmd/bugseti
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Go
|
||||
uses: host-uk/build/actions/setup/go@v4.0.0
|
||||
with:
|
||||
go-version: "1.25"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Install Wails CLI
|
||||
run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: cmd/bugseti/frontend
|
||||
run: npm ci
|
||||
|
||||
- name: Generate bindings
|
||||
run: wails3 generate bindings -f '-tags production' -clean=false -ts -i
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: cmd/bugseti/frontend
|
||||
run: npm run build
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: matrix.goos == 'linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev
|
||||
|
||||
- name: Build BugSETI
|
||||
shell: bash
|
||||
env:
|
||||
EXT: ${{ matrix.ext }}
|
||||
ARCHIVE: ${{ matrix.archive }}
|
||||
COMMIT_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
BINARY="${APP_NAME}${EXT}"
|
||||
ARCHIVE_PREFIX="${APP_NAME}-${GOOS}-${GOARCH}"
|
||||
|
||||
BUILD_FLAGS="-tags production -trimpath -buildvcs=false"
|
||||
|
||||
# Version injection via ldflags
|
||||
LDFLAGS="-s -w"
|
||||
LDFLAGS="${LDFLAGS} -X github.com/host-uk/core/internal/bugseti.Version=${VERSION}"
|
||||
LDFLAGS="${LDFLAGS} -X github.com/host-uk/core/internal/bugseti.Channel=${CHANNEL}"
|
||||
LDFLAGS="${LDFLAGS} -X github.com/host-uk/core/internal/bugseti.Commit=${COMMIT_SHA}"
|
||||
LDFLAGS="${LDFLAGS} -X github.com/host-uk/core/internal/bugseti.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
if [ "$GOOS" = "windows" ]; then
|
||||
export CGO_ENABLED=0
|
||||
LDFLAGS="${LDFLAGS} -H windowsgui"
|
||||
|
||||
# Generate Windows syso resource
|
||||
cd build
|
||||
wails3 generate syso -arch ${GOARCH} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_${GOARCH}.syso 2>/dev/null || true
|
||||
cd ..
|
||||
elif [ "$GOOS" = "darwin" ]; then
|
||||
export CGO_ENABLED=1
|
||||
export CGO_CFLAGS="-mmacosx-version-min=10.15"
|
||||
export CGO_LDFLAGS="-mmacosx-version-min=10.15"
|
||||
export MACOSX_DEPLOYMENT_TARGET="10.15"
|
||||
else
|
||||
export CGO_ENABLED=1
|
||||
fi
|
||||
|
||||
mkdir -p bin
|
||||
go build ${BUILD_FLAGS} -ldflags="${LDFLAGS}" -o "./bin/${BINARY}"
|
||||
|
||||
# Clean up syso files
|
||||
rm -f *.syso
|
||||
|
||||
# Package based on platform
|
||||
if [ "$GOOS" = "darwin" ]; then
|
||||
# Create .app bundle
|
||||
mkdir -p "./bin/BugSETI.app/Contents/"{MacOS,Resources}
|
||||
cp build/darwin/icons.icns "./bin/BugSETI.app/Contents/Resources/" 2>/dev/null || true
|
||||
cp "./bin/${BINARY}" "./bin/BugSETI.app/Contents/MacOS/"
|
||||
cp build/darwin/Info.plist "./bin/BugSETI.app/Contents/"
|
||||
codesign --force --deep --sign - "./bin/BugSETI.app" 2>/dev/null || true
|
||||
tar czf "./bin/${ARCHIVE_PREFIX}.tar.gz" -C ./bin "BugSETI.app"
|
||||
elif [ "$GOOS" = "windows" ]; then
|
||||
cd ./bin && zip "${ARCHIVE_PREFIX}.zip" "${BINARY}" && cd ..
|
||||
else
|
||||
tar czf "./bin/${ARCHIVE_PREFIX}.tar.gz" -C ./bin "${BINARY}"
|
||||
fi
|
||||
|
||||
# Rename raw binary for individual download
|
||||
mv "./bin/${BINARY}" "./bin/${ARCHIVE_PREFIX}${EXT}"
|
||||
|
||||
# Generate checksum
|
||||
cd ./bin
|
||||
sha256sum "${ARCHIVE_PREFIX}.${ARCHIVE}" > "${ARCHIVE_PREFIX}.${ARCHIVE}.sha256"
|
||||
sha256sum "${ARCHIVE_PREFIX}${EXT}" > "${ARCHIVE_PREFIX}${EXT}.sha256"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bugseti-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: |
|
||||
cmd/bugseti/bin/bugseti-*
|
||||
retention-days: 7
|
||||
|
||||
release:
|
||||
needs: [prepare, build]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
CHANNEL: ${{ needs.prepare.outputs.channel }}
|
||||
PRERELEASE: ${{ needs.prepare.outputs.prerelease }}
|
||||
REPO: ${{ github.repository }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: List release files
|
||||
run: |
|
||||
echo "=== Release files ==="
|
||||
ls -la dist/
|
||||
echo "=== Checksums ==="
|
||||
cat dist/*.sha256
|
||||
|
||||
- name: Create release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Determine release title
|
||||
if [ "$CHANNEL" = "nightly" ]; then
|
||||
TITLE="BugSETI Nightly (${VERSION})"
|
||||
elif [ "$CHANNEL" = "beta" ]; then
|
||||
TITLE="BugSETI v${VERSION} (Beta)"
|
||||
else
|
||||
TITLE="BugSETI v${VERSION}"
|
||||
fi
|
||||
|
||||
# Create release notes
|
||||
cat > release-notes.md << EOF
|
||||
## BugSETI ${VERSION}
|
||||
|
||||
**Channel:** ${CHANNEL}
|
||||
|
||||
### Downloads
|
||||
|
||||
| Platform | Architecture | Binary | Archive |
|
||||
|----------|-------------|--------|---------|
|
||||
| macOS | ARM64 (Apple Silicon) | [bugseti-darwin-arm64](https://github.com/${REPO}/releases/download/${TAG_NAME}/bugseti-darwin-arm64) | [tar.gz](https://github.com/${REPO}/releases/download/${TAG_NAME}/bugseti-darwin-arm64.tar.gz) |
|
||||
| macOS | AMD64 (Intel) | [bugseti-darwin-amd64](https://github.com/${REPO}/releases/download/${TAG_NAME}/bugseti-darwin-amd64) | [tar.gz](https://github.com/${REPO}/releases/download/${TAG_NAME}/bugseti-darwin-amd64.tar.gz) |
|
||||
| Linux | AMD64 | [bugseti-linux-amd64](https://github.com/${REPO}/releases/download/${TAG_NAME}/bugseti-linux-amd64) | [tar.gz](https://github.com/${REPO}/releases/download/${TAG_NAME}/bugseti-linux-amd64.tar.gz) |
|
||||
| Linux | ARM64 | [bugseti-linux-arm64](https://github.com/${REPO}/releases/download/${TAG_NAME}/bugseti-linux-arm64) | [tar.gz](https://github.com/${REPO}/releases/download/${TAG_NAME}/bugseti-linux-arm64.tar.gz) |
|
||||
| Windows | AMD64 | [bugseti-windows-amd64.exe](https://github.com/${REPO}/releases/download/${TAG_NAME}/bugseti-windows-amd64.exe) | [zip](https://github.com/${REPO}/releases/download/${TAG_NAME}/bugseti-windows-amd64.zip) |
|
||||
|
||||
### Checksums (SHA256)
|
||||
|
||||
\`\`\`
|
||||
$(cat dist/*.sha256)
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
*BugSETI - Distributed Bug Fixing, like SETI@home but for code*
|
||||
EOF
|
||||
|
||||
# Build release command
|
||||
RELEASE_ARGS=(
|
||||
--title "$TITLE"
|
||||
--notes-file release-notes.md
|
||||
)
|
||||
|
||||
if [ "$PRERELEASE" = "true" ]; then
|
||||
RELEASE_ARGS+=(--prerelease)
|
||||
fi
|
||||
|
||||
# Create the release
|
||||
gh release create "$TAG_NAME" \
|
||||
"${RELEASE_ARGS[@]}" \
|
||||
dist/*
|
||||
|
||||
# Scheduled nightly builds
|
||||
nightly:
|
||||
if: github.event_name == 'schedule'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Create nightly tag
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
DATE=$(date -u +%Y%m%d)
|
||||
TAG="bugseti-nightly-${DATE}"
|
||||
|
||||
# Delete existing nightly tag for today if it exists
|
||||
gh release delete "$TAG" --yes 2>/dev/null || true
|
||||
git push origin ":refs/tags/$TAG" 2>/dev/null || true
|
||||
|
||||
# Create new tag
|
||||
git tag "$TAG"
|
||||
git push origin "$TAG"
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch
|
||||
name: "CI: Manual"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CORE_VERSION: dev
|
||||
|
||||
jobs:
|
||||
qa:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev
|
||||
|
||||
- name: Build core CLI
|
||||
run: |
|
||||
go build -ldflags "-X github.com/host-uk/core/pkg/cli.AppVersion=${{ env.CORE_VERSION }}" -o /usr/local/bin/core .
|
||||
core --version
|
||||
|
||||
- name: Generate code
|
||||
run: go generate ./internal/cmd/updater/...
|
||||
|
||||
- name: Run QA
|
||||
# Skip lint until golangci-lint supports Go 1.25
|
||||
run: core go qa --skip=lint
|
||||
|
||||
- name: Verify build
|
||||
run: |
|
||||
core build --targets=linux/amd64 --ci
|
||||
dist/linux_amd64/core --version
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
||||
name: "CI: Pull Request"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [dev, main]
|
||||
|
||||
env:
|
||||
CORE_VERSION: dev
|
||||
|
||||
jobs:
|
||||
qa:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev
|
||||
|
||||
- name: Build core CLI
|
||||
run: |
|
||||
go build -ldflags "-X github.com/host-uk/core/pkg/cli.AppVersion=${{ env.CORE_VERSION }}" -o /usr/local/bin/core .
|
||||
core --version
|
||||
|
||||
- name: Generate code
|
||||
run: go generate ./internal/cmd/updater/...
|
||||
|
||||
- name: Run QA
|
||||
# Skip lint until golangci-lint supports Go 1.25
|
||||
run: core go qa --skip=lint
|
||||
|
||||
- name: Verify build
|
||||
run: |
|
||||
core build --targets=linux/amd64 --ci
|
||||
dist/linux_amd64/core --version
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push
|
||||
name: "CI: Push"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev, main]
|
||||
|
||||
env:
|
||||
CORE_VERSION: dev
|
||||
|
||||
jobs:
|
||||
qa:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev
|
||||
|
||||
- name: Build core CLI
|
||||
run: |
|
||||
go build -ldflags "-X github.com/host-uk/core/pkg/cli.AppVersion=${{ env.CORE_VERSION }}" -o /usr/local/bin/core .
|
||||
core --version
|
||||
|
||||
- name: Generate code
|
||||
run: go generate ./internal/cmd/updater/...
|
||||
|
||||
- name: Run QA
|
||||
# Skip lint until golangci-lint supports Go 1.25
|
||||
run: core go qa --skip=lint
|
||||
|
||||
- name: Verify build
|
||||
run: |
|
||||
core build --targets=linux/amd64 --ci
|
||||
dist/linux_amd64/core --version
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev, main]
|
||||
pull_request:
|
||||
branches: [dev, main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CORE_VERSION: dev
|
||||
|
||||
jobs:
|
||||
qa:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
# Try 4.1 first (Ubuntu 22.04+), fall back to 4.0 (Ubuntu 20.04)
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev || \
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev
|
||||
|
||||
- name: Build core CLI
|
||||
run: |
|
||||
go build -ldflags "-X github.com/host-uk/core/pkg/cli.AppVersion=${{ env.CORE_VERSION }}" -o /usr/local/bin/core .
|
||||
core --version
|
||||
|
||||
- name: Generate code
|
||||
run: go generate ./internal/cmd/updater/...
|
||||
|
||||
- name: Run QA
|
||||
# Skip lint until golangci-lint supports Go 1.25
|
||||
run: core go qa --skip=lint
|
||||
|
||||
- name: Verify build
|
||||
run: |
|
||||
core build --targets=linux/amd64 --ci
|
||||
dist/linux_amd64/core --version
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
||||
name: "CodeQL: Pull Request"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [dev, main]
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: go
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:go"
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push
|
||||
name: "CodeQL: Push"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev, main]
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: go
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:go"
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule
|
||||
name: "CodeQL: Schedule"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 6 * * 1"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: go
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:go"
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
||||
name: "Code Scanning: Pull Request"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["dev"]
|
||||
|
||||
jobs:
|
||||
CodeQL:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
security-events: write
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: "Initialize CodeQL"
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: go,javascript,typescript
|
||||
|
||||
- name: "Autobuild"
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
- name: "Perform CodeQL Analysis"
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push
|
||||
name: "Code Scanning: Push"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["dev"]
|
||||
|
||||
jobs:
|
||||
CodeQL:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
security-events: write
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: "Initialize CodeQL"
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: go,javascript,typescript
|
||||
|
||||
- name: "Autobuild"
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
- name: "Perform CodeQL Analysis"
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule
|
||||
name: "Code Scanning: Schedule"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 2 * * 1-5"
|
||||
|
||||
jobs:
|
||||
CodeQL:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
security-events: write
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: "Initialize CodeQL"
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: go,javascript,typescript
|
||||
|
||||
- name: "Autobuild"
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
- name: "Perform CodeQL Analysis"
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch
|
||||
name: "Coverage: Manual"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CORE_VERSION: dev
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev
|
||||
|
||||
- name: Build core CLI
|
||||
run: |
|
||||
go build -ldflags "-X github.com/host-uk/core/pkg/cli.AppVersion=${{ env.CORE_VERSION }}" -o /usr/local/bin/core .
|
||||
core --version
|
||||
|
||||
- name: Generate code
|
||||
run: go generate ./internal/cmd/updater/...
|
||||
|
||||
- name: Run coverage
|
||||
run: core go cov
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage.txt
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
||||
name: "Coverage: Pull Request"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [dev, main]
|
||||
|
||||
env:
|
||||
CORE_VERSION: dev
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev
|
||||
|
||||
- name: Build core CLI
|
||||
run: |
|
||||
go build -ldflags "-X github.com/host-uk/core/pkg/cli.AppVersion=${{ env.CORE_VERSION }}" -o /usr/local/bin/core .
|
||||
core --version
|
||||
|
||||
- name: Generate code
|
||||
run: go generate ./internal/cmd/updater/...
|
||||
|
||||
- name: Run coverage
|
||||
run: core go cov
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage.txt
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push
|
||||
name: "Coverage: Push"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev, main]
|
||||
|
||||
env:
|
||||
CORE_VERSION: dev
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev
|
||||
|
||||
- name: Build core CLI
|
||||
run: |
|
||||
go build -ldflags "-X github.com/host-uk/core/pkg/cli.AppVersion=${{ env.CORE_VERSION }}" -o /usr/local/bin/core .
|
||||
core --version
|
||||
|
||||
- name: Generate code
|
||||
run: go generate ./internal/cmd/updater/...
|
||||
|
||||
- name: Run coverage
|
||||
run: core go cov
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage.txt
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
name: Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev, main]
|
||||
pull_request:
|
||||
branches: [dev, main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CORE_VERSION: dev
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
# Try 4.1 first (Ubuntu 22.04+), fall back to 4.0 (Ubuntu 20.04)
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev || \
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev
|
||||
|
||||
- name: Build core CLI
|
||||
run: |
|
||||
go build -ldflags "-X github.com/host-uk/core/pkg/cli.AppVersion=${{ env.CORE_VERSION }}" -o /usr/local/bin/core .
|
||||
core --version
|
||||
|
||||
- name: Generate code
|
||||
run: go generate ./internal/cmd/updater/...
|
||||
|
||||
- name: Run coverage
|
||||
run: core go cov --output coverage.txt --threshold 40 --branch-threshold 35
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage.txt
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch
|
||||
name: "PR Build: Manual"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to build'
|
||||
required: true
|
||||
type: number
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
|
||||
env:
|
||||
NEXT_VERSION: "0.0.4"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
platform: linux/amd64
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Build
|
||||
uses: host-uk/build@v3
|
||||
with:
|
||||
build-name: core
|
||||
build-platform: ${{ matrix.platform }}
|
||||
build: true
|
||||
package: true
|
||||
sign: false
|
||||
|
||||
draft-release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PR_NUM: ${{ inputs.pr_number }}
|
||||
PR_SHA: ${{ github.sha }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Prepare release files
|
||||
run: |
|
||||
mkdir -p release
|
||||
cp dist/* release/ 2>/dev/null || true
|
||||
ls -la release/
|
||||
|
||||
- name: Create draft release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
TAG="v${{ env.NEXT_VERSION }}.pr.${PR_NUM}.bid.${{ github.run_id }}"
|
||||
|
||||
# Delete existing draft for this PR if it exists
|
||||
gh release delete "$TAG" -y 2>/dev/null || true
|
||||
git push origin ":refs/tags/$TAG" 2>/dev/null || true
|
||||
|
||||
gh release create "$TAG" \
|
||||
--title "Draft: PR #${PR_NUM}" \
|
||||
--notes "Draft build for PR #${PR_NUM}.
|
||||
|
||||
**Version:** $TAG
|
||||
**PR:** #${PR_NUM}
|
||||
**Commit:** ${PR_SHA}
|
||||
**Built:** $(date -u +'%Y-%m-%d %H:%M:%S UTC')
|
||||
**Run:** ${{ github.run_id }}
|
||||
|
||||
## Channel: Draft
|
||||
|
||||
This is a draft build for testing PR changes before merge.
|
||||
Not intended for production use.
|
||||
|
||||
Build artifacts available for download and testing.
|
||||
" \
|
||||
--draft \
|
||||
--prerelease \
|
||||
release/*
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
||||
name: "PR Build: Pull Request"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
|
||||
env:
|
||||
NEXT_VERSION: "0.0.4"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# Only build if PR is from the same repo (not forks)
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
platform: linux/amd64
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Build
|
||||
uses: host-uk/build@v3
|
||||
with:
|
||||
build-name: core
|
||||
build-platform: ${{ matrix.platform }}
|
||||
build: true
|
||||
package: true
|
||||
sign: false
|
||||
|
||||
draft-release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PR_NUM: ${{ github.event.pull_request.number }}
|
||||
PR_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Prepare release files
|
||||
run: |
|
||||
mkdir -p release
|
||||
cp dist/* release/ 2>/dev/null || true
|
||||
ls -la release/
|
||||
|
||||
- name: Create draft release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
TAG="v${{ env.NEXT_VERSION }}.pr.${PR_NUM}.bid.${{ github.run_id }}"
|
||||
|
||||
# Delete existing draft for this PR if it exists
|
||||
gh release delete "$TAG" -y 2>/dev/null || true
|
||||
git push origin ":refs/tags/$TAG" 2>/dev/null || true
|
||||
|
||||
gh release create "$TAG" \
|
||||
--title "Draft: PR #${PR_NUM}" \
|
||||
--notes "Draft build for PR #${PR_NUM}.
|
||||
|
||||
**Version:** $TAG
|
||||
**PR:** #${PR_NUM}
|
||||
**Commit:** ${PR_SHA}
|
||||
**Built:** $(date -u +'%Y-%m-%d %H:%M:%S UTC')
|
||||
**Run:** ${{ github.run_id }}
|
||||
|
||||
## Channel: Draft
|
||||
|
||||
This is a draft build for testing PR changes before merge.
|
||||
Not intended for production use.
|
||||
|
||||
Build artifacts available for download and testing.
|
||||
" \
|
||||
--draft \
|
||||
--prerelease \
|
||||
release/*
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
name: PR Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to build'
|
||||
required: true
|
||||
type: number
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
|
||||
env:
|
||||
# Next version - update when releasing
|
||||
NEXT_VERSION: "0.0.4"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# Only build if PR is from the same repo (not forks) or manually triggered
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'workflow_dispatch'
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
# GUI build disabled until build action supports Wails v3
|
||||
# - name: Wails Build Action
|
||||
# uses: host-uk/build@v4.0.0
|
||||
# with:
|
||||
# build-name: core
|
||||
# build-platform: ${{ matrix.goos }}/${{ matrix.goarch }}
|
||||
# build: true
|
||||
# package: true
|
||||
# sign: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: host-uk/build/actions/setup/go@v4.0.0
|
||||
with:
|
||||
go-version: "1.25"
|
||||
|
||||
- name: Build CLI
|
||||
run: go build -o ./bin/core .
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: core-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: ./bin/core
|
||||
|
||||
draft-release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# Safe: PR number is numeric, not user-controlled string
|
||||
PR_NUM: ${{ github.event.pull_request.number || inputs.pr_number }}
|
||||
PR_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Prepare release files
|
||||
run: |
|
||||
mkdir -p release
|
||||
cp dist/* release/ 2>/dev/null || true
|
||||
ls -la release/
|
||||
|
||||
- name: Create draft release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Use dots for build metadata (semver v1 compatible)
|
||||
TAG="v${{ env.NEXT_VERSION }}.pr.${PR_NUM}.bid.${{ github.run_id }}"
|
||||
|
||||
# Delete existing draft for this PR if it exists
|
||||
gh release delete "$TAG" -y 2>/dev/null || true
|
||||
git push origin ":refs/tags/$TAG" 2>/dev/null || true
|
||||
|
||||
gh release create "$TAG" \
|
||||
--title "Draft: PR #${PR_NUM}" \
|
||||
--notes "Draft build for PR #${PR_NUM}.
|
||||
|
||||
**Version:** $TAG
|
||||
**PR:** #${PR_NUM}
|
||||
**Commit:** ${PR_SHA}
|
||||
**Built:** $(date -u +'%Y-%m-%d %H:%M:%S UTC')
|
||||
**Run:** ${{ github.run_id }}
|
||||
|
||||
## Channel: Draft
|
||||
|
||||
This is a draft build for testing PR changes before merge.
|
||||
Not intended for production use.
|
||||
|
||||
Build artifacts available for download and testing.
|
||||
" \
|
||||
--draft \
|
||||
--prerelease \
|
||||
release/*
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
name: PR Gate
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
org-gate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check org membership or approval label
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const author = context.payload.pull_request.user.login;
|
||||
const association = context.payload.pull_request.author_association;
|
||||
|
||||
// Trusted accounts
|
||||
const trustedAuthors = ['google-labs-jules[bot]', 'Snider'];
|
||||
if (trustedAuthors.includes(author)) {
|
||||
core.info(`${author} is trusted — gate passed`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check author association
|
||||
const trustedAssociations = ['MEMBER', 'OWNER', 'COLLABORATOR'];
|
||||
if (trustedAssociations.includes(association)) {
|
||||
core.info(`${author} is ${association} — gate passed`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for external-approved label
|
||||
const labels = context.payload.pull_request.labels.map(l => l.name);
|
||||
if (labels.includes('external-approved')) {
|
||||
core.info('external-approved label present — gate passed');
|
||||
return;
|
||||
}
|
||||
|
||||
core.setFailed(
|
||||
`External PR from ${author} requires an org member to add the "external-approved" label before merge.`
|
||||
);
|
||||
|
|
@ -1,454 +0,0 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push
|
||||
name: "Release: Tag Push"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
- os: ubuntu-latest
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
- os: macos-latest
|
||||
goos: darwin
|
||||
goarch: arm64
|
||||
- os: windows-latest
|
||||
goos: windows
|
||||
goarch: amd64
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Go
|
||||
uses: host-uk/build/actions/setup/go@v4.0.0
|
||||
with:
|
||||
go-version: "1.25"
|
||||
|
||||
- name: Build CLI
|
||||
shell: bash
|
||||
run: |
|
||||
EXT=""
|
||||
if [ "$GOOS" = "windows" ]; then EXT=".exe"; fi
|
||||
BINARY="core${EXT}"
|
||||
ARCHIVE_PREFIX="core-${GOOS}-${GOARCH}"
|
||||
|
||||
APP_VERSION="${GITHUB_REF_NAME#v}"
|
||||
go build -ldflags "-s -w -X github.com/host-uk/core/pkg/cli.AppVersion=${APP_VERSION}" -o "./bin/${BINARY}" .
|
||||
|
||||
# Create tar.gz for Homebrew (non-Windows)
|
||||
if [ "$GOOS" != "windows" ]; then
|
||||
tar czf "./bin/${ARCHIVE_PREFIX}.tar.gz" -C ./bin "${BINARY}"
|
||||
fi
|
||||
|
||||
# Create zip for Scoop (Windows)
|
||||
if [ "$GOOS" = "windows" ]; then
|
||||
cd ./bin && zip "${ARCHIVE_PREFIX}.zip" "${BINARY}" && cd ..
|
||||
fi
|
||||
|
||||
# Rename raw binary to platform-specific name for release
|
||||
mv "./bin/${BINARY}" "./bin/${ARCHIVE_PREFIX}${EXT}"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: core-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: ./bin/core-*
|
||||
|
||||
build-ide:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-latest
|
||||
goos: darwin
|
||||
goarch: arm64
|
||||
- os: ubuntu-latest
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
- os: windows-latest
|
||||
goos: windows
|
||||
goarch: amd64
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: internal/core-ide
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Go
|
||||
uses: host-uk/build/actions/setup/go@v4.0.0
|
||||
with:
|
||||
go-version: "1.25"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Install Wails CLI
|
||||
run: go install github.com/wailsapp/wails/v3/cmd/wails3@latest
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: internal/core-ide/frontend
|
||||
run: npm ci
|
||||
|
||||
- name: Generate bindings
|
||||
run: wails3 generate bindings -f '-tags production' -clean=false -ts -i
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: internal/core-ide/frontend
|
||||
run: npm run build
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: matrix.goos == 'linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev
|
||||
|
||||
- name: Build IDE
|
||||
shell: bash
|
||||
run: |
|
||||
EXT=""
|
||||
if [ "$GOOS" = "windows" ]; then EXT=".exe"; fi
|
||||
BINARY="core-ide${EXT}"
|
||||
ARCHIVE_PREFIX="core-ide-${GOOS}-${GOARCH}"
|
||||
|
||||
BUILD_FLAGS="-tags production -trimpath -buildvcs=false"
|
||||
|
||||
if [ "$GOOS" = "windows" ]; then
|
||||
# Windows: no CGO, use windowsgui linker flag
|
||||
export CGO_ENABLED=0
|
||||
LDFLAGS="-w -s -H windowsgui"
|
||||
|
||||
# Generate Windows syso resource
|
||||
cd build
|
||||
wails3 generate syso -arch ${GOARCH} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_${GOARCH}.syso
|
||||
cd ..
|
||||
elif [ "$GOOS" = "darwin" ]; then
|
||||
export CGO_ENABLED=1
|
||||
export CGO_CFLAGS="-mmacosx-version-min=10.15"
|
||||
export CGO_LDFLAGS="-mmacosx-version-min=10.15"
|
||||
export MACOSX_DEPLOYMENT_TARGET="10.15"
|
||||
LDFLAGS="-w -s"
|
||||
else
|
||||
export CGO_ENABLED=1
|
||||
LDFLAGS="-w -s"
|
||||
fi
|
||||
|
||||
go build ${BUILD_FLAGS} -ldflags="${LDFLAGS}" -o "./bin/${BINARY}"
|
||||
|
||||
# Clean up syso files
|
||||
rm -f *.syso
|
||||
|
||||
# Package
|
||||
if [ "$GOOS" = "darwin" ]; then
|
||||
# Create .app bundle
|
||||
mkdir -p "./bin/Core IDE.app/Contents/"{MacOS,Resources}
|
||||
cp build/darwin/icons.icns "./bin/Core IDE.app/Contents/Resources/"
|
||||
cp "./bin/${BINARY}" "./bin/Core IDE.app/Contents/MacOS/"
|
||||
cp build/darwin/Info.plist "./bin/Core IDE.app/Contents/"
|
||||
codesign --force --deep --sign - "./bin/Core IDE.app"
|
||||
tar czf "./bin/${ARCHIVE_PREFIX}.tar.gz" -C ./bin "Core IDE.app"
|
||||
elif [ "$GOOS" = "windows" ]; then
|
||||
cd ./bin && zip "${ARCHIVE_PREFIX}.zip" "${BINARY}" && cd ..
|
||||
else
|
||||
tar czf "./bin/${ARCHIVE_PREFIX}.tar.gz" -C ./bin "${BINARY}"
|
||||
fi
|
||||
|
||||
# Rename raw binary
|
||||
mv "./bin/${BINARY}" "./bin/${ARCHIVE_PREFIX}${EXT}"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: core-ide-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: internal/core-ide/bin/core-ide-*
|
||||
|
||||
release:
|
||||
needs: [build, build-ide]
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set version
|
||||
id: version
|
||||
run: echo "version=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Prepare release files
|
||||
run: |
|
||||
mkdir -p release
|
||||
cp dist/* release/ 2>/dev/null || true
|
||||
ls -la release/
|
||||
|
||||
- name: Create release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
gh release create "$TAG_NAME" \
|
||||
--title "Release $TAG_NAME" \
|
||||
--generate-notes \
|
||||
release/*
|
||||
|
||||
update-tap:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
cd dist
|
||||
for f in *.tar.gz; do
|
||||
sha256sum "$f" | awk '{print $1}' > "${f}.sha256"
|
||||
done
|
||||
echo "=== Checksums ==="
|
||||
cat *.sha256
|
||||
|
||||
- name: Update Homebrew formula
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
VERSION: ${{ needs.release.outputs.version }}
|
||||
run: |
|
||||
# Strip leading 'v' for formula version
|
||||
FORMULA_VERSION="${VERSION#v}"
|
||||
|
||||
# Read checksums
|
||||
DARWIN_ARM64=$(cat dist/core-darwin-arm64.tar.gz.sha256)
|
||||
LINUX_AMD64=$(cat dist/core-linux-amd64.tar.gz.sha256)
|
||||
LINUX_ARM64=$(cat dist/core-linux-arm64.tar.gz.sha256)
|
||||
|
||||
# Clone tap repo (configure auth for push)
|
||||
gh repo clone host-uk/homebrew-tap /tmp/tap -- --depth=1
|
||||
cd /tmp/tap
|
||||
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/host-uk/homebrew-tap.git"
|
||||
cd -
|
||||
mkdir -p /tmp/tap/Formula
|
||||
|
||||
# Write formula
|
||||
cat > /tmp/tap/Formula/core.rb << FORMULA
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Core < Formula
|
||||
desc "Host UK development CLI"
|
||||
homepage "https://github.com/host-uk/core"
|
||||
version "${FORMULA_VERSION}"
|
||||
license "EUPL-1.2"
|
||||
|
||||
on_macos do
|
||||
url "https://github.com/host-uk/core/releases/download/${VERSION}/core-darwin-arm64.tar.gz"
|
||||
sha256 "${DARWIN_ARM64}"
|
||||
end
|
||||
|
||||
on_linux do
|
||||
if Hardware::CPU.arm?
|
||||
url "https://github.com/host-uk/core/releases/download/${VERSION}/core-linux-arm64.tar.gz"
|
||||
sha256 "${LINUX_ARM64}"
|
||||
else
|
||||
url "https://github.com/host-uk/core/releases/download/${VERSION}/core-linux-amd64.tar.gz"
|
||||
sha256 "${LINUX_AMD64}"
|
||||
end
|
||||
end
|
||||
|
||||
def install
|
||||
bin.install "core"
|
||||
end
|
||||
|
||||
test do
|
||||
system "\#{bin}/core", "--version"
|
||||
end
|
||||
end
|
||||
FORMULA
|
||||
|
||||
# Remove leading whitespace from heredoc
|
||||
sed -i 's/^ //' /tmp/tap/Formula/core.rb
|
||||
|
||||
# Read IDE checksums (may not exist if build-ide failed)
|
||||
IDE_DARWIN_ARM64=$(cat dist/core-ide-darwin-arm64.tar.gz.sha256 2>/dev/null || echo "")
|
||||
IDE_LINUX_AMD64=$(cat dist/core-ide-linux-amd64.tar.gz.sha256 2>/dev/null || echo "")
|
||||
|
||||
# Write core-ide Formula (Linux binary)
|
||||
if [ -n "${IDE_LINUX_AMD64}" ]; then
|
||||
cat > /tmp/tap/Formula/core-ide.rb << FORMULA
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CoreIde < Formula
|
||||
desc "Host UK desktop development environment"
|
||||
homepage "https://github.com/host-uk/core"
|
||||
version "${FORMULA_VERSION}"
|
||||
license "EUPL-1.2"
|
||||
|
||||
on_linux do
|
||||
url "https://github.com/host-uk/core/releases/download/${VERSION}/core-ide-linux-amd64.tar.gz"
|
||||
sha256 "${IDE_LINUX_AMD64}"
|
||||
end
|
||||
|
||||
def install
|
||||
bin.install "core-ide"
|
||||
end
|
||||
end
|
||||
FORMULA
|
||||
sed -i 's/^ //' /tmp/tap/Formula/core-ide.rb
|
||||
fi
|
||||
|
||||
# Write core-ide Cask (macOS .app bundle)
|
||||
if [ -n "${IDE_DARWIN_ARM64}" ]; then
|
||||
mkdir -p /tmp/tap/Casks
|
||||
cat > /tmp/tap/Casks/core-ide.rb << CASK
|
||||
cask "core-ide" do
|
||||
version "${FORMULA_VERSION}"
|
||||
sha256 "${IDE_DARWIN_ARM64}"
|
||||
|
||||
url "https://github.com/host-uk/core/releases/download/${VERSION}/core-ide-darwin-arm64.tar.gz"
|
||||
name "Core IDE"
|
||||
desc "Host UK desktop development environment"
|
||||
homepage "https://github.com/host-uk/core"
|
||||
|
||||
app "Core IDE.app"
|
||||
end
|
||||
CASK
|
||||
sed -i 's/^ //' /tmp/tap/Casks/core-ide.rb
|
||||
fi
|
||||
|
||||
cd /tmp/tap
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add .
|
||||
git diff --cached --quiet && echo "No changes to tap" && exit 0
|
||||
git commit -m "Update core to ${FORMULA_VERSION}"
|
||||
git push
|
||||
|
||||
update-scoop:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
cd dist
|
||||
for f in *.zip; do
|
||||
[ -f "$f" ] || continue
|
||||
sha256sum "$f" | awk '{print $1}' > "${f}.sha256"
|
||||
done
|
||||
echo "=== Checksums ==="
|
||||
cat *.sha256 2>/dev/null || echo "No zip checksums"
|
||||
|
||||
- name: Update Scoop manifests
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
VERSION: ${{ needs.release.outputs.version }}
|
||||
run: |
|
||||
# Strip leading 'v' for manifest version
|
||||
MANIFEST_VERSION="${VERSION#v}"
|
||||
|
||||
# Read checksums
|
||||
WIN_AMD64=$(cat dist/core-windows-amd64.zip.sha256 2>/dev/null || echo "")
|
||||
IDE_WIN_AMD64=$(cat dist/core-ide-windows-amd64.zip.sha256 2>/dev/null || echo "")
|
||||
|
||||
# Clone scoop bucket
|
||||
gh repo clone host-uk/scoop-bucket /tmp/scoop -- --depth=1
|
||||
cd /tmp/scoop
|
||||
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/host-uk/scoop-bucket.git"
|
||||
|
||||
# Write core.json manifest
|
||||
cat > core.json << 'MANIFEST'
|
||||
{
|
||||
"version": "VERSION_PLACEHOLDER",
|
||||
"description": "Host UK development CLI",
|
||||
"homepage": "https://github.com/host-uk/core",
|
||||
"license": "EUPL-1.2",
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": "URL_PLACEHOLDER",
|
||||
"hash": "HASH_PLACEHOLDER",
|
||||
"bin": "core.exe"
|
||||
}
|
||||
},
|
||||
"checkver": "github",
|
||||
"autoupdate": {
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": "https://github.com/host-uk/core/releases/download/v$version/core-windows-amd64.zip"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MANIFEST
|
||||
|
||||
sed -i "s|VERSION_PLACEHOLDER|${MANIFEST_VERSION}|g" core.json
|
||||
sed -i "s|URL_PLACEHOLDER|https://github.com/host-uk/core/releases/download/${VERSION}/core-windows-amd64.zip|g" core.json
|
||||
sed -i "s|HASH_PLACEHOLDER|${WIN_AMD64}|g" core.json
|
||||
sed -i 's/^ //' core.json
|
||||
|
||||
# Write core-ide.json manifest
|
||||
if [ -n "${IDE_WIN_AMD64}" ]; then
|
||||
cat > core-ide.json << 'MANIFEST'
|
||||
{
|
||||
"version": "VERSION_PLACEHOLDER",
|
||||
"description": "Host UK desktop development environment",
|
||||
"homepage": "https://github.com/host-uk/core",
|
||||
"license": "EUPL-1.2",
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": "URL_PLACEHOLDER",
|
||||
"hash": "HASH_PLACEHOLDER",
|
||||
"bin": "core-ide.exe"
|
||||
}
|
||||
},
|
||||
"checkver": "github",
|
||||
"autoupdate": {
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": "https://github.com/host-uk/core/releases/download/v$version/core-ide-windows-amd64.zip"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MANIFEST
|
||||
sed -i "s|VERSION_PLACEHOLDER|${MANIFEST_VERSION}|g" core-ide.json
|
||||
sed -i "s|URL_PLACEHOLDER|https://github.com/host-uk/core/releases/download/${VERSION}/core-ide-windows-amd64.zip|g" core-ide.json
|
||||
sed -i "s|HASH_PLACEHOLDER|${IDE_WIN_AMD64}|g" core-ide.json
|
||||
sed -i 's/^ //' core-ide.json
|
||||
fi
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add .
|
||||
git diff --cached --quiet && echo "No changes to scoop bucket" && exit 0
|
||||
git commit -m "Update core to ${MANIFEST_VERSION}"
|
||||
git push
|
||||
|
|
@ -41,7 +41,7 @@ steps:
|
|||
settings:
|
||||
api_key:
|
||||
from_secret: forgejo_token
|
||||
base_url: https://forge.lthn.ai
|
||||
base_url: https://forge.lthn.io
|
||||
files:
|
||||
- bin/bugseti-linux-amd64.tar.gz
|
||||
- bin/bugseti-linux-amd64.tar.gz.sha256
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ steps:
|
|||
- go mod download
|
||||
- >-
|
||||
go build
|
||||
-ldflags "-X github.com/host-uk/core/pkg/cli.AppVersion=ci
|
||||
-X github.com/host-uk/core/pkg/cli.BuildCommit=${CI_COMMIT_SHA:0:7}
|
||||
-X github.com/host-uk/core/pkg/cli.BuildDate=$(date -u +%Y%m%d)"
|
||||
-ldflags "-X forge.lthn.ai/core/cli/pkg/cli.AppVersion=ci
|
||||
-X forge.lthn.ai/core/cli/pkg/cli.BuildCommit=${CI_COMMIT_SHA:0:7}
|
||||
-X forge.lthn.ai/core/cli/pkg/cli.BuildDate=$(date -u +%Y%m%d)"
|
||||
-o ./bin/core .
|
||||
- ./bin/core --version
|
||||
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -1,14 +1,14 @@
|
|||
# Core
|
||||
|
||||
[](https://codecov.io/gh/host-uk/core)
|
||||
[](https://github.com/host-uk/core/actions/workflows/coverage.yml)
|
||||
[](https://github.com/host-uk/core/actions/workflows/codescan.yml)
|
||||
[](https://forge.lthn.ai/core/cli/actions/workflows/coverage.yml)
|
||||
[](https://forge.lthn.ai/core/cli/actions/workflows/codescan.yml)
|
||||
[](https://go.dev/)
|
||||
[](https://opensource.org/licenses/EUPL-1.2)
|
||||
|
||||
Core is a Web3 Framework, written in Go using Wails.io to replace Electron and the bloat of browsers that, at their core, still live in their mum's basement.
|
||||
|
||||
- Repo: https://github.com/host-uk/core
|
||||
- Repo: https://forge.lthn.ai/core/cli
|
||||
|
||||
## Vision
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ Core is an **opinionated Web3 desktop application framework** providing:
|
|||
|
||||
```bash
|
||||
# 1. Install Core
|
||||
go install github.com/host-uk/core/cmd/core@latest
|
||||
go install forge.lthn.ai/core/cli/cmd/core@latest
|
||||
|
||||
# 2. Verify environment
|
||||
core doctor
|
||||
|
|
@ -44,7 +44,7 @@ For more details, see the [User Guide](docs/user-guide.md).
|
|||
## Framework Quick Start (Go)
|
||||
|
||||
```go
|
||||
import core "github.com/host-uk/core/pkg/framework/core"
|
||||
import core "forge.lthn.ai/core/cli/pkg/framework/core"
|
||||
|
||||
app, err := core.New(
|
||||
core.WithServiceLock(),
|
||||
|
|
@ -210,7 +210,7 @@ app.RegisterService(application.NewService(coreService)) // Only Core is regist
|
|||
**Currently exposed** (see `cmd/core-gui/public/bindings/`):
|
||||
```typescript
|
||||
// From frontend:
|
||||
import { ACTION, Config, Service } from './bindings/github.com/host-uk/core/pkg/core'
|
||||
import { ACTION, Config, Service } from './bindings/forge.lthn.ai/core/cli/pkg/core'
|
||||
|
||||
ACTION(msg) // Broadcast IPC message
|
||||
Config() // Get config service reference
|
||||
|
|
@ -259,7 +259,7 @@ Sub-services are accessed via Core's **IPC/ACTION system**, not direct Wails bin
|
|||
|
||||
```typescript
|
||||
// Frontend calls Core.ACTION() with typed messages
|
||||
import { ACTION } from './bindings/github.com/host-uk/core/pkg/core'
|
||||
import { ACTION } from './bindings/forge.lthn.ai/core/cli/pkg/core'
|
||||
|
||||
// Open a window
|
||||
ACTION({ action: "display.open_window", name: "settings", options: { Title: "Settings", Width: 800 } })
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ vars:
|
|||
SEMVER_PRERELEASE:
|
||||
sh: '[ "{{.SEMVER_COMMITS}}" = "0" ] && echo "" || echo "dev.{{.SEMVER_COMMITS}}"'
|
||||
# ldflags
|
||||
PKG: "github.com/host-uk/core/pkg/cli"
|
||||
PKG: "forge.lthn.ai/core/cli/pkg/cli"
|
||||
LDFLAGS_BASE: >-
|
||||
-X {{.PKG}}.AppVersion={{.SEMVER_VERSION}}
|
||||
-X {{.PKG}}.BuildCommit={{.SEMVER_COMMIT}}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ BugSETI is a system tray application that helps developers contribute to open so
|
|||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/host-uk/core.git
|
||||
git clone https://forge.lthn.ai/core/cli.git
|
||||
cd core
|
||||
|
||||
# Build BugSETI
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ description: |
|
|||
it pulls OSS issues from GitHub, AI prepares context,
|
||||
you fix bugs, and it auto-submits PRs.
|
||||
vendor: "Lethean"
|
||||
homepage: "https://github.com/host-uk/core"
|
||||
homepage: "https://forge.lthn.ai/core/cli"
|
||||
license: "MIT"
|
||||
|
||||
contents:
|
||||
|
|
|
|||
|
|
@ -1,40 +1,53 @@
|
|||
module github.com/host-uk/core/cmd/bugseti
|
||||
module forge.lthn.ai/core/cli/cmd/bugseti
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
forge.lthn.ai/core/cli v0.0.0
|
||||
forge.lthn.ai/core/cli/internal/bugseti v0.0.0
|
||||
forge.lthn.ai/core/cli/internal/bugseti/updater v0.0.0
|
||||
github.com/Snider/Borg v0.2.0
|
||||
github.com/host-uk/core v0.0.0
|
||||
github.com/host-uk/core/internal/bugseti v0.0.0
|
||||
github.com/host-uk/core/internal/bugseti/updater v0.0.0
|
||||
forge.lthn.ai/core/cli v0.0.0
|
||||
forge.lthn.ai/core/cli/internal/bugseti v0.0.0
|
||||
forge.lthn.ai/core/cli/internal/bugseti/updater v0.0.0
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.64
|
||||
)
|
||||
|
||||
replace github.com/host-uk/core => ../..
|
||||
replace forge.lthn.ai/core/cli => ../..
|
||||
|
||||
replace github.com/host-uk/core/internal/bugseti => ../../internal/bugseti
|
||||
replace forge.lthn.ai/core/cli/internal/bugseti => ../../internal/bugseti
|
||||
|
||||
replace github.com/host-uk/core/internal/bugseti/updater => ../../internal/bugseti/updater
|
||||
replace forge.lthn.ai/core/cli/internal/bugseti/updater => ../../internal/bugseti/updater
|
||||
|
||||
require (
|
||||
codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/42wim/httpsig v1.2.3 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/Snider/Enchantrix v0.0.2 // indirect
|
||||
github.com/adrg/xdg v0.5.3 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/coder/websocket v1.8.14 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.7.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.16.4 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
|
||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
||||
|
|
@ -42,20 +55,34 @@ require (
|
|||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/lmittmann/tint v1.1.2 // indirect
|
||||
github.com/mailru/easyjson v0.9.1 // indirect
|
||||
github.com/mark3labs/mcp-go v0.43.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spf13/viper v1.21.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.23 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 h1:HTCWpzyWQOHDWt3LzI6/d2jvUDsw/vgGRWm/8BTvcqI=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
|
|
@ -15,8 +17,10 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
|
|||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
|
|
@ -27,14 +31,18 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
|
||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
|
||||
|
|
@ -47,6 +55,7 @@ github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRko
|
|||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
|
|
@ -55,6 +64,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
|
||||
|
|
@ -76,6 +87,8 @@ github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
|||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
|
||||
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
|
||||
github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
|
|
@ -85,6 +98,7 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
|
|
@ -99,6 +113,7 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
|
|
@ -106,17 +121,26 @@ github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepq
|
|||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
|
||||
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0=
|
||||
github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.64 h1:xAhLFVfdbg7XdZQ5mMQmBv2BglWu8hMqe50Z+3UJvBs=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.64/go.mod h1:zvgNL/mlFcX8aRGu6KOz9AHrMmTBD+4hJRQIONqF/Yw=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// BugSETI - "Distributed Bug Fixing like SETI@home but for code"
|
||||
//
|
||||
// The application runs as a system tray app that:
|
||||
// - Pulls OSS issues from GitHub
|
||||
// - Pulls OSS issues from Forgejo
|
||||
// - Uses AI to prepare context for each issue
|
||||
// - Presents issues to users for fixing
|
||||
// - Automates PR submission
|
||||
|
|
@ -16,9 +16,9 @@ import (
|
|||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/host-uk/core/cmd/bugseti/icons"
|
||||
"github.com/host-uk/core/internal/bugseti"
|
||||
"github.com/host-uk/core/internal/bugseti/updater"
|
||||
"forge.lthn.ai/core/cli/cmd/bugseti/icons"
|
||||
"forge.lthn.ai/core/cli/internal/bugseti"
|
||||
"forge.lthn.ai/core/cli/internal/bugseti/updater"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
|
@ -39,13 +39,20 @@ func main() {
|
|||
log.Printf("Warning: Could not load config: %v", err)
|
||||
}
|
||||
|
||||
// Check Forgejo API availability
|
||||
forgeClient, err := bugseti.CheckForge()
|
||||
if err != nil {
|
||||
log.Fatalf("Forgejo check failed: %v\n\nConfigure with: core forge config --url URL --token TOKEN", err)
|
||||
}
|
||||
|
||||
// Initialize core services
|
||||
notifyService := bugseti.NewNotifyService(configService)
|
||||
statsService := bugseti.NewStatsService(configService)
|
||||
fetcherService := bugseti.NewFetcherService(configService, notifyService)
|
||||
fetcherService := bugseti.NewFetcherService(configService, notifyService, forgeClient)
|
||||
queueService := bugseti.NewQueueService(configService)
|
||||
seederService := bugseti.NewSeederService(configService)
|
||||
submitService := bugseti.NewSubmitService(configService, notifyService, statsService)
|
||||
seederService := bugseti.NewSeederService(configService, forgeClient.URL(), forgeClient.Token())
|
||||
submitService := bugseti.NewSubmitService(configService, notifyService, statsService, forgeClient)
|
||||
hubService := bugseti.NewHubService(configService)
|
||||
versionService := bugseti.NewVersionService()
|
||||
workspaceService := NewWorkspaceService(configService)
|
||||
|
||||
|
|
@ -69,6 +76,7 @@ func main() {
|
|||
application.NewService(submitService),
|
||||
application.NewService(versionService),
|
||||
application.NewService(workspaceService),
|
||||
application.NewService(hubService),
|
||||
application.NewService(trayService),
|
||||
}
|
||||
|
||||
|
|
@ -107,6 +115,19 @@ func main() {
|
|||
log.Println(" - Waiting for issues...")
|
||||
log.Printf(" - Version: %s (%s)", bugseti.GetVersion(), bugseti.GetChannel())
|
||||
|
||||
// Attempt hub registration (non-blocking)
|
||||
if hubURL := configService.GetHubURL(); hubURL != "" {
|
||||
if err := hubService.AutoRegister(); err != nil {
|
||||
log.Printf(" - Hub: auto-register skipped: %v", err)
|
||||
} else if err := hubService.Register(); err != nil {
|
||||
log.Printf(" - Hub: registration failed: %v", err)
|
||||
} else {
|
||||
log.Println(" - Hub: registered with portal")
|
||||
}
|
||||
} else {
|
||||
log.Println(" - Hub: not configured (set hubUrl in config)")
|
||||
}
|
||||
|
||||
if err := app.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import (
|
|||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/host-uk/core/internal/bugseti"
|
||||
"forge.lthn.ai/core/cli/internal/bugseti"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,22 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Snider/Borg/pkg/tim"
|
||||
"github.com/host-uk/core/internal/bugseti"
|
||||
"github.com/host-uk/core/pkg/io/datanode"
|
||||
"forge.lthn.ai/core/cli/internal/bugseti"
|
||||
"forge.lthn.ai/core/cli/pkg/io/datanode"
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultMaxWorkspaces is the fallback upper bound when config is unavailable.
|
||||
defaultMaxWorkspaces = 100
|
||||
// defaultWorkspaceTTL is the fallback TTL when config is unavailable.
|
||||
defaultWorkspaceTTL = 24 * time.Hour
|
||||
// sweepInterval is how often the background sweeper runs.
|
||||
sweepInterval = 5 * time.Minute
|
||||
)
|
||||
|
||||
// WorkspaceService manages DataNode-backed workspaces for issues.
|
||||
|
|
@ -20,8 +30,10 @@ import (
|
|||
// snapshotted, packaged as a TIM container, or shipped as a crash report.
|
||||
type WorkspaceService struct {
|
||||
config *bugseti.ConfigService
|
||||
workspaces map[string]*Workspace // issue ID → workspace
|
||||
workspaces map[string]*Workspace // issue ID -> workspace
|
||||
mu sync.RWMutex
|
||||
done chan struct{} // signals the background sweeper to stop
|
||||
stopped chan struct{} // closed when the sweeper goroutine exits
|
||||
}
|
||||
|
||||
// Workspace tracks a DataNode-backed workspace for an issue.
|
||||
|
|
@ -47,10 +59,13 @@ type CrashReport struct {
|
|||
}
|
||||
|
||||
// NewWorkspaceService creates a new WorkspaceService.
|
||||
// Call Start() to begin the background TTL sweeper.
|
||||
func NewWorkspaceService(config *bugseti.ConfigService) *WorkspaceService {
|
||||
return &WorkspaceService{
|
||||
config: config,
|
||||
workspaces: make(map[string]*Workspace),
|
||||
done: make(chan struct{}),
|
||||
stopped: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,6 +74,56 @@ func (w *WorkspaceService) ServiceName() string {
|
|||
return "WorkspaceService"
|
||||
}
|
||||
|
||||
// Start launches the background sweeper goroutine that periodically
|
||||
// evicts expired workspaces. This prevents unbounded map growth even
|
||||
// when no new Capture calls arrive.
|
||||
func (w *WorkspaceService) Start() {
|
||||
go func() {
|
||||
defer close(w.stopped)
|
||||
ticker := time.NewTicker(sweepInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
w.mu.Lock()
|
||||
evicted := w.cleanup()
|
||||
w.mu.Unlock()
|
||||
if evicted > 0 {
|
||||
log.Printf("Workspace sweeper: evicted %d stale entries, %d remaining", evicted, w.ActiveWorkspaces())
|
||||
}
|
||||
case <-w.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
log.Printf("Workspace sweeper started (interval=%s, ttl=%s, max=%d)",
|
||||
sweepInterval, w.ttl(), w.maxCap())
|
||||
}
|
||||
|
||||
// Stop signals the background sweeper to exit and waits for it to finish.
|
||||
func (w *WorkspaceService) Stop() {
|
||||
close(w.done)
|
||||
<-w.stopped
|
||||
log.Printf("Workspace sweeper stopped")
|
||||
}
|
||||
|
||||
// ttl returns the configured workspace TTL, falling back to the default.
|
||||
func (w *WorkspaceService) ttl() time.Duration {
|
||||
if w.config != nil {
|
||||
return w.config.GetWorkspaceTTL()
|
||||
}
|
||||
return defaultWorkspaceTTL
|
||||
}
|
||||
|
||||
// maxCap returns the configured max workspace count, falling back to the default.
|
||||
func (w *WorkspaceService) maxCap() int {
|
||||
if w.config != nil {
|
||||
return w.config.GetMaxWorkspaces()
|
||||
}
|
||||
return defaultMaxWorkspaces
|
||||
}
|
||||
|
||||
// Capture loads a filesystem workspace into a DataNode Medium.
|
||||
// Call this after git clone to create the in-memory snapshot.
|
||||
func (w *WorkspaceService) Capture(issue *bugseti.Issue, diskPath string) error {
|
||||
|
|
@ -109,6 +174,7 @@ func (w *WorkspaceService) Capture(issue *bugseti.Issue, diskPath string) error
|
|||
}
|
||||
|
||||
w.mu.Lock()
|
||||
w.cleanup()
|
||||
w.workspaces[issue.ID] = &Workspace{
|
||||
Issue: issue,
|
||||
Medium: m,
|
||||
|
|
@ -240,6 +306,46 @@ func (w *WorkspaceService) SaveCrashReport(report *CrashReport) (string, error)
|
|||
return path, nil
|
||||
}
|
||||
|
||||
// cleanup evicts expired workspaces and enforces the max size cap.
|
||||
// Must be called with w.mu held for writing.
|
||||
// Returns the number of evicted entries.
|
||||
func (w *WorkspaceService) cleanup() int {
|
||||
now := time.Now()
|
||||
ttl := w.ttl()
|
||||
cap := w.maxCap()
|
||||
evicted := 0
|
||||
|
||||
// First pass: evict entries older than TTL.
|
||||
for id, ws := range w.workspaces {
|
||||
if now.Sub(ws.CreatedAt) > ttl {
|
||||
delete(w.workspaces, id)
|
||||
evicted++
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: if still over cap, evict oldest entries.
|
||||
if len(w.workspaces) > cap {
|
||||
type entry struct {
|
||||
id string
|
||||
createdAt time.Time
|
||||
}
|
||||
entries := make([]entry, 0, len(w.workspaces))
|
||||
for id, ws := range w.workspaces {
|
||||
entries = append(entries, entry{id, ws.CreatedAt})
|
||||
}
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return entries[i].createdAt.Before(entries[j].createdAt)
|
||||
})
|
||||
toEvict := len(w.workspaces) - cap
|
||||
for i := 0; i < toEvict; i++ {
|
||||
delete(w.workspaces, entries[i].id)
|
||||
evicted++
|
||||
}
|
||||
}
|
||||
|
||||
return evicted
|
||||
}
|
||||
|
||||
// Release removes a workspace from memory.
|
||||
func (w *WorkspaceService) Release(issueID string) {
|
||||
w.mu.Lock()
|
||||
|
|
|
|||
151
cmd/bugseti/workspace_test.go
Normal file
151
cmd/bugseti/workspace_test.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forge.lthn.ai/core/cli/internal/bugseti"
|
||||
)
|
||||
|
||||
func TestCleanup_TTL(t *testing.T) {
|
||||
svc := NewWorkspaceService(bugseti.NewConfigService())
|
||||
|
||||
// Seed with entries that are older than TTL.
|
||||
svc.mu.Lock()
|
||||
for i := 0; i < 5; i++ {
|
||||
svc.workspaces[fmt.Sprintf("old-%d", i)] = &Workspace{
|
||||
CreatedAt: time.Now().Add(-25 * time.Hour),
|
||||
}
|
||||
}
|
||||
// Add one fresh entry.
|
||||
svc.workspaces["fresh"] = &Workspace{
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
svc.cleanup()
|
||||
svc.mu.Unlock()
|
||||
|
||||
if got := svc.ActiveWorkspaces(); got != 1 {
|
||||
t.Errorf("expected 1 workspace after TTL cleanup, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanup_MaxSize(t *testing.T) {
|
||||
svc := NewWorkspaceService(bugseti.NewConfigService())
|
||||
|
||||
maxCap := svc.maxCap()
|
||||
|
||||
// Fill beyond the cap with fresh entries.
|
||||
svc.mu.Lock()
|
||||
for i := 0; i < maxCap+20; i++ {
|
||||
svc.workspaces[fmt.Sprintf("ws-%d", i)] = &Workspace{
|
||||
CreatedAt: time.Now().Add(-time.Duration(i) * time.Minute),
|
||||
}
|
||||
}
|
||||
svc.cleanup()
|
||||
svc.mu.Unlock()
|
||||
|
||||
if got := svc.ActiveWorkspaces(); got != maxCap {
|
||||
t.Errorf("expected %d workspaces after cap cleanup, got %d", maxCap, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanup_EvictsOldestWhenOverCap(t *testing.T) {
|
||||
svc := NewWorkspaceService(bugseti.NewConfigService())
|
||||
|
||||
maxCap := svc.maxCap()
|
||||
|
||||
// Create maxCap+1 entries; the newest should survive.
|
||||
svc.mu.Lock()
|
||||
for i := 0; i <= maxCap; i++ {
|
||||
svc.workspaces[fmt.Sprintf("ws-%d", i)] = &Workspace{
|
||||
CreatedAt: time.Now().Add(-time.Duration(maxCap-i) * time.Minute),
|
||||
}
|
||||
}
|
||||
svc.cleanup()
|
||||
svc.mu.Unlock()
|
||||
|
||||
// The newest entry (ws-<maxCap>) should still exist.
|
||||
newest := fmt.Sprintf("ws-%d", maxCap)
|
||||
|
||||
svc.mu.RLock()
|
||||
_, exists := svc.workspaces[newest]
|
||||
svc.mu.RUnlock()
|
||||
if !exists {
|
||||
t.Error("expected newest workspace to survive eviction")
|
||||
}
|
||||
|
||||
// The oldest entry (ws-0) should have been evicted.
|
||||
svc.mu.RLock()
|
||||
_, exists = svc.workspaces["ws-0"]
|
||||
svc.mu.RUnlock()
|
||||
if exists {
|
||||
t.Error("expected oldest workspace to be evicted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanup_ReturnsEvictedCount(t *testing.T) {
|
||||
svc := NewWorkspaceService(bugseti.NewConfigService())
|
||||
|
||||
svc.mu.Lock()
|
||||
for i := 0; i < 3; i++ {
|
||||
svc.workspaces[fmt.Sprintf("old-%d", i)] = &Workspace{
|
||||
CreatedAt: time.Now().Add(-25 * time.Hour),
|
||||
}
|
||||
}
|
||||
svc.workspaces["fresh"] = &Workspace{
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
evicted := svc.cleanup()
|
||||
svc.mu.Unlock()
|
||||
|
||||
if evicted != 3 {
|
||||
t.Errorf("expected 3 evicted entries, got %d", evicted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartStop(t *testing.T) {
|
||||
svc := NewWorkspaceService(bugseti.NewConfigService())
|
||||
svc.Start()
|
||||
|
||||
// Add a stale entry while the sweeper is running.
|
||||
svc.mu.Lock()
|
||||
svc.workspaces["stale"] = &Workspace{
|
||||
CreatedAt: time.Now().Add(-25 * time.Hour),
|
||||
}
|
||||
svc.mu.Unlock()
|
||||
|
||||
// Stop should return without hanging.
|
||||
svc.Stop()
|
||||
}
|
||||
|
||||
func TestConfigurableTTL(t *testing.T) {
|
||||
cfg := bugseti.NewConfigService()
|
||||
svc := NewWorkspaceService(cfg)
|
||||
|
||||
// Default TTL should be 24h (1440 minutes).
|
||||
if got := svc.ttl(); got != 24*time.Hour {
|
||||
t.Errorf("expected default TTL of 24h, got %s", got)
|
||||
}
|
||||
|
||||
// Default max cap should be 100.
|
||||
if got := svc.maxCap(); got != 100 {
|
||||
t.Errorf("expected default max cap of 100, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNilConfigFallback(t *testing.T) {
|
||||
svc := &WorkspaceService{
|
||||
config: nil,
|
||||
workspaces: make(map[string]*Workspace),
|
||||
done: make(chan struct{}),
|
||||
stopped: make(chan struct{}),
|
||||
}
|
||||
|
||||
if got := svc.ttl(); got != defaultWorkspaceTTL {
|
||||
t.Errorf("expected fallback TTL %s, got %s", defaultWorkspaceTTL, got)
|
||||
}
|
||||
if got := svc.maxCap(); got != defaultMaxWorkspaces {
|
||||
t.Errorf("expected fallback max cap %d, got %d", defaultMaxWorkspaces, got)
|
||||
}
|
||||
}
|
||||
|
|
@ -186,7 +186,7 @@
|
|||
<div class="flex items-center gap-6 text-sm">
|
||||
<a href="#how-it-works" class="text-lethean-400 hover:text-lethean-200 transition-colors link-underline">How it works</a>
|
||||
<a href="#ecosystem" class="text-lethean-400 hover:text-lethean-200 transition-colors link-underline">Ecosystem</a>
|
||||
<a href="https://github.com/host-uk/core" target="_blank" rel="noopener" class="text-lethean-400 hover:text-lethean-200 transition-colors link-underline">GitHub</a>
|
||||
<a href="https://forge.lthn.ai/core/cli" target="_blank" rel="noopener" class="text-lethean-400 hover:text-lethean-200 transition-colors link-underline">GitHub</a>
|
||||
<a href="#join" class="inline-flex items-center gap-1.5 px-4 py-1.5 rounded-md bg-cyan-400/10 text-cyan-400 border border-cyan-400/20 hover:bg-cyan-400/20 hover:border-cyan-400/30 transition-all text-sm font-medium">
|
||||
Get BugSETI
|
||||
</a>
|
||||
|
|
@ -249,7 +249,7 @@
|
|||
Download BugSETI
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/></svg>
|
||||
</a>
|
||||
<a href="https://github.com/host-uk/core" target="_blank" rel="noopener" class="inline-flex items-center gap-2 px-6 py-3 rounded-lg border border-lethean-600/50 text-lethean-300 font-medium text-sm hover:bg-lethean-800/50 hover:border-lethean-500/50 transition-all">
|
||||
<a href="https://forge.lthn.ai/core/cli" target="_blank" rel="noopener" class="inline-flex items-center gap-2 px-6 py-3 rounded-lg border border-lethean-600/50 text-lethean-300 font-medium text-sm hover:bg-lethean-800/50 hover:border-lethean-500/50 transition-all">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
|
||||
View Source
|
||||
</a>
|
||||
|
|
@ -518,13 +518,13 @@
|
|||
|
||||
<!-- Download buttons -->
|
||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-3 mb-12">
|
||||
<a href="https://github.com/host-uk/core/releases" target="_blank" rel="noopener" class="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-6 py-3.5 rounded-lg bg-lethean-800 border border-lethean-600/40 text-lethean-200 font-medium text-sm hover:bg-lethean-700 hover:border-lethean-500/50 transition-all">
|
||||
<a href="https://forge.lthn.ai/core/cli/releases" target="_blank" rel="noopener" class="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-6 py-3.5 rounded-lg bg-lethean-800 border border-lethean-600/40 text-lethean-200 font-medium text-sm hover:bg-lethean-700 hover:border-lethean-500/50 transition-all">
|
||||
<span class="text-lg">🐧</span> Linux
|
||||
</a>
|
||||
<a href="https://github.com/host-uk/core/releases" target="_blank" rel="noopener" class="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-6 py-3.5 rounded-lg bg-lethean-800 border border-lethean-600/40 text-lethean-200 font-medium text-sm hover:bg-lethean-700 hover:border-lethean-500/50 transition-all">
|
||||
<a href="https://forge.lthn.ai/core/cli/releases" target="_blank" rel="noopener" class="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-6 py-3.5 rounded-lg bg-lethean-800 border border-lethean-600/40 text-lethean-200 font-medium text-sm hover:bg-lethean-700 hover:border-lethean-500/50 transition-all">
|
||||
<span class="text-lg">🍎</span> macOS
|
||||
</a>
|
||||
<a href="https://github.com/host-uk/core/releases" target="_blank" rel="noopener" class="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-6 py-3.5 rounded-lg bg-lethean-800 border border-lethean-600/40 text-lethean-200 font-medium text-sm hover:bg-lethean-700 hover:border-lethean-500/50 transition-all">
|
||||
<a href="https://forge.lthn.ai/core/cli/releases" target="_blank" rel="noopener" class="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-6 py-3.5 rounded-lg bg-lethean-800 border border-lethean-600/40 text-lethean-200 font-medium text-sm hover:bg-lethean-700 hover:border-lethean-500/50 transition-all">
|
||||
<span class="text-lg">🪟</span> Windows
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -533,7 +533,7 @@
|
|||
<div class="gradient-border rounded-lg overflow-hidden max-w-md mx-auto">
|
||||
<div class="bg-lethean-900 rounded-lg px-5 py-3 font-mono text-sm text-left">
|
||||
<span class="text-lethean-500"># or build from source</span><br>
|
||||
<span class="text-cyan-400">$</span> <span class="text-lethean-300">git clone https://github.com/host-uk/core</span><br>
|
||||
<span class="text-cyan-400">$</span> <span class="text-lethean-300">git clone https://forge.lthn.ai/core/cli</span><br>
|
||||
<span class="text-cyan-400">$</span> <span class="text-lethean-300">cd core && go build ./cmd/bugseti</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ go build -tags nowatcher -o ../../bin/core-app .
|
|||
## CRITICAL WARNINGS
|
||||
|
||||
- **DO NOT push to GitHub** — GitHub remotes have been removed deliberately. The host-uk org is flagged.
|
||||
- **DO NOT add GitHub as a remote** — Forge (forge.lthn.ai / git.lthn.ai) is the source of truth.
|
||||
- **DO NOT add GitHub as a remote** — Forge (forge.lthn.io / git.lthn.ai) is the source of truth.
|
||||
- **DO NOT modify files outside `cmd/core-app/`** — This is a workspace module, keep changes scoped.
|
||||
- **DO NOT remove the `-tags nowatcher` build flag** — It will fail without libwatcher-c.
|
||||
- **DO NOT change the PHP-ZTS path** — It must be the ZTS variant, not the default Homebrew PHP.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
module github.com/host-uk/core/cmd/core-app
|
||||
module forge.lthn.ai/core/cli/cmd/core-app
|
||||
|
||||
go 1.25.5
|
||||
|
||||
|
|
@ -64,4 +64,4 @@ require (
|
|||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
||||
replace github.com/host-uk/core => ../..
|
||||
replace forge.lthn.ai/core/cli => ../..
|
||||
|
|
|
|||
46
cmd/core-app/laravel/app/Http/Middleware/QuotaMiddleware.php
Normal file
46
cmd/core-app/laravel/app/Http/Middleware/QuotaMiddleware.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\AllowanceService;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class QuotaMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AllowanceService $allowanceService,
|
||||
) {}
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$agentId = $request->header('X-Agent-ID', $request->input('agent_id', ''));
|
||||
$model = $request->input('model', '');
|
||||
|
||||
if ($agentId === '') {
|
||||
return response()->json([
|
||||
'error' => 'agent_id is required',
|
||||
], 400);
|
||||
}
|
||||
|
||||
$result = $this->allowanceService->check($agentId, $model);
|
||||
|
||||
if (! $result['allowed']) {
|
||||
return response()->json([
|
||||
'error' => 'quota_exceeded',
|
||||
'status' => $result['status'],
|
||||
'reason' => $result['reason'],
|
||||
'remaining_tokens' => $result['remaining_tokens'],
|
||||
'remaining_jobs' => $result['remaining_jobs'],
|
||||
], 429);
|
||||
}
|
||||
|
||||
// Attach quota info to request for downstream use
|
||||
$request->merge(['_quota' => $result]);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
111
cmd/core-app/laravel/app/Livewire/Dashboard/ActivityFeed.php
Normal file
111
cmd/core-app/laravel/app/Livewire/Dashboard/ActivityFeed.php
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Livewire\Dashboard;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class ActivityFeed extends Component
|
||||
{
|
||||
public array $entries = [];
|
||||
public string $agentFilter = 'all';
|
||||
public string $typeFilter = 'all';
|
||||
public bool $showOnlyQuestions = false;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadEntries();
|
||||
}
|
||||
|
||||
public function loadEntries(): void
|
||||
{
|
||||
// Placeholder data — will be replaced with real-time WebSocket feed
|
||||
$this->entries = [
|
||||
[
|
||||
'id' => 'act-001',
|
||||
'agent' => 'Athena',
|
||||
'type' => 'code_write',
|
||||
'message' => 'Created AgentFleet Livewire component',
|
||||
'job' => '#96',
|
||||
'timestamp' => now()->subMinutes(2)->toIso8601String(),
|
||||
'is_question' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'act-002',
|
||||
'agent' => 'Athena',
|
||||
'type' => 'tool_call',
|
||||
'message' => 'Read file: cmd/core-app/laravel/composer.json',
|
||||
'job' => '#96',
|
||||
'timestamp' => now()->subMinutes(5)->toIso8601String(),
|
||||
'is_question' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'act-003',
|
||||
'agent' => 'Clotho',
|
||||
'type' => 'question',
|
||||
'message' => 'Should I apply the fix to both the TCP and Unix socket transports, or just TCP?',
|
||||
'job' => '#84',
|
||||
'timestamp' => now()->subMinutes(8)->toIso8601String(),
|
||||
'is_question' => true,
|
||||
],
|
||||
[
|
||||
'id' => 'act-004',
|
||||
'agent' => 'Virgil',
|
||||
'type' => 'pr_created',
|
||||
'message' => 'Opened PR #89: fix WebSocket reconnection logic',
|
||||
'job' => '#89',
|
||||
'timestamp' => now()->subMinutes(15)->toIso8601String(),
|
||||
'is_question' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'act-005',
|
||||
'agent' => 'Virgil',
|
||||
'type' => 'test_run',
|
||||
'message' => 'All 47 tests passed (0.8s)',
|
||||
'job' => '#89',
|
||||
'timestamp' => now()->subMinutes(18)->toIso8601String(),
|
||||
'is_question' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'act-006',
|
||||
'agent' => 'Athena',
|
||||
'type' => 'git_push',
|
||||
'message' => 'Pushed branch feat/agentic-dashboard',
|
||||
'job' => '#96',
|
||||
'timestamp' => now()->subMinutes(22)->toIso8601String(),
|
||||
'is_question' => false,
|
||||
],
|
||||
[
|
||||
'id' => 'act-007',
|
||||
'agent' => 'Clotho',
|
||||
'type' => 'code_write',
|
||||
'message' => 'Added input validation for MCP file_write paths',
|
||||
'job' => '#84',
|
||||
'timestamp' => now()->subMinutes(30)->toIso8601String(),
|
||||
'is_question' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getFilteredEntriesProperty(): array
|
||||
{
|
||||
return array_filter($this->entries, function ($entry) {
|
||||
if ($this->showOnlyQuestions && !$entry['is_question']) {
|
||||
return false;
|
||||
}
|
||||
if ($this->agentFilter !== 'all' && $entry['agent'] !== $this->agentFilter) {
|
||||
return false;
|
||||
}
|
||||
if ($this->typeFilter !== 'all' && $entry['type'] !== $this->typeFilter) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.dashboard.activity-feed');
|
||||
}
|
||||
}
|
||||
85
cmd/core-app/laravel/app/Livewire/Dashboard/AgentFleet.php
Normal file
85
cmd/core-app/laravel/app/Livewire/Dashboard/AgentFleet.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Livewire\Dashboard;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class AgentFleet extends Component
|
||||
{
|
||||
/** @var array<int, array{name: string, host: string, model: string, status: string, job: string, heartbeat: string, uptime: string}> */
|
||||
public array $agents = [];
|
||||
|
||||
public ?string $selectedAgent = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadAgents();
|
||||
}
|
||||
|
||||
public function loadAgents(): void
|
||||
{
|
||||
// Placeholder data — will be replaced with real API calls to Go backend
|
||||
$this->agents = [
|
||||
[
|
||||
'id' => 'athena',
|
||||
'name' => 'Athena',
|
||||
'host' => 'studio.snider.dev',
|
||||
'model' => 'claude-opus-4-6',
|
||||
'status' => 'working',
|
||||
'job' => '#96 agentic dashboard',
|
||||
'heartbeat' => 'green',
|
||||
'uptime' => '4h 23m',
|
||||
'tokens_today' => 142_580,
|
||||
'jobs_completed' => 3,
|
||||
],
|
||||
[
|
||||
'id' => 'virgil',
|
||||
'name' => 'Virgil',
|
||||
'host' => 'studio.snider.dev',
|
||||
'model' => 'claude-opus-4-6',
|
||||
'status' => 'idle',
|
||||
'job' => '',
|
||||
'heartbeat' => 'green',
|
||||
'uptime' => '12h 07m',
|
||||
'tokens_today' => 89_230,
|
||||
'jobs_completed' => 5,
|
||||
],
|
||||
[
|
||||
'id' => 'clotho',
|
||||
'name' => 'Clotho',
|
||||
'host' => 'darwin-au',
|
||||
'model' => 'claude-sonnet-4-5',
|
||||
'status' => 'working',
|
||||
'job' => '#84 security audit',
|
||||
'heartbeat' => 'yellow',
|
||||
'uptime' => '1h 45m',
|
||||
'tokens_today' => 34_100,
|
||||
'jobs_completed' => 1,
|
||||
],
|
||||
[
|
||||
'id' => 'charon',
|
||||
'name' => 'Charon',
|
||||
'host' => 'linux.snider.dev',
|
||||
'model' => 'claude-haiku-4-5',
|
||||
'status' => 'unhealthy',
|
||||
'job' => '',
|
||||
'heartbeat' => 'red',
|
||||
'uptime' => '0m',
|
||||
'tokens_today' => 0,
|
||||
'jobs_completed' => 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function selectAgent(string $agentId): void
|
||||
{
|
||||
$this->selectedAgent = $this->selectedAgent === $agentId ? null : $agentId;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.dashboard.agent-fleet');
|
||||
}
|
||||
}
|
||||
93
cmd/core-app/laravel/app/Livewire/Dashboard/HumanActions.php
Normal file
93
cmd/core-app/laravel/app/Livewire/Dashboard/HumanActions.php
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Livewire\Dashboard;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class HumanActions extends Component
|
||||
{
|
||||
public array $pendingQuestions = [];
|
||||
public array $reviewGates = [];
|
||||
public string $answerText = '';
|
||||
public ?string $answeringId = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadPending();
|
||||
}
|
||||
|
||||
public function loadPending(): void
|
||||
{
|
||||
// Placeholder data — will be replaced with real data from Go backend
|
||||
$this->pendingQuestions = [
|
||||
[
|
||||
'id' => 'q-001',
|
||||
'agent' => 'Clotho',
|
||||
'job' => '#84',
|
||||
'question' => 'Should I apply the fix to both the TCP and Unix socket transports, or just TCP?',
|
||||
'asked_at' => now()->subMinutes(8)->toIso8601String(),
|
||||
'context' => 'Working on security audit — found unvalidated input in transport layer.',
|
||||
],
|
||||
];
|
||||
|
||||
$this->reviewGates = [
|
||||
[
|
||||
'id' => 'rg-001',
|
||||
'agent' => 'Virgil',
|
||||
'job' => '#89',
|
||||
'type' => 'pr_review',
|
||||
'title' => 'PR #89: fix WebSocket reconnection logic',
|
||||
'description' => 'Adds exponential backoff and connection state tracking.',
|
||||
'submitted_at' => now()->subMinutes(15)->toIso8601String(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function startAnswer(string $questionId): void
|
||||
{
|
||||
$this->answeringId = $questionId;
|
||||
$this->answerText = '';
|
||||
}
|
||||
|
||||
public function submitAnswer(): void
|
||||
{
|
||||
if (! $this->answeringId || trim($this->answerText) === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove answered question from list
|
||||
$this->pendingQuestions = array_values(
|
||||
array_filter($this->pendingQuestions, fn ($q) => $q['id'] !== $this->answeringId)
|
||||
);
|
||||
|
||||
$this->answeringId = null;
|
||||
$this->answerText = '';
|
||||
}
|
||||
|
||||
public function cancelAnswer(): void
|
||||
{
|
||||
$this->answeringId = null;
|
||||
$this->answerText = '';
|
||||
}
|
||||
|
||||
public function approveGate(string $gateId): void
|
||||
{
|
||||
$this->reviewGates = array_values(
|
||||
array_filter($this->reviewGates, fn ($g) => $g['id'] !== $gateId)
|
||||
);
|
||||
}
|
||||
|
||||
public function rejectGate(string $gateId): void
|
||||
{
|
||||
$this->reviewGates = array_values(
|
||||
array_filter($this->reviewGates, fn ($g) => $g['id'] !== $gateId)
|
||||
);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.dashboard.human-actions');
|
||||
}
|
||||
}
|
||||
125
cmd/core-app/laravel/app/Livewire/Dashboard/JobQueue.php
Normal file
125
cmd/core-app/laravel/app/Livewire/Dashboard/JobQueue.php
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Livewire\Dashboard;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class JobQueue extends Component
|
||||
{
|
||||
public array $jobs = [];
|
||||
public string $statusFilter = 'all';
|
||||
public string $agentFilter = 'all';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadJobs();
|
||||
}
|
||||
|
||||
public function loadJobs(): void
|
||||
{
|
||||
// Placeholder data — will be replaced with real API calls to Go backend
|
||||
$this->jobs = [
|
||||
[
|
||||
'id' => 'job-001',
|
||||
'issue' => '#96',
|
||||
'repo' => 'host-uk/core',
|
||||
'title' => 'feat(agentic): real-time dashboard',
|
||||
'agent' => 'Athena',
|
||||
'status' => 'in_progress',
|
||||
'priority' => 1,
|
||||
'queued_at' => now()->subMinutes(45)->toIso8601String(),
|
||||
'started_at' => now()->subMinutes(30)->toIso8601String(),
|
||||
],
|
||||
[
|
||||
'id' => 'job-002',
|
||||
'issue' => '#84',
|
||||
'repo' => 'host-uk/core',
|
||||
'title' => 'fix: security audit findings',
|
||||
'agent' => 'Clotho',
|
||||
'status' => 'in_progress',
|
||||
'priority' => 2,
|
||||
'queued_at' => now()->subHours(2)->toIso8601String(),
|
||||
'started_at' => now()->subHours(1)->toIso8601String(),
|
||||
],
|
||||
[
|
||||
'id' => 'job-003',
|
||||
'issue' => '#102',
|
||||
'repo' => 'host-uk/core',
|
||||
'title' => 'feat: add rate limiting to MCP',
|
||||
'agent' => null,
|
||||
'status' => 'queued',
|
||||
'priority' => 3,
|
||||
'queued_at' => now()->subMinutes(10)->toIso8601String(),
|
||||
'started_at' => null,
|
||||
],
|
||||
[
|
||||
'id' => 'job-004',
|
||||
'issue' => '#89',
|
||||
'repo' => 'host-uk/core',
|
||||
'title' => 'fix: WebSocket reconnection',
|
||||
'agent' => 'Virgil',
|
||||
'status' => 'review',
|
||||
'priority' => 2,
|
||||
'queued_at' => now()->subHours(4)->toIso8601String(),
|
||||
'started_at' => now()->subHours(3)->toIso8601String(),
|
||||
],
|
||||
[
|
||||
'id' => 'job-005',
|
||||
'issue' => '#78',
|
||||
'repo' => 'host-uk/core',
|
||||
'title' => 'docs: update CLAUDE.md',
|
||||
'agent' => 'Virgil',
|
||||
'status' => 'completed',
|
||||
'priority' => 4,
|
||||
'queued_at' => now()->subHours(6)->toIso8601String(),
|
||||
'started_at' => now()->subHours(5)->toIso8601String(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function updatedStatusFilter(): void
|
||||
{
|
||||
// Livewire auto-updates the view
|
||||
}
|
||||
|
||||
public function cancelJob(string $jobId): void
|
||||
{
|
||||
$this->jobs = array_map(function ($job) use ($jobId) {
|
||||
if ($job['id'] === $jobId && in_array($job['status'], ['queued', 'in_progress'])) {
|
||||
$job['status'] = 'cancelled';
|
||||
}
|
||||
return $job;
|
||||
}, $this->jobs);
|
||||
}
|
||||
|
||||
public function retryJob(string $jobId): void
|
||||
{
|
||||
$this->jobs = array_map(function ($job) use ($jobId) {
|
||||
if ($job['id'] === $jobId && in_array($job['status'], ['failed', 'cancelled'])) {
|
||||
$job['status'] = 'queued';
|
||||
$job['agent'] = null;
|
||||
}
|
||||
return $job;
|
||||
}, $this->jobs);
|
||||
}
|
||||
|
||||
public function getFilteredJobsProperty(): array
|
||||
{
|
||||
return array_filter($this->jobs, function ($job) {
|
||||
if ($this->statusFilter !== 'all' && $job['status'] !== $this->statusFilter) {
|
||||
return false;
|
||||
}
|
||||
if ($this->agentFilter !== 'all' && ($job['agent'] ?? '') !== $this->agentFilter) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.dashboard.job-queue');
|
||||
}
|
||||
}
|
||||
60
cmd/core-app/laravel/app/Livewire/Dashboard/Metrics.php
Normal file
60
cmd/core-app/laravel/app/Livewire/Dashboard/Metrics.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Livewire\Dashboard;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class Metrics extends Component
|
||||
{
|
||||
public array $stats = [];
|
||||
public array $throughputData = [];
|
||||
public array $costBreakdown = [];
|
||||
public float $budgetUsed = 0;
|
||||
public float $budgetLimit = 0;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadMetrics();
|
||||
}
|
||||
|
||||
public function loadMetrics(): void
|
||||
{
|
||||
// Placeholder data — will be replaced with real metrics from Go backend
|
||||
$this->stats = [
|
||||
'jobs_completed' => 12,
|
||||
'prs_merged' => 8,
|
||||
'tokens_used' => 1_245_800,
|
||||
'cost_today' => 18.42,
|
||||
'active_agents' => 3,
|
||||
'queue_depth' => 4,
|
||||
];
|
||||
|
||||
$this->budgetUsed = 18.42;
|
||||
$this->budgetLimit = 50.00;
|
||||
|
||||
// Hourly throughput for chart
|
||||
$this->throughputData = [
|
||||
['hour' => '00:00', 'jobs' => 0, 'tokens' => 0],
|
||||
['hour' => '02:00', 'jobs' => 0, 'tokens' => 0],
|
||||
['hour' => '04:00', 'jobs' => 1, 'tokens' => 45_000],
|
||||
['hour' => '06:00', 'jobs' => 2, 'tokens' => 120_000],
|
||||
['hour' => '08:00', 'jobs' => 3, 'tokens' => 195_000],
|
||||
['hour' => '10:00', 'jobs' => 2, 'tokens' => 280_000],
|
||||
['hour' => '12:00', 'jobs' => 1, 'tokens' => 340_000],
|
||||
['hour' => '14:00', 'jobs' => 3, 'tokens' => 450_000],
|
||||
];
|
||||
|
||||
$this->costBreakdown = [
|
||||
['model' => 'claude-opus-4-6', 'cost' => 12.80, 'tokens' => 856_000],
|
||||
['model' => 'claude-sonnet-4-5', 'cost' => 4.20, 'tokens' => 312_000],
|
||||
['model' => 'claude-haiku-4-5', 'cost' => 1.42, 'tokens' => 77_800],
|
||||
];
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.dashboard.metrics');
|
||||
}
|
||||
}
|
||||
43
cmd/core-app/laravel/app/Models/AgentAllowance.php
Normal file
43
cmd/core-app/laravel/app/Models/AgentAllowance.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class AgentAllowance extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'agent_id',
|
||||
'daily_token_limit',
|
||||
'daily_job_limit',
|
||||
'concurrent_jobs',
|
||||
'max_job_duration_minutes',
|
||||
'model_allowlist',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'daily_token_limit' => 'integer',
|
||||
'daily_job_limit' => 'integer',
|
||||
'concurrent_jobs' => 'integer',
|
||||
'max_job_duration_minutes' => 'integer',
|
||||
'model_allowlist' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function usageRecords(): HasMany
|
||||
{
|
||||
return $this->hasMany(QuotaUsage::class, 'agent_id', 'agent_id');
|
||||
}
|
||||
|
||||
public function todayUsage(): ?QuotaUsage
|
||||
{
|
||||
return $this->usageRecords()
|
||||
->where('period_date', now()->toDateString())
|
||||
->first();
|
||||
}
|
||||
}
|
||||
26
cmd/core-app/laravel/app/Models/ModelQuota.php
Normal file
26
cmd/core-app/laravel/app/Models/ModelQuota.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ModelQuota extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'model',
|
||||
'daily_token_budget',
|
||||
'hourly_rate_limit',
|
||||
'cost_ceiling',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'daily_token_budget' => 'integer',
|
||||
'hourly_rate_limit' => 'integer',
|
||||
'cost_ceiling' => 'integer',
|
||||
];
|
||||
}
|
||||
}
|
||||
36
cmd/core-app/laravel/app/Models/QuotaUsage.php
Normal file
36
cmd/core-app/laravel/app/Models/QuotaUsage.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class QuotaUsage extends Model
|
||||
{
|
||||
protected $table = 'quota_usage';
|
||||
|
||||
protected $fillable = [
|
||||
'agent_id',
|
||||
'tokens_used',
|
||||
'jobs_started',
|
||||
'active_jobs',
|
||||
'period_date',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'tokens_used' => 'integer',
|
||||
'jobs_started' => 'integer',
|
||||
'active_jobs' => 'integer',
|
||||
'period_date' => 'date',
|
||||
];
|
||||
}
|
||||
|
||||
public function allowance(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(AgentAllowance::class, 'agent_id', 'agent_id');
|
||||
}
|
||||
}
|
||||
29
cmd/core-app/laravel/app/Models/UsageReport.php
Normal file
29
cmd/core-app/laravel/app/Models/UsageReport.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UsageReport extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'agent_id',
|
||||
'job_id',
|
||||
'model',
|
||||
'tokens_in',
|
||||
'tokens_out',
|
||||
'event',
|
||||
'reported_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'tokens_in' => 'integer',
|
||||
'tokens_out' => 'integer',
|
||||
'reported_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,29 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\Forgejo\ForgejoService;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Throwable;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(ForgejoService::class, function ($app): ForgejoService {
|
||||
/** @var array<string, mixed> $config */
|
||||
$config = $app['config']->get('forgejo', []);
|
||||
|
||||
return new ForgejoService(
|
||||
instances: $config['instances'] ?? [],
|
||||
defaultInstance: $config['default'] ?? 'forge',
|
||||
timeout: $config['timeout'] ?? 30,
|
||||
retryTimes: $config['retry_times'] ?? 3,
|
||||
retrySleep: $config['retry_sleep'] ?? 500,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
// Auto-migrate on first boot. Single-user desktop app with
|
||||
|
|
|
|||
183
cmd/core-app/laravel/app/Services/AllowanceService.php
Normal file
183
cmd/core-app/laravel/app/Services/AllowanceService.php
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\AgentAllowance;
|
||||
use App\Models\ModelQuota;
|
||||
use App\Models\QuotaUsage;
|
||||
use App\Models\UsageReport;
|
||||
|
||||
class AllowanceService
|
||||
{
|
||||
/**
|
||||
* Pre-dispatch check: verify agent has remaining allowance.
|
||||
*
|
||||
* @return array{allowed: bool, status: string, remaining_tokens: int, remaining_jobs: int, reason: ?string}
|
||||
*/
|
||||
public function check(string $agentId, string $model = ''): array
|
||||
{
|
||||
$allowance = AgentAllowance::where('agent_id', $agentId)->first();
|
||||
|
||||
if (! $allowance) {
|
||||
return [
|
||||
'allowed' => false,
|
||||
'status' => 'exceeded',
|
||||
'remaining_tokens' => 0,
|
||||
'remaining_jobs' => 0,
|
||||
'reason' => 'no allowance configured for agent',
|
||||
];
|
||||
}
|
||||
|
||||
$usage = QuotaUsage::firstOrCreate(
|
||||
['agent_id' => $agentId, 'period_date' => now()->toDateString()],
|
||||
['tokens_used' => 0, 'jobs_started' => 0, 'active_jobs' => 0],
|
||||
);
|
||||
|
||||
$result = [
|
||||
'allowed' => true,
|
||||
'status' => 'ok',
|
||||
'remaining_tokens' => -1,
|
||||
'remaining_jobs' => -1,
|
||||
'reason' => null,
|
||||
];
|
||||
|
||||
// Check model allowlist
|
||||
if ($model !== '' && ! empty($allowance->model_allowlist)) {
|
||||
if (! in_array($model, $allowance->model_allowlist, true)) {
|
||||
return array_merge($result, [
|
||||
'allowed' => false,
|
||||
'status' => 'exceeded',
|
||||
'reason' => "model not in allowlist: {$model}",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Check daily token limit
|
||||
if ($allowance->daily_token_limit > 0) {
|
||||
$remaining = $allowance->daily_token_limit - $usage->tokens_used;
|
||||
$result['remaining_tokens'] = $remaining;
|
||||
|
||||
if ($remaining <= 0) {
|
||||
return array_merge($result, [
|
||||
'allowed' => false,
|
||||
'status' => 'exceeded',
|
||||
'reason' => 'daily token limit exceeded',
|
||||
]);
|
||||
}
|
||||
|
||||
$ratio = $usage->tokens_used / $allowance->daily_token_limit;
|
||||
if ($ratio >= 0.8) {
|
||||
$result['status'] = 'warning';
|
||||
}
|
||||
}
|
||||
|
||||
// Check daily job limit
|
||||
if ($allowance->daily_job_limit > 0) {
|
||||
$remaining = $allowance->daily_job_limit - $usage->jobs_started;
|
||||
$result['remaining_jobs'] = $remaining;
|
||||
|
||||
if ($remaining <= 0) {
|
||||
return array_merge($result, [
|
||||
'allowed' => false,
|
||||
'status' => 'exceeded',
|
||||
'reason' => 'daily job limit exceeded',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Check concurrent jobs
|
||||
if ($allowance->concurrent_jobs > 0 && $usage->active_jobs >= $allowance->concurrent_jobs) {
|
||||
return array_merge($result, [
|
||||
'allowed' => false,
|
||||
'status' => 'exceeded',
|
||||
'reason' => 'concurrent job limit reached',
|
||||
]);
|
||||
}
|
||||
|
||||
// Check global model quota
|
||||
if ($model !== '') {
|
||||
$modelQuota = ModelQuota::where('model', $model)->first();
|
||||
|
||||
if ($modelQuota && $modelQuota->daily_token_budget > 0) {
|
||||
$modelUsage = UsageReport::where('model', $model)
|
||||
->whereDate('reported_at', now()->toDateString())
|
||||
->sum(\DB::raw('tokens_in + tokens_out'));
|
||||
|
||||
if ($modelUsage >= $modelQuota->daily_token_budget) {
|
||||
return array_merge($result, [
|
||||
'allowed' => false,
|
||||
'status' => 'exceeded',
|
||||
'reason' => "global model token budget exceeded for: {$model}",
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record usage from an agent runner report.
|
||||
*/
|
||||
public function recordUsage(array $report): void
|
||||
{
|
||||
$agentId = $report['agent_id'];
|
||||
$totalTokens = ($report['tokens_in'] ?? 0) + ($report['tokens_out'] ?? 0);
|
||||
|
||||
$usage = QuotaUsage::firstOrCreate(
|
||||
['agent_id' => $agentId, 'period_date' => now()->toDateString()],
|
||||
['tokens_used' => 0, 'jobs_started' => 0, 'active_jobs' => 0],
|
||||
);
|
||||
|
||||
// Persist the raw report
|
||||
UsageReport::create([
|
||||
'agent_id' => $report['agent_id'],
|
||||
'job_id' => $report['job_id'],
|
||||
'model' => $report['model'] ?? null,
|
||||
'tokens_in' => $report['tokens_in'] ?? 0,
|
||||
'tokens_out' => $report['tokens_out'] ?? 0,
|
||||
'event' => $report['event'],
|
||||
'reported_at' => $report['timestamp'] ?? now(),
|
||||
]);
|
||||
|
||||
match ($report['event']) {
|
||||
'job_started' => $usage->increment('jobs_started') || $usage->increment('active_jobs'),
|
||||
'job_completed' => $this->handleCompleted($usage, $totalTokens),
|
||||
'job_failed' => $this->handleFailed($usage, $totalTokens),
|
||||
'job_cancelled' => $this->handleCancelled($usage, $totalTokens),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset daily usage counters for an agent.
|
||||
*/
|
||||
public function resetAgent(string $agentId): void
|
||||
{
|
||||
QuotaUsage::updateOrCreate(
|
||||
['agent_id' => $agentId, 'period_date' => now()->toDateString()],
|
||||
['tokens_used' => 0, 'jobs_started' => 0, 'active_jobs' => 0],
|
||||
);
|
||||
}
|
||||
|
||||
private function handleCompleted(QuotaUsage $usage, int $totalTokens): void
|
||||
{
|
||||
$usage->increment('tokens_used', $totalTokens);
|
||||
$usage->decrement('active_jobs');
|
||||
}
|
||||
|
||||
private function handleFailed(QuotaUsage $usage, int $totalTokens): void
|
||||
{
|
||||
$returnAmount = intdiv($totalTokens, 2);
|
||||
$usage->increment('tokens_used', $totalTokens - $returnAmount);
|
||||
$usage->decrement('active_jobs');
|
||||
}
|
||||
|
||||
private function handleCancelled(QuotaUsage $usage, int $totalTokens): void
|
||||
{
|
||||
$usage->decrement('active_jobs');
|
||||
// 100% returned — no token charge
|
||||
}
|
||||
}
|
||||
155
cmd/core-app/laravel/app/Services/Forgejo/ForgejoClient.php
Normal file
155
cmd/core-app/laravel/app/Services/Forgejo/ForgejoClient.php
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Forgejo;
|
||||
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Low-level HTTP client for a single Forgejo instance.
|
||||
*
|
||||
* Wraps the Laravel HTTP client with token auth, retry, and
|
||||
* base-URL scoping so callers never deal with raw HTTP details.
|
||||
*/
|
||||
class ForgejoClient
|
||||
{
|
||||
private PendingRequest $http;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $baseUrl,
|
||||
private readonly string $token,
|
||||
int $timeout = 30,
|
||||
int $retryTimes = 3,
|
||||
int $retrySleep = 500,
|
||||
) {
|
||||
if ($this->token === '') {
|
||||
throw new RuntimeException("Forgejo API token is required for {$this->baseUrl}");
|
||||
}
|
||||
|
||||
$this->http = Http::baseUrl(rtrim($this->baseUrl, '/') . '/api/v1')
|
||||
->withHeaders([
|
||||
'Authorization' => "token {$this->token}",
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json',
|
||||
])
|
||||
->timeout($timeout)
|
||||
->retry($retryTimes, $retrySleep, fn (?\Throwable $e, PendingRequest $req): bool =>
|
||||
$e instanceof \Illuminate\Http\Client\ConnectionException
|
||||
);
|
||||
}
|
||||
|
||||
public function baseUrl(): string
|
||||
{
|
||||
return $this->baseUrl;
|
||||
}
|
||||
|
||||
// ----- Generic verbs -----
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function get(string $path, array $query = []): array
|
||||
{
|
||||
return $this->decodeOrFail($this->http->get($path, $query));
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function post(string $path, array $data = []): array
|
||||
{
|
||||
return $this->decodeOrFail($this->http->post($path, $data));
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function patch(string $path, array $data = []): array
|
||||
{
|
||||
return $this->decodeOrFail($this->http->patch($path, $data));
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function put(string $path, array $data = []): array
|
||||
{
|
||||
return $this->decodeOrFail($this->http->put($path, $data));
|
||||
}
|
||||
|
||||
public function delete(string $path): void
|
||||
{
|
||||
$response = $this->http->delete($path);
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new RuntimeException(
|
||||
"Forgejo DELETE {$path} failed [{$response->status()}]: {$response->body()}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET a path and return the raw response body as a string.
|
||||
* Useful for endpoints that return non-JSON content (e.g. diffs).
|
||||
*/
|
||||
public function getRaw(string $path, array $query = []): string
|
||||
{
|
||||
$response = $this->http->get($path, $query);
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new RuntimeException(
|
||||
"Forgejo GET {$path} failed [{$response->status()}]: {$response->body()}"
|
||||
);
|
||||
}
|
||||
|
||||
return $response->body();
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginate through all pages of a list endpoint.
|
||||
*
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function paginate(string $path, array $query = [], int $limit = 50): array
|
||||
{
|
||||
$all = [];
|
||||
$page = 1;
|
||||
|
||||
do {
|
||||
$response = $this->http->get($path, array_merge($query, [
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
]));
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new RuntimeException(
|
||||
"Forgejo GET {$path} page {$page} failed [{$response->status()}]: {$response->body()}"
|
||||
);
|
||||
}
|
||||
|
||||
$items = $response->json();
|
||||
|
||||
if (!is_array($items) || $items === []) {
|
||||
break;
|
||||
}
|
||||
|
||||
array_push($all, ...$items);
|
||||
|
||||
// Forgejo returns total count in x-total-count header.
|
||||
$total = (int) $response->header('x-total-count');
|
||||
$page++;
|
||||
} while (count($all) < $total);
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
// ----- Internals -----
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function decodeOrFail(Response $response): array
|
||||
{
|
||||
if ($response->failed()) {
|
||||
throw new RuntimeException(
|
||||
"Forgejo API error [{$response->status()}]: {$response->body()}"
|
||||
);
|
||||
}
|
||||
|
||||
return $response->json() ?? [];
|
||||
}
|
||||
}
|
||||
302
cmd/core-app/laravel/app/Services/Forgejo/ForgejoService.php
Normal file
302
cmd/core-app/laravel/app/Services/Forgejo/ForgejoService.php
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Forgejo;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Business-logic layer for Forgejo operations.
|
||||
*
|
||||
* Manages multiple Forgejo instances (forge, dev, qa) and provides
|
||||
* a unified API for issues, pull requests, repositories, and user
|
||||
* management. Mirrors the Go pkg/forge API surface.
|
||||
*/
|
||||
class ForgejoService
|
||||
{
|
||||
/** @var array<string, ForgejoClient> */
|
||||
private array $clients = [];
|
||||
|
||||
private string $defaultInstance;
|
||||
|
||||
/**
|
||||
* @param array<string, array{url: string, token: string}> $instances
|
||||
*/
|
||||
public function __construct(
|
||||
array $instances,
|
||||
string $defaultInstance = 'forge',
|
||||
private readonly int $timeout = 30,
|
||||
private readonly int $retryTimes = 3,
|
||||
private readonly int $retrySleep = 500,
|
||||
) {
|
||||
$this->defaultInstance = $defaultInstance;
|
||||
|
||||
foreach ($instances as $name => $cfg) {
|
||||
if (($cfg['token'] ?? '') === '') {
|
||||
continue; // skip unconfigured instances
|
||||
}
|
||||
|
||||
$this->clients[$name] = new ForgejoClient(
|
||||
baseUrl: $cfg['url'],
|
||||
token: $cfg['token'],
|
||||
timeout: $this->timeout,
|
||||
retryTimes: $this->retryTimes,
|
||||
retrySleep: $this->retrySleep,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Instance resolution
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
public function client(?string $instance = null): ForgejoClient
|
||||
{
|
||||
$name = $instance ?? $this->defaultInstance;
|
||||
|
||||
return $this->clients[$name]
|
||||
?? throw new RuntimeException("Forgejo instance '{$name}' is not configured or has no token");
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public function instances(): array
|
||||
{
|
||||
return array_keys($this->clients);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Issue Operations
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function createIssue(
|
||||
string $owner,
|
||||
string $repo,
|
||||
string $title,
|
||||
string $body = '',
|
||||
array $labels = [],
|
||||
string $assignee = '',
|
||||
?string $instance = null,
|
||||
): array {
|
||||
$data = ['title' => $title, 'body' => $body];
|
||||
|
||||
if ($labels !== []) {
|
||||
$data['labels'] = $labels;
|
||||
}
|
||||
if ($assignee !== '') {
|
||||
$data['assignees'] = [$assignee];
|
||||
}
|
||||
|
||||
return $this->client($instance)->post("/repos/{$owner}/{$repo}/issues", $data);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function updateIssue(
|
||||
string $owner,
|
||||
string $repo,
|
||||
int $number,
|
||||
array $fields,
|
||||
?string $instance = null,
|
||||
): array {
|
||||
return $this->client($instance)->patch("/repos/{$owner}/{$repo}/issues/{$number}", $fields);
|
||||
}
|
||||
|
||||
public function closeIssue(string $owner, string $repo, int $number, ?string $instance = null): array
|
||||
{
|
||||
return $this->updateIssue($owner, $repo, $number, ['state' => 'closed'], $instance);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function addComment(
|
||||
string $owner,
|
||||
string $repo,
|
||||
int $number,
|
||||
string $body,
|
||||
?string $instance = null,
|
||||
): array {
|
||||
return $this->client($instance)->post(
|
||||
"/repos/{$owner}/{$repo}/issues/{$number}/comments",
|
||||
['body' => $body],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function listIssues(
|
||||
string $owner,
|
||||
string $repo,
|
||||
string $state = 'open',
|
||||
int $page = 1,
|
||||
int $limit = 50,
|
||||
?string $instance = null,
|
||||
): array {
|
||||
return $this->client($instance)->get("/repos/{$owner}/{$repo}/issues", [
|
||||
'state' => $state,
|
||||
'type' => 'issues',
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
]);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Pull Request Operations
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function createPR(
|
||||
string $owner,
|
||||
string $repo,
|
||||
string $head,
|
||||
string $base,
|
||||
string $title,
|
||||
string $body = '',
|
||||
?string $instance = null,
|
||||
): array {
|
||||
return $this->client($instance)->post("/repos/{$owner}/{$repo}/pulls", [
|
||||
'head' => $head,
|
||||
'base' => $base,
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
]);
|
||||
}
|
||||
|
||||
public function mergePR(
|
||||
string $owner,
|
||||
string $repo,
|
||||
int $number,
|
||||
string $strategy = 'merge',
|
||||
?string $instance = null,
|
||||
): void {
|
||||
$this->client($instance)->post("/repos/{$owner}/{$repo}/pulls/{$number}/merge", [
|
||||
'Do' => $strategy,
|
||||
'delete_branch_after_merge' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function listPRs(
|
||||
string $owner,
|
||||
string $repo,
|
||||
string $state = 'open',
|
||||
?string $instance = null,
|
||||
): array {
|
||||
return $this->client($instance)->paginate("/repos/{$owner}/{$repo}/pulls", [
|
||||
'state' => $state,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getPRDiff(string $owner, string $repo, int $number, ?string $instance = null): string
|
||||
{
|
||||
return $this->client($instance)->getRaw("/repos/{$owner}/{$repo}/pulls/{$number}.diff");
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Repository Operations
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function listRepos(string $org, ?string $instance = null): array
|
||||
{
|
||||
return $this->client($instance)->paginate("/orgs/{$org}/repos");
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function getRepo(string $owner, string $name, ?string $instance = null): array
|
||||
{
|
||||
return $this->client($instance)->get("/repos/{$owner}/{$name}");
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function createBranch(
|
||||
string $owner,
|
||||
string $repo,
|
||||
string $name,
|
||||
string $from = '',
|
||||
?string $instance = null,
|
||||
): array {
|
||||
$data = ['new_branch_name' => $name];
|
||||
|
||||
if ($from !== '') {
|
||||
$data['old_branch_name'] = $from;
|
||||
}
|
||||
|
||||
return $this->client($instance)->post("/repos/{$owner}/{$repo}/branches", $data);
|
||||
}
|
||||
|
||||
public function deleteBranch(
|
||||
string $owner,
|
||||
string $repo,
|
||||
string $name,
|
||||
?string $instance = null,
|
||||
): void {
|
||||
$this->client($instance)->delete("/repos/{$owner}/{$repo}/branches/{$name}");
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// User / Token Management
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function createUser(
|
||||
string $username,
|
||||
string $email,
|
||||
string $password,
|
||||
?string $instance = null,
|
||||
): array {
|
||||
return $this->client($instance)->post('/admin/users', [
|
||||
'username' => $username,
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
'must_change_password' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function createToken(
|
||||
string $username,
|
||||
string $name,
|
||||
array $scopes = [],
|
||||
?string $instance = null,
|
||||
): array {
|
||||
$data = ['name' => $name];
|
||||
|
||||
if ($scopes !== []) {
|
||||
$data['scopes'] = $scopes;
|
||||
}
|
||||
|
||||
return $this->client($instance)->post("/users/{$username}/tokens", $data);
|
||||
}
|
||||
|
||||
public function revokeToken(string $username, int $tokenId, ?string $instance = null): void
|
||||
{
|
||||
$this->client($instance)->delete("/users/{$username}/tokens/{$tokenId}");
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function addToOrg(
|
||||
string $username,
|
||||
string $org,
|
||||
int $teamId,
|
||||
?string $instance = null,
|
||||
): array {
|
||||
return $this->client($instance)->put("/teams/{$teamId}/members/{$username}");
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Org Operations
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function listOrgs(?string $instance = null): array
|
||||
{
|
||||
return $this->client($instance)->paginate('/user/orgs');
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ use Illuminate\Foundation\Configuration\Middleware;
|
|||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
|
|
|
|||
51
cmd/core-app/laravel/config/forgejo.php
Normal file
51
cmd/core-app/laravel/config/forgejo.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Forgejo Instance
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The instance name to use when no explicit instance is specified.
|
||||
|
|
||||
*/
|
||||
'default' => env('FORGEJO_DEFAULT', 'forge'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Forgejo Instances
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Each entry defines a Forgejo instance the platform can talk to.
|
||||
| The service auto-routes by matching the configured URL.
|
||||
|
|
||||
| url — Base URL of the Forgejo instance (no trailing slash)
|
||||
| token — Admin API token for the instance
|
||||
|
|
||||
*/
|
||||
'instances' => [
|
||||
'forge' => [
|
||||
'url' => env('FORGEJO_FORGE_URL', 'https://forge.lthn.ai'),
|
||||
'token' => env('FORGEJO_FORGE_TOKEN', ''),
|
||||
],
|
||||
'dev' => [
|
||||
'url' => env('FORGEJO_DEV_URL', 'https://dev.lthn.ai'),
|
||||
'token' => env('FORGEJO_DEV_TOKEN', ''),
|
||||
],
|
||||
'qa' => [
|
||||
'url' => env('FORGEJO_QA_URL', 'https://qa.lthn.ai'),
|
||||
'token' => env('FORGEJO_QA_TOKEN', ''),
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTP Client Settings
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'timeout' => (int) env('FORGEJO_TIMEOUT', 30),
|
||||
'retry_times' => (int) env('FORGEJO_RETRY_TIMES', 3),
|
||||
'retry_sleep' => (int) env('FORGEJO_RETRY_SLEEP', 500),
|
||||
];
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('agent_allowances', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('agent_id')->unique();
|
||||
$table->bigInteger('daily_token_limit')->default(0);
|
||||
$table->integer('daily_job_limit')->default(0);
|
||||
$table->integer('concurrent_jobs')->default(1);
|
||||
$table->integer('max_job_duration_minutes')->default(0);
|
||||
$table->json('model_allowlist')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('quota_usage', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('agent_id')->index();
|
||||
$table->bigInteger('tokens_used')->default(0);
|
||||
$table->integer('jobs_started')->default(0);
|
||||
$table->integer('active_jobs')->default(0);
|
||||
$table->date('period_date')->index();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['agent_id', 'period_date']);
|
||||
});
|
||||
|
||||
Schema::create('model_quotas', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('model')->unique();
|
||||
$table->bigInteger('daily_token_budget')->default(0);
|
||||
$table->integer('hourly_rate_limit')->default(0);
|
||||
$table->bigInteger('cost_ceiling')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('usage_reports', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('agent_id')->index();
|
||||
$table->string('job_id')->index();
|
||||
$table->string('model')->nullable();
|
||||
$table->bigInteger('tokens_in')->default(0);
|
||||
$table->bigInteger('tokens_out')->default(0);
|
||||
$table->string('event');
|
||||
$table->timestamp('reported_at');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('repo_limits', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('repo')->unique();
|
||||
$table->integer('max_daily_prs')->default(0);
|
||||
$table->integer('max_daily_issues')->default(0);
|
||||
$table->integer('cooldown_after_failure_minutes')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('repo_limits');
|
||||
Schema::dropIfExists('usage_reports');
|
||||
Schema::dropIfExists('model_quotas');
|
||||
Schema::dropIfExists('quota_usage');
|
||||
Schema::dropIfExists('agent_allowances');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ $title ?? 'Agentic Dashboard' }} — Core</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
surface: { DEFAULT: '#0d1117', raised: '#161b22', overlay: '#21262d' },
|
||||
border: { DEFAULT: '#30363d', subtle: '#21262d' },
|
||||
accent: { DEFAULT: '#39d0d8', dim: '#1b6b6f' },
|
||||
success: '#238636',
|
||||
warning: '#d29922',
|
||||
danger: '#da3633',
|
||||
muted: '#8b949e',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
|
||||
<style>
|
||||
[x-cloak] { display: none !important; }
|
||||
@keyframes pulse-dot { 0%, 100% { opacity: 1; } 50% { opacity: .4; } }
|
||||
.heartbeat { animation: pulse-dot 2s ease-in-out infinite; }
|
||||
.scrollbar-thin::-webkit-scrollbar { width: 6px; }
|
||||
.scrollbar-thin::-webkit-scrollbar-track { background: transparent; }
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; }
|
||||
</style>
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body class="h-full bg-surface text-gray-200 antialiased">
|
||||
<div class="flex h-full" x-data="{ sidebarOpen: true }">
|
||||
{{-- Sidebar --}}
|
||||
<aside class="flex flex-col w-56 border-r border-border bg-surface-raised shrink-0 transition-all"
|
||||
:class="sidebarOpen ? 'w-56' : 'w-16'">
|
||||
<div class="flex items-center gap-2 px-4 h-14 border-b border-border">
|
||||
<svg class="w-6 h-6 text-accent shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||||
</svg>
|
||||
<span class="font-semibold text-sm tracking-wide" x-show="sidebarOpen" x-cloak>Agentic</span>
|
||||
</div>
|
||||
<nav class="flex-1 py-2 space-y-0.5 px-2">
|
||||
<a href="{{ route('dashboard') }}"
|
||||
class="flex items-center gap-3 px-3 py-2 text-sm rounded-md {{ request()->routeIs('dashboard') ? 'bg-surface-overlay text-white' : 'text-muted hover:bg-surface-overlay hover:text-white' }} transition">
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/></svg>
|
||||
<span x-show="sidebarOpen">Dashboard</span>
|
||||
</a>
|
||||
<a href="{{ route('dashboard.agents') }}"
|
||||
class="flex items-center gap-3 px-3 py-2 text-sm rounded-md {{ request()->routeIs('dashboard.agents') ? 'bg-surface-overlay text-white' : 'text-muted hover:bg-surface-overlay hover:text-white' }} transition">
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
||||
<span x-show="sidebarOpen">Agent Fleet</span>
|
||||
</a>
|
||||
<a href="{{ route('dashboard.jobs') }}"
|
||||
class="flex items-center gap-3 px-3 py-2 text-sm rounded-md {{ request()->routeIs('dashboard.jobs') ? 'bg-surface-overlay text-white' : 'text-muted hover:bg-surface-overlay hover:text-white' }} transition">
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
|
||||
<span x-show="sidebarOpen">Job Queue</span>
|
||||
</a>
|
||||
<a href="{{ route('dashboard.activity') }}"
|
||||
class="flex items-center gap-3 px-3 py-2 text-sm rounded-md {{ request()->routeIs('dashboard.activity') ? 'bg-surface-overlay text-white' : 'text-muted hover:bg-surface-overlay hover:text-white' }} transition">
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg>
|
||||
<span x-show="sidebarOpen">Activity</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="border-t border-border p-2">
|
||||
<button @click="sidebarOpen = !sidebarOpen"
|
||||
class="flex items-center justify-center w-full px-3 py-2 text-muted hover:text-white rounded-md hover:bg-surface-overlay transition">
|
||||
<svg class="w-4 h-4 transition-transform" :class="sidebarOpen ? '' : 'rotate-180'" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{{-- Main content --}}
|
||||
<main class="flex-1 overflow-auto">
|
||||
<header class="sticky top-0 z-10 flex items-center justify-between h-14 px-6 border-b border-border bg-surface/80 backdrop-blur">
|
||||
<h1 class="text-sm font-semibold">{{ $title ?? 'Dashboard' }}</h1>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2 text-xs text-muted"
|
||||
x-data="{ connected: true }"
|
||||
x-init="
|
||||
setInterval(() => {
|
||||
connected = navigator.onLine;
|
||||
}, 3000)
|
||||
">
|
||||
<span class="w-2 h-2 rounded-full heartbeat"
|
||||
:class="connected ? 'bg-green-500' : 'bg-red-500'"></span>
|
||||
<span x-text="connected ? 'Connected' : 'Disconnected'"></span>
|
||||
</div>
|
||||
<span class="text-xs text-muted font-mono">{{ now()->format('H:i') }}</span>
|
||||
</div>
|
||||
</header>
|
||||
<div class="p-6">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<x-dashboard-layout title="Live Activity">
|
||||
<livewire:dashboard.activity-feed />
|
||||
</x-dashboard-layout>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<x-dashboard-layout title="Agent Fleet">
|
||||
<livewire:dashboard.agent-fleet />
|
||||
</x-dashboard-layout>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<x-dashboard-layout title="Dashboard">
|
||||
{{-- Metrics overview at top --}}
|
||||
<section class="mb-8">
|
||||
<livewire:dashboard.metrics />
|
||||
</section>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||
{{-- Left column: Agent fleet + Human actions --}}
|
||||
<div class="xl:col-span-2 space-y-6">
|
||||
<section>
|
||||
<h2 class="text-sm font-semibold text-muted uppercase tracking-wider mb-3">Agent Fleet</h2>
|
||||
<livewire:dashboard.agent-fleet />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-sm font-semibold text-muted uppercase tracking-wider mb-3">Job Queue</h2>
|
||||
<livewire:dashboard.job-queue />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{{-- Right column: Actions + Activity --}}
|
||||
<div class="space-y-6">
|
||||
<section>
|
||||
<h2 class="text-sm font-semibold text-muted uppercase tracking-wider mb-3">Human Actions</h2>
|
||||
<livewire:dashboard.human-actions />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-sm font-semibold text-muted uppercase tracking-wider mb-3">Live Activity</h2>
|
||||
<livewire:dashboard.activity-feed />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</x-dashboard-layout>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<x-dashboard-layout title="Job Queue">
|
||||
<livewire:dashboard.job-queue />
|
||||
</x-dashboard-layout>
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
<div wire:poll.3s="loadEntries">
|
||||
{{-- Filters --}}
|
||||
<div class="flex flex-wrap items-center gap-3 mb-4">
|
||||
<select wire:model.live="agentFilter"
|
||||
class="bg-surface-overlay border border-border rounded-md px-3 py-1.5 text-xs text-gray-300 focus:border-accent focus:outline-none">
|
||||
<option value="all">All agents</option>
|
||||
<option value="Athena">Athena</option>
|
||||
<option value="Virgil">Virgil</option>
|
||||
<option value="Clotho">Clotho</option>
|
||||
<option value="Charon">Charon</option>
|
||||
</select>
|
||||
<select wire:model.live="typeFilter"
|
||||
class="bg-surface-overlay border border-border rounded-md px-3 py-1.5 text-xs text-gray-300 focus:border-accent focus:outline-none">
|
||||
<option value="all">All types</option>
|
||||
<option value="code_write">Code write</option>
|
||||
<option value="tool_call">Tool call</option>
|
||||
<option value="test_run">Test run</option>
|
||||
<option value="pr_created">PR created</option>
|
||||
<option value="git_push">Git push</option>
|
||||
<option value="question">Question</option>
|
||||
</select>
|
||||
<label class="flex items-center gap-2 text-xs text-muted cursor-pointer">
|
||||
<input type="checkbox" wire:model.live="showOnlyQuestions"
|
||||
class="rounded border-border bg-surface-overlay text-accent focus:ring-accent">
|
||||
Waiting for answer only
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{{-- Feed --}}
|
||||
<div class="space-y-2 max-h-[600px] overflow-y-auto scrollbar-thin">
|
||||
@forelse ($this->filteredEntries as $entry)
|
||||
<div class="bg-surface-raised border rounded-lg px-4 py-3 transition
|
||||
{{ $entry['is_question'] ? 'border-yellow-500/50 bg-yellow-500/5' : 'border-border' }}">
|
||||
<div class="flex items-start gap-3">
|
||||
{{-- Type icon --}}
|
||||
@php
|
||||
$typeIcons = [
|
||||
'code_write' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>',
|
||||
'tool_call' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>',
|
||||
'test_run' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>',
|
||||
'pr_created' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"/>',
|
||||
'git_push' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"/>',
|
||||
'question' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01"/>',
|
||||
];
|
||||
$iconPath = $typeIcons[$entry['type']] ?? $typeIcons['tool_call'];
|
||||
$iconColor = $entry['is_question'] ? 'text-yellow-400' : 'text-muted';
|
||||
@endphp
|
||||
<svg class="w-4 h-4 mt-0.5 shrink-0 {{ $iconColor }}" fill="none" viewBox="0 0 24 24" stroke="currentColor">{!! $iconPath !!}</svg>
|
||||
|
||||
{{-- Content --}}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-0.5">
|
||||
<span class="text-xs font-semibold text-gray-300">{{ $entry['agent'] }}</span>
|
||||
<span class="text-[10px] text-muted font-mono">{{ $entry['job'] }}</span>
|
||||
@if ($entry['is_question'])
|
||||
<span class="text-[10px] px-1.5 py-0.5 rounded bg-yellow-500/20 text-yellow-400 font-medium">NEEDS ANSWER</span>
|
||||
@endif
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 leading-relaxed">{{ $entry['message'] }}</p>
|
||||
</div>
|
||||
|
||||
{{-- Timestamp --}}
|
||||
<span class="text-[11px] text-muted shrink-0">
|
||||
{{ \Carbon\Carbon::parse($entry['timestamp'])->diffForHumans(short: true) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-center py-8 text-muted text-sm">No activity matching filters.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<div wire:poll.5s="loadAgents">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
@foreach ($agents as $agent)
|
||||
<div wire:click="selectAgent('{{ $agent['id'] }}')"
|
||||
class="bg-surface-raised border rounded-lg p-4 cursor-pointer transition hover:border-accent
|
||||
{{ $selectedAgent === $agent['id'] ? 'border-accent' : 'border-border' }}">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2.5 h-2.5 rounded-full heartbeat
|
||||
{{ $agent['heartbeat'] === 'green' ? 'bg-green-500' : ($agent['heartbeat'] === 'yellow' ? 'bg-yellow-500' : 'bg-red-500') }}"></span>
|
||||
<span class="font-semibold text-sm">{{ $agent['name'] }}</span>
|
||||
</div>
|
||||
<span class="text-[10px] px-2 py-0.5 rounded-full font-medium uppercase tracking-wider
|
||||
{{ $agent['status'] === 'working' ? 'bg-blue-500/20 text-blue-400' : ($agent['status'] === 'idle' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400') }}">
|
||||
{{ $agent['status'] }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{-- Info --}}
|
||||
<div class="space-y-1.5 text-xs text-muted">
|
||||
<div class="flex justify-between">
|
||||
<span>Host</span>
|
||||
<span class="text-gray-300 font-mono">{{ $agent['host'] }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Model</span>
|
||||
<span class="text-gray-300 font-mono text-[11px]">{{ $agent['model'] }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Uptime</span>
|
||||
<span class="text-gray-300">{{ $agent['uptime'] }}</span>
|
||||
</div>
|
||||
@if ($agent['job'])
|
||||
<div class="flex justify-between">
|
||||
<span>Job</span>
|
||||
<span class="text-accent text-[11px]">{{ $agent['job'] }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Expanded detail --}}
|
||||
@if ($selectedAgent === $agent['id'])
|
||||
<div class="mt-3 pt-3 border-t border-border space-y-1.5 text-xs text-muted">
|
||||
<div class="flex justify-between">
|
||||
<span>Tokens today</span>
|
||||
<span class="text-gray-300">{{ number_format($agent['tokens_today']) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Jobs completed</span>
|
||||
<span class="text-gray-300">{{ $agent['jobs_completed'] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
<div wire:poll.3s="loadPending">
|
||||
{{-- Pending questions --}}
|
||||
@if (count($pendingQuestions) > 0)
|
||||
<div class="mb-6">
|
||||
<h3 class="text-sm font-semibold mb-3 flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full bg-yellow-500 heartbeat"></span>
|
||||
Agent Questions ({{ count($pendingQuestions) }})
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
@foreach ($pendingQuestions as $q)
|
||||
<div class="bg-yellow-500/5 border border-yellow-500/30 rounded-lg p-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-xs font-semibold text-yellow-400">{{ $q['agent'] }}</span>
|
||||
<span class="text-[10px] text-muted font-mono">{{ $q['job'] }}</span>
|
||||
<span class="text-[10px] text-muted">{{ \Carbon\Carbon::parse($q['asked_at'])->diffForHumans(short: true) }}</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-300 mb-2">{{ $q['question'] }}</p>
|
||||
@if (!empty($q['context']))
|
||||
<p class="text-xs text-muted mb-3">{{ $q['context'] }}</p>
|
||||
@endif
|
||||
|
||||
@if ($answeringId === $q['id'])
|
||||
<div class="mt-3">
|
||||
<textarea wire:model="answerText"
|
||||
rows="3"
|
||||
placeholder="Type your answer..."
|
||||
class="w-full bg-surface-overlay border border-border rounded-md px-3 py-2 text-sm text-gray-300 placeholder-muted focus:border-accent focus:outline-none resize-none"></textarea>
|
||||
<div class="flex gap-2 mt-2">
|
||||
<button wire:click="submitAnswer"
|
||||
class="px-3 py-1.5 text-xs font-medium rounded bg-accent text-surface hover:opacity-90 transition">
|
||||
Send Answer
|
||||
</button>
|
||||
<button wire:click="cancelAnswer"
|
||||
class="px-3 py-1.5 text-xs font-medium rounded bg-surface-overlay text-muted hover:text-white border border-border transition">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<button wire:click="startAnswer('{{ $q['id'] }}')"
|
||||
class="px-3 py-1.5 text-xs font-medium rounded bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/30 transition">
|
||||
Answer
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Review gates --}}
|
||||
@if (count($reviewGates) > 0)
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold mb-3 flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full bg-purple-500 heartbeat"></span>
|
||||
Review Gates ({{ count($reviewGates) }})
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
@foreach ($reviewGates as $gate)
|
||||
<div class="bg-surface-raised border border-purple-500/30 rounded-lg p-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-xs font-semibold text-purple-400">{{ $gate['agent'] }}</span>
|
||||
<span class="text-[10px] text-muted font-mono">{{ $gate['job'] }}</span>
|
||||
<span class="text-[10px] px-1.5 py-0.5 rounded bg-purple-500/20 text-purple-400 font-medium uppercase">{{ str_replace('_', ' ', $gate['type']) }}</span>
|
||||
</div>
|
||||
<p class="text-sm font-medium text-gray-300 mb-1">{{ $gate['title'] }}</p>
|
||||
<p class="text-xs text-muted mb-3">{{ $gate['description'] }}</p>
|
||||
<div class="flex gap-2">
|
||||
<button wire:click="approveGate('{{ $gate['id'] }}')"
|
||||
class="px-3 py-1.5 text-xs font-medium rounded bg-green-500/20 text-green-400 hover:bg-green-500/30 transition">
|
||||
Approve
|
||||
</button>
|
||||
<button wire:click="rejectGate('{{ $gate['id'] }}')"
|
||||
class="px-3 py-1.5 text-xs font-medium rounded bg-red-500/20 text-red-400 hover:bg-red-500/30 transition">
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (count($pendingQuestions) === 0 && count($reviewGates) === 0)
|
||||
<div class="text-center py-12 text-muted">
|
||||
<svg class="w-8 h-8 mx-auto mb-3 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<p class="text-sm">No pending actions. All agents are autonomous.</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
<div wire:poll.5s="loadJobs">
|
||||
{{-- Filters --}}
|
||||
<div class="flex flex-wrap gap-3 mb-4">
|
||||
<select wire:model.live="statusFilter"
|
||||
class="bg-surface-overlay border border-border rounded-md px-3 py-1.5 text-xs text-gray-300 focus:border-accent focus:outline-none">
|
||||
<option value="all">All statuses</option>
|
||||
<option value="queued">Queued</option>
|
||||
<option value="in_progress">In Progress</option>
|
||||
<option value="review">Review</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="failed">Failed</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
<select wire:model.live="agentFilter"
|
||||
class="bg-surface-overlay border border-border rounded-md px-3 py-1.5 text-xs text-gray-300 focus:border-accent focus:outline-none">
|
||||
<option value="all">All agents</option>
|
||||
<option value="Athena">Athena</option>
|
||||
<option value="Virgil">Virgil</option>
|
||||
<option value="Clotho">Clotho</option>
|
||||
<option value="Charon">Charon</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- Table --}}
|
||||
<div class="bg-surface-raised border border-border rounded-lg overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-border text-xs text-muted uppercase tracking-wider">
|
||||
<th class="text-left px-4 py-3 font-medium">Job</th>
|
||||
<th class="text-left px-4 py-3 font-medium">Issue</th>
|
||||
<th class="text-left px-4 py-3 font-medium">Agent</th>
|
||||
<th class="text-left px-4 py-3 font-medium">Status</th>
|
||||
<th class="text-left px-4 py-3 font-medium">Priority</th>
|
||||
<th class="text-left px-4 py-3 font-medium">Queued</th>
|
||||
<th class="text-right px-4 py-3 font-medium">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border">
|
||||
@forelse ($this->filteredJobs as $job)
|
||||
<tr class="hover:bg-surface-overlay/50 transition">
|
||||
<td class="px-4 py-3">
|
||||
<div class="font-mono text-xs text-muted">{{ $job['id'] }}</div>
|
||||
<div class="text-xs text-gray-300 mt-0.5 truncate max-w-[200px]">{{ $job['title'] }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="text-accent font-mono text-xs">{{ $job['issue'] }}</span>
|
||||
<div class="text-[11px] text-muted">{{ $job['repo'] }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-xs">
|
||||
{{ $job['agent'] ?? '—' }}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
@php
|
||||
$statusColors = [
|
||||
'queued' => 'bg-yellow-500/20 text-yellow-400',
|
||||
'in_progress' => 'bg-blue-500/20 text-blue-400',
|
||||
'review' => 'bg-purple-500/20 text-purple-400',
|
||||
'completed' => 'bg-green-500/20 text-green-400',
|
||||
'failed' => 'bg-red-500/20 text-red-400',
|
||||
'cancelled' => 'bg-gray-500/20 text-gray-400',
|
||||
];
|
||||
@endphp
|
||||
<span class="text-[10px] px-2 py-0.5 rounded-full font-medium uppercase tracking-wider {{ $statusColors[$job['status']] ?? '' }}">
|
||||
{{ str_replace('_', ' ', $job['status']) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="text-xs font-mono text-muted">P{{ $job['priority'] }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-xs text-muted">
|
||||
{{ \Carbon\Carbon::parse($job['queued_at'])->diffForHumans(short: true) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
@if (in_array($job['status'], ['queued', 'in_progress']))
|
||||
<button wire:click="cancelJob('{{ $job['id'] }}')"
|
||||
class="text-[11px] px-2 py-1 rounded bg-red-500/10 text-red-400 hover:bg-red-500/20 transition">
|
||||
Cancel
|
||||
</button>
|
||||
@endif
|
||||
@if (in_array($job['status'], ['failed', 'cancelled']))
|
||||
<button wire:click="retryJob('{{ $job['id'] }}')"
|
||||
class="text-[11px] px-2 py-1 rounded bg-blue-500/10 text-blue-400 hover:bg-blue-500/20 transition">
|
||||
Retry
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="px-4 py-8 text-center text-muted text-sm">No jobs match the selected filters.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<div wire:poll.10s="loadMetrics">
|
||||
{{-- Stat cards --}}
|
||||
<div class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4 mb-6">
|
||||
@php
|
||||
$statCards = [
|
||||
['label' => 'Jobs Completed', 'value' => $stats['jobs_completed'], 'icon' => 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z', 'color' => 'text-green-400'],
|
||||
['label' => 'PRs Merged', 'value' => $stats['prs_merged'], 'icon' => 'M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4', 'color' => 'text-purple-400'],
|
||||
['label' => 'Tokens Used', 'value' => number_format($stats['tokens_used']), 'icon' => 'M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z', 'color' => 'text-blue-400'],
|
||||
['label' => 'Cost Today', 'value' => '$' . number_format($stats['cost_today'], 2), 'icon' => 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z', 'color' => 'text-yellow-400'],
|
||||
['label' => 'Active Agents', 'value' => $stats['active_agents'], 'icon' => 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z', 'color' => 'text-accent'],
|
||||
['label' => 'Queue Depth', 'value' => $stats['queue_depth'], 'icon' => 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10', 'color' => 'text-orange-400'],
|
||||
];
|
||||
@endphp
|
||||
@foreach ($statCards as $card)
|
||||
<div class="bg-surface-raised border border-border rounded-lg p-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<svg class="w-4 h-4 {{ $card['color'] }}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ $card['icon'] }}"/>
|
||||
</svg>
|
||||
<span class="text-[11px] text-muted uppercase tracking-wider">{{ $card['label'] }}</span>
|
||||
</div>
|
||||
<div class="text-xl font-bold font-mono {{ $card['color'] }}">{{ $card['value'] }}</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{{-- Budget gauge --}}
|
||||
<div class="bg-surface-raised border border-border rounded-lg p-5">
|
||||
<h3 class="text-sm font-semibold mb-4">Budget</h3>
|
||||
<div class="flex items-end gap-3 mb-3">
|
||||
<span class="text-3xl font-bold font-mono text-accent">${{ number_format($budgetUsed, 2) }}</span>
|
||||
<span class="text-sm text-muted mb-1">/ ${{ number_format($budgetLimit, 2) }}</span>
|
||||
</div>
|
||||
@php
|
||||
$pct = $budgetLimit > 0 ? min(100, ($budgetUsed / $budgetLimit) * 100) : 0;
|
||||
$barColor = $pct > 80 ? 'bg-red-500' : ($pct > 60 ? 'bg-yellow-500' : 'bg-accent');
|
||||
@endphp
|
||||
<div class="w-full h-3 bg-surface-overlay rounded-full overflow-hidden">
|
||||
<div class="{{ $barColor }} h-full rounded-full transition-all duration-500" style="width: {{ $pct }}%"></div>
|
||||
</div>
|
||||
<div class="text-xs text-muted mt-2">{{ number_format($pct, 0) }}% of daily budget used</div>
|
||||
</div>
|
||||
|
||||
{{-- Cost breakdown by model --}}
|
||||
<div class="bg-surface-raised border border-border rounded-lg p-5">
|
||||
<h3 class="text-sm font-semibold mb-4">Cost by Model</h3>
|
||||
<div class="space-y-3">
|
||||
@foreach ($costBreakdown as $model)
|
||||
@php
|
||||
$modelPct = $budgetUsed > 0 ? ($model['cost'] / $budgetUsed) * 100 : 0;
|
||||
$modelColors = [
|
||||
'claude-opus-4-6' => 'bg-purple-500',
|
||||
'claude-sonnet-4-5' => 'bg-blue-500',
|
||||
'claude-haiku-4-5' => 'bg-green-500',
|
||||
];
|
||||
$barCol = $modelColors[$model['model']] ?? 'bg-gray-500';
|
||||
@endphp
|
||||
<div>
|
||||
<div class="flex items-center justify-between text-xs mb-1">
|
||||
<span class="font-mono text-gray-300">{{ $model['model'] }}</span>
|
||||
<span class="text-muted">${{ number_format($model['cost'], 2) }} ({{ number_format($model['tokens']) }} tokens)</span>
|
||||
</div>
|
||||
<div class="w-full h-2 bg-surface-overlay rounded-full overflow-hidden">
|
||||
<div class="{{ $barCol }} h-full rounded-full transition-all duration-500" style="width: {{ $modelPct }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Throughput chart --}}
|
||||
<div class="bg-surface-raised border border-border rounded-lg p-5 mt-6"
|
||||
x-data="{
|
||||
chart: null,
|
||||
init() {
|
||||
this.chart = new ApexCharts(this.$refs.chart, {
|
||||
chart: {
|
||||
type: 'area',
|
||||
height: 240,
|
||||
background: 'transparent',
|
||||
toolbar: { show: false },
|
||||
zoom: { enabled: false },
|
||||
},
|
||||
theme: { mode: 'dark' },
|
||||
colors: ['#39d0d8', '#8b5cf6'],
|
||||
series: [
|
||||
{ name: 'Jobs', data: {{ json_encode(array_column($throughputData, 'jobs')) }} },
|
||||
{ name: 'Tokens (k)', data: {{ json_encode(array_map(fn($t) => round($t / 1000, 1), array_column($throughputData, 'tokens'))) }} },
|
||||
],
|
||||
xaxis: {
|
||||
categories: {{ json_encode(array_column($throughputData, 'hour')) }},
|
||||
labels: { style: { colors: '#8b949e', fontSize: '11px' } },
|
||||
},
|
||||
yaxis: [
|
||||
{ labels: { style: { colors: '#39d0d8' } }, title: { text: 'Jobs', style: { color: '#39d0d8' } } },
|
||||
{ opposite: true, labels: { style: { colors: '#8b5cf6' } }, title: { text: 'Tokens (k)', style: { color: '#8b5cf6' } } },
|
||||
],
|
||||
grid: { borderColor: '#21262d', strokeDashArray: 3 },
|
||||
stroke: { curve: 'smooth', width: 2 },
|
||||
fill: { type: 'gradient', gradient: { opacityFrom: 0.3, opacityTo: 0.05 } },
|
||||
dataLabels: { enabled: false },
|
||||
legend: { labels: { colors: '#8b949e' } },
|
||||
tooltip: { theme: 'dark' },
|
||||
});
|
||||
this.chart.render();
|
||||
}
|
||||
}">
|
||||
<h3 class="text-sm font-semibold mb-4">Throughput</h3>
|
||||
<div x-ref="chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
146
cmd/core-app/laravel/routes/api.php
Normal file
146
cmd/core-app/laravel/routes/api.php
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\AgentAllowance;
|
||||
use App\Models\ModelQuota;
|
||||
use App\Models\RepoLimit;
|
||||
use App\Services\AllowanceService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Allowance API Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Endpoints for managing agent quotas, checking allowances, and recording
|
||||
| usage. Protected endpoints use QuotaMiddleware for enforcement.
|
||||
|
|
||||
*/
|
||||
|
||||
// Health check for quota service
|
||||
Route::get('/allowances/health', fn () => response()->json(['status' => 'ok']));
|
||||
|
||||
// Agent allowance CRUD
|
||||
Route::prefix('allowances/agents')->group(function () {
|
||||
Route::get('/', function () {
|
||||
return AgentAllowance::all();
|
||||
});
|
||||
|
||||
Route::get('/{agentId}', function (string $agentId) {
|
||||
$allowance = AgentAllowance::where('agent_id', $agentId)->first();
|
||||
|
||||
if (! $allowance) {
|
||||
return response()->json(['error' => 'not found'], 404);
|
||||
}
|
||||
|
||||
return $allowance;
|
||||
});
|
||||
|
||||
Route::post('/', function (Request $request) {
|
||||
$validated = $request->validate([
|
||||
'agent_id' => 'required|string|unique:agent_allowances,agent_id',
|
||||
'daily_token_limit' => 'integer|min:0',
|
||||
'daily_job_limit' => 'integer|min:0',
|
||||
'concurrent_jobs' => 'integer|min:0',
|
||||
'max_job_duration_minutes' => 'integer|min:0',
|
||||
'model_allowlist' => 'array',
|
||||
'model_allowlist.*' => 'string',
|
||||
]);
|
||||
|
||||
return AgentAllowance::create($validated);
|
||||
});
|
||||
|
||||
Route::put('/{agentId}', function (Request $request, string $agentId) {
|
||||
$allowance = AgentAllowance::where('agent_id', $agentId)->first();
|
||||
|
||||
if (! $allowance) {
|
||||
return response()->json(['error' => 'not found'], 404);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'daily_token_limit' => 'integer|min:0',
|
||||
'daily_job_limit' => 'integer|min:0',
|
||||
'concurrent_jobs' => 'integer|min:0',
|
||||
'max_job_duration_minutes' => 'integer|min:0',
|
||||
'model_allowlist' => 'array',
|
||||
'model_allowlist.*' => 'string',
|
||||
]);
|
||||
|
||||
$allowance->update($validated);
|
||||
|
||||
return $allowance;
|
||||
});
|
||||
|
||||
Route::delete('/{agentId}', function (string $agentId) {
|
||||
AgentAllowance::where('agent_id', $agentId)->delete();
|
||||
|
||||
return response()->json(['status' => 'deleted']);
|
||||
});
|
||||
});
|
||||
|
||||
// Quota check endpoint
|
||||
Route::get('/allowances/check/{agentId}', function (Request $request, string $agentId, AllowanceService $svc) {
|
||||
$model = $request->query('model', '');
|
||||
|
||||
return response()->json($svc->check($agentId, $model));
|
||||
});
|
||||
|
||||
// Usage reporting endpoint
|
||||
Route::post('/allowances/usage', function (Request $request, AllowanceService $svc) {
|
||||
$validated = $request->validate([
|
||||
'agent_id' => 'required|string',
|
||||
'job_id' => 'required|string',
|
||||
'model' => 'nullable|string',
|
||||
'tokens_in' => 'integer|min:0',
|
||||
'tokens_out' => 'integer|min:0',
|
||||
'event' => 'required|in:job_started,job_completed,job_failed,job_cancelled',
|
||||
'timestamp' => 'nullable|date',
|
||||
]);
|
||||
|
||||
$svc->recordUsage($validated);
|
||||
|
||||
return response()->json(['status' => 'recorded']);
|
||||
});
|
||||
|
||||
// Daily reset endpoint
|
||||
Route::post('/allowances/reset/{agentId}', function (string $agentId, AllowanceService $svc) {
|
||||
$svc->resetAgent($agentId);
|
||||
|
||||
return response()->json(['status' => 'reset']);
|
||||
});
|
||||
|
||||
// Model quota management
|
||||
Route::prefix('allowances/models')->group(function () {
|
||||
Route::get('/', fn () => ModelQuota::all());
|
||||
|
||||
Route::post('/', function (Request $request) {
|
||||
$validated = $request->validate([
|
||||
'model' => 'required|string|unique:model_quotas,model',
|
||||
'daily_token_budget' => 'integer|min:0',
|
||||
'hourly_rate_limit' => 'integer|min:0',
|
||||
'cost_ceiling' => 'integer|min:0',
|
||||
]);
|
||||
|
||||
return ModelQuota::create($validated);
|
||||
});
|
||||
|
||||
Route::put('/{model}', function (Request $request, string $model) {
|
||||
$quota = ModelQuota::where('model', $model)->first();
|
||||
|
||||
if (! $quota) {
|
||||
return response()->json(['error' => 'not found'], 404);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'daily_token_budget' => 'integer|min:0',
|
||||
'hourly_rate_limit' => 'integer|min:0',
|
||||
'cost_ceiling' => 'integer|min:0',
|
||||
]);
|
||||
|
||||
$quota->update($validated);
|
||||
|
||||
return $quota;
|
||||
});
|
||||
});
|
||||
|
|
@ -7,3 +7,9 @@ use Illuminate\Support\Facades\Route;
|
|||
Route::get('/', function () {
|
||||
return view('welcome');
|
||||
});
|
||||
|
||||
// Agentic Dashboard
|
||||
Route::get('/dashboard', fn () => view('dashboard.index'))->name('dashboard');
|
||||
Route::get('/dashboard/agents', fn () => view('dashboard.agents'))->name('dashboard.agents');
|
||||
Route::get('/dashboard/jobs', fn () => view('dashboard.jobs'))->name('dashboard.jobs');
|
||||
Route::get('/dashboard/activity', fn () => view('dashboard.activity'))->name('dashboard.activity');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,206 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Services\Forgejo;
|
||||
|
||||
use App\Services\Forgejo\ForgejoClient;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Orchestra\Testbench\TestCase;
|
||||
use RuntimeException;
|
||||
|
||||
class ForgejoClientTest extends TestCase
|
||||
{
|
||||
private const BASE_URL = 'https://forge.test';
|
||||
private const TOKEN = 'test-token-abc123';
|
||||
|
||||
// ---- Construction ----
|
||||
|
||||
public function test_constructor_good(): void
|
||||
{
|
||||
Http::fake();
|
||||
|
||||
$client = new ForgejoClient(self::BASE_URL, self::TOKEN);
|
||||
|
||||
$this->assertSame(self::BASE_URL, $client->baseUrl());
|
||||
}
|
||||
|
||||
public function test_constructor_bad_empty_token(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('API token is required');
|
||||
|
||||
new ForgejoClient(self::BASE_URL, '');
|
||||
}
|
||||
|
||||
// ---- GET ----
|
||||
|
||||
public function test_get_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/repos/owner/repo' => Http::response(['id' => 1, 'name' => 'repo'], 200),
|
||||
]);
|
||||
|
||||
$client = new ForgejoClient(self::BASE_URL, self::TOKEN, retryTimes: 0);
|
||||
$result = $client->get('/repos/owner/repo');
|
||||
|
||||
$this->assertSame(1, $result['id']);
|
||||
$this->assertSame('repo', $result['name']);
|
||||
}
|
||||
|
||||
public function test_get_bad_server_error(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/repos/owner/repo' => Http::response('Internal Server Error', 500),
|
||||
]);
|
||||
|
||||
$client = new ForgejoClient(self::BASE_URL, self::TOKEN, retryTimes: 0);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Forgejo API error [500]');
|
||||
|
||||
$client->get('/repos/owner/repo');
|
||||
}
|
||||
|
||||
// ---- POST ----
|
||||
|
||||
public function test_post_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/repos/owner/repo/issues' => Http::response(['number' => 42], 201),
|
||||
]);
|
||||
|
||||
$client = new ForgejoClient(self::BASE_URL, self::TOKEN, retryTimes: 0);
|
||||
$result = $client->post('/repos/owner/repo/issues', ['title' => 'Bug']);
|
||||
|
||||
$this->assertSame(42, $result['number']);
|
||||
}
|
||||
|
||||
// ---- PATCH ----
|
||||
|
||||
public function test_patch_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/repos/owner/repo/issues/1' => Http::response(['state' => 'closed'], 200),
|
||||
]);
|
||||
|
||||
$client = new ForgejoClient(self::BASE_URL, self::TOKEN, retryTimes: 0);
|
||||
$result = $client->patch('/repos/owner/repo/issues/1', ['state' => 'closed']);
|
||||
|
||||
$this->assertSame('closed', $result['state']);
|
||||
}
|
||||
|
||||
// ---- PUT ----
|
||||
|
||||
public function test_put_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/teams/5/members/alice' => Http::response([], 204),
|
||||
]);
|
||||
|
||||
$client = new ForgejoClient(self::BASE_URL, self::TOKEN, retryTimes: 0);
|
||||
$result = $client->put('/teams/5/members/alice');
|
||||
|
||||
$this->assertIsArray($result);
|
||||
}
|
||||
|
||||
// ---- DELETE ----
|
||||
|
||||
public function test_delete_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/repos/owner/repo/branches/old' => Http::response('', 204),
|
||||
]);
|
||||
|
||||
$client = new ForgejoClient(self::BASE_URL, self::TOKEN, retryTimes: 0);
|
||||
|
||||
// Should not throw
|
||||
$client->delete('/repos/owner/repo/branches/old');
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function test_delete_bad_not_found(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/repos/owner/repo/branches/gone' => Http::response('Not Found', 404),
|
||||
]);
|
||||
|
||||
$client = new ForgejoClient(self::BASE_URL, self::TOKEN, retryTimes: 0);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('failed [404]');
|
||||
|
||||
$client->delete('/repos/owner/repo/branches/gone');
|
||||
}
|
||||
|
||||
// ---- getRaw ----
|
||||
|
||||
public function test_getRaw_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/repos/owner/repo/pulls/1.diff' => Http::response(
|
||||
"diff --git a/file.txt b/file.txt\n",
|
||||
200,
|
||||
['Content-Type' => 'text/plain'],
|
||||
),
|
||||
]);
|
||||
|
||||
$client = new ForgejoClient(self::BASE_URL, self::TOKEN, retryTimes: 0);
|
||||
$diff = $client->getRaw('/repos/owner/repo/pulls/1.diff');
|
||||
|
||||
$this->assertStringContainsString('diff --git', $diff);
|
||||
}
|
||||
|
||||
// ---- Pagination ----
|
||||
|
||||
public function test_paginate_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/orgs/myorg/repos?page=1&limit=2' => Http::response(
|
||||
[['id' => 1], ['id' => 2]],
|
||||
200,
|
||||
['x-total-count' => '3'],
|
||||
),
|
||||
'forge.test/api/v1/orgs/myorg/repos?page=2&limit=2' => Http::response(
|
||||
[['id' => 3]],
|
||||
200,
|
||||
['x-total-count' => '3'],
|
||||
),
|
||||
]);
|
||||
|
||||
$client = new ForgejoClient(self::BASE_URL, self::TOKEN, retryTimes: 0);
|
||||
$repos = $client->paginate('/orgs/myorg/repos', [], 2);
|
||||
|
||||
$this->assertCount(3, $repos);
|
||||
$this->assertSame(1, $repos[0]['id']);
|
||||
$this->assertSame(3, $repos[2]['id']);
|
||||
}
|
||||
|
||||
public function test_paginate_good_empty(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/orgs/empty/repos?page=1&limit=50' => Http::response([], 200),
|
||||
]);
|
||||
|
||||
$client = new ForgejoClient(self::BASE_URL, self::TOKEN, retryTimes: 0);
|
||||
$repos = $client->paginate('/orgs/empty/repos');
|
||||
|
||||
$this->assertSame([], $repos);
|
||||
}
|
||||
|
||||
// ---- Auth header ----
|
||||
|
||||
public function test_auth_header_sent(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/user' => Http::response(['login' => 'bot'], 200),
|
||||
]);
|
||||
|
||||
$client = new ForgejoClient(self::BASE_URL, self::TOKEN, retryTimes: 0);
|
||||
$client->get('/user');
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
return $request->hasHeader('Authorization', 'token ' . self::TOKEN);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Services\Forgejo;
|
||||
|
||||
use App\Services\Forgejo\ForgejoService;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Orchestra\Testbench\TestCase;
|
||||
use RuntimeException;
|
||||
|
||||
class ForgejoServiceTest extends TestCase
|
||||
{
|
||||
private const INSTANCES = [
|
||||
'forge' => ['url' => 'https://forge.test', 'token' => 'tok-forge'],
|
||||
'dev' => ['url' => 'https://dev.test', 'token' => 'tok-dev'],
|
||||
];
|
||||
|
||||
private function service(): ForgejoService
|
||||
{
|
||||
return new ForgejoService(
|
||||
instances: self::INSTANCES,
|
||||
defaultInstance: 'forge',
|
||||
timeout: 5,
|
||||
retryTimes: 0,
|
||||
retrySleep: 0,
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Instance management ----
|
||||
|
||||
public function test_instances_good(): void
|
||||
{
|
||||
$svc = $this->service();
|
||||
|
||||
$this->assertSame(['forge', 'dev'], $svc->instances());
|
||||
}
|
||||
|
||||
public function test_instances_skips_empty_token(): void
|
||||
{
|
||||
$svc = new ForgejoService(
|
||||
instances: [
|
||||
'forge' => ['url' => 'https://forge.test', 'token' => 'tok'],
|
||||
'qa' => ['url' => 'https://qa.test', 'token' => ''],
|
||||
],
|
||||
);
|
||||
|
||||
$this->assertSame(['forge'], $svc->instances());
|
||||
}
|
||||
|
||||
public function test_client_bad_unknown_instance(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage("instance 'nope' is not configured");
|
||||
|
||||
$this->service()->client('nope');
|
||||
}
|
||||
|
||||
// ---- Issues ----
|
||||
|
||||
public function test_createIssue_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/repos/org/repo/issues' => Http::response([
|
||||
'number' => 99,
|
||||
'title' => 'New bug',
|
||||
], 201),
|
||||
]);
|
||||
|
||||
$result = $this->service()->createIssue('org', 'repo', 'New bug', 'Description');
|
||||
|
||||
$this->assertSame(99, $result['number']);
|
||||
|
||||
Http::assertSent(fn ($r) => $r['title'] === 'New bug' && $r['body'] === 'Description');
|
||||
}
|
||||
|
||||
public function test_createIssue_good_with_labels_and_assignee(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/repos/org/repo/issues' => Http::response(['number' => 1], 201),
|
||||
]);
|
||||
|
||||
$this->service()->createIssue('org', 'repo', 'Task', assignee: 'alice', labels: [1, 2]);
|
||||
|
||||
Http::assertSent(fn ($r) => $r['assignees'] === ['alice'] && $r['labels'] === [1, 2]);
|
||||
}
|
||||
|
||||
public function test_closeIssue_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/repos/org/repo/issues/5' => Http::response(['state' => 'closed'], 200),
|
||||
]);
|
||||
|
||||
$result = $this->service()->closeIssue('org', 'repo', 5);
|
||||
|
||||
$this->assertSame('closed', $result['state']);
|
||||
}
|
||||
|
||||
public function test_addComment_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/repos/org/repo/issues/5/comments' => Http::response(['id' => 100], 201),
|
||||
]);
|
||||
|
||||
$result = $this->service()->addComment('org', 'repo', 5, 'LGTM');
|
||||
|
||||
$this->assertSame(100, $result['id']);
|
||||
}
|
||||
|
||||
public function test_listIssues_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/repos/org/repo/issues*' => Http::response([
|
||||
['number' => 1],
|
||||
['number' => 2],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$issues = $this->service()->listIssues('org', 'repo');
|
||||
|
||||
$this->assertCount(2, $issues);
|
||||
}
|
||||
|
||||
// ---- Pull Requests ----
|
||||
|
||||
public function test_createPR_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/repos/org/repo/pulls' => Http::response([
|
||||
'number' => 10,
|
||||
'title' => 'Feature X',
|
||||
], 201),
|
||||
]);
|
||||
|
||||
$result = $this->service()->createPR('org', 'repo', 'feat/x', 'main', 'Feature X');
|
||||
|
||||
$this->assertSame(10, $result['number']);
|
||||
}
|
||||
|
||||
public function test_mergePR_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/repos/org/repo/pulls/10/merge' => Http::response([], 200),
|
||||
]);
|
||||
|
||||
// Should not throw
|
||||
$this->service()->mergePR('org', 'repo', 10, 'squash');
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function test_getPRDiff_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/repos/org/repo/pulls/10.diff' => Http::response(
|
||||
"diff --git a/f.go b/f.go\n+new line\n",
|
||||
200,
|
||||
),
|
||||
]);
|
||||
|
||||
$diff = $this->service()->getPRDiff('org', 'repo', 10);
|
||||
|
||||
$this->assertStringContainsString('diff --git', $diff);
|
||||
}
|
||||
|
||||
// ---- Repositories ----
|
||||
|
||||
public function test_getRepo_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/repos/org/core' => Http::response(['full_name' => 'org/core'], 200),
|
||||
]);
|
||||
|
||||
$result = $this->service()->getRepo('org', 'core');
|
||||
|
||||
$this->assertSame('org/core', $result['full_name']);
|
||||
}
|
||||
|
||||
public function test_createBranch_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/repos/org/repo/branches' => Http::response(['name' => 'feat/y'], 201),
|
||||
]);
|
||||
|
||||
$result = $this->service()->createBranch('org', 'repo', 'feat/y', 'main');
|
||||
|
||||
$this->assertSame('feat/y', $result['name']);
|
||||
|
||||
Http::assertSent(fn ($r) =>
|
||||
$r['new_branch_name'] === 'feat/y' && $r['old_branch_name'] === 'main'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_deleteBranch_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/repos/org/repo/branches/old' => Http::response('', 204),
|
||||
]);
|
||||
|
||||
$this->service()->deleteBranch('org', 'repo', 'old');
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
// ---- User / Token Management ----
|
||||
|
||||
public function test_createUser_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/admin/users' => Http::response(['login' => 'bot'], 201),
|
||||
]);
|
||||
|
||||
$result = $this->service()->createUser('bot', 'bot@test.io', 's3cret');
|
||||
|
||||
$this->assertSame('bot', $result['login']);
|
||||
|
||||
Http::assertSent(fn ($r) =>
|
||||
$r['username'] === 'bot'
|
||||
&& $r['must_change_password'] === false
|
||||
);
|
||||
}
|
||||
|
||||
public function test_createToken_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/users/bot/tokens' => Http::response(['sha1' => 'abc123'], 201),
|
||||
]);
|
||||
|
||||
$result = $this->service()->createToken('bot', 'ci-token', ['repo', 'user']);
|
||||
|
||||
$this->assertSame('abc123', $result['sha1']);
|
||||
}
|
||||
|
||||
public function test_revokeToken_good(): void
|
||||
{
|
||||
Http::fake([
|
||||
'forge.test/api/v1/users/bot/tokens/42' => Http::response('', 204),
|
||||
]);
|
||||
|
||||
$this->service()->revokeToken('bot', 42);
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
// ---- Multi-instance routing ----
|
||||
|
||||
public function test_explicit_instance_routing(): void
|
||||
{
|
||||
Http::fake([
|
||||
'dev.test/api/v1/repos/org/repo' => Http::response(['full_name' => 'org/repo'], 200),
|
||||
]);
|
||||
|
||||
$result = $this->service()->getRepo('org', 'repo', instance: 'dev');
|
||||
|
||||
$this->assertSame('org/repo', $result['full_name']);
|
||||
|
||||
Http::assertSent(fn ($r) => str_contains($r->url(), 'dev.test'));
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import (
|
|||
"log"
|
||||
"runtime"
|
||||
|
||||
"github.com/host-uk/core/cmd/core-app/icons"
|
||||
"forge.lthn.ai/core/cli/cmd/core-app/icons"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import (
|
|||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/host-uk/core/pkg/mcp/ide"
|
||||
"forge.lthn.ai/core/cli/pkg/mcp/ide"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import (
|
|||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/host-uk/core/pkg/mcp/ide"
|
||||
"forge.lthn.ai/core/cli/pkg/mcp/ide"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
module github.com/host-uk/core/cmd/core-ide
|
||||
module forge.lthn.ai/core/cli/cmd/core-ide
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/host-uk/core v0.0.0
|
||||
forge.lthn.ai/core/cli v0.0.0
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.64
|
||||
)
|
||||
|
||||
|
|
@ -54,4 +54,4 @@ require (
|
|||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
||||
replace github.com/host-uk/core => ../..
|
||||
replace forge.lthn.ai/core/cli => ../..
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import (
|
|||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/host-uk/core/pkg/mcp/ide"
|
||||
"github.com/host-uk/core/pkg/ws"
|
||||
"forge.lthn.ai/core/cli/pkg/mcp/ide"
|
||||
"forge.lthn.ai/core/cli/pkg/ws"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ import (
|
|||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/host-uk/core/cmd/core-ide/icons"
|
||||
"github.com/host-uk/core/pkg/mcp/ide"
|
||||
"github.com/host-uk/core/pkg/ws"
|
||||
"forge.lthn.ai/core/cli/cmd/core-ide/icons"
|
||||
"forge.lthn.ai/core/cli/pkg/mcp/ide"
|
||||
"forge.lthn.ai/core/cli/pkg/ws"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/host-uk/core/pkg/ws"
|
||||
"forge.lthn.ai/core/cli/pkg/ws"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//
|
||||
// When a Go tool requests ?go-get=1, this server responds with HTML
|
||||
// containing <meta name="go-import"> tags that map dappco.re module
|
||||
// paths to their Git repositories on forge.lthn.ai.
|
||||
// paths to their Git repositories on forge.lthn.io.
|
||||
//
|
||||
// For browser requests (no ?go-get=1), it redirects to the Forgejo
|
||||
// repository web UI.
|
||||
|
|
@ -22,7 +22,7 @@ var modules = map[string]string{
|
|||
}
|
||||
|
||||
const (
|
||||
forgeBase = "https://forge.lthn.ai"
|
||||
forgeBase = "https://forge.lthn.io"
|
||||
vanityHost = "dappco.re"
|
||||
defaultAddr = ":8080"
|
||||
)
|
||||
|
|
|
|||
BIN
core-ide
Executable file
BIN
core-ide
Executable file
Binary file not shown.
|
|
@ -26,8 +26,8 @@ core dev work --status
|
|||
repos:
|
||||
- name: core
|
||||
path: ./core
|
||||
url: https://github.com/host-uk/core
|
||||
url: https://forge.lthn.ai/core/cli
|
||||
- name: core-php
|
||||
path: ./core-php
|
||||
url: https://github.com/host-uk/core-php
|
||||
url: https://forge.lthn.ai/core/cli-php
|
||||
```
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ core go mod graph | dot -Tpng -o deps.png
|
|||
## Output
|
||||
|
||||
```
|
||||
github.com/host-uk/core github.com/stretchr/testify@v1.11.1
|
||||
forge.lthn.ai/core/cli github.com/stretchr/testify@v1.11.1
|
||||
github.com/stretchr/testify@v1.11.1 github.com/davecgh/go-spew@v1.1.2
|
||||
github.com/stretchr/testify@v1.11.1 github.com/pmezard/go-difflib@v1.0.1
|
||||
...
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ Unified interface for Go/PHP development, multi-repo management, and deployment.
|
|||
## Installation
|
||||
|
||||
```bash
|
||||
go install github.com/host-uk/core/cmd/core@latest
|
||||
go install forge.lthn.ai/core/cli/cmd/core@latest
|
||||
```
|
||||
|
||||
Verify: `core doctor`
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ It is both. The Core Framework (`pkg/core`) is a library for building Go desktop
|
|||
The recommended way is via Go:
|
||||
|
||||
```bash
|
||||
go install github.com/host-uk/core/cmd/core@latest
|
||||
go install forge.lthn.ai/core/cli/cmd/core@latest
|
||||
```
|
||||
|
||||
Ensure your Go bin directory is in your PATH. See [Getting Started](getting-started.md) for more options.
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ Optional (for specific features):
|
|||
|
||||
```bash
|
||||
# Install latest release
|
||||
go install github.com/host-uk/core/cmd/core@latest
|
||||
go install forge.lthn.ai/core/cli/cmd/core@latest
|
||||
|
||||
# Verify installation
|
||||
core doctor
|
||||
|
|
@ -39,21 +39,21 @@ export PATH="$PATH:$(go env GOPATH)/bin"
|
|||
|
||||
### Option 2: Download Binary
|
||||
|
||||
Download pre-built binaries from [GitHub Releases](https://github.com/host-uk/core/releases):
|
||||
Download pre-built binaries from [GitHub Releases](https://forge.lthn.ai/core/cli/releases):
|
||||
|
||||
```bash
|
||||
# macOS (Apple Silicon)
|
||||
curl -Lo core https://github.com/host-uk/core/releases/latest/download/core-darwin-arm64
|
||||
curl -Lo core https://forge.lthn.ai/core/cli/releases/latest/download/core-darwin-arm64
|
||||
chmod +x core
|
||||
sudo mv core /usr/local/bin/
|
||||
|
||||
# macOS (Intel)
|
||||
curl -Lo core https://github.com/host-uk/core/releases/latest/download/core-darwin-amd64
|
||||
curl -Lo core https://forge.lthn.ai/core/cli/releases/latest/download/core-darwin-amd64
|
||||
chmod +x core
|
||||
sudo mv core /usr/local/bin/
|
||||
|
||||
# Linux (x86_64)
|
||||
curl -Lo core https://github.com/host-uk/core/releases/latest/download/core-linux-amd64
|
||||
curl -Lo core https://forge.lthn.ai/core/cli/releases/latest/download/core-linux-amd64
|
||||
chmod +x core
|
||||
sudo mv core /usr/local/bin/
|
||||
```
|
||||
|
|
@ -62,7 +62,7 @@ sudo mv core /usr/local/bin/
|
|||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/host-uk/core.git
|
||||
git clone https://forge.lthn.ai/core/cli.git
|
||||
cd core
|
||||
|
||||
# Build with Task (recommended)
|
||||
|
|
@ -181,7 +181,7 @@ core doctor
|
|||
core <command> --help
|
||||
|
||||
# Full documentation
|
||||
https://github.com/host-uk/core/tree/main/docs
|
||||
https://forge.lthn.ai/core/cli/tree/main/docs
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ Core is a unified CLI for the host-uk ecosystem - build, release, and deploy Go,
|
|||
|
||||
```bash
|
||||
# Via Go (recommended)
|
||||
go install github.com/host-uk/core/cmd/core@latest
|
||||
go install forge.lthn.ai/core/cli/cmd/core@latest
|
||||
|
||||
# Or download binary from releases
|
||||
curl -Lo core https://github.com/host-uk/core/releases/latest/download/core-$(go env GOOS)-$(go env GOARCH)
|
||||
curl -Lo core https://forge.lthn.ai/core/cli/releases/latest/download/core-$(go env GOOS)-$(go env GOARCH)
|
||||
chmod +x core && sudo mv core /usr/local/bin/
|
||||
|
||||
# Verify
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ import (
|
|||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/host-uk/core/pkg/webview"
|
||||
"forge.lthn.ai/core/cli/pkg/webview"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -424,7 +424,7 @@ import (
|
|||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/host-uk/core/pkg/webview"
|
||||
"forge.lthn.ai/core/cli/pkg/webview"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ Here is the technical documentation for the Core framework packages.
|
|||
* **Framework Integration**: The `Service` struct embeds `framework.ServiceRuntime`, utilizing the Actor pattern (Queries and Tasks) to allow dynamic log level adjustment at runtime without restarting the application.
|
||||
|
||||
### 4. Dependencies
|
||||
* `github.com/host-uk/core/pkg/io`: Used by `rotation.go` to handle file operations (renaming, deleting, writing) abstractly.
|
||||
* `github.com/host-uk/core/pkg/framework`: Used by `service.go` to hook into the application lifecycle and message bus.
|
||||
* `forge.lthn.ai/core/cli/pkg/io`: Used by `rotation.go` to handle file operations (renaming, deleting, writing) abstractly.
|
||||
* `forge.lthn.ai/core/cli/pkg/framework`: Used by `service.go` to hook into the application lifecycle and message bus.
|
||||
* Standard Lib: `errors`, `fmt`, `os`, `sync`, `time`.
|
||||
|
||||
### 5. Test Coverage Notes
|
||||
|
|
@ -88,8 +88,8 @@ Here is the technical documentation for the Core framework packages.
|
|||
### 4. Dependencies
|
||||
* `github.com/spf13/viper`: Core logic for map merging and unmarshalling.
|
||||
* `gopkg.in/yaml.v3`: For marshalling data when saving.
|
||||
* `github.com/host-uk/core/pkg/io`: For reading/writing config files.
|
||||
* `github.com/host-uk/core/pkg/framework/core`: For service integration and error handling.
|
||||
* `forge.lthn.ai/core/cli/pkg/io`: For reading/writing config files.
|
||||
* `forge.lthn.ai/core/cli/pkg/framework/core`: For service integration and error handling.
|
||||
|
||||
### 5. Test Coverage Notes
|
||||
* **Precedence**: Verify that Environment variables override File values.
|
||||
|
|
@ -122,7 +122,7 @@ Here is the technical documentation for the Core framework packages.
|
|||
|
||||
### 4. Dependencies
|
||||
* Standard Lib: `io`, `io/fs`, `os`, `path/filepath`, `strings`, `time`.
|
||||
* `github.com/host-uk/core/pkg/io/local`: (Implied) The concrete implementation for OS disk access.
|
||||
* `forge.lthn.ai/core/cli/pkg/io/local`: (Implied) The concrete implementation for OS disk access.
|
||||
|
||||
### 5. Test Coverage Notes
|
||||
* **Mock fidelity**: The `MockMedium` must behave exactly like the OS. E.g., `Rename` should fail if the source doesn't exist; `Delete` should fail if a directory is not empty.
|
||||
|
|
@ -198,10 +198,10 @@ Here is the technical documentation for the Core framework packages.
|
|||
4. Server validates signature against User Public Key.
|
||||
|
||||
### 4. Dependencies
|
||||
* `github.com/host-uk/core/pkg/io`: For user database storage.
|
||||
* `github.com/host-uk/core/pkg/crypt/lthn`: (Implied) Specific password hashing.
|
||||
* `github.com/host-uk/core/pkg/crypt/pgp`: (Implied) OpenPGP operations.
|
||||
* `github.com/host-uk/core/pkg/framework/core`: Error handling.
|
||||
* `forge.lthn.ai/core/cli/pkg/io`: For user database storage.
|
||||
* `forge.lthn.ai/core/cli/pkg/crypt/lthn`: (Implied) Specific password hashing.
|
||||
* `forge.lthn.ai/core/cli/pkg/crypt/pgp`: (Implied) OpenPGP operations.
|
||||
* `forge.lthn.ai/core/cli/pkg/framework/core`: Error handling.
|
||||
|
||||
### 5. Test Coverage Notes
|
||||
* **Flow Verification**: Full integration test simulating a client: Register -> Get Challenge -> Decrypt/Sign (Mock Client) -> Validate -> Get Token.
|
||||
|
|
|
|||
|
|
@ -60,9 +60,9 @@ The `cli` package is a comprehensive application runtime and UI framework design
|
|||
|
||||
### 4. Dependencies
|
||||
- `github.com/spf13/cobra`: The underlying command routing engine.
|
||||
- `github.com/host-uk/core/pkg/framework`: The dependency injection and service lifecycle container.
|
||||
- `github.com/host-uk/core/pkg/i18n`: For translation and semantic grammar generation.
|
||||
- `github.com/host-uk/core/pkg/log`: For structured logging.
|
||||
- `forge.lthn.ai/core/cli/pkg/framework`: The dependency injection and service lifecycle container.
|
||||
- `forge.lthn.ai/core/cli/pkg/i18n`: For translation and semantic grammar generation.
|
||||
- `forge.lthn.ai/core/cli/pkg/log`: For structured logging.
|
||||
- `golang.org/x/term`: For TTY detection.
|
||||
|
||||
### 5. Test Coverage Notes
|
||||
|
|
@ -162,8 +162,8 @@ The `workspace` package implements the `core.Workspace` interface, providing iso
|
|||
- **Key Management**: Delegates actual key generation to the core's `Crypt()` service but manages the storage of the resulting keys within the workspace layout.
|
||||
|
||||
### 4. Dependencies
|
||||
- `github.com/host-uk/core/pkg/framework/core`: Interfaces.
|
||||
- `github.com/host-uk/core/pkg/io`: File system abstraction (`io.Medium`).
|
||||
- `forge.lthn.ai/core/cli/pkg/framework/core`: Interfaces.
|
||||
- `forge.lthn.ai/core/cli/pkg/io`: File system abstraction (`io.Medium`).
|
||||
- `crypt` service (Runtime dependency): Required for `CreateWorkspace`.
|
||||
|
||||
### 5. Test Coverage Notes
|
||||
|
|
|
|||
|
|
@ -87,8 +87,8 @@ type Builder interface {
|
|||
### 4. Dependencies
|
||||
* `archive/tar`, `archive/zip`, `compress/gzip`: Standard library for archiving.
|
||||
* `github.com/Snider/Borg/pkg/compress`: External dependency for XZ compression support.
|
||||
* `github.com/host-uk/core/pkg/io`: Internal interface for filesystem abstraction.
|
||||
* `github.com/host-uk/core/pkg/config`: Internal centralized configuration loading.
|
||||
* `forge.lthn.ai/core/cli/pkg/io`: Internal interface for filesystem abstraction.
|
||||
* `forge.lthn.ai/core/cli/pkg/config`: Internal centralized configuration loading.
|
||||
|
||||
### 5. Test Coverage Notes
|
||||
* **Mocking IO**: Tests must implement a mock `io.Medium` to simulate file existence (`Detect`) and write operations (`Archive`) without touching the disk.
|
||||
|
|
@ -158,7 +158,7 @@ type RunOptions struct {
|
|||
### 4. Dependencies
|
||||
* `os/exec`: Essential for spawning the hypervisor processes.
|
||||
* `embed`: For built-in templates.
|
||||
* `github.com/host-uk/core/pkg/io`: Filesystem access for state and logs.
|
||||
* `forge.lthn.ai/core/cli/pkg/io`: Filesystem access for state and logs.
|
||||
|
||||
### 5. Test Coverage Notes
|
||||
* **Process Management**: Difficult to test `Run` in standard CI. Mocking `exec.Command` or the `Hypervisor` interface is required.
|
||||
|
|
@ -224,7 +224,7 @@ func (r *Runner) RunParallel(ctx context.Context, specs []RunSpec) (*RunAllResul
|
|||
|
||||
### 4. Dependencies
|
||||
* `os/exec`: The underlying execution engine.
|
||||
* `github.com/host-uk/core/pkg/framework`: Creates the `ServiceRuntime` and provides the IPC/Action bus.
|
||||
* `forge.lthn.ai/core/cli/pkg/framework`: Creates the `ServiceRuntime` and provides the IPC/Action bus.
|
||||
|
||||
### 5. Test Coverage Notes
|
||||
* **Concurrency**: The `Runner` needs tests for race conditions during parallel execution.
|
||||
|
|
@ -286,7 +286,7 @@ type JobHandler interface {
|
|||
* **Journaling**: Writes `jsonl` (JSON Lines) files partitioned by repository and date (`baseDir/owner/repo/YYYY-MM-DD.jsonl`), ensuring an append-only audit trail.
|
||||
|
||||
### 4. Dependencies
|
||||
* `github.com/host-uk/core/pkg/log`: Internal logging.
|
||||
* `forge.lthn.ai/core/cli/pkg/log`: Internal logging.
|
||||
* `encoding/json`: For journal serialization.
|
||||
|
||||
### 5. Test Coverage Notes
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ func NewService(opts ServiceOptions) func(*framework.Core) (any, error)
|
|||
|
||||
### Dependencies
|
||||
* `os/exec`: For invoking git commands.
|
||||
* `github.com/host-uk/core/pkg/framework`: For service registration and message passing types.
|
||||
* `forge.lthn.ai/core/cli/pkg/framework`: For service registration and message passing types.
|
||||
|
||||
### Test Coverage Notes
|
||||
* **Mocking**: Testing requires abstracting `exec.Command`. Since this package calls `exec.CommandContext` directly, tests likely require overriding a package-level variable or using a "fake exec" pattern during test initialization.
|
||||
|
|
@ -135,7 +135,7 @@ func (repo *Repo) IsGitRepo() bool
|
|||
|
||||
### Dependencies
|
||||
* `gopkg.in/yaml.v3`: For parsing `repos.yaml`.
|
||||
* `github.com/host-uk/core/pkg/io`: For filesystem abstraction (`io.Medium`).
|
||||
* `forge.lthn.ai/core/cli/pkg/io`: For filesystem abstraction (`io.Medium`).
|
||||
|
||||
### Test Coverage Notes
|
||||
* **Circular Dependencies**: Critical test cases must define a registry with `A->B->A` dependencies to ensure `TopologicalOrder` returns a clear error and doesn't stack overflow.
|
||||
|
|
@ -197,7 +197,7 @@ func (c *Client) ListUserRepos(...)
|
|||
### Dependencies
|
||||
* `code.gitea.io/sdk/gitea` (for `pkg/gitea`)
|
||||
* `codeberg.org/mvdkleijn/forgejo-sdk` (for `pkg/forge`)
|
||||
* `github.com/host-uk/core/pkg/config`: For persistent auth storage.
|
||||
* `forge.lthn.ai/core/cli/pkg/config`: For persistent auth storage.
|
||||
|
||||
### Test Coverage Notes
|
||||
* **Draft Status**: The raw HTTP patch in `pkg/forge` needs integration testing against a real instance or a high-fidelity HTTP mock to ensure payload format matches Forgejo's API expectation.
|
||||
|
|
@ -250,8 +250,8 @@ func IncrementVersion(current string) string
|
|||
* **SDK Generation**: Includes a specialized sub-pipeline (`RunSDK`) that handles OpenAPI diffing and client generation.
|
||||
|
||||
### Dependencies
|
||||
* `github.com/host-uk/core/pkg/build`: For compiling artifacts.
|
||||
* `github.com/host-uk/core/pkg/release/publishers`: Interface definitions for publishing targets.
|
||||
* `forge.lthn.ai/core/cli/pkg/build`: For compiling artifacts.
|
||||
* `forge.lthn.ai/core/cli/pkg/release/publishers`: Interface definitions for publishing targets.
|
||||
* `golang.org/x/text`: For title casing in changelogs.
|
||||
|
||||
### Test Coverage Notes
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ func ListAgents(cfg *config.Config) (map[string]AgentConfig, error)
|
|||
* **Defaults Handling**: `LoadAgents` applies specific logic defaults (e.g., default queue directories, default models like "sonnet") to ensure the system works with minimal configuration.
|
||||
|
||||
### 4. Dependencies
|
||||
* `github.com/host-uk/core/pkg/config`: For reading/writing the persistent configuration state.
|
||||
* `github.com/host-uk/core/pkg/jobrunner/handlers`: To map local config structs to the runtime types used by the job dispatch system.
|
||||
* `forge.lthn.ai/core/cli/pkg/config`: For reading/writing the persistent configuration state.
|
||||
* `forge.lthn.ai/core/cli/pkg/jobrunner/handlers`: To map local config structs to the runtime types used by the job dispatch system.
|
||||
|
||||
### 5. Test Coverage Notes
|
||||
* **Configuration Persistence**: Tests should verify that `SaveAgent` correctly updates the underlying config file and that `LoadAgents` retrieves it accurately.
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue