feat/ml-integration (#2)

Co-authored-by: Charon (snider-linux) <charon@lethean.io>
Co-authored-by: Snider <snider@host.uk.com>
Co-authored-by: Virgil <virgil@lethean.io>
Co-authored-by: Claude <developers@lethean.io>
Reviewed-on: #2
Co-authored-by: Snider <snider@lethean.io>
Co-committed-by: Snider <snider@lethean.io>
This commit is contained in:
Snider 2026-02-16 06:19:09 +00:00 committed by Snider
parent 50da0adcb7
commit 6f84531bd3
77 changed files with 1375 additions and 4500 deletions

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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):"

View file

@ -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

View file

@ -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/*

View file

@ -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/*

View file

@ -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

View file

@ -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(', ')}`);
}
}

View file

@ -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;
}

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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/*

View file

@ -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/*

View file

@ -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/*

View file

@ -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.`
);

View file

@ -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

View file

@ -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

View file

@ -1,14 +1,14 @@
# Core
[![codecov](https://codecov.io/gh/host-uk/core/branch/dev/graph/badge.svg)](https://codecov.io/gh/host-uk/core)
[![Go Test Coverage](https://github.com/host-uk/core/actions/workflows/coverage.yml/badge.svg)](https://github.com/host-uk/core/actions/workflows/coverage.yml)
[![Code Scanning](https://github.com/host-uk/core/actions/workflows/codescan.yml/badge.svg)](https://github.com/host-uk/core/actions/workflows/codescan.yml)
[![Go Test Coverage](https://forge.lthn.ai/core/cli/actions/workflows/coverage.yml/badge.svg)](https://forge.lthn.ai/core/cli/actions/workflows/coverage.yml)
[![Code Scanning](https://forge.lthn.ai/core/cli/actions/workflows/codescan.yml/badge.svg)](https://forge.lthn.ai/core/cli/actions/workflows/codescan.yml)
[![Go Version](https://img.shields.io/github/go-mod/go-version/host-uk/core)](https://go.dev/)
[![License](https://img.shields.io/badge/License-EUPL--1.2-blue.svg)](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 } })

View file

@ -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}}

View file

@ -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

View file

@ -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:

View file

@ -7,6 +7,9 @@ require (
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
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
)

View file

@ -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>

BIN
core-ide Executable file

Binary file not shown.

View file

@ -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
```

View file

@ -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
...

View file

@ -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`

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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() {

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -58,7 +58,7 @@ type TaskResult struct { Changed, Failed bool; Msg, Stdout string; ... }
* **SSH Abstraction**: `ssh.go` wraps `golang.org/x/crypto/ssh` to handle connection pooling, key management, and `sudo` escalation (become).
### 4. Dependencies
* `github.com/host-uk/core/pkg/log`: structured logging.
* `forge.lthn.ai/core/cli/pkg/log`: structured logging.
* `golang.org/x/crypto/ssh`: Underlying SSH transport.
* `gopkg.in/yaml.v3`: YAML parsing.
@ -129,7 +129,7 @@ Re-exports `Core`, `Option`, `Message`, `Startable`, `Stoppable`, and constructo
Purely structural; contains type aliases and variable assignments to expose the internal `core` package.
### 4. Dependencies
* `github.com/host-uk/core/pkg/framework/core`
* `forge.lthn.ai/core/cli/pkg/framework/core`
### 5. Test Coverage Notes
No logic to test directly; coverage belongs in `pkg/framework/core`.

View file

@ -40,7 +40,7 @@ package mypackage
import (
"sync"
"github.com/host-uk/core/pkg/framework"
"forge.lthn.ai/core/cli/pkg/framework"
)
// Service provides mypackage functionality with Core integration.
@ -120,7 +120,7 @@ import (
"sync"
"sync/atomic"
"github.com/host-uk/core/pkg/framework"
"forge.lthn.ai/core/cli/pkg/framework"
)
// Global default service

View file

@ -5,7 +5,7 @@ The `pkg/i18n` package provides internationalisation and localisation for Go CLI
## Quick Start
```go
import "github.com/host-uk/core/pkg/i18n"
import "forge.lthn.ai/core/cli/pkg/i18n"
func main() {
// Initialise with embedded locales

View file

@ -15,7 +15,7 @@
**Files:**
- Create: `go.work`
**Context:** The repo has two real modules — the root (`github.com/host-uk/core`) and core-ide (`github.com/host-uk/core/internal/core-ide`). Without a workspace, core-ide can't import `pkg/jobrunner` from the root module during local development without fragile `replace` directives. A `go.work` file makes cross-module imports resolve locally, keeps each module's `go.mod` clean, and lets CI build each variant independently.
**Context:** The repo has two real modules — the root (`forge.lthn.ai/core/cli`) and core-ide (`forge.lthn.ai/core/cli/internal/core-ide`). Without a workspace, core-ide can't import `pkg/jobrunner` from the root module during local development without fragile `replace` directives. A `go.work` file makes cross-module imports resolve locally, keeps each module's `go.mod` clean, and lets CI build each variant independently.
**Step 1: Create the workspace file**
@ -580,7 +580,7 @@ import (
"fmt"
"time"
"github.com/host-uk/core/pkg/log"
"forge.lthn.ai/core/cli/pkg/log"
)
// PollerConfig configures the job runner poller.
@ -732,7 +732,7 @@ func (p *Poller) AddHandler(h JobHandler) {
_ = fmt.Sprintf // ensure fmt imported for future use
```
Wait — remove that last line. The `fmt` import is only needed if used. Let me correct: the implementation above doesn't use `fmt` directly, so remove it from imports. The `log` package import path is `github.com/host-uk/core/pkg/log`.
Wait — remove that last line. The `fmt` import is only needed if used. Let me correct: the implementation above doesn't use `fmt` directly, so remove it from imports. The `log` package import path is `forge.lthn.ai/core/cli/pkg/log`.
**Step 4: Run tests**
@ -755,7 +755,7 @@ git commit -m "feat(jobrunner): add Poller with multi-source dispatch and journa
- Create: `pkg/jobrunner/github/signals.go`
- Test: `pkg/jobrunner/github/source_test.go`
**Context:** This package lives in the root go.mod (`github.com/host-uk/core`), NOT in the core-ide module. It uses `oauth2` and the GitHub REST API (same pattern as `internal/cmd/updater/github.go`). Uses conditional requests (ETag/If-None-Match) to conserve rate limit.
**Context:** This package lives in the root go.mod (`forge.lthn.ai/core/cli`), NOT in the core-ide module. It uses `oauth2` and the GitHub REST API (same pattern as `internal/cmd/updater/github.go`). Uses conditional requests (ETag/If-None-Match) to conserve rate limit.
**Step 1: Write the test**
@ -769,7 +769,7 @@ import (
"net/http/httptest"
"testing"
"github.com/host-uk/core/pkg/jobrunner"
"forge.lthn.ai/core/cli/pkg/jobrunner"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -856,7 +856,7 @@ import (
"strings"
"time"
"github.com/host-uk/core/pkg/jobrunner"
"forge.lthn.ai/core/cli/pkg/jobrunner"
)
// ghIssue is the minimal structure from GitHub Issues API.
@ -985,8 +985,8 @@ import (
"os"
"strings"
"github.com/host-uk/core/pkg/jobrunner"
"github.com/host-uk/core/pkg/log"
"forge.lthn.ai/core/cli/pkg/jobrunner"
"forge.lthn.ai/core/cli/pkg/log"
"golang.org/x/oauth2"
)
@ -1176,7 +1176,7 @@ import (
"net/http/httptest"
"testing"
"github.com/host-uk/core/pkg/jobrunner"
"forge.lthn.ai/core/cli/pkg/jobrunner"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -1259,7 +1259,7 @@ import (
"net/http"
"time"
"github.com/host-uk/core/pkg/jobrunner"
"forge.lthn.ai/core/cli/pkg/jobrunner"
)
// PublishDraft marks a draft PR as ready for review.
@ -1355,7 +1355,7 @@ import (
"net/http/httptest"
"testing"
"github.com/host-uk/core/pkg/jobrunner"
"forge.lthn.ai/core/cli/pkg/jobrunner"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -1438,7 +1438,7 @@ import (
"net/http"
"time"
"github.com/host-uk/core/pkg/jobrunner"
"forge.lthn.ai/core/cli/pkg/jobrunner"
)
// SendFixCommand comments on a PR to request a fix.
@ -1559,7 +1559,7 @@ import (
"os/exec"
"time"
"github.com/host-uk/core/pkg/jobrunner"
"forge.lthn.ai/core/cli/pkg/jobrunner"
)
type EnableAutoMerge struct{}
@ -1657,7 +1657,7 @@ import (
"net/http/httptest"
"testing"
"github.com/host-uk/core/pkg/jobrunner"
"forge.lthn.ai/core/cli/pkg/jobrunner"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -1758,7 +1758,7 @@ import (
"net/http"
"time"
"github.com/host-uk/core/pkg/jobrunner"
"forge.lthn.ai/core/cli/pkg/jobrunner"
)
// ResolveThreads resolves all unresolved review threads on a PR.
@ -1918,16 +1918,16 @@ git commit -m "feat(jobrunner): add resolve_threads handler with GraphQL"
**Context:** core-ide currently always creates a Wails app. We need to branch: headless starts the poller + MCP bridge directly; desktop mode keeps the existing Wails app with poller as an optional service.
Note: core-ide has its own `go.mod` (`github.com/host-uk/core/internal/core-ide`). The jobrunner package lives in the root module. We need to add the root module as a dependency of core-ide, OR move the handler wiring into the root module. **Simplest approach:** core-ide imports `github.com/host-uk/core/pkg/jobrunner` — this requires adding the root module as a dependency in core-ide's go.mod.
Note: core-ide has its own `go.mod` (`forge.lthn.ai/core/cli/internal/core-ide`). The jobrunner package lives in the root module. We need to add the root module as a dependency of core-ide, OR move the handler wiring into the root module. **Simplest approach:** core-ide imports `forge.lthn.ai/core/cli/pkg/jobrunner` — this requires adding the root module as a dependency in core-ide's go.mod.
**Step 1: Update core-ide go.mod**
Run: `cd /Users/snider/Code/host-uk/core/internal/core-ide && go get github.com/host-uk/core/pkg/jobrunner`
Run: `cd /Users/snider/Code/host-uk/core/internal/core-ide && go get forge.lthn.ai/core/cli/pkg/jobrunner`
If this fails because the package isn't published yet, use a `replace` directive temporarily:
```
replace github.com/host-uk/core => ../..
replace forge.lthn.ai/core/cli => ../..
```
Then `go mod tidy`.
@ -2020,7 +2020,7 @@ git commit -m "feat(core-ide): register job handlers as MCP tools"
```go
// In startHeadless(), before starting poller:
updaterSvc, err := updater.NewUpdateService(updater.UpdateServiceConfig{
RepoURL: "https://github.com/host-uk/core",
RepoURL: "https://forge.lthn.ai/core/cli",
Channel: "alpha",
CheckOnStartup: updater.CheckAndUpdateOnStartup,
})

View file

@ -81,8 +81,8 @@ import (
"context"
"fmt"
ragcmd "github.com/host-uk/core/internal/cmd/rag"
"github.com/host-uk/core/pkg/rag"
ragcmd "forge.lthn.ai/core/cli/internal/cmd/rag"
"forge.lthn.ai/core/cli/pkg/rag"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@ -368,7 +368,7 @@ import (
"fmt"
"time"
"github.com/host-uk/core/pkg/ai"
"forge.lthn.ai/core/cli/pkg/ai"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
@ -608,9 +608,9 @@ import (
"os/signal"
"syscall"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/mcp"
"forge.lthn.ai/core/cli/pkg/cli"
"forge.lthn.ai/core/cli/pkg/i18n"
"forge.lthn.ai/core/cli/pkg/mcp"
)
func init() {
@ -695,7 +695,7 @@ Modify `internal/variants/full.go` to add:
```go
import (
// ... existing imports ...
_ "github.com/host-uk/core/internal/cmd/mcpcmd"
_ "forge.lthn.ai/core/cli/internal/cmd/mcpcmd"
)
```

View file

@ -18,7 +18,7 @@ export PATH="$PATH:$(go env GOPATH)/bin"
source ~/.bashrc # or ~/.zshrc
```
### "go: module github.com/host-uk/core: no matching versions"
### "go: module forge.lthn.ai/core/cli: no matching versions"
**Cause:** Go module proxy hasn't cached the latest version yet.
@ -26,7 +26,7 @@ source ~/.bashrc # or ~/.zshrc
```bash
# Bypass proxy
GOPROXY=direct go install github.com/host-uk/core/cmd/core@latest
GOPROXY=direct go install forge.lthn.ai/core/cli/cmd/core@latest
```
---
@ -340,7 +340,7 @@ This verifies all required tools are installed and configured.
If you've found a bug:
1. Check existing issues: https://github.com/host-uk/core/issues
1. Check existing issues: https://forge.lthn.ai/core/cli/issues
2. Create a new issue with:
- Core version (`core --version`)
- OS and architecture (`go env GOOS GOARCH`)

View file

@ -173,7 +173,7 @@ jobs:
go-version: '1.23'
- name: Install Core
run: go install github.com/host-uk/core/cmd/core@latest
run: go install forge.lthn.ai/core/cli/cmd/core@latest
- name: Build
run: core build --ci

View file

@ -2,9 +2,6 @@ go 1.25.5
use (
.
./cmd/bugseti
./cmd/core-app
./cmd/core-ide
./internal/bugseti
./internal/bugseti/updater
./internal/core-ide

View file

@ -313,7 +313,7 @@ func (s *SubmitService) generatePRBody(issue *Issue) string {
body.WriteString("<!-- Describe how you tested your changes -->\n\n")
body.WriteString("---\n\n")
body.WriteString("*Submitted via [BugSETI](https://bugseti.app) - Distributed Bug Fixing*\n")
body.WriteString("*Submitted via [BugSETI](https://forge.lthn.ai/core/cli) - Distributed Bug Fixing*\n")
return body.String()
}

View file

@ -1,6 +1,6 @@
[Unit]
Description=Core IDE Job Runner (Headless Mode)
Documentation=https://github.com/host-uk/core
Documentation=https://forge.lthn.ai/core/cli
After=network-online.target
Wants=network-online.target

View file

@ -1,6 +1,6 @@
[Unit]
Description=Core IDE Job Runner (User Mode)
Documentation=https://github.com/host-uk/core
Documentation=https://forge.lthn.ai/core/cli
After=network-online.target
Wants=network-online.target

View file

@ -5495,17 +5495,6 @@
"node": ">= 0.4"
}
},
"node_modules/hono": {
"version": "4.11.7",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
"integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=16.9.0"
}
},
"node_modules/hosted-git-info": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz",

View file

@ -49,6 +49,8 @@ require (
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3
forge.lthn.ai/core/cli v0.0.0
forge.lthn.ai/core/cli-gui v0.0.0
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

View file

@ -2,7 +2,7 @@ site_name: Core Framework
site_url: https://core.help
site_description: 'A Web3 Framework for building Go desktop applications with Wails v3'
site_author: 'Snider'
repo_url: 'https://github.com/host-uk/core'
repo_url: 'https://forge.lthn.ai/core/cli'
repo_name: 'host-uk/core'
theme:

560
pkg/devkit/devkit.go Normal file
View file

@ -0,0 +1,560 @@
// Package devkit provides a developer toolkit for common automation commands.
// Designed by Gemini 3 Pro (Hypnos) + Claude Opus (Charon), signed LEK-1 | lthn.ai | EUPL-1.2
package devkit
import (
"bufio"
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
)
// --- Code Quality ---
// Finding represents a single issue found by a linting tool.
type Finding struct {
File string
Line int
Message string
Tool string
}
// CoverageReport holds the test coverage percentage for a package.
type CoverageReport struct {
Package string
Percentage float64
}
// RaceCondition represents a data race detected by the Go race detector.
type RaceCondition struct {
File string
Line int
Desc string
}
// TODO represents a tracked code comment like TODO, FIXME, or HACK.
type TODO struct {
File string
Line int
Type string
Message string
}
// --- Security ---
// Vulnerability represents a dependency vulnerability.
type Vulnerability struct {
ID string
Package string
Version string
Description string
}
// SecretLeak represents a potential secret found in the codebase.
type SecretLeak struct {
File string
Line int
RuleID string
Match string
}
// PermIssue represents a file permission issue.
type PermIssue struct {
File string
Permission string
Issue string
}
// --- Git Operations ---
// DiffSummary provides a summary of changes.
type DiffSummary struct {
FilesChanged int
Insertions int
Deletions int
}
// Commit represents a single git commit.
type Commit struct {
Hash string
Author string
Date time.Time
Message string
}
// --- Build & Dependencies ---
// BuildResult holds the outcome of a single build target.
type BuildResult struct {
Target string
Path string
Error error
}
// Graph represents a dependency graph.
type Graph struct {
Nodes []string
Edges map[string][]string
}
// --- Metrics ---
// ComplexFunc represents a function with its cyclomatic complexity score.
type ComplexFunc struct {
Package string
FuncName string
File string
Line int
Score int
}
// Toolkit wraps common dev automation commands into structured Go APIs.
type Toolkit struct {
Dir string // Working directory for commands
}
// New creates a Toolkit rooted at the given directory.
func New(dir string) *Toolkit {
return &Toolkit{Dir: dir}
}
// Run executes a command and captures stdout, stderr, and exit code.
func (t *Toolkit) Run(name string, args ...string) (stdout, stderr string, exitCode int, err error) {
cmd := exec.Command(name, args...)
cmd.Dir = t.Dir
var stdoutBuf, stderrBuf bytes.Buffer
cmd.Stdout = &stdoutBuf
cmd.Stderr = &stderrBuf
err = cmd.Run()
stdout = stdoutBuf.String()
stderr = stderrBuf.String()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode = exitErr.ExitCode()
} else {
exitCode = -1
}
}
return
}
// FindTODOs greps for TODO/FIXME/HACK comments within a directory.
func (t *Toolkit) FindTODOs(dir string) ([]TODO, error) {
pattern := `\b(TODO|FIXME|HACK)\b(\(.*\))?:`
stdout, stderr, exitCode, err := t.Run("git", "grep", "--line-number", "-E", pattern, "--", dir)
if exitCode == 1 && stdout == "" {
return nil, nil
}
if err != nil && exitCode != 1 {
return nil, fmt.Errorf("git grep failed (exit %d): %s\n%s", exitCode, err, stderr)
}
var todos []TODO
re := regexp.MustCompile(pattern)
for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") {
if line == "" {
continue
}
parts := strings.SplitN(line, ":", 3)
if len(parts) < 3 {
continue
}
lineNum, _ := strconv.Atoi(parts[1])
match := re.FindStringSubmatch(parts[2])
todoType := ""
if len(match) > 1 {
todoType = match[1]
}
msg := strings.TrimSpace(re.Split(parts[2], 2)[1])
todos = append(todos, TODO{
File: parts[0],
Line: lineNum,
Type: todoType,
Message: msg,
})
}
return todos, nil
}
// AuditDeps runs govulncheck to find dependency vulnerabilities.
func (t *Toolkit) AuditDeps() ([]Vulnerability, error) {
stdout, stderr, exitCode, err := t.Run("govulncheck", "./...")
if err != nil && exitCode != 0 && !strings.Contains(stdout, "Vulnerability") {
return nil, fmt.Errorf("govulncheck failed (exit %d): %s\n%s", exitCode, err, stderr)
}
var vulns []Vulnerability
scanner := bufio.NewScanner(strings.NewReader(stdout))
var cur Vulnerability
inBlock := false
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "Vulnerability #") {
if cur.ID != "" {
vulns = append(vulns, cur)
}
fields := strings.Fields(line)
cur = Vulnerability{}
if len(fields) > 1 {
cur.ID = fields[1]
}
inBlock = true
} else if inBlock {
switch {
case strings.Contains(line, "Package:"):
cur.Package = strings.TrimSpace(strings.SplitN(line, ":", 2)[1])
case strings.Contains(line, "Found in version:"):
cur.Version = strings.TrimSpace(strings.SplitN(line, ":", 2)[1])
case line == "":
if cur.ID != "" {
vulns = append(vulns, cur)
cur = Vulnerability{}
}
inBlock = false
default:
if !strings.HasPrefix(line, " ") && cur.Description == "" {
cur.Description = strings.TrimSpace(line)
}
}
}
}
if cur.ID != "" {
vulns = append(vulns, cur)
}
return vulns, nil
}
// DiffStat returns a summary of uncommitted changes.
func (t *Toolkit) DiffStat() (DiffSummary, error) {
stdout, stderr, exitCode, err := t.Run("git", "diff", "--stat")
if err != nil && exitCode != 0 {
return DiffSummary{}, fmt.Errorf("git diff failed (exit %d): %s\n%s", exitCode, err, stderr)
}
var s DiffSummary
lines := strings.Split(strings.TrimSpace(stdout), "\n")
if len(lines) == 0 || lines[0] == "" {
return s, nil
}
last := lines[len(lines)-1]
for _, part := range strings.Split(last, ",") {
part = strings.TrimSpace(part)
fields := strings.Fields(part)
if len(fields) < 2 {
continue
}
val, _ := strconv.Atoi(fields[0])
switch {
case strings.Contains(part, "file"):
s.FilesChanged = val
case strings.Contains(part, "insertion"):
s.Insertions = val
case strings.Contains(part, "deletion"):
s.Deletions = val
}
}
return s, nil
}
// UncommittedFiles returns paths of files with uncommitted changes.
func (t *Toolkit) UncommittedFiles() ([]string, error) {
stdout, stderr, exitCode, err := t.Run("git", "status", "--porcelain")
if err != nil && exitCode != 0 {
return nil, fmt.Errorf("git status failed: %s\n%s", err, stderr)
}
var files []string
for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") {
if len(line) > 3 {
files = append(files, strings.TrimSpace(line[3:]))
}
}
return files, nil
}
// Lint runs go vet on the given package pattern.
func (t *Toolkit) Lint(pkg string) ([]Finding, error) {
_, stderr, exitCode, err := t.Run("go", "vet", pkg)
if exitCode == 0 {
return nil, nil
}
if err != nil && exitCode != 2 {
return nil, fmt.Errorf("go vet failed: %w", err)
}
var findings []Finding
for _, line := range strings.Split(strings.TrimSpace(stderr), "\n") {
if line == "" {
continue
}
parts := strings.SplitN(line, ":", 4)
if len(parts) < 4 {
continue
}
lineNum, _ := strconv.Atoi(parts[1])
findings = append(findings, Finding{
File: parts[0],
Line: lineNum,
Message: strings.TrimSpace(parts[3]),
Tool: "go vet",
})
}
return findings, nil
}
// ScanSecrets runs gitleaks to find potential secret leaks.
func (t *Toolkit) ScanSecrets(dir string) ([]SecretLeak, error) {
stdout, _, exitCode, err := t.Run("gitleaks", "detect", "--source", dir, "--report-format", "csv", "--no-git")
if exitCode == 0 {
return nil, nil
}
if err != nil && exitCode != 1 {
return nil, fmt.Errorf("gitleaks failed: %w", err)
}
var leaks []SecretLeak
for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") {
if line == "" || strings.HasPrefix(line, "RuleID") {
continue
}
parts := strings.SplitN(line, ",", 4)
if len(parts) < 4 {
continue
}
lineNum, _ := strconv.Atoi(parts[2])
leaks = append(leaks, SecretLeak{
RuleID: parts[0],
File: parts[1],
Line: lineNum,
Match: parts[3],
})
}
return leaks, nil
}
// ModTidy runs go mod tidy.
func (t *Toolkit) ModTidy() error {
_, stderr, exitCode, err := t.Run("go", "mod", "tidy")
if err != nil && exitCode != 0 {
return fmt.Errorf("go mod tidy failed: %s", stderr)
}
return nil
}
// Build compiles the given targets.
func (t *Toolkit) Build(targets ...string) ([]BuildResult, error) {
var results []BuildResult
for _, target := range targets {
_, stderr, _, err := t.Run("go", "build", "-o", "/dev/null", target)
r := BuildResult{Target: target}
if err != nil {
r.Error = fmt.Errorf("%s", strings.TrimSpace(stderr))
}
results = append(results, r)
}
return results, nil
}
// TestCount returns the number of test functions in a package.
func (t *Toolkit) TestCount(pkg string) (int, error) {
stdout, stderr, exitCode, err := t.Run("go", "test", "-list", ".*", pkg)
if err != nil && exitCode != 0 {
return 0, fmt.Errorf("go test -list failed: %s\n%s", err, stderr)
}
count := 0
for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") {
if strings.HasPrefix(line, "Test") || strings.HasPrefix(line, "Benchmark") {
count++
}
}
return count, nil
}
// Coverage runs go test -cover and parses per-package coverage percentages.
func (t *Toolkit) Coverage(pkg string) ([]CoverageReport, error) {
if pkg == "" {
pkg = "./..."
}
stdout, stderr, exitCode, err := t.Run("go", "test", "-cover", pkg)
if err != nil && exitCode != 0 && !strings.Contains(stdout, "coverage:") {
return nil, fmt.Errorf("go test -cover failed (exit %d): %s\n%s", exitCode, err, stderr)
}
var reports []CoverageReport
re := regexp.MustCompile(`ok\s+(\S+)\s+.*coverage:\s+([\d.]+)%`)
scanner := bufio.NewScanner(strings.NewReader(stdout))
for scanner.Scan() {
matches := re.FindStringSubmatch(scanner.Text())
if len(matches) == 3 {
pct, _ := strconv.ParseFloat(matches[2], 64)
reports = append(reports, CoverageReport{
Package: matches[1],
Percentage: pct,
})
}
}
return reports, nil
}
// RaceDetect runs go test -race and parses data race warnings.
func (t *Toolkit) RaceDetect(pkg string) ([]RaceCondition, error) {
if pkg == "" {
pkg = "./..."
}
_, stderr, _, err := t.Run("go", "test", "-race", pkg)
if err != nil && !strings.Contains(stderr, "WARNING: DATA RACE") {
return nil, fmt.Errorf("go test -race failed: %w", err)
}
var races []RaceCondition
lines := strings.Split(stderr, "\n")
reFile := regexp.MustCompile(`\s+(.*\.go):(\d+)`)
for i, line := range lines {
if strings.Contains(line, "WARNING: DATA RACE") {
rc := RaceCondition{Desc: "Data race detected"}
for j := i + 1; j < len(lines) && j < i+15; j++ {
if match := reFile.FindStringSubmatch(lines[j]); len(match) == 3 {
rc.File = strings.TrimSpace(match[1])
rc.Line, _ = strconv.Atoi(match[2])
break
}
}
races = append(races, rc)
}
}
return races, nil
}
// Complexity runs gocyclo and returns functions exceeding the threshold.
func (t *Toolkit) Complexity(threshold int) ([]ComplexFunc, error) {
stdout, stderr, exitCode, err := t.Run("gocyclo", "-over", strconv.Itoa(threshold), ".")
if err != nil && exitCode == -1 {
return nil, fmt.Errorf("gocyclo not available: %s\n%s", err, stderr)
}
var funcs []ComplexFunc
scanner := bufio.NewScanner(strings.NewReader(stdout))
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < 4 {
continue
}
score, _ := strconv.Atoi(fields[0])
fileParts := strings.Split(fields[3], ":")
line := 0
if len(fileParts) > 1 {
line, _ = strconv.Atoi(fileParts[1])
}
funcs = append(funcs, ComplexFunc{
Score: score,
Package: fields[1],
FuncName: fields[2],
File: fileParts[0],
Line: line,
})
}
return funcs, nil
}
// DepGraph runs go mod graph and builds a dependency graph.
func (t *Toolkit) DepGraph(pkg string) (*Graph, error) {
stdout, stderr, exitCode, err := t.Run("go", "mod", "graph")
if err != nil && exitCode != 0 {
return nil, fmt.Errorf("go mod graph failed (exit %d): %s\n%s", exitCode, err, stderr)
}
graph := &Graph{Edges: make(map[string][]string)}
nodes := make(map[string]struct{})
scanner := bufio.NewScanner(strings.NewReader(stdout))
for scanner.Scan() {
parts := strings.Fields(scanner.Text())
if len(parts) >= 2 {
src, dst := parts[0], parts[1]
graph.Edges[src] = append(graph.Edges[src], dst)
nodes[src] = struct{}{}
nodes[dst] = struct{}{}
}
}
for node := range nodes {
graph.Nodes = append(graph.Nodes, node)
}
return graph, nil
}
// GitLog returns the last n commits from git history.
func (t *Toolkit) GitLog(n int) ([]Commit, error) {
stdout, stderr, exitCode, err := t.Run("git", "log", fmt.Sprintf("-n%d", n), "--format=%H|%an|%aI|%s")
if err != nil && exitCode != 0 {
return nil, fmt.Errorf("git log failed (exit %d): %s\n%s", exitCode, err, stderr)
}
var commits []Commit
scanner := bufio.NewScanner(strings.NewReader(stdout))
for scanner.Scan() {
parts := strings.SplitN(scanner.Text(), "|", 4)
if len(parts) < 4 {
continue
}
date, _ := time.Parse(time.RFC3339, parts[2])
commits = append(commits, Commit{
Hash: parts[0],
Author: parts[1],
Date: date,
Message: parts[3],
})
}
return commits, nil
}
// CheckPerms walks a directory and flags files with overly permissive modes.
func (t *Toolkit) CheckPerms(dir string) ([]PermIssue, error) {
var issues []PermIssue
err := filepath.Walk(filepath.Join(t.Dir, dir), func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if info.IsDir() {
return nil
}
mode := info.Mode().Perm()
if mode&0o002 != 0 {
issues = append(issues, PermIssue{
File: path,
Permission: fmt.Sprintf("%04o", mode),
Issue: "World-writable",
})
} else if mode&0o020 != 0 && mode&0o002 != 0 {
issues = append(issues, PermIssue{
File: path,
Permission: fmt.Sprintf("%04o", mode),
Issue: "Group and world-writable",
})
}
return nil
})
if err != nil {
return nil, fmt.Errorf("walk failed: %w", err)
}
return issues, nil
}
// LEK-1 | lthn.ai | EUPL-1.2

270
pkg/devkit/devkit_test.go Normal file
View file

@ -0,0 +1,270 @@
// Designed by Gemini 3 Pro (Hypnos) + Claude Opus (Charon), signed LEK-1 | lthn.ai | EUPL-1.2
package devkit
import (
"fmt"
"os"
"path/filepath"
"testing"
"time"
)
// setupMockCmd creates a shell script in a temp dir that echoes predetermined
// content, and prepends that dir to PATH so Run() picks it up.
func setupMockCmd(t *testing.T, name, content string) {
t.Helper()
tmpDir := t.TempDir()
scriptPath := filepath.Join(tmpDir, name)
script := fmt.Sprintf("#!/bin/sh\ncat <<'MOCK_EOF'\n%s\nMOCK_EOF\n", content)
if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
t.Fatalf("failed to write mock command %s: %v", name, err)
}
oldPath := os.Getenv("PATH")
t.Setenv("PATH", tmpDir+string(os.PathListSeparator)+oldPath)
}
// setupMockCmdExit creates a mock that echoes to stdout/stderr and exits with a code.
func setupMockCmdExit(t *testing.T, name, stdout, stderr string, exitCode int) {
t.Helper()
tmpDir := t.TempDir()
scriptPath := filepath.Join(tmpDir, name)
script := fmt.Sprintf("#!/bin/sh\ncat <<'MOCK_EOF'\n%s\nMOCK_EOF\ncat <<'MOCK_ERR' >&2\n%s\nMOCK_ERR\nexit %d\n", stdout, stderr, exitCode)
if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
t.Fatalf("failed to write mock command %s: %v", name, err)
}
oldPath := os.Getenv("PATH")
t.Setenv("PATH", tmpDir+string(os.PathListSeparator)+oldPath)
}
func TestCoverage_Good(t *testing.T) {
output := `? example.com/skipped [no test files]
ok example.com/pkg1 0.5s coverage: 85.0% of statements
ok example.com/pkg2 0.2s coverage: 100.0% of statements`
setupMockCmd(t, "go", output)
tk := New(t.TempDir())
reports, err := tk.Coverage("./...")
if err != nil {
t.Fatalf("Coverage failed: %v", err)
}
if len(reports) != 2 {
t.Fatalf("expected 2 reports, got %d", len(reports))
}
if reports[0].Package != "example.com/pkg1" || reports[0].Percentage != 85.0 {
t.Errorf("report 0: want pkg1@85%%, got %s@%.1f%%", reports[0].Package, reports[0].Percentage)
}
if reports[1].Package != "example.com/pkg2" || reports[1].Percentage != 100.0 {
t.Errorf("report 1: want pkg2@100%%, got %s@%.1f%%", reports[1].Package, reports[1].Percentage)
}
}
func TestCoverage_Bad(t *testing.T) {
// No coverage lines in output
setupMockCmd(t, "go", "FAIL\texample.com/broken [build failed]")
tk := New(t.TempDir())
reports, err := tk.Coverage("./...")
if err != nil {
t.Fatalf("Coverage should not error on partial output: %v", err)
}
if len(reports) != 0 {
t.Errorf("expected 0 reports from failed build, got %d", len(reports))
}
}
func TestGitLog_Good(t *testing.T) {
now := time.Now().Truncate(time.Second)
nowStr := now.Format(time.RFC3339)
output := fmt.Sprintf("abc123|Alice|%s|Fix the bug\ndef456|Bob|%s|Add feature", nowStr, nowStr)
setupMockCmd(t, "git", output)
tk := New(t.TempDir())
commits, err := tk.GitLog(2)
if err != nil {
t.Fatalf("GitLog failed: %v", err)
}
if len(commits) != 2 {
t.Fatalf("expected 2 commits, got %d", len(commits))
}
if commits[0].Hash != "abc123" {
t.Errorf("hash: want abc123, got %s", commits[0].Hash)
}
if commits[0].Author != "Alice" {
t.Errorf("author: want Alice, got %s", commits[0].Author)
}
if commits[0].Message != "Fix the bug" {
t.Errorf("message: want 'Fix the bug', got %q", commits[0].Message)
}
if !commits[0].Date.Equal(now) {
t.Errorf("date: want %v, got %v", now, commits[0].Date)
}
}
func TestGitLog_Bad(t *testing.T) {
// Malformed lines should be skipped
setupMockCmd(t, "git", "incomplete|line\nabc|Bob|2025-01-01T00:00:00Z|Good commit")
tk := New(t.TempDir())
commits, err := tk.GitLog(5)
if err != nil {
t.Fatalf("GitLog failed: %v", err)
}
if len(commits) != 1 {
t.Errorf("expected 1 valid commit (skip malformed), got %d", len(commits))
}
}
func TestComplexity_Good(t *testing.T) {
output := "15 main ComplexFunc file.go:10:1\n20 pkg VeryComplex other.go:50:1"
setupMockCmd(t, "gocyclo", output)
tk := New(t.TempDir())
funcs, err := tk.Complexity(10)
if err != nil {
t.Fatalf("Complexity failed: %v", err)
}
if len(funcs) != 2 {
t.Fatalf("expected 2 funcs, got %d", len(funcs))
}
if funcs[0].Score != 15 || funcs[0].FuncName != "ComplexFunc" || funcs[0].File != "file.go" || funcs[0].Line != 10 {
t.Errorf("func 0: unexpected %+v", funcs[0])
}
if funcs[1].Score != 20 || funcs[1].Package != "pkg" {
t.Errorf("func 1: unexpected %+v", funcs[1])
}
}
func TestComplexity_Bad(t *testing.T) {
// No functions above threshold = empty output
setupMockCmd(t, "gocyclo", "")
tk := New(t.TempDir())
funcs, err := tk.Complexity(50)
if err != nil {
t.Fatalf("Complexity should not error on empty output: %v", err)
}
if len(funcs) != 0 {
t.Errorf("expected 0 funcs, got %d", len(funcs))
}
}
func TestDepGraph_Good(t *testing.T) {
output := "modA@v1 modB@v2\nmodA@v1 modC@v3\nmodB@v2 modD@v1"
setupMockCmd(t, "go", output)
tk := New(t.TempDir())
graph, err := tk.DepGraph("./...")
if err != nil {
t.Fatalf("DepGraph failed: %v", err)
}
if len(graph.Nodes) != 4 {
t.Errorf("expected 4 nodes, got %d: %v", len(graph.Nodes), graph.Nodes)
}
edgesA := graph.Edges["modA@v1"]
if len(edgesA) != 2 {
t.Errorf("expected 2 edges from modA@v1, got %d", len(edgesA))
}
}
func TestRaceDetect_Good(t *testing.T) {
// No races = clean run
setupMockCmd(t, "go", "ok\texample.com/safe\t0.1s")
tk := New(t.TempDir())
races, err := tk.RaceDetect("./...")
if err != nil {
t.Fatalf("RaceDetect failed on clean run: %v", err)
}
if len(races) != 0 {
t.Errorf("expected 0 races, got %d", len(races))
}
}
func TestRaceDetect_Bad(t *testing.T) {
stderrOut := `WARNING: DATA RACE
Read at 0x00c000123456 by goroutine 7:
/home/user/project/main.go:42
Previous write at 0x00c000123456 by goroutine 6:
/home/user/project/main.go:38`
setupMockCmdExit(t, "go", "", stderrOut, 1)
tk := New(t.TempDir())
races, err := tk.RaceDetect("./...")
if err != nil {
t.Fatalf("RaceDetect should parse races, not error: %v", err)
}
if len(races) != 1 {
t.Fatalf("expected 1 race, got %d", len(races))
}
if races[0].File != "/home/user/project/main.go" || races[0].Line != 42 {
t.Errorf("race: unexpected %+v", races[0])
}
}
func TestDiffStat_Good(t *testing.T) {
output := ` file1.go | 10 +++++++---
file2.go | 5 +++++
2 files changed, 12 insertions(+), 3 deletions(-)`
setupMockCmd(t, "git", output)
tk := New(t.TempDir())
s, err := tk.DiffStat()
if err != nil {
t.Fatalf("DiffStat failed: %v", err)
}
if s.FilesChanged != 2 {
t.Errorf("files: want 2, got %d", s.FilesChanged)
}
if s.Insertions != 12 {
t.Errorf("insertions: want 12, got %d", s.Insertions)
}
if s.Deletions != 3 {
t.Errorf("deletions: want 3, got %d", s.Deletions)
}
}
func TestCheckPerms_Good(t *testing.T) {
dir := t.TempDir()
// Create a world-writable file
badFile := filepath.Join(dir, "bad.txt")
if err := os.WriteFile(badFile, []byte("test"), 0644); err != nil {
t.Fatal(err)
}
if err := os.Chmod(badFile, 0666); err != nil {
t.Fatal(err)
}
// Create a safe file
goodFile := filepath.Join(dir, "good.txt")
if err := os.WriteFile(goodFile, []byte("test"), 0644); err != nil {
t.Fatal(err)
}
tk := New("/")
issues, err := tk.CheckPerms(dir)
if err != nil {
t.Fatalf("CheckPerms failed: %v", err)
}
if len(issues) != 1 {
t.Fatalf("expected 1 issue (world-writable), got %d", len(issues))
}
if issues[0].Issue != "World-writable" {
t.Errorf("issue: want 'World-writable', got %q", issues[0].Issue)
}
}
func TestNew(t *testing.T) {
tk := New("/tmp")
if tk.Dir != "/tmp" {
t.Errorf("Dir: want /tmp, got %s", tk.Dir)
}
}
// LEK-1 | lthn.ai | EUPL-1.2

View file

@ -1146,277 +1146,129 @@
"error.gh_not_found": "'gh' CLI not found. Install from https://cli.github.com/",
"error.registry_not_found": "No repos.yaml found",
"error.repo_not_found": "Repository '{{.Name}}' not found",
"gram.article.definite": "the",
"gram.article.definite.feminine": "",
"gram.article.definite.masculine": "",
"gram.article.definite.neuter": "",
"gram.article.indefinite.default": "a",
"gram.article.indefinite.feminine": "",
"gram.article.indefinite.masculine": "",
"gram.article.indefinite.neuter": "",
"gram.article.indefinite.vowel": "an",
"gram.noun.artifact.one": "artifact",
"gram.noun.artifact.other": "artifacts",
"gram.noun.branch.gender": "",
"gram.noun.branch.one": "branch",
"gram.noun.branch.other": "branches",
"gram.noun.category.one": "category",
"gram.noun.category.other": "categories",
"gram.noun.change.gender": "",
"gram.noun.change.one": "change",
"gram.noun.change.other": "changes",
"gram.noun.check.one": "check",
"gram.noun.check.other": "checks",
"gram.noun.child.one": "child",
"gram.noun.child.other": "children",
"gram.noun.commit.gender": "",
"gram.noun.commit.one": "commit",
"gram.noun.commit.other": "commits",
"gram.noun.dependency.one": "dependency",
"gram.noun.dependency.other": "dependencies",
"gram.noun.directory.one": "directory",
"gram.noun.directory.other": "directories",
"gram.noun.failed.one": "failed",
"gram.noun.failed.other": "failed",
"gram.noun.file.gender": "",
"gram.noun.file.one": "file",
"gram.noun.file.other": "files",
"gram.noun.issue.one": "issue",
"gram.noun.issue.other": "issues",
"gram.noun.item.gender": "",
"gram.noun.item.one": "item",
"gram.noun.item.other": "items",
"gram.noun.package.one": "package",
"gram.noun.package.other": "packages",
"gram.noun.passed.one": "passed",
"gram.noun.passed.other": "passed",
"gram.noun.person.one": "person",
"gram.noun.person.other": "people",
"gram.noun.query.one": "query",
"gram.noun.query.other": "queries",
"gram.noun.repo.gender": "",
"gram.noun.repo.one": "repo",
"gram.noun.repo.other": "repos",
"gram.noun.repository.one": "repository",
"gram.noun.repository.other": "repositories",
"gram.noun.skipped.one": "skipped",
"gram.noun.skipped.other": "skipped",
"gram.noun.task.one": "task",
"gram.noun.task.other": "tasks",
"gram.noun.test.one": "test",
"gram.noun.test.other": "tests",
"gram.noun.vulnerability.one": "vulnerability",
"gram.noun.vulnerability.other": "vulnerabilities",
"gram.number.decimal": ".",
"gram.number.percent": "%s%%",
"gram.number.thousands": ",",
"gram.punct.label": ":",
"gram.punct.progress": "...",
"gram.verb.analyse.base": "",
"gram.verb.analyse.gerund": "",
"gram.verb.analyse.past": "",
"gram.verb.be.base": "be",
"gram.verb.be.gerund": "being",
"gram.verb.be.past": "was",
"gram.verb.begin.base": "begin",
"gram.verb.begin.gerund": "beginning",
"gram.verb.begin.past": "began",
"gram.verb.bring.base": "bring",
"gram.verb.bring.gerund": "bringing",
"gram.verb.bring.past": "brought",
"gram.verb.build.base": "build",
"gram.verb.build.gerund": "building",
"gram.verb.build.past": "built",
"gram.verb.buy.base": "buy",
"gram.verb.buy.gerund": "buying",
"gram.verb.buy.past": "bought",
"gram.verb.catch.base": "catch",
"gram.verb.catch.gerund": "catching",
"gram.verb.catch.past": "caught",
"gram.verb.check.base": "",
"gram.verb.check.gerund": "",
"gram.verb.check.past": "",
"gram.verb.choose.base": "choose",
"gram.verb.choose.gerund": "choosing",
"gram.verb.choose.past": "chose",
"gram.verb.commit.base": "commit",
"gram.verb.commit.gerund": "committing",
"gram.verb.commit.past": "committed",
"gram.verb.create.base": "",
"gram.verb.create.gerund": "",
"gram.verb.create.past": "",
"gram.verb.cut.base": "cut",
"gram.verb.cut.gerund": "cutting",
"gram.verb.cut.past": "cut",
"gram.verb.delete.base": "",
"gram.verb.delete.gerund": "",
"gram.verb.delete.past": "",
"gram.verb.do.base": "do",
"gram.verb.do.gerund": "doing",
"gram.verb.do.past": "did",
"gram.verb.find.base": "find",
"gram.verb.find.gerund": "finding",
"gram.verb.find.past": "found",
"gram.verb.format.base": "format",
"gram.verb.format.gerund": "formatting",
"gram.verb.format.past": "formatted",
"gram.verb.get.base": "get",
"gram.verb.get.gerund": "getting",
"gram.verb.get.past": "got",
"gram.verb.go.base": "go",
"gram.verb.go.gerund": "going",
"gram.verb.go.past": "went",
"gram.verb.have.base": "have",
"gram.verb.have.gerund": "having",
"gram.verb.have.past": "had",
"gram.verb.hit.base": "hit",
"gram.verb.hit.gerund": "hitting",
"gram.verb.hit.past": "hit",
"gram.verb.hold.base": "hold",
"gram.verb.hold.gerund": "holding",
"gram.verb.hold.past": "held",
"gram.verb.install.base": "",
"gram.verb.install.gerund": "",
"gram.verb.install.past": "",
"gram.verb.keep.base": "keep",
"gram.verb.keep.gerund": "keeping",
"gram.verb.keep.past": "kept",
"gram.verb.lead.base": "lead",
"gram.verb.lead.gerund": "leading",
"gram.verb.lead.past": "led",
"gram.verb.leave.base": "leave",
"gram.verb.leave.gerund": "leaving",
"gram.verb.leave.past": "left",
"gram.verb.lose.base": "lose",
"gram.verb.lose.gerund": "losing",
"gram.verb.lose.past": "lost",
"gram.verb.make.base": "make",
"gram.verb.make.gerund": "making",
"gram.verb.make.past": "made",
"gram.verb.meet.base": "meet",
"gram.verb.meet.gerund": "meeting",
"gram.verb.meet.past": "met",
"gram.verb.organise.base": "",
"gram.verb.organise.gerund": "",
"gram.verb.organise.past": "",
"gram.verb.pay.base": "pay",
"gram.verb.pay.gerund": "paying",
"gram.verb.pay.past": "paid",
"gram.verb.pull.base": "",
"gram.verb.pull.gerund": "",
"gram.verb.pull.past": "",
"gram.verb.push.base": "",
"gram.verb.push.gerund": "",
"gram.verb.push.past": "",
"gram.verb.put.base": "put",
"gram.verb.put.gerund": "putting",
"gram.verb.put.past": "put",
"gram.verb.realise.base": "",
"gram.verb.realise.gerund": "",
"gram.verb.realise.past": "",
"gram.verb.recognise.base": "",
"gram.verb.recognise.gerund": "",
"gram.verb.recognise.past": "",
"gram.verb.run.base": "run",
"gram.verb.run.gerund": "running",
"gram.verb.run.past": "ran",
"gram.verb.save.base": "",
"gram.verb.save.gerund": "",
"gram.verb.save.past": "",
"gram.verb.scan.base": "scan",
"gram.verb.scan.gerund": "scanning",
"gram.verb.scan.past": "scanned",
"gram.verb.sell.base": "sell",
"gram.verb.sell.gerund": "selling",
"gram.verb.sell.past": "sold",
"gram.verb.send.base": "send",
"gram.verb.send.gerund": "sending",
"gram.verb.send.past": "sent",
"gram.verb.set.base": "set",
"gram.verb.set.gerund": "setting",
"gram.verb.set.past": "set",
"gram.verb.shut.base": "shut",
"gram.verb.shut.gerund": "shutting",
"gram.verb.shut.past": "shut",
"gram.verb.sit.base": "sit",
"gram.verb.sit.gerund": "sitting",
"gram.verb.sit.past": "sat",
"gram.verb.spend.base": "spend",
"gram.verb.spend.gerund": "spending",
"gram.verb.spend.past": "spent",
"gram.verb.split.base": "split",
"gram.verb.split.gerund": "splitting",
"gram.verb.split.past": "split",
"gram.verb.stop.base": "stop",
"gram.verb.stop.gerund": "stopping",
"gram.verb.stop.past": "stopped",
"gram.verb.take.base": "take",
"gram.verb.take.gerund": "taking",
"gram.verb.take.past": "took",
"gram.verb.think.base": "think",
"gram.verb.think.gerund": "thinking",
"gram.verb.think.past": "thought",
"gram.verb.update.base": "",
"gram.verb.update.gerund": "",
"gram.verb.update.past": "",
"gram.verb.win.base": "win",
"gram.verb.win.gerund": "winning",
"gram.verb.win.past": "won",
"gram.verb.write.base": "write",
"gram.verb.write.gerund": "writing",
"gram.verb.write.past": "wrote",
"gram.word.api": "API",
"gram.word.app_url": "app URL",
"gram.word.blocked_by": "blocked by",
"gram.word.cgo": "CGO",
"gram.word.ci": "CI",
"gram.word.claimed_by": "claimed by",
"gram.word.coverage": "coverage",
"gram.word.cpus": "CPUs",
"gram.word.dry_run": "dry run",
"gram.word.failed": "failed",
"gram.word.filter": "filter",
"gram.word.go_mod": "go.mod",
"gram.word.html": "HTML",
"gram.word.id": "ID",
"gram.word.ok": "OK",
"gram.word.package": "package",
"gram.word.passed": "passed",
"gram.word.php": "PHP",
"gram.word.pid": "PID",
"gram.word.pnpm": "pnpm",
"gram.word.pr": "PR",
"gram.word.qa": "QA",
"gram.word.related_files": "related files",
"gram.word.sdk": "SDK",
"gram.word.skipped": "skipped",
"gram.word.ssh": "SSH",
"gram.word.ssl": "SSL",
"gram.word.test": "test",
"gram.word.up_to_date": "up to date",
"gram.word.url": "URL",
"gram.word.vite": "Vite",
"lang.de": "German",
"lang.en": "English",
"lang.es": "Spanish",
"lang.fr": "French",
"lang.zh": "Chinese",
"prompt.confirm": "Are you sure?",
"prompt.continue": "Continue?",
"prompt.discard": "Discard changes?",
"prompt.no": "n",
"prompt.overwrite": "Overwrite?",
"prompt.proceed": "Proceed?",
"prompt.yes": "y",
"time.ago.day.one": "{{.Count}} day ago",
"time.ago.day.other": "{{.Count}} days ago",
"time.ago.hour.one": "{{.Count}} hour ago",
"time.ago.hour.other": "{{.Count}} hours ago",
"time.ago.minute.one": "{{.Count}} minute ago",
"time.ago.minute.other": "{{.Count}} minutes ago",
"time.ago.second.one": "{{.Count}} second ago",
"time.ago.second.other": "{{.Count}} seconds ago",
"time.ago.week.one": "{{.Count}} week ago",
"time.ago.week.other": "{{.Count}} weeks ago",
"time.just_now": "just now"
"gram": {
"verb": {
"be": { "base": "be", "past": "was", "gerund": "being" },
"go": { "base": "go", "past": "went", "gerund": "going" },
"do": { "base": "do", "past": "did", "gerund": "doing" },
"have": { "base": "have", "past": "had", "gerund": "having" },
"make": { "base": "make", "past": "made", "gerund": "making" },
"get": { "base": "get", "past": "got", "gerund": "getting" },
"run": { "base": "run", "past": "ran", "gerund": "running" },
"write": { "base": "write", "past": "wrote", "gerund": "writing" },
"build": { "base": "build", "past": "built", "gerund": "building" },
"send": { "base": "send", "past": "sent", "gerund": "sending" },
"find": { "base": "find", "past": "found", "gerund": "finding" },
"take": { "base": "take", "past": "took", "gerund": "taking" },
"begin": { "base": "begin", "past": "began", "gerund": "beginning" },
"keep": { "base": "keep", "past": "kept", "gerund": "keeping" },
"hold": { "base": "hold", "past": "held", "gerund": "holding" },
"bring": { "base": "bring", "past": "brought", "gerund": "bringing" },
"think": { "base": "think", "past": "thought", "gerund": "thinking" },
"buy": { "base": "buy", "past": "bought", "gerund": "buying" },
"catch": { "base": "catch", "past": "caught", "gerund": "catching" },
"choose": { "base": "choose", "past": "chose", "gerund": "choosing" },
"lose": { "base": "lose", "past": "lost", "gerund": "losing" },
"win": { "base": "win", "past": "won", "gerund": "winning" },
"meet": { "base": "meet", "past": "met", "gerund": "meeting" },
"lead": { "base": "lead", "past": "led", "gerund": "leading" },
"leave": { "base": "leave", "past": "left", "gerund": "leaving" },
"spend": { "base": "spend", "past": "spent", "gerund": "spending" },
"pay": { "base": "pay", "past": "paid", "gerund": "paying" },
"sell": { "base": "sell", "past": "sold", "gerund": "selling" },
"commit": { "base": "commit", "past": "committed", "gerund": "committing" },
"stop": { "base": "stop", "past": "stopped", "gerund": "stopping" },
"scan": { "base": "scan", "past": "scanned", "gerund": "scanning" },
"format": { "base": "format", "past": "formatted", "gerund": "formatting" },
"set": { "base": "set", "past": "set", "gerund": "setting" },
"put": { "base": "put", "past": "put", "gerund": "putting" },
"cut": { "base": "cut", "past": "cut", "gerund": "cutting" },
"hit": { "base": "hit", "past": "hit", "gerund": "hitting" },
"sit": { "base": "sit", "past": "sat", "gerund": "sitting" },
"split": { "base": "split", "past": "split", "gerund": "splitting" },
"shut": { "base": "shut", "past": "shut", "gerund": "shutting" },
"check": { "base": "check", "past": "checked", "gerund": "checking" },
"create": { "base": "create", "past": "created", "gerund": "creating" },
"delete": { "base": "delete", "past": "deleted", "gerund": "deleting" },
"install": { "base": "install", "past": "installed", "gerund": "installing" },
"update": { "base": "update", "past": "updated", "gerund": "updating" },
"pull": { "base": "pull", "past": "pulled", "gerund": "pulling" },
"push": { "base": "push", "past": "pushed", "gerund": "pushing" },
"save": { "base": "save", "past": "saved", "gerund": "saving" },
"analyse": { "base": "analyse", "past": "analysed", "gerund": "analysing" },
"organise": { "base": "organise", "past": "organised", "gerund": "organising" },
"realise": { "base": "realise", "past": "realised", "gerund": "realising" },
"recognise": { "base": "recognise", "past": "recognised", "gerund": "recognising" }
},
"noun": {
"file": { "one": "file", "other": "files" },
"repo": { "one": "repo", "other": "repos" },
"repository": { "one": "repository", "other": "repositories" },
"commit": { "one": "commit", "other": "commits" },
"branch": { "one": "branch", "other": "branches" },
"change": { "one": "change", "other": "changes" },
"item": { "one": "item", "other": "items" },
"issue": { "one": "issue", "other": "issues" },
"task": { "one": "task", "other": "tasks" },
"person": { "one": "person", "other": "people" },
"child": { "one": "child", "other": "children" },
"package": { "one": "package", "other": "packages" },
"artifact": { "one": "artifact", "other": "artifacts" },
"vulnerability": { "one": "vulnerability", "other": "vulnerabilities" },
"dependency": { "one": "dependency", "other": "dependencies" },
"directory": { "one": "directory", "other": "directories" },
"category": { "one": "category", "other": "categories" },
"query": { "one": "query", "other": "queries" },
"check": { "one": "check", "other": "checks" },
"test": { "one": "test", "other": "tests" }
},
"article": {
"indefinite": { "default": "a", "vowel": "an" },
"definite": "the"
},
"word": {
"url": "URL", "id": "ID", "ok": "OK", "ci": "CI", "qa": "QA",
"php": "PHP", "sdk": "SDK", "html": "HTML", "cgo": "CGO", "pid": "PID",
"cpus": "CPUs", "ssh": "SSH", "ssl": "SSL", "api": "API", "pr": "PR",
"vite": "Vite", "pnpm": "pnpm",
"app_url": "app URL", "blocked_by": "blocked by", "claimed_by": "claimed by",
"related_files": "related files", "up_to_date": "up to date",
"dry_run": "dry run", "go_mod": "go.mod",
"coverage": "coverage", "failed": "failed", "filter": "filter",
"package": "package", "passed": "passed", "skipped": "skipped", "test": "test"
},
"punct": {
"label": ":",
"progress": "..."
},
"number": {
"thousands": ",",
"decimal": ".",
"percent": "%s%%"
}
},
"lang": {
"de": "German", "en": "English", "es": "Spanish",
"fr": "French", "ru": "Russian", "zh": "Chinese"
},
"prompt": {
"yes": "y", "no": "n",
"continue": "Continue?", "proceed": "Proceed?",
"confirm": "Are you sure?", "overwrite": "Overwrite?",
"discard": "Discard changes?"
},
"time": {
"just_now": "just now",
"ago": {
"second": { "one": "{{.Count}} second ago", "other": "{{.Count}} seconds ago" },
"minute": { "one": "{{.Count}} minute ago", "other": "{{.Count}} minutes ago" },
"hour": { "one": "{{.Count}} hour ago", "other": "{{.Count}} hours ago" },
"day": { "one": "{{.Count}} day ago", "other": "{{.Count}} days ago" },
"week": { "one": "{{.Count}} week ago", "other": "{{.Count}} weeks ago" }
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1 +1,148 @@
{}
{
"gram": {
"verb": {
"be": { "base": "是", "past": "是", "gerund": "状态" },
"go": { "base": "前往", "past": "前往", "gerund": "前往" },
"do": { "base": "执行", "past": "执行", "gerund": "执行" },
"have": { "base": "拥有", "past": "拥有", "gerund": "拥有" },
"make": { "base": "创建", "past": "创建", "gerund": "创建" },
"get": { "base": "获取", "past": "获取", "gerund": "获取" },
"run": { "base": "运行", "past": "运行", "gerund": "运行" },
"write": { "base": "写入", "past": "写入", "gerund": "写入" },
"build": { "base": "构建", "past": "构建", "gerund": "构建" },
"send": { "base": "发送", "past": "发送", "gerund": "发送" },
"find": { "base": "查找", "past": "查找", "gerund": "查找" },
"take": { "base": "获取", "past": "获取", "gerund": "获取" },
"begin": { "base": "开始", "past": "开始", "gerund": "开始" },
"keep": { "base": "保持", "past": "保持", "gerund": "保持" },
"hold": { "base": "持有", "past": "持有", "gerund": "持有" },
"bring": { "base": "带来", "past": "带来", "gerund": "带来" },
"think": { "base": "思考", "past": "思考", "gerund": "思考" },
"choose": { "base": "选择", "past": "选择", "gerund": "选择" },
"lose": { "base": "丢失", "past": "丢失", "gerund": "丢失" },
"win": { "base": "成功", "past": "成功", "gerund": "成功" },
"meet": { "base": "匹配", "past": "匹配", "gerund": "匹配" },
"lead": { "base": "引导", "past": "引导", "gerund": "引导" },
"leave": { "base": "离开", "past": "离开", "gerund": "离开" },
"commit": { "base": "提交", "past": "提交", "gerund": "提交" },
"stop": { "base": "停止", "past": "停止", "gerund": "停止" },
"scan": { "base": "扫描", "past": "扫描", "gerund": "扫描" },
"format": { "base": "格式化", "past": "格式化", "gerund": "格式化" },
"set": { "base": "设置", "past": "设置", "gerund": "设置" },
"check": { "base": "检查", "past": "检查", "gerund": "检查" },
"create": { "base": "创建", "past": "创建", "gerund": "创建" },
"delete": { "base": "删除", "past": "删除", "gerund": "删除" },
"install": { "base": "安装", "past": "安装", "gerund": "安装" },
"update": { "base": "更新", "past": "更新", "gerund": "更新" },
"pull": { "base": "拉取", "past": "拉取", "gerund": "拉取" },
"push": { "base": "推送", "past": "推送", "gerund": "推送" },
"save": { "base": "保存", "past": "保存", "gerund": "保存" },
"analyse": { "base": "分析", "past": "分析", "gerund": "分析" },
"organise": { "base": "整理", "past": "整理", "gerund": "整理" },
"test": { "base": "测试", "past": "测试", "gerund": "测试" },
"deploy": { "base": "部署", "past": "部署", "gerund": "部署" },
"clone": { "base": "克隆", "past": "克隆", "gerund": "克隆" },
"compile": { "base": "编译", "past": "编译", "gerund": "编译" },
"download": { "base": "下载", "past": "下载", "gerund": "下载" },
"upload": { "base": "上传", "past": "上传", "gerund": "上传" }
},
"noun": {
"file": { "one": "文件", "other": "文件" },
"repo": { "one": "仓库", "other": "仓库" },
"repository": { "one": "仓库", "other": "仓库" },
"commit": { "one": "提交", "other": "提交" },
"branch": { "one": "分支", "other": "分支" },
"change": { "one": "更改", "other": "更改" },
"item": { "one": "项", "other": "项" },
"issue": { "one": "问题", "other": "问题" },
"task": { "one": "任务", "other": "任务" },
"person": { "one": "人", "other": "人" },
"child": { "one": "子项", "other": "子项" },
"package": { "one": "包", "other": "包" },
"artifact": { "one": "构件", "other": "构件" },
"vulnerability": { "one": "漏洞", "other": "漏洞" },
"dependency": { "one": "依赖", "other": "依赖" },
"directory": { "one": "目录", "other": "目录" },
"category": { "one": "分类", "other": "分类" },
"query": { "one": "查询", "other": "查询" },
"check": { "one": "检查", "other": "检查" },
"test": { "one": "测试", "other": "测试" },
"error": { "one": "错误", "other": "错误" },
"warning": { "one": "警告", "other": "警告" },
"service": { "one": "服务", "other": "服务" },
"config": { "one": "配置", "other": "配置" },
"workflow": { "one": "工作流", "other": "工作流" }
},
"article": {
"indefinite": { "default": "", "vowel": "" },
"definite": ""
},
"word": {
"url": "URL", "id": "ID", "ok": "OK", "ci": "CI", "qa": "QA",
"php": "PHP", "sdk": "SDK", "html": "HTML", "cgo": "CGO", "pid": "PID",
"cpus": "CPU", "ssh": "SSH", "ssl": "SSL", "api": "API", "pr": "PR",
"vite": "Vite", "pnpm": "pnpm",
"app_url": "应用 URL", "blocked_by": "被阻塞",
"claimed_by": "已认领", "related_files": "相关文件",
"up_to_date": "已是最新", "dry_run": "模拟运行",
"go_mod": "go.mod", "coverage": "覆盖率", "failed": "失败",
"filter": "过滤器", "package": "包", "passed": "通过",
"skipped": "跳过", "test": "测试"
},
"punct": {
"label": "",
"progress": "..."
},
"number": {
"thousands": ",",
"decimal": ".",
"percent": "%s%%"
}
},
"cli.aborted": "已中止。",
"cli.fail": "失败",
"cli.pass": "通过",
"lang": {
"de": "德语", "en": "英语", "es": "西班牙语",
"fr": "法语", "ru": "俄语", "zh": "中文"
},
"prompt": {
"yes": "是", "no": "否",
"continue": "继续?", "proceed": "执行?",
"confirm": "确定吗?", "overwrite": "覆盖?",
"discard": "放弃更改?"
},
"time": {
"just_now": "刚刚",
"ago": {
"second": { "other": "{{.Count}} 秒前" },
"minute": { "other": "{{.Count}} 分钟前" },
"hour": { "other": "{{.Count}} 小时前" },
"day": { "other": "{{.Count}} 天前" },
"week": { "other": "{{.Count}} 周前" }
}
},
"error.gh_not_found": "未找到 'gh' CLI 工具。请安装https://cli.github.com/",
"error.registry_not_found": "未找到 repos.yaml",
"error.repo_not_found": "未找到仓库 '{{.Name}}'",
"common.label.done": "完成",
"common.label.error": "错误",
"common.label.info": "信息",
"common.label.success": "成功",
"common.label.warning": "警告",
"common.status.clean": "干净",
"common.status.dirty": "已修改",
"common.status.running": "运行中",
"common.status.stopped": "已停止",
"common.status.up_to_date": "已是最新",
"common.result.all_passed": "所有测试通过",
"common.result.no_issues": "未发现问题",
"common.prompt.abort": "已中止。",
"common.success.completed": "{{.Action}} 成功完成"
}

View file

@ -201,7 +201,7 @@ else
# Install from releases
ARCH=$(dpkg --print-architecture)
CORE_URL="https://github.com/host-uk/core/releases/latest/download/core-linux-${ARCH}"
CORE_URL="https://forge.lthn.ai/core/cli/releases/latest/download/core-linux-${ARCH}"
curl -fsSL "$CORE_URL" -o /tmp/core
chmod +x /tmp/core
@ -220,7 +220,7 @@ if [[ -z "${SKIP_GUI:-}" ]]; then
log_info "Installing core-ide..."
ARCH=$(dpkg --print-architecture)
IDE_URL="https://github.com/host-uk/core/releases/latest/download/core-ide-linux-${ARCH}.deb"
IDE_URL="https://forge.lthn.ai/core/cli/releases/latest/download/core-ide-linux-${ARCH}.deb"
curl -fsSL "$IDE_URL" -o /tmp/core-ide.deb
sudo dpkg -i /tmp/core-ide.deb || sudo apt-get install -f -y

View file

@ -491,7 +491,7 @@ In `pkg/build/config.go`, add to the `BuildConfig` struct:
```go
// Add import
import "github.com/host-uk/core/pkg/build/signing"
import "forge.lthn.ai/core/cli/pkg/build/signing"
// Add to BuildConfig struct after Targets field:
// Sign contains code signing configuration.
@ -590,7 +590,7 @@ import (
"fmt"
"runtime"
"github.com/host-uk/core/pkg/build"
"forge.lthn.ai/core/cli/pkg/build"
)
// SignBinaries signs macOS binaries in the artifacts list.
@ -727,7 +727,7 @@ buildCmd.Action(func() error {
Add to imports:
```go
"github.com/host-uk/core/pkg/build/signing"
"forge.lthn.ai/core/cli/pkg/build/signing"
```
**Step 4: Add signing after build, before archive**
@ -820,7 +820,7 @@ import (
"runtime"
"testing"
"github.com/host-uk/core/pkg/build"
"forge.lthn.ai/core/cli/pkg/build"
)
func TestSignBinaries_Good_SkipsNonDarwin(t *testing.T) {

View file

@ -19,17 +19,17 @@
**Step 1: Create go.mod**
```go
module github.com/host-uk/core/pkg/devops
module forge.lthn.ai/core/cli/pkg/devops
go 1.25
require (
github.com/host-uk/core/pkg/container v0.0.0
forge.lthn.ai/core/cli/pkg/container v0.0.0
golang.org/x/crypto v0.32.0
gopkg.in/yaml.v3 v3.0.1
)
replace github.com/host-uk/core/pkg/container => ../container
replace forge.lthn.ai/core/cli/pkg/container => ../container
```
**Step 2: Create devops.go with core types**
@ -45,7 +45,7 @@ import (
"path/filepath"
"runtime"
"github.com/host-uk/core/pkg/container"
"forge.lthn.ai/core/cli/pkg/container"
)
// DevOps manages the portable development environment.
@ -744,7 +744,7 @@ import (
"path/filepath"
"time"
"github.com/host-uk/core/pkg/devops/sources"
"forge.lthn.ai/core/cli/pkg/devops/sources"
)
// ImageManager handles image downloads and updates.
@ -1786,7 +1786,7 @@ import (
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/pkg/devops"
"forge.lthn.ai/core/cli/pkg/devops"
"github.com/leaanthony/clir"
)

View file

@ -19,7 +19,7 @@
**Step 1: Create go.mod for sdk package**
```go
module github.com/host-uk/core/pkg/sdk
module forge.lthn.ai/core/cli/pkg/sdk
go 1.25
@ -1390,7 +1390,7 @@ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"
// Add to sdk.go, replacing the stub Generate method
import (
"github.com/host-uk/core/pkg/sdk/generators"
"forge.lthn.ai/core/cli/pkg/sdk/generators"
)
// Generate generates SDKs for all configured languages.
@ -1494,7 +1494,7 @@ import (
"os"
"github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/pkg/sdk"
"forge.lthn.ai/core/cli/pkg/sdk"
"github.com/leaanthony/clir"
)

View file

@ -108,7 +108,7 @@ import (
"context"
"fmt"
"github.com/host-uk/core/pkg/sdk"
"forge.lthn.ai/core/cli/pkg/sdk"
)
// SDKRelease holds the result of an SDK release.