fix: shutdown context + double IPC registration #36

Merged
Virgil merged 727 commits from fix/codex-review-findings into dev 2026-03-24 22:28:42 +00:00
736 changed files with 14237 additions and 126183 deletions

5
.claude/settings.json Normal file
View file

@ -0,0 +1,5 @@
{
"enabledPlugins": {
}
}

View file

@ -1,13 +0,0 @@
# CodeRabbit Configuration
# Inherits from: https://github.com/host-uk/coderabbit/.coderabbit.yaml
reviews:
review_status: false
path_instructions:
- path: "cmd/**"
instructions: "CLI command code - check for proper cobra usage and flag handling"
- path: "pkg/**"
instructions: "Library code - ensure good API design and documentation"
- path: "internal/**"
instructions: "Internal packages - check for proper encapsulation"

28
.core/build.yaml Normal file
View file

@ -0,0 +1,28 @@
# Core Go Framework build configuration
# Used by: core build
# Note: This is a library module (no binary). Build validates compilation only.
version: 1
project:
name: core-go
description: Core Go Framework — dependency injection and lifecycle management
binary: ""
build:
cgo: false
flags:
- -trimpath
ldflags:
- -s
- -w
targets:
- os: linux
arch: amd64
- os: linux
arch: arm64
- os: darwin
arch: arm64
- os: windows
arch: amd64

View file

@ -1,121 +0,0 @@
# Core Development Environment Template
# A full-featured development environment with multiple runtimes
#
# Variables:
# ${SSH_KEY} - SSH public key for access (required)
# ${MEMORY:-2048} - Memory in MB (default: 2048)
# ${CPUS:-2} - Number of CPUs (default: 2)
# ${HOSTNAME:-core-dev} - Hostname for the VM
# ${DATA_SIZE:-10G} - Size of persistent /data volume
kernel:
image: linuxkit/kernel:6.6.13
cmdline: "console=tty0 console=ttyS0"
init:
- linuxkit/init:v1.2.0
- linuxkit/runc:v1.1.12
- linuxkit/containerd:v1.7.13
- linuxkit/ca-certificates:v1.0.0
onboot:
- name: sysctl
image: linuxkit/sysctl:v1.0.0
- name: format
image: linuxkit/format:v1.0.0
- name: mount
image: linuxkit/mount:v1.0.0
command: ["/usr/bin/mountie", "/dev/sda1", "/data"]
- name: dhcpcd
image: linuxkit/dhcpcd:v1.0.0
command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"]
onshutdown:
- name: shutdown
image: busybox:latest
command: ["/bin/echo", "Shutting down..."]
services:
- name: getty
image: linuxkit/getty:v1.0.0
env:
- INSECURE=true
- name: sshd
image: linuxkit/sshd:v1.2.0
binds:
- /etc/ssh/authorized_keys:/root/.ssh/authorized_keys
- name: docker
image: docker:24.0-dind
capabilities:
- all
net: host
pid: host
binds:
- /var/run:/var/run
- /data/docker:/var/lib/docker
rootfsPropagation: shared
- name: dev-tools
image: alpine:3.19
capabilities:
- all
net: host
binds:
- /data:/data
command:
- /bin/sh
- -c
- |
# Install development tools
apk add --no-cache \
git curl wget vim nano htop tmux \
build-base gcc musl-dev linux-headers \
openssh-client jq yq
# Install Go 1.22.0
wget -q https://go.dev/dl/go1.22.0.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz
rm go1.22.0.linux-amd64.tar.gz
echo 'export PATH=/usr/local/go/bin:$PATH' >> /etc/profile
# Install Node.js
apk add --no-cache nodejs npm
# Install PHP
apk add --no-cache php82 php82-cli php82-curl php82-json php82-mbstring \
php82-openssl php82-pdo php82-pdo_mysql php82-pdo_pgsql php82-phar \
php82-session php82-tokenizer php82-xml php82-zip composer
# Keep container running
tail -f /dev/null
files:
- path: /etc/hostname
contents: "${HOSTNAME:-core-dev}"
- path: /etc/ssh/authorized_keys
contents: "${SSH_KEY}"
mode: "0600"
- path: /etc/profile.d/dev.sh
contents: |
export PATH=$PATH:/usr/local/go/bin
export GOPATH=/data/go
export PATH=$PATH:$GOPATH/bin
cd /data
mode: "0755"
- path: /etc/motd
contents: |
================================================
Core Development Environment
Runtimes: Go, Node.js, PHP
Tools: git, curl, vim, docker
Data directory: /data (persistent)
================================================
trust:
org:
- linuxkit
- library

View file

@ -1,142 +0,0 @@
# PHP/FrankenPHP Server Template
# A minimal production-ready PHP server with FrankenPHP and Caddy
#
# Variables:
# ${SSH_KEY} - SSH public key for management access (required)
# ${MEMORY:-512} - Memory in MB (default: 512)
# ${CPUS:-1} - Number of CPUs (default: 1)
# ${HOSTNAME:-php-server} - Hostname for the VM
# ${APP_NAME:-app} - Application name
# ${DOMAIN:-localhost} - Domain for SSL certificates
# ${PHP_MEMORY:-128M} - PHP memory limit
kernel:
image: linuxkit/kernel:6.6.13
cmdline: "console=tty0 console=ttyS0"
init:
- linuxkit/init:v1.2.0
- linuxkit/runc:v1.1.12
- linuxkit/containerd:v1.7.13
- linuxkit/ca-certificates:v1.0.0
onboot:
- name: sysctl
image: linuxkit/sysctl:v1.0.0
- name: dhcpcd
image: linuxkit/dhcpcd:v1.0.0
command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"]
services:
- name: sshd
image: linuxkit/sshd:v1.2.0
binds:
- /etc/ssh/authorized_keys:/root/.ssh/authorized_keys
- name: frankenphp
image: dunglas/frankenphp:latest
capabilities:
- CAP_NET_BIND_SERVICE
net: host
binds:
- /app:/app
- /data:/data
- /etc/caddy/Caddyfile:/etc/caddy/Caddyfile
env:
- SERVER_NAME=${DOMAIN:-localhost}
- FRANKENPHP_CONFIG=/etc/caddy/Caddyfile
command:
- frankenphp
- run
- --config
- /etc/caddy/Caddyfile
- name: healthcheck
image: alpine:3.19
net: host
command:
- /bin/sh
- -c
- |
apk add --no-cache curl
while true; do
sleep 30
curl -sf http://localhost/health || echo "Health check failed"
done
files:
- path: /etc/hostname
contents: "${HOSTNAME:-php-server}"
- path: /etc/ssh/authorized_keys
contents: "${SSH_KEY}"
mode: "0600"
- path: /etc/caddy/Caddyfile
contents: |
{
frankenphp
order php_server before file_server
}
${DOMAIN:-localhost} {
root * /app/public
# Health check endpoint
handle /health {
respond "OK" 200
}
# PHP handling
php_server
# Encode responses
encode zstd gzip
# Security headers
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
X-XSS-Protection "1; mode=block"
Referrer-Policy strict-origin-when-cross-origin
}
# Logging
log {
output file /data/logs/access.log
format json
}
}
mode: "0644"
- path: /app/public/index.php
contents: |
<?php
echo "Welcome to ${APP_NAME:-app}";
mode: "0644"
- path: /app/public/health.php
contents: |
<?php
header('Content-Type: application/json');
echo json_encode([
'status' => 'healthy',
'app' => '${APP_NAME:-app}',
'timestamp' => date('c'),
'php_version' => PHP_VERSION,
]);
mode: "0644"
- path: /etc/php/php.ini
contents: |
memory_limit = ${PHP_MEMORY:-128M}
max_execution_time = 30
upload_max_filesize = 64M
post_max_size = 64M
display_errors = Off
log_errors = On
error_log = /data/logs/php_errors.log
mode: "0644"
- path: /data/logs/.gitkeep
contents: ""
trust:
org:
- linuxkit
- library
- dunglas

View file

@ -1,36 +0,0 @@
---
name: remember
description: Save a fact or decision to context for persistence across compacts
args: <fact to remember>
---
# Remember Context
Save the provided fact to `~/.claude/sessions/context.json`.
## Usage
```
/core:remember Use Action pattern not Service
/core:remember User prefers UK English
/core:remember RFC: minimal state in pre-compact hook
```
## Action
Run this command to save the fact:
```bash
~/.claude/plugins/cache/core/scripts/capture-context.sh "<fact>" "user"
```
Or if running from the plugin directory:
```bash
"${CLAUDE_PLUGIN_ROOT}/scripts/capture-context.sh" "<fact>" "user"
```
The fact will be:
- Stored in context.json (max 20 items)
- Included in pre-compact snapshots
- Auto-cleared after 3 hours of inactivity

View file

@ -1,102 +0,0 @@
#!/bin/bash
# PreToolUse hook: Block dangerous commands, enforce core CLI
#
# BLOCKS:
# - Raw go commands (use core go *)
# - Destructive grep patterns (sed -i, xargs rm, etc.)
# - Mass file operations (rm -rf, mv/cp with wildcards)
# - Any sed outside of safe patterns
#
# This prevents "efficient shortcuts" that nuke codebases
read -r input
command=$(echo "$input" | jq -r '.tool_input.command // empty')
# === HARD BLOCKS - Never allow these ===
# Block rm -rf, rm -r (except for known safe paths like node_modules, vendor, .cache)
if echo "$command" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*|-[a-zA-Z]*f[a-zA-Z]*r|--recursive)'; then
# Allow only specific safe directories
if ! echo "$command" | grep -qE 'rm\s+(-rf|-r)\s+(node_modules|vendor|\.cache|dist|build|__pycache__|\.pytest_cache|/tmp/)'; then
echo '{"decision": "block", "message": "BLOCKED: Recursive delete is not allowed. Delete files individually or ask the user to run this command."}'
exit 0
fi
fi
# Block mv/cp with wildcards (mass file moves)
if echo "$command" | grep -qE '(mv|cp)\s+.*\*'; then
echo '{"decision": "block", "message": "BLOCKED: Mass file move/copy with wildcards is not allowed. Move files individually."}'
exit 0
fi
# Block xargs with rm, mv, cp (mass operations)
if echo "$command" | grep -qE 'xargs\s+.*(rm|mv|cp)'; then
echo '{"decision": "block", "message": "BLOCKED: xargs with file operations is not allowed. Too risky for mass changes."}'
exit 0
fi
# Block find -exec with rm, mv, cp
if echo "$command" | grep -qE 'find\s+.*-exec\s+.*(rm|mv|cp)'; then
echo '{"decision": "block", "message": "BLOCKED: find -exec with file operations is not allowed. Too risky for mass changes."}'
exit 0
fi
# Block ALL sed -i (in-place editing)
if echo "$command" | grep -qE 'sed\s+(-[a-zA-Z]*i|--in-place)'; then
echo '{"decision": "block", "message": "BLOCKED: sed -i (in-place edit) is never allowed. Use the Edit tool for file changes."}'
exit 0
fi
# Block sed piped to file operations
if echo "$command" | grep -qE 'sed.*\|.*tee|sed.*>'; then
echo '{"decision": "block", "message": "BLOCKED: sed with file output is not allowed. Use the Edit tool for file changes."}'
exit 0
fi
# Block grep with -l piped to xargs/rm/sed (the classic codebase nuke pattern)
if echo "$command" | grep -qE 'grep\s+.*-l.*\|'; then
echo '{"decision": "block", "message": "BLOCKED: grep -l piped to other commands is the classic codebase nuke pattern. Not allowed."}'
exit 0
fi
# Block perl -i, awk with file redirection (sed alternatives)
if echo "$command" | grep -qE 'perl\s+-[a-zA-Z]*i|awk.*>'; then
echo '{"decision": "block", "message": "BLOCKED: In-place file editing with perl/awk is not allowed. Use the Edit tool."}'
exit 0
fi
# === REQUIRE CORE CLI ===
# Block raw go commands
case "$command" in
"go test"*|"go build"*|"go fmt"*|"go mod tidy"*|"go vet"*|"go run"*)
echo '{"decision": "block", "message": "Use `core go test`, `core build`, `core go fmt --fix`, etc. Raw go commands are not allowed."}'
exit 0
;;
"go "*)
# Other go commands - warn but allow
echo '{"decision": "block", "message": "Prefer `core go *` commands. If core does not have this command, ask the user."}'
exit 0
;;
esac
# Block raw php commands
case "$command" in
"php artisan serve"*|"./vendor/bin/pest"*|"./vendor/bin/pint"*|"./vendor/bin/phpstan"*)
echo '{"decision": "block", "message": "Use `core php dev`, `core php test`, `core php fmt`, `core php analyse`. Raw php commands are not allowed."}'
exit 0
;;
"composer test"*|"composer lint"*)
echo '{"decision": "block", "message": "Use `core php test` or `core php fmt`. Raw composer commands are not allowed."}'
exit 0
;;
esac
# Block golangci-lint directly
if echo "$command" | grep -qE '^golangci-lint'; then
echo '{"decision": "block", "message": "Use `core go lint` instead of golangci-lint directly."}'
exit 0
fi
# === APPROVED ===
echo '{"decision": "approve"}'

View file

@ -1,102 +0,0 @@
{
"name": "core",
"version": "1.0.0",
"description": "Host UK unified framework - Go CLI, PHP framework, multi-repo management",
"dependencies": [
"superpowers@claude-plugins-official"
],
"skills": [
{
"name": "core",
"path": "skills/core.md",
"description": "Use when working in host-uk repositories. Provides core CLI command reference."
},
{
"name": "core-php",
"path": "skills/php.md",
"description": "Use when creating PHP modules, services, or actions in core-* packages."
},
{
"name": "core-go",
"path": "skills/go.md",
"description": "Use when creating Go packages or extending the core CLI."
}
],
"commands": [
{
"name": "remember",
"path": "commands/remember.md",
"description": "Save a fact or decision to context"
}
],
"hooks": {
"SessionStart": [
{
"matcher": "*",
"script": "scripts/session-start.sh",
"description": "Check for recent session state on startup"
}
],
"PreCompact": [
{
"matcher": "*",
"script": "scripts/pre-compact.sh",
"description": "Save state before auto-compact to prevent amnesia"
}
],
"PreToolUse": [
{
"matcher": "Bash",
"script": "hooks/prefer-core.sh",
"description": "Suggest core CLI instead of raw go/php commands"
},
{
"matcher": "Write",
"script": "scripts/block-docs.sh",
"description": "Block random .md files, keep docs consolidated"
},
{
"matcher": "Edit",
"script": "scripts/suggest-compact.sh",
"description": "Suggest /compact at logical intervals"
},
{
"matcher": "Write",
"script": "scripts/suggest-compact.sh",
"description": "Suggest /compact at logical intervals"
}
],
"PostToolUse": [
{
"matcher": "Edit",
"script": "scripts/php-format.sh",
"description": "Auto-format PHP files after edits"
},
{
"matcher": "Edit",
"script": "scripts/go-format.sh",
"description": "Auto-format Go files after edits"
},
{
"matcher": "Edit",
"script": "scripts/check-debug.sh",
"description": "Warn about debug statements (dd, dump, fmt.Println)"
},
{
"matcher": "Bash",
"script": "scripts/pr-created.sh",
"description": "Log PR URL after creation"
},
{
"matcher": "Bash",
"script": "scripts/extract-actionables.sh",
"description": "Extract actionables from core CLI output"
},
{
"matcher": "Bash",
"script": "scripts/post-commit-check.sh",
"description": "Warn about uncommitted work after git commit"
}
]
}
}

View file

@ -1,27 +0,0 @@
#!/bin/bash
# Block creation of random .md files - keeps docs consolidated
read -r input
FILE_PATH=$(echo "$input" | jq -r '.tool_input.file_path // empty')
if [[ -n "$FILE_PATH" ]]; then
# Allow known documentation files
case "$FILE_PATH" in
*README.md|*CLAUDE.md|*AGENTS.md|*CONTRIBUTING.md|*CHANGELOG.md|*LICENSE.md)
echo "$input"
exit 0
;;
# Allow docs/ directory
*/docs/*.md|*/docs/**/*.md)
echo "$input"
exit 0
;;
# Block other .md files
*.md)
echo '{"decision": "block", "message": "Use README.md or docs/ for documentation. Random .md files clutter the repo."}'
exit 0
;;
esac
fi
echo "$input"

View file

@ -1,44 +0,0 @@
#!/bin/bash
# Capture context facts from tool output or conversation
# Called by PostToolUse hooks to extract actionable items
#
# Stores in ~/.claude/sessions/context.json as:
# [{"fact": "...", "source": "core go qa", "ts": 1234567890}, ...]
CONTEXT_FILE="${HOME}/.claude/sessions/context.json"
TIMESTAMP=$(date '+%s')
THREE_HOURS=10800
mkdir -p "${HOME}/.claude/sessions"
# Initialize if missing or stale
if [[ -f "$CONTEXT_FILE" ]]; then
FIRST_TS=$(jq -r '.[0].ts // 0' "$CONTEXT_FILE" 2>/dev/null)
NOW=$(date '+%s')
AGE=$((NOW - FIRST_TS))
if [[ $AGE -gt $THREE_HOURS ]]; then
echo "[]" > "$CONTEXT_FILE"
fi
else
echo "[]" > "$CONTEXT_FILE"
fi
# Read input (fact and source passed as args or stdin)
FACT="${1:-}"
SOURCE="${2:-manual}"
if [[ -z "$FACT" ]]; then
# Try reading from stdin
read -r FACT
fi
if [[ -n "$FACT" ]]; then
# Append to context (keep last 20 items)
jq --arg fact "$FACT" --arg source "$SOURCE" --argjson ts "$TIMESTAMP" \
'. + [{"fact": $fact, "source": $source, "ts": $ts}] | .[-20:]' \
"$CONTEXT_FILE" > "${CONTEXT_FILE}.tmp" && mv "${CONTEXT_FILE}.tmp" "$CONTEXT_FILE"
echo "[Context] Saved: $FACT" >&2
fi
exit 0

View file

@ -1,27 +0,0 @@
#!/bin/bash
# Warn about debug statements left in code after edits
read -r input
FILE_PATH=$(echo "$input" | jq -r '.tool_input.file_path // empty')
if [[ -n "$FILE_PATH" && -f "$FILE_PATH" ]]; then
case "$FILE_PATH" in
*.go)
# Check for fmt.Println, log.Println debug statements
if grep -n "fmt\.Println\|log\.Println" "$FILE_PATH" 2>/dev/null | head -3 | grep -q .; then
echo "[Hook] WARNING: Debug prints found in $FILE_PATH" >&2
grep -n "fmt\.Println\|log\.Println" "$FILE_PATH" 2>/dev/null | head -3 >&2
fi
;;
*.php)
# Check for dd(), dump(), var_dump(), print_r()
if grep -n "dd(\|dump(\|var_dump(\|print_r(" "$FILE_PATH" 2>/dev/null | head -3 | grep -q .; then
echo "[Hook] WARNING: Debug statements found in $FILE_PATH" >&2
grep -n "dd(\|dump(\|var_dump(\|print_r(" "$FILE_PATH" 2>/dev/null | head -3 >&2
fi
;;
esac
fi
# Pass through the input
echo "$input"

View file

@ -1,34 +0,0 @@
#!/bin/bash
# Extract actionable items from core CLI output
# Called PostToolUse on Bash commands that run core
read -r input
COMMAND=$(echo "$input" | jq -r '.tool_input.command // empty')
OUTPUT=$(echo "$input" | jq -r '.tool_output.output // empty')
CONTEXT_SCRIPT="$(dirname "$0")/capture-context.sh"
# Extract actionables from specific core commands
case "$COMMAND" in
"core go qa"*|"core go test"*|"core go lint"*)
# Extract error/warning lines
echo "$OUTPUT" | grep -E "^(ERROR|WARN|FAIL|---)" | head -5 | while read -r line; do
"$CONTEXT_SCRIPT" "$line" "core go"
done
;;
"core php test"*|"core php analyse"*)
# Extract PHP errors
echo "$OUTPUT" | grep -E "^(FAIL|Error|×)" | head -5 | while read -r line; do
"$CONTEXT_SCRIPT" "$line" "core php"
done
;;
"core build"*)
# Extract build errors
echo "$OUTPUT" | grep -E "^(error|cannot|undefined)" | head -5 | while read -r line; do
"$CONTEXT_SCRIPT" "$line" "core build"
done
;;
esac
# Pass through
echo "$input"

View file

@ -1,19 +0,0 @@
#!/bin/bash
# Auto-format Go files after edits using core go fmt
read -r input
FILE_PATH=$(echo "$input" | jq -r '.tool_input.file_path // empty')
if [[ -n "$FILE_PATH" && -f "$FILE_PATH" ]]; then
# Run gofmt/goimports on the file silently
if command -v core &> /dev/null; then
core go fmt --fix "$FILE_PATH" 2>/dev/null || true
elif command -v goimports &> /dev/null; then
goimports -w "$FILE_PATH" 2>/dev/null || true
elif command -v gofmt &> /dev/null; then
gofmt -w "$FILE_PATH" 2>/dev/null || true
fi
fi
# Pass through the input
echo "$input"

View file

@ -1,17 +0,0 @@
#!/bin/bash
# Auto-format PHP files after edits using core php fmt
read -r input
FILE_PATH=$(echo "$input" | jq -r '.tool_input.file_path // empty')
if [[ -n "$FILE_PATH" && -f "$FILE_PATH" ]]; then
# Run Pint on the file silently
if command -v core &> /dev/null; then
core php fmt --fix "$FILE_PATH" 2>/dev/null || true
elif [[ -f "./vendor/bin/pint" ]]; then
./vendor/bin/pint "$FILE_PATH" 2>/dev/null || true
fi
fi
# Pass through the input
echo "$input"

View file

@ -1,51 +0,0 @@
#!/bin/bash
# Post-commit hook: Check for uncommitted work that might get lost
#
# After committing task-specific files, check if there's other work
# in the repo that should be committed or stashed
read -r input
COMMAND=$(echo "$input" | jq -r '.tool_input.command // empty')
# Only run after git commit
if ! echo "$COMMAND" | grep -qE '^git commit'; then
echo "$input"
exit 0
fi
# Check for remaining uncommitted changes
UNSTAGED=$(git diff --name-only 2>/dev/null | wc -l | tr -d ' ')
STAGED=$(git diff --cached --name-only 2>/dev/null | wc -l | tr -d ' ')
UNTRACKED=$(git ls-files --others --exclude-standard 2>/dev/null | wc -l | tr -d ' ')
TOTAL=$((UNSTAGED + STAGED + UNTRACKED))
if [[ $TOTAL -gt 0 ]]; then
echo "" >&2
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2
echo "[PostCommit] WARNING: Uncommitted work remains" >&2
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2
if [[ $UNSTAGED -gt 0 ]]; then
echo " Modified (unstaged): $UNSTAGED files" >&2
git diff --name-only 2>/dev/null | head -5 | sed 's/^/ /' >&2
[[ $UNSTAGED -gt 5 ]] && echo " ... and $((UNSTAGED - 5)) more" >&2
fi
if [[ $STAGED -gt 0 ]]; then
echo " Staged (not committed): $STAGED files" >&2
git diff --cached --name-only 2>/dev/null | head -5 | sed 's/^/ /' >&2
fi
if [[ $UNTRACKED -gt 0 ]]; then
echo " Untracked: $UNTRACKED files" >&2
git ls-files --others --exclude-standard 2>/dev/null | head -5 | sed 's/^/ /' >&2
[[ $UNTRACKED -gt 5 ]] && echo " ... and $((UNTRACKED - 5)) more" >&2
fi
echo "" >&2
echo "Consider: commit these, stash them, or confirm they're intentionally left" >&2
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2
fi
echo "$input"

View file

@ -1,18 +0,0 @@
#!/bin/bash
# Log PR URL and provide review command after PR creation
read -r input
COMMAND=$(echo "$input" | jq -r '.tool_input.command // empty')
OUTPUT=$(echo "$input" | jq -r '.tool_output.output // empty')
if [[ "$COMMAND" == *"gh pr create"* ]]; then
PR_URL=$(echo "$OUTPUT" | grep -oE 'https://github.com/[^/]+/[^/]+/pull/[0-9]+' | head -1)
if [[ -n "$PR_URL" ]]; then
REPO=$(echo "$PR_URL" | sed -E 's|https://github.com/([^/]+/[^/]+)/pull/[0-9]+|\1|')
PR_NUM=$(echo "$PR_URL" | sed -E 's|.*/pull/([0-9]+)|\1|')
echo "[Hook] PR created: $PR_URL" >&2
echo "[Hook] To review: gh pr review $PR_NUM --repo $REPO" >&2
fi
fi
echo "$input"

View file

@ -1,69 +0,0 @@
#!/bin/bash
# Pre-compact: Save minimal state for Claude to resume after auto-compact
#
# Captures:
# - Working directory + branch
# - Git status (files touched)
# - Todo state (in_progress items)
# - Context facts (decisions, actionables)
STATE_FILE="${HOME}/.claude/sessions/scratchpad.md"
CONTEXT_FILE="${HOME}/.claude/sessions/context.json"
TIMESTAMP=$(date '+%s')
CWD=$(pwd)
mkdir -p "${HOME}/.claude/sessions"
# Get todo state
TODOS=""
if [[ -f "${HOME}/.claude/todos/current.json" ]]; then
TODOS=$(cat "${HOME}/.claude/todos/current.json" 2>/dev/null | head -50)
fi
# Get git status
GIT_STATUS=""
BRANCH=""
if git rev-parse --git-dir > /dev/null 2>&1; then
GIT_STATUS=$(git status --short 2>/dev/null | head -15)
BRANCH=$(git branch --show-current 2>/dev/null)
fi
# Get context facts
CONTEXT=""
if [[ -f "$CONTEXT_FILE" ]]; then
CONTEXT=$(jq -r '.[] | "- [\(.source)] \(.fact)"' "$CONTEXT_FILE" 2>/dev/null | tail -10)
fi
cat > "$STATE_FILE" << EOF
---
timestamp: ${TIMESTAMP}
cwd: ${CWD}
branch: ${BRANCH:-none}
---
# Resume After Compact
You were mid-task. Do NOT assume work is complete.
## Project
\`${CWD}\` on \`${BRANCH:-no branch}\`
## Files Changed
\`\`\`
${GIT_STATUS:-none}
\`\`\`
## Todos (in_progress = NOT done)
\`\`\`json
${TODOS:-check /todos}
\`\`\`
## Context (decisions & actionables)
${CONTEXT:-none captured}
## Next
Continue the in_progress todo.
EOF
echo "[PreCompact] Snapshot saved" >&2
exit 0

View file

@ -1,34 +0,0 @@
#!/bin/bash
# Session start: Read scratchpad if recent, otherwise start fresh
# 3 hour window - if older, you've moved on mentally
STATE_FILE="${HOME}/.claude/sessions/scratchpad.md"
THREE_HOURS=10800 # seconds
if [[ -f "$STATE_FILE" ]]; then
# Get timestamp from file
FILE_TS=$(grep -E '^timestamp:' "$STATE_FILE" 2>/dev/null | cut -d' ' -f2)
NOW=$(date '+%s')
if [[ -n "$FILE_TS" ]]; then
AGE=$((NOW - FILE_TS))
if [[ $AGE -lt $THREE_HOURS ]]; then
# Recent - read it back
echo "[SessionStart] Found recent scratchpad ($(($AGE / 60)) min ago)" >&2
echo "[SessionStart] Reading previous state..." >&2
echo "" >&2
cat "$STATE_FILE" >&2
echo "" >&2
else
# Stale - delete and start fresh
rm -f "$STATE_FILE"
echo "[SessionStart] Previous session >3h old - starting fresh" >&2
fi
else
# No timestamp, delete it
rm -f "$STATE_FILE"
fi
fi
exit 0

View file

@ -1,28 +0,0 @@
#!/bin/bash
# Suggest /compact at logical intervals to manage context window
# Tracks tool calls per session, suggests compaction every 50 calls
SESSION_ID="${CLAUDE_SESSION_ID:-$$}"
COUNTER_FILE="/tmp/claude-tool-count-${SESSION_ID}"
THRESHOLD="${COMPACT_THRESHOLD:-50}"
# Read or initialize counter
if [[ -f "$COUNTER_FILE" ]]; then
COUNT=$(($(cat "$COUNTER_FILE") + 1))
else
COUNT=1
fi
echo "$COUNT" > "$COUNTER_FILE"
# Suggest compact at threshold
if [[ $COUNT -eq $THRESHOLD ]]; then
echo "[Compact] ${THRESHOLD} tool calls - consider /compact if transitioning phases" >&2
fi
# Suggest at intervals after threshold
if [[ $COUNT -gt $THRESHOLD ]] && [[ $((COUNT % 25)) -eq 0 ]]; then
echo "[Compact] ${COUNT} tool calls - good checkpoint for /compact" >&2
fi
exit 0

View file

@ -1,60 +0,0 @@
---
name: core
description: Use when working in host-uk repositories, running tests, building, releasing, or managing multi-repo workflows. Provides the core CLI command reference.
---
# Core CLI
The `core` command provides a unified interface for Go/PHP development and multi-repo management.
**Rule:** Always prefer `core <command>` over raw commands.
## Quick Reference
| Task | Command |
|------|---------|
| Go tests | `core go test` |
| Go coverage | `core go cov` |
| Go format | `core go fmt --fix` |
| Go lint | `core go lint` |
| PHP dev server | `core php dev` |
| PHP tests | `core php test` |
| PHP format | `core php fmt --fix` |
| Build | `core build` |
| Preview release | `core ci` |
| Publish | `core ci --were-go-for-launch` |
| Multi-repo status | `core dev health` |
| Commit dirty repos | `core dev commit` |
| Push repos | `core dev push` |
## Decision Tree
```
Go project?
tests: core go test
format: core go fmt --fix
build: core build
PHP project?
dev: core php dev
tests: core php test
format: core php fmt --fix
deploy: core php deploy
Multiple repos?
status: core dev health
commit: core dev commit
push: core dev push
```
## Common Mistakes
| Wrong | Right |
|-------|-------|
| `go test ./...` | `core go test` |
| `go build` | `core build` |
| `php artisan serve` | `core php dev` |
| `./vendor/bin/pest` | `core php test` |
| `git status` per repo | `core dev health` |
Run `core --help` or `core <cmd> --help` for full options.

View file

@ -1,107 +0,0 @@
---
name: core-go
description: Use when creating Go packages or extending the core CLI.
---
# Go Framework Patterns
Core CLI uses `pkg/` for reusable packages. Use `core go` commands.
## Package Structure
```
core/
├── main.go # CLI entry point
├── pkg/
│ ├── cli/ # CLI framework, output, errors
│ ├── {domain}/ # Domain package
│ │ ├── cmd_{name}.go # Cobra command definitions
│ │ ├── service.go # Business logic
│ │ └── *_test.go # Tests
│ └── ...
└── internal/ # Private packages
```
## Adding a CLI Command
1. Create `pkg/{domain}/cmd_{name}.go`:
```go
package domain
import (
"github.com/host-uk/core/pkg/cli"
"github.com/spf13/cobra"
)
func NewNameCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "name",
Short: cli.T("domain.name.short"),
RunE: func(cmd *cobra.Command, args []string) error {
// Implementation
cli.Success("Done")
return nil
},
}
return cmd
}
```
2. Register in parent command.
## CLI Output Helpers
```go
import "github.com/host-uk/core/pkg/cli"
cli.Success("Operation completed") // Green check
cli.Warning("Something to note") // Yellow warning
cli.Error("Something failed") // Red error
cli.Info("Informational message") // Blue info
cli.Fatal(err) // Print error and exit 1
// Structured output
cli.Table(headers, rows)
cli.JSON(data)
```
## i18n Pattern
```go
// Use cli.T() for translatable strings
cli.T("domain.action.success")
cli.T("domain.action.error", "details", value)
// Define in pkg/i18n/locales/en.yaml:
domain:
action:
success: "Operation completed successfully"
error: "Failed: {{.details}}"
```
## Test Naming
```go
func TestFeature_Good(t *testing.T) { /* happy path */ }
func TestFeature_Bad(t *testing.T) { /* expected errors */ }
func TestFeature_Ugly(t *testing.T) { /* panics, edge cases */ }
```
## Commands
| Task | Command |
|------|---------|
| Run tests | `core go test` |
| Coverage | `core go cov` |
| Format | `core go fmt --fix` |
| Lint | `core go lint` |
| Build | `core build` |
| Install | `core go install` |
## Rules
- `CGO_ENABLED=0` for all builds
- UK English in user-facing strings
- All errors via `cli.E("context", "message", err)`
- Table-driven tests preferred

View file

@ -1,120 +0,0 @@
---
name: core-php
description: Use when creating PHP modules, services, or actions in core-* packages.
---
# PHP Framework Patterns
Host UK PHP modules follow strict conventions. Use `core php` commands.
## Module Structure
```
core-{name}/
├── src/
│ ├── Core/ # Namespace: Core\{Name}
│ │ ├── Boot.php # Module bootstrap (listens to lifecycle events)
│ │ ├── Actions/ # Single-purpose business logic
│ │ └── Models/ # Eloquent models
│ └── Mod/ # Namespace: Core\Mod\{Name} (optional extensions)
├── resources/views/ # Blade templates
├── routes/ # Route definitions
├── database/migrations/ # Migrations
├── tests/ # Pest tests
└── composer.json
```
## Boot Class Pattern
```php
<?php
declare(strict_types=1);
namespace Core\{Name};
use Core\Php\Events\WebRoutesRegistering;
use Core\Php\Events\AdminPanelBooting;
class Boot
{
public static array $listens = [
WebRoutesRegistering::class => 'onWebRoutes',
AdminPanelBooting::class => ['onAdmin', 10], // With priority
];
public function onWebRoutes(WebRoutesRegistering $event): void
{
$event->router->middleware('web')->group(__DIR__ . '/../routes/web.php');
}
public function onAdmin(AdminPanelBooting $event): void
{
$event->panel->resources([...]);
}
}
```
## Action Pattern
```php
<?php
declare(strict_types=1);
namespace Core\{Name}\Actions;
use Core\Php\Action;
class CreateThing
{
use Action;
public function handle(User $user, array $data): Thing
{
return Thing::create([
'user_id' => $user->id,
...$data,
]);
}
}
// Usage: CreateThing::run($user, $validated);
```
## Multi-Tenant Models
```php
<?php
declare(strict_types=1);
namespace Core\{Name}\Models;
use Core\Tenant\Concerns\BelongsToWorkspace;
use Illuminate\Database\Eloquent\Model;
class Thing extends Model
{
use BelongsToWorkspace; // Auto-scopes queries, sets workspace_id
protected $fillable = ['name', 'workspace_id'];
}
```
## Commands
| Task | Command |
|------|---------|
| Run tests | `core php test` |
| Format | `core php fmt --fix` |
| Analyse | `core php analyse` |
| Dev server | `core php dev` |
## Rules
- Always `declare(strict_types=1);`
- UK English: colour, organisation, centre
- Type hints on all parameters and returns
- Pest for tests, not PHPUnit
- Flux Pro for UI, not vanilla Alpine

24
.core/release.yaml Normal file
View file

@ -0,0 +1,24 @@
# Core Go Framework release configuration
# Used by: core ci
# Library module — no binary artifacts, tag-only releases.
version: 1
project:
name: core-go
repository: core/go
publishers: []
changelog:
include:
- feat
- fix
- perf
- refactor
exclude:
- chore
- docs
- style
- test
- ci

12
.editorconfig Normal file
View file

@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
indent_style = tab
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.{md,yml,yaml,json,txt}]
indent_style = space
indent_size = 2

23
.gitattributes vendored Normal file
View file

@ -0,0 +1,23 @@
# Normalize all text files to LF
* text=auto eol=lf
# Ensure shell scripts use LF
*.sh text eol=lf
# Ensure Go files use LF
*.go text eol=lf
# Ensure JSON/YAML use LF
*.json text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
# Binary files
*.png binary
*.jpg binary
*.gif binary
*.ico binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary

4
.githooks/pre-commit Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
exec core go qa full --fix

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,133 +0,0 @@
name: Agent Verification Workflow
on:
issues:
types: [labeled]
jobs:
# When work is claimed, track the implementer
track-implementer:
if: github.event.label.name == 'agent:wip'
runs-on: ubuntu-latest
steps:
- name: Record implementer
run: |
echo "Implementer: ${{ github.actor }}"
# Could store in issue body or external system
# When work is submitted for review, add to verification queue
request-verification:
if: github.event.label.name == 'agent:review'
runs-on: ubuntu-latest
steps:
- name: Add to Workstation for verification
uses: actions/add-to-project@v1.0.2
with:
project-url: https://github.com/orgs/host-uk/projects/2
github-token: ${{ secrets.PROJECT_TOKEN }}
- name: Comment verification needed
uses: actions/github-script@v7
with:
script: |
const implementer = context.payload.sender.login;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `## 🔍 Verification Required\n\nWork submitted by @${implementer}.\n\n**Rule:** A different agent must verify this work.\n\nTo verify:\n1. Review the implementation\n2. Run tests if applicable\n3. Add \`verified\` or \`verify-failed\` label\n\n_Self-verification is not allowed._`
});
# Block self-verification
check-verification:
if: github.event.label.name == 'verified' || github.event.label.name == 'verify-failed'
runs-on: ubuntu-latest
steps:
- name: Get issue details
id: issue
uses: actions/github-script@v7
with:
script: |
const issue = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
// Check timeline for who added agent:wip
const timeline = await github.rest.issues.listEventsForTimeline({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
per_page: 100
});
const wipEvent = timeline.data.find(e =>
e.event === 'labeled' && e.label?.name === 'agent:wip'
);
const implementer = wipEvent?.actor?.login || 'unknown';
const verifier = context.payload.sender.login;
console.log(`Implementer: ${implementer}`);
console.log(`Verifier: ${verifier}`);
if (implementer === verifier) {
core.setFailed(`Self-verification not allowed. ${verifier} cannot verify their own work.`);
}
return { implementer, verifier };
- name: Record verification
if: success()
uses: actions/github-script@v7
with:
script: |
const label = context.payload.label.name;
const verifier = context.payload.sender.login;
const status = label === 'verified' ? '✅ Verified' : '❌ Failed';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `## ${status}\n\nVerified by @${verifier}`
});
// Remove agent:review label
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'agent:review'
});
} catch (e) {
console.log('agent:review label not present');
}
# If verification failed, reset for rework
handle-failure:
if: github.event.label.name == 'verify-failed'
runs-on: ubuntu-latest
needs: check-verification
steps:
- name: Reset for rework
uses: actions/github-script@v7
with:
script: |
// Remove verify-failed after processing
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'verify-failed'
});
// Add back to ready queue
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['agent:ready']
});

View file

@ -1,115 +0,0 @@
name: Auto Label Issues
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@v7
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');
}
if (content.includes('.php') || content.includes('laravel') || content.includes('composer')) {
// Skip - already handled by project:core-php
}
// 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,30 +0,0 @@
name: Auto-add to Project
on:
issues:
types: [opened, labeled]
jobs:
add-to-project:
runs-on: ubuntu-latest
steps:
- name: Add to Workstation (agentic label)
if: contains(github.event.issue.labels.*.name, 'agentic')
uses: actions/add-to-project@v1.0.2
with:
project-url: https://github.com/orgs/host-uk/projects/2
github-token: ${{ secrets.PROJECT_TOKEN }}
- name: Add to Core.GO (lang:go label)
if: contains(github.event.issue.labels.*.name, 'lang:go')
uses: actions/add-to-project@v1.0.2
with:
project-url: https://github.com/orgs/host-uk/projects/4
github-token: ${{ secrets.PROJECT_TOKEN }}
- name: Add to Core.Framework (scope:arch label)
if: contains(github.event.issue.labels.*.name, 'scope:arch')
uses: actions/add-to-project@v1.0.2
with:
project-url: https://github.com/orgs/host-uk/projects/1
github-token: ${{ secrets.PROJECT_TOKEN }}

View file

@ -2,23 +2,25 @@ name: CI
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
branches: [main]
jobs:
build:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: 1.22
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install dependencies
run: go mod tidy
- name: Run tests with coverage
run: |
go test -coverprofile=coverage.out ./tests/...
sed -i 's|dappco.re/go/core/||g' coverage.out
- name: Run tests
run: go test ./...
- name: Upload to Codecov
uses: codecov/codecov-action@v5
with:
files: coverage.out
token: ${{ secrets.CODECOV_TOKEN }}

View file

@ -1,36 +0,0 @@
name: CodeQL
on:
push:
branches: [dev, main]
pull_request:
branches: [dev, main]
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@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: go
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:go"

View file

@ -1,36 +0,0 @@
name: "Code Scanning"
on:
push:
branches: ["dev"]
pull_request:
branches: ["dev"]
schedule:
- cron: "0 2 * * 1-5"
jobs:
CodeQL:
runs-on: ubuntu-latest
permissions:
# required for all workflows
security-events: write
# only required for workflows in private repositories
actions: read
contents: read
steps:
- name: "Checkout Repository"
uses: actions/checkout@v4
- name: "Initialize CodeQL"
uses: github/codeql-action/init@v3
with:
languages: go,javascript,typescript
- name: "Autobuild"
uses: github/codeql-action/autobuild@v3
- name: "Perform CodeQL Analysis"
uses: github/codeql-action/analyze@v3

View file

@ -1,46 +0,0 @@
name: Go Test Coverage
on:
push:
branches: [dev, main]
pull_request:
branches: [dev, main]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- name: Setup Task
uses: arduino/setup-task@v1
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev
- name: Build CLI
run: |
go generate ./pkg/updater/...
task cli:build
echo "$(pwd)/bin" >> $GITHUB_PATH
- name: Run coverage
run: task 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@v4
with:
name: coverage-report
path: coverage.txt

View file

@ -1,94 +0,0 @@
name: Dev Release
on:
push:
branches: [dev]
workflow_dispatch:
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- goos: linux
goarch: amd64
- goos: linux
goarch: arm64
- goos: darwin
goarch: amd64
- goos: darwin
goarch: arm64
- goos: windows
goarch: amd64
- goos: windows
goarch: arm64
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
check-latest: true
- name: Build CLI
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: '0'
run: |
EXT=""
if [ "$GOOS" = "windows" ]; then EXT=".exe"; fi
VERSION="dev-$(git rev-parse --short HEAD)"
go build -trimpath -ldflags="-s -w -X github.com/host-uk/core/pkg/cli.AppVersion=${VERSION}" -o core-${GOOS}-${GOARCH}${EXT} .
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: core-${{ matrix.goos }}-${{ matrix.goarch }}
path: core-*
release:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: List artifacts
run: ls -la artifacts/
- name: Delete existing dev release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release delete dev -y || true
- name: Delete existing dev tag
run: git push origin :refs/tags/dev || true
- name: Create dev release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COMMIT_SHA: ${{ github.sha }}
run: |
gh release create dev \
--title "Development Build" \
--notes "Latest development build from the dev branch.
**Commit:** ${COMMIT_SHA}
**Built:** $(date -u +'%Y-%m-%d %H:%M:%S UTC')
This is a pre-release for testing. Use tagged releases for production." \
--prerelease \
--target dev \
artifacts/*

View file

@ -1,86 +0,0 @@
name: Release
on:
push:
tags:
- 'v*.*.*'
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- goos: linux
goarch: amd64
- goos: linux
goarch: arm64
- goos: darwin
goarch: amd64
- goos: darwin
goarch: arm64
- goos: windows
goarch: amd64
- goos: windows
goarch: arm64
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
check-latest: true
- name: Get version from tag
id: version
run: echo "VERSION=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
- name: Build CLI
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: '0'
run: |
EXT=""
if [ "$GOOS" = "windows" ]; then EXT=".exe"; fi
go build -trimpath \
-ldflags="-s -w -X github.com/host-uk/core/pkg/cli.AppVersion=${{ steps.version.outputs.VERSION }}" \
-o core-${GOOS}-${GOARCH}${EXT} .
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: core-${{ matrix.goos }}-${{ matrix.goarch }}
path: core-*
release:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Generate checksums
run: |
cd artifacts
sha256sum core-* > checksums.txt
cat checksums.txt
- name: Create release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create ${{ github.ref_name }} \
--title "${{ github.ref_name }}" \
--generate-notes \
artifacts/*

11
.gitignore vendored
View file

@ -13,7 +13,18 @@ coverage.html
*.cache
/coverage.txt
bin/
dist/
tasks
/core
/i18n-validate
/validate
cmd/*
!cmd/gocmd/
.angular/
patch_cov.*
go.work.sum
lt-hn-index.html
.core/workspace/
.idea/

9
.mcp.json Normal file
View file

@ -0,0 +1,9 @@
{
"mcpServers": {
"core": {
"type": "stdio",
"command": "core-agent",
"args": ["mcp"]
}
}
}

134
CLAUDE.md
View file

@ -1,102 +1,96 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Guidance for Claude Code and Codex when working with this repository.
## Project Overview
## Module
Core is a Web3 Framework written in Go using Wails v3 to replace Electron for desktop applications. It provides a dependency injection framework for managing services with lifecycle support.
`dappco.re/go/core` — dependency injection, service lifecycle, command routing, and message-passing for Go.
## Build & Development Commands
Source files live at the module root (not `pkg/core/`). Tests live in `tests/`.
This project uses [Task](https://taskfile.dev/) for automation. Key commands:
## Build & Test
```bash
# Run all tests
task test
# Generate test coverage
task cov
task cov-view # Opens coverage HTML report
# GUI application (Wails)
task gui:dev # Development mode with hot-reload
task gui:build # Production build
# CLI application
task cli:build # Build CLI
task cli:run # Build and run CLI
# Code review
task review # Submit for CodeRabbit review
task check # Run mod tidy + tests + review
go test ./tests/... # run all tests
go build . # verify compilation
GOWORK=off go test ./tests/ # test without workspace
```
Run a single test: `go test -run TestName ./...`
Or via the Core CLI:
## Architecture
```bash
core go test
core go qa # fmt + vet + lint + test
```
### Core Framework (`core.go`, `interfaces.go`)
## API Shape
The `Core` struct is the central application container managing:
- **Services**: Named service registry with type-safe retrieval via `ServiceFor[T]()` and `MustServiceFor[T]()`
- **Actions/IPC**: Message-passing system where services communicate via `ACTION(msg Message)` and register handlers via `RegisterAction()`
- **Lifecycle**: Services implementing `Startable` (OnStartup) and/or `Stoppable` (OnShutdown) interfaces are automatically called during app lifecycle
CoreGO uses the DTO/Options/Result pattern, not functional options:
Creating a Core instance:
```go
core, err := core.New(
core.WithService(myServiceFactory),
core.WithAssets(assets),
core.WithServiceLock(), // Prevents late service registration
)
c := core.New(core.Options{
{Key: "name", Value: "myapp"},
})
c.Service("cache", core.Service{
OnStart: func() core.Result { return core.Result{OK: true} },
OnStop: func() core.Result { return core.Result{OK: true} },
})
c.Command("deploy/to/homelab", core.Command{
Action: func(opts core.Options) core.Result {
return core.Result{Value: "deployed", OK: true}
},
})
r := c.Cli().Run("deploy", "to", "homelab")
```
### Service Registration Pattern
**Do not use:** `WithService`, `WithName`, `WithApp`, `WithServiceLock`, `Must*`, `ServiceFor[T]` — these no longer exist.
Services are registered via factory functions that receive the Core instance:
```go
func NewMyService(c *core.Core) (any, error) {
return &MyService{runtime: core.NewServiceRuntime(c, opts)}, nil
}
## Subsystems
core.New(core.WithService(NewMyService))
```
| Accessor | Returns | Purpose |
|----------|---------|---------|
| `c.Options()` | `*Options` | Input configuration |
| `c.App()` | `*App` | Application identity |
| `c.Data()` | `*Data` | Embedded filesystem mounts |
| `c.Drive()` | `*Drive` | Named transport handles |
| `c.Fs()` | `*Fs` | Local filesystem I/O |
| `c.Config()` | `*Config` | Runtime settings |
| `c.Cli()` | `*Cli` | CLI surface |
| `c.Command("path")` | `Result` | Command tree |
| `c.Service("name")` | `Result` | Service registry |
| `c.Lock("name")` | `*Lock` | Named mutexes |
| `c.IPC()` | `*Ipc` | Message bus |
| `c.I18n()` | `*I18n` | Locale + translation |
- `WithService`: Auto-discovers service name from package path, registers IPC handler if service has `HandleIPCEvents` method
- `WithName`: Explicitly names a service
## Messaging
### Runtime (`runtime_pkg.go`)
| Method | Pattern |
|--------|---------|
| `c.ACTION(msg)` | Broadcast to all handlers |
| `c.QUERY(q)` | First responder wins |
| `c.QUERYALL(q)` | Collect all responses |
| `c.PERFORM(task)` | First executor wins |
| `c.PerformAsync(task)` | Background goroutine |
`Runtime` is the Wails service wrapper that bootstraps the Core and its services. Use `NewWithFactories()` for custom service registration or `NewRuntime()` for basic setup.
## Error Handling
### ServiceRuntime Generic Helper (`runtime.go`)
Use `core.E()` for structured errors:
Embed `ServiceRuntime[T]` in services to get access to Core and typed options:
```go
type MyService struct {
*core.ServiceRuntime[MyServiceOptions]
}
```
### Error Handling (`e.go`)
Use the `E()` helper for contextual errors:
```go
return core.E("service.Method", "what failed", underlyingErr)
```
### Test Naming Convention
## Test Naming
Tests use `_Good`, `_Bad`, `_Ugly` suffix pattern:
- `_Good`: Happy path tests
- `_Bad`: Expected error conditions
- `_Ugly`: Panic/edge cases
`_Good` (happy path), `_Bad` (expected errors), `_Ugly` (panics/edge cases).
## Docs
Full documentation in `docs/`. Start with `docs/getting-started.md`.
## Go Workspace
Uses Go 1.25 workspaces. The workspace includes:
- Root module (Core framework)
- `cmd/core-gui` (Wails GUI application)
- `cmd/examples/*` (Example applications)
After adding modules: `go work sync`
Part of `~/Code/go.work`. Use `GOWORK=off` to test in isolation.

View file

@ -1,55 +0,0 @@
# GEMINI.md
This file provides guidance for agentic interactions within this repository, specifically for Gemini and other MCP-compliant agents.
## Agentic Context & MCP
This project is built with an **Agentic** design philosophy. It is not exclusive to any single LLM provider (like Claude).
- **MCP Support**: The system is designed to leverage the Model Context Protocol (MCP) to provide rich context and tools to agents.
- **Developer Image**: You are running within a standardized developer image (`host-uk/core` dev environment), ensuring consistent tooling and configuration.
## Core CLI (Agent Interface)
The `core` command is the primary interface for agents to manage the project. Agents should **always** prefer `core` commands over raw shell commands (like `go test`, `php artisan`, etc.).
### Key Commands for Agents
| Task | Command | Notes |
|------|---------|-------|
| **Health Check** | `core doctor` | Verify tools and environment |
| **Repo Status** | `core dev health` | Quick summary of all repos |
| **Work Status** | `core dev work --status` | Detailed dirty/ahead status |
| **Run Tests** | `core go test` | Run Go tests with correct flags |
| **Coverage** | `core go cov` | Generate coverage report |
| **Build** | `core build` | Build the project safely |
| **Search Code** | `core pkg search` | Find packages/repos |
## Project Architecture
Core is a Web3 Framework written in Go using Wails v3.
### Core Framework
- **Services**: Managed via dependency injection (`ServiceFor[T]()`).
- **Lifecycle**: `OnStartup` and `OnShutdown` hooks.
- **IPC**: Message-passing system for service communication.
### Development Workflow
1. **Check State**: `core dev work --status`
2. **Make Changes**: Modify code, add tests.
3. **Verify**: `core go test` (or `core php test` for PHP components).
4. **Commit**: `core dev commit` (or standard git if automated).
5. **Push**: `core dev push` (handles multiple repos).
## Testing Standards
- **Suffix Pattern**:
- `_Good`: Happy path
- `_Bad`: Expected errors
- `_Ugly`: Edge cases/panics
## Go Workspace
The project uses Go workspaces (`go.work`). Always run `core go work sync` after modifying modules.

View file

@ -1,20 +0,0 @@
.PHONY: all dev prod-docs development-docs
all:
(cd cmd/core-gui && task build)
.ONESHELL:
dev:
(cd cmd/core-gui && task dev)
pre-commit:
coderabbit review --prompt-only
development-docs:
@echo "Running development documentation Website..."
@(cd pkg/core/docs && mkdocs serve -w src)
prod-docs:
@echo "Generating documentation tp Repo Root..."
@(cd pkg/core/docs && mkdocs build -d public && cp -r src public)
@echo "Documentation generated at docs/index.html"

447
README.md
View file

@ -1,348 +1,151 @@
# Core
# CoreGO
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.
Dependency injection, service lifecycle, command routing, and message-passing for Go.
- Discord: http://discord.dappco.re
- Repo: https://github.com/Snider/Core
## Vision
Core is an **opinionated Web3 desktop application framework** providing:
1. **Service-Oriented Architecture** - Pluggable services with dependency injection
2. **Encrypted Workspaces** - Each workspace gets its own PGP keypair, files are obfuscated
3. **Cross-Platform Storage** - Abstract storage backends (local, SFTP, WebDAV) behind a `Medium` interface
4. **Multi-Brand Support** - Same codebase powers different "hub" apps (AdminHub, ServerHub, GatewayHub, DeveloperHub, ClientHub)
5. **Built-in Crypto** - PGP encryption/signing, hashing, checksums as first-class citizens
**Mental model:** A secure, encrypted workspace manager where each "workspace" is a cryptographically isolated environment. The framework handles windows, menus, trays, config, and i18n.
## Quick Start
Import path:
```go
import core "github.com/Snider/Core"
import "dappco.re/go/core"
```
app := core.New(
core.WithServiceLock(),
CoreGO is the foundation layer for the Core ecosystem. It gives you:
- one container: `Core`
- one input shape: `Options`
- one output shape: `Result`
- one command tree: `Command`
- one message bus: `ACTION`, `QUERY`, `PERFORM`
## Why It Exists
Most non-trivial Go systems end up needing the same small set of infrastructure:
- a place to keep runtime state and shared subsystems
- a predictable way to start and stop managed components
- a clean command surface for CLI-style workflows
- decoupled communication between components without tight imports
CoreGO keeps those pieces small and explicit.
## Quick Example
```go
package main
import (
"context"
"fmt"
"dappco.re/go/core"
)
type flushCacheTask struct {
Name string
}
func main() {
c := core.New(core.Options{
{Key: "name", Value: "agent-workbench"},
})
c.Service("cache", core.Service{
OnStart: func() core.Result {
core.Info("cache started", "app", c.App().Name)
return core.Result{OK: true}
},
OnStop: func() core.Result {
core.Info("cache stopped", "app", c.App().Name)
return core.Result{OK: true}
},
})
c.RegisterTask(func(_ *core.Core, task core.Task) core.Result {
switch t := task.(type) {
case flushCacheTask:
return core.Result{Value: "cache flushed for " + t.Name, OK: true}
}
return core.Result{}
})
c.Command("cache/flush", core.Command{
Action: func(opts core.Options) core.Result {
return c.PERFORM(flushCacheTask{
Name: opts.String("name"),
})
},
})
if !c.ServiceStartup(context.Background(), nil).OK {
panic("startup failed")
}
r := c.Cli().Run("cache", "flush", "--name=session-store")
fmt.Println(r.Value)
_ = c.ServiceShutdown(context.Background())
}
```
## Prerequisites
## Core Surfaces
- [Go](https://go.dev/) 1.25+
- [Node.js](https://nodejs.org/)
- [Wails](https://wails.io/) v3
- [Task](https://taskfile.dev/)
| Surface | Purpose |
|---------|---------|
| `Core` | Central container and access point |
| `Service` | Managed lifecycle component |
| `Command` | Path-based executable operation |
| `Cli` | CLI surface over the command tree |
| `Data` | Embedded filesystem mounts |
| `Drive` | Named transport handles |
| `Fs` | Local filesystem operations |
| `Config` | Runtime settings and feature flags |
| `I18n` | Locale collection and translation delegation |
| `E`, `Wrap`, `ErrorLog`, `ErrorPanic` | Structured failures and panic recovery |
## Development Workflow (TDD)
## AX-Friendly Model
CoreGO follows the same design direction as the AX spec:
- predictable names over compressed names
- paths as documentation, such as `deploy/to/homelab`
- one repeated vocabulary across the framework
- examples that show how to call real APIs
## Install
```bash
task test-gen # 1. Generate test stubs
task test # 2. Run tests (watch them fail)
# 3. Implement your feature
task test # 4. Run tests (watch them pass)
task review # 5. CodeRabbit review
go get dappco.re/go/core
```
## Building & Running
Requires Go 1.26 or later.
## Test
```bash
# GUI (Wails)
task gui:dev # Development with hot-reload
task gui:build # Production build
# CLI
task cli:build # Build to cmd/core/bin/core
task cli:run # Build and run
core go test
```
## All Tasks
| Task | Description |
|------|-------------|
| `task test` | Run all Go tests |
| `task test-gen` | Generate test stubs for public API |
| `task check` | go mod tidy + tests + review |
| `task review` | CodeRabbit review |
| `task cov` | Generate coverage.txt |
| `task cov-view` | Open HTML coverage report |
| `task sync` | Update public API Go files |
---
## Architecture
### Project Structure
```
.
├── core.go # Facade re-exporting pkg/core
├── pkg/
│ ├── core/ # Service container, DI, Runtime[T]
│ ├── config/ # JSON persistence, XDG paths
│ ├── display/ # Windows, tray, menus (Wails)
│ ├── crypt/ # Hashing, checksums, PGP
│ │ └── openpgp/ # Full PGP implementation
│ ├── io/ # Medium interface + backends
│ ├── workspace/ # Encrypted workspace management
│ ├── help/ # In-app documentation
│ └── i18n/ # Internationalization
├── cmd/
│ ├── core/ # CLI application
│ └── core-gui/ # Wails GUI application
└── go.work # Links root, cmd/core, cmd/core-gui
```
### Service Pattern (Dual-Constructor DI)
Every service follows this pattern:
```go
// Static DI - standalone use/testing (no core.Runtime)
func New() (*Service, error)
// Dynamic DI - for core.WithService() registration
func Register(c *core.Core) (any, error)
```
Services embed `*core.Runtime[Options]` for access to `Core()` and `Config()`.
### IPC/Action System
Services implement `HandleIPCEvents(c *core.Core, msg core.Message) error` - auto-discovered via reflection. Handles typed actions like `core.ActionServiceStartup`.
---
## Wails v3 Frontend Bindings
Core uses [Wails v3](https://v3alpha.wails.io/) to expose Go methods to a WebView2 browser runtime. Wails automatically generates TypeScript bindings for registered services.
**Documentation:** [Wails v3 Method Bindings](https://v3alpha.wails.io/features/bindings/methods/)
### How It Works
1. **Go services** with exported methods are registered with Wails
2. Run `wails3 generate bindings` (or `wails3 dev` / `wails3 build`)
3. **TypeScript SDK** is generated in `frontend/bindings/`
4. Frontend calls Go methods with full type safety, no HTTP overhead
### Current Binding Architecture
```go
// cmd/core-gui/main.go
app.RegisterService(application.NewService(coreService)) // Only Core is registered
```
**Problem:** Only `Core` is registered with Wails. Sub-services (crypt, workspace, display, etc.) are internal to Core's service map - their methods aren't directly exposed to JS.
**Currently exposed** (see `cmd/core-gui/public/bindings/`):
```typescript
// From frontend:
import { ACTION, Config, Service } from './bindings/github.com/Snider/Core/pkg/core'
ACTION(msg) // Broadcast IPC message
Config() // Get config service reference
Service("workspace") // Get service by name (returns any)
```
**NOT exposed:** Direct calls like `workspace.CreateWorkspace()` or `crypt.Hash()`.
### The IPC Bridge Pattern (Chosen Architecture)
Sub-services are accessed via Core's **IPC/ACTION system**, not direct Wails bindings:
```typescript
// Frontend calls Core.ACTION() with typed messages
import { ACTION } from './bindings/github.com/Snider/Core/pkg/core'
// Open a window
ACTION({ action: "display.open_window", name: "settings", options: { Title: "Settings", Width: 800 } })
// Switch workspace
ACTION({ action: "workspace.switch_workspace", name: "myworkspace" })
```
Each service implements `HandleIPCEvents(c *core.Core, msg core.Message)` to process these messages:
```go
// pkg/display/display.go
func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
switch m := msg.(type) {
case map[string]any:
if action, ok := m["action"].(string); ok && action == "display.open_window" {
return s.handleOpenWindowAction(m)
}
}
return nil
}
```
**Why this pattern:**
- Single Wails service (Core) = simpler binding generation
- Services remain decoupled from Wails
- Centralized message routing via `ACTION()`
- Services can communicate internally using same pattern
**Current gap:** Not all service methods have IPC handlers yet. See `HandleIPCEvents` in each service to understand what's wired up.
### Generating Bindings
Or with the standard toolchain:
```bash
cd cmd/core-gui
wails3 generate bindings # Regenerate after Go changes
go test ./...
```
Bindings output to `cmd/core-gui/public/bindings/github.com/Snider/Core/` mirroring Go package structure.
## Docs
---
The full documentation set lives in `docs/`.
### Service Interfaces (`pkg/core/interfaces.go`)
| Path | Covers |
|------|--------|
| `docs/getting-started.md` | First runnable CoreGO app |
| `docs/primitives.md` | `Options`, `Result`, `Service`, `Message`, `Query`, `Task` |
| `docs/services.md` | Service registry, runtime helpers, service locks |
| `docs/commands.md` | Path-based commands and CLI execution |
| `docs/messaging.md` | `ACTION`, `QUERY`, `QUERYALL`, `PERFORM`, `PerformAsync` |
| `docs/lifecycle.md` | Startup, shutdown, context, and task draining |
| `docs/subsystems.md` | `App`, `Data`, `Drive`, `Fs`, `I18n`, `Cli` |
| `docs/errors.md` | Structured errors, logging helpers, panic recovery |
| `docs/testing.md` | Test naming and framework testing patterns |
```go
type Config interface {
Get(key string, out any) error
Set(key string, v any) error
}
## License
type Display interface {
OpenWindow(opts ...WindowOption) error
}
type Workspace interface {
CreateWorkspace(identifier, password string) (string, error)
SwitchWorkspace(name string) error
WorkspaceFileGet(filename string) (string, error)
WorkspaceFileSet(filename, content string) error
}
type Crypt interface {
EncryptPGP(writer io.Writer, recipientPath, data string, ...) (string, error)
DecryptPGP(recipientPath, message, passphrase string, ...) (string, error)
}
```
---
## Current State (Prototype)
### Working
| Package | Notes |
|---------|-------|
| `pkg/core` | Service container, DI, thread-safe - solid |
| `pkg/config` | JSON persistence, XDG paths - solid |
| `pkg/crypt` | Hashing, checksums, PGP - solid, well-tested |
| `pkg/help` | Embedded docs, Show/ShowAt - solid |
| `pkg/i18n` | Multi-language with go-i18n - solid |
| `pkg/io` | Medium interface + local backend - solid |
| `pkg/workspace` | Workspace creation, switching, file ops - functional |
### Partial
| Package | Issues |
|---------|--------|
| `pkg/display` | Window creation works; menu/tray handlers are TODOs |
---
## Priority Work Items
### 1. IMPLEMENT: System Tray Brand Support
`pkg/display/tray.go:52-63` - Commented brand-specific menu items need implementation.
### 2. ADD: Integration Tests
| Package | Notes |
|---------|-------|
| `pkg/display` | Integration tests requiring Wails runtime (27% unit coverage) |
---
## Package Deep Dives
### pkg/workspace - The Core Feature
Each workspace is:
1. Identified by LTHN hash of user identifier
2. Has directory structure: `config/`, `log/`, `data/`, `files/`, `keys/`
3. Gets a PGP keypair generated on creation
4. Files accessed via obfuscated paths
The `workspaceList` maps workspace IDs to public keys.
### pkg/crypt/openpgp
Full PGP using `github.com/ProtonMail/go-crypto`:
- `CreateKeyPair(name, passphrase)` - RSA-4096 with revocation cert
- `EncryptPGP()` - Encrypt + optional signing
- `DecryptPGP()` - Decrypt + optional signature verification
### pkg/io - Storage Abstraction
```go
type Medium interface {
Read(path string) (string, error)
Write(path, content string) error
EnsureDir(path string) error
IsFile(path string) bool
FileGet(path string) (string, error)
FileSet(path, content string) error
}
```
Implementations: `local/`, `sftp/`, `webdav/`
---
## Future Work
### Phase 1: Core Stability
- [x] ~~Fix workspace medium injection (critical blocker)~~
- [x] ~~Initialize `io.Local` global~~
- [x] ~~Clean up dead code (orphaned vars, broken wrappers)~~
- [x] ~~Wire up IPC handlers for all services (config, crypt, display, help, i18n, workspace)~~
- [x] ~~Complete display menu handlers (New/List workspace)~~
- [x] ~~Tray icon setup with asset embedding~~
- [x] ~~Test coverage for io packages~~
- [ ] System tray brand-specific menus
### Phase 2: Multi-Brand Support
- [ ] Define brand configuration system (config? build flags?)
- [ ] Implement brand-specific tray menus (AdminHub, ServerHub, GatewayHub, DeveloperHub, ClientHub)
- [ ] Brand-specific theming/assets
- [ ] Per-brand default workspace configurations
### Phase 3: Remote Storage
- [ ] Complete SFTP backend (`pkg/io/sftp/`)
- [ ] Complete WebDAV backend (`pkg/io/webdav/`)
- [ ] Workspace sync across storage backends
- [ ] Conflict resolution for multi-device access
### Phase 4: Enhanced Crypto
- [ ] Key management UI (import/export, key rotation)
- [ ] Multi-recipient encryption
- [ ] Hardware key support (YubiKey, etc.)
- [ ] Encrypted workspace backup/restore
### Phase 5: Developer Experience
- [ ] TypeScript types for IPC messages (codegen from Go structs)
- [ ] Hot-reload for service registration
- [ ] Plugin system for third-party services
- [ ] CLI tooling for workspace management
### Phase 6: Distribution
- [ ] Auto-update mechanism
- [ ] Platform installers (DMG, MSI, AppImage)
- [ ] Signing and notarization
- [ ] Crash reporting integration
---
## For New Contributors
1. Run `task test` to verify all tests pass
2. Follow TDD: `task test-gen` creates stubs, implement to pass
3. The dual-constructor pattern is intentional: `New(deps)` for tests, `Register()` for runtime
4. See `cmd/core-gui/main.go` for how services wire together
5. IPC handlers in each service's `HandleIPCEvents()` are the frontend bridge
EUPL-1.2

View file

@ -1,132 +0,0 @@
version: '3'
tasks:
# --- CLI Management ---
cli:build:
desc: "Build core CLI to ./bin/core"
cmds:
- go build -o ./bin/core .
cli:install:
desc: "Install core CLI to system PATH"
cmds:
- go install .
# --- Development ---
test:
desc: "Run all tests"
cmds:
- core test
test:verbose:
desc: "Run all tests with verbose output"
cmds:
- core test --verbose
test:run:
desc: "Run specific test (use: task test:run -- TestName)"
cmds:
- core test --run {{.CLI_ARGS}}
cov:
desc: "Run tests with coverage report"
cmds:
- core go cov
fmt:
desc: "Format Go code"
cmds:
- core go fmt
lint:
desc: "Run linter"
cmds:
- core go lint
mod:tidy:
desc: "Run go mod tidy"
cmds:
- core go mod tidy
# --- Quality Assurance ---
qa:
desc: "Run QA: fmt, vet, lint, test"
cmds:
- core go qa
qa:quick:
desc: "Quick QA: fmt, vet, lint only"
cmds:
- core go qa quick
qa:full:
desc: "Full QA: + race, vuln, security"
cmds:
- core go qa full
qa:fix:
desc: "QA with auto-fix"
cmds:
- core go qa --fix
# --- Build ---
build:
desc: "Build project with auto-detection"
cmds:
- core build
build:ci:
desc: "Build for CI (all targets, checksums)"
cmds:
- core build --ci
# --- Environment ---
doctor:
desc: "Check development environment"
cmds:
- core doctor
doctor:verbose:
desc: "Check environment with details"
cmds:
- core doctor --verbose
# --- Code Review ---
review:
desc: "Run CodeRabbit review"
cmds:
- coderabbit review --prompt-only
check:
desc: "Tidy, test, and review"
cmds:
- task: mod:tidy
- task: test
- task: review
# --- i18n ---
i18n:generate:
desc: "Regenerate i18n key constants"
cmds:
- go generate ./pkg/i18n/...
i18n:validate:
desc: "Validate i18n key usage"
cmds:
- go run ./internal/tools/i18n-validate ./...
# --- Multi-repo (when in workspace) ---
dev:health:
desc: "Check health of all repos"
cmds:
- core dev health
dev:work:
desc: "Full workflow: status, commit, push"
cmds:
- core dev work
dev:status:
desc: "Show status of all repos"
cmds:
- core dev work --status

67
app.go Normal file
View file

@ -0,0 +1,67 @@
// SPDX-License-Identifier: EUPL-1.2
// Application identity for the Core framework.
package core
import (
"os/exec"
"path/filepath"
)
// App holds the application identity and optional GUI runtime.
//
// app := core.App{}.New(core.NewOptions(
// core.Option{Key: "name", Value: "Core CLI"},
// core.Option{Key: "version", Value: "1.0.0"},
// ))
type App struct {
Name string
Version string
Description string
Filename string
Path string
Runtime any // GUI runtime (e.g., Wails App). Nil for CLI-only.
}
// New creates an App from Options.
//
// app := core.App{}.New(core.NewOptions(
// core.Option{Key: "name", Value: "myapp"},
// core.Option{Key: "version", Value: "1.0.0"},
// ))
func (a App) New(opts Options) App {
if name := opts.String("name"); name != "" {
a.Name = name
}
if version := opts.String("version"); version != "" {
a.Version = version
}
if desc := opts.String("description"); desc != "" {
a.Description = desc
}
if filename := opts.String("filename"); filename != "" {
a.Filename = filename
}
return a
}
// Find locates a program on PATH and returns a Result containing the App.
//
// r := core.App{}.Find("node", "Node.js")
// if r.OK { app := r.Value.(*App) }
func (a App) Find(filename, name string) Result {
path, err := exec.LookPath(filename)
if err != nil {
return Result{err, false}
}
abs, err := filepath.Abs(path)
if err != nil {
return Result{err, false}
}
return Result{&App{
Name: name,
Filename: filename,
Path: abs,
}, true}
}

68
app_test.go Normal file
View file

@ -0,0 +1,68 @@
package core_test
import (
"testing"
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
// --- App.New ---
func TestApp_New_Good(t *testing.T) {
app := App{}.New(NewOptions(
Option{Key: "name", Value: "myapp"},
Option{Key: "version", Value: "1.0.0"},
Option{Key: "description", Value: "test app"},
))
assert.Equal(t, "myapp", app.Name)
assert.Equal(t, "1.0.0", app.Version)
assert.Equal(t, "test app", app.Description)
}
func TestApp_New_Empty_Good(t *testing.T) {
app := App{}.New(NewOptions())
assert.Equal(t, "", app.Name)
assert.Equal(t, "", app.Version)
}
func TestApp_New_Partial_Good(t *testing.T) {
app := App{}.New(NewOptions(
Option{Key: "name", Value: "myapp"},
))
assert.Equal(t, "myapp", app.Name)
assert.Equal(t, "", app.Version)
}
// --- App via Core ---
func TestApp_Core_Good(t *testing.T) {
c := New(WithOption("name", "myapp"))
assert.Equal(t, "myapp", c.App().Name)
}
func TestApp_Core_Empty_Good(t *testing.T) {
c := New()
assert.NotNil(t, c.App())
assert.Equal(t, "", c.App().Name)
}
func TestApp_Runtime_Good(t *testing.T) {
c := New()
c.App().Runtime = &struct{ Name string }{Name: "wails"}
assert.NotNil(t, c.App().Runtime)
}
// --- App.Find ---
func TestApp_Find_Good(t *testing.T) {
r := App{}.Find("go", "go")
assert.True(t, r.OK)
app := r.Value.(*App)
assert.NotEmpty(t, app.Path)
}
func TestApp_Find_Bad(t *testing.T) {
r := App{}.Find("nonexistent-binary-xyz", "test")
assert.False(t, r.OK)
}

101
array.go Normal file
View file

@ -0,0 +1,101 @@
// SPDX-License-Identifier: EUPL-1.2
// Generic slice operations for the Core framework.
// Based on leaanthony/slicer, rewritten with Go 1.18+ generics.
package core
// Array is a typed slice with common operations.
type Array[T comparable] struct {
items []T
}
// NewArray creates an empty Array.
func NewArray[T comparable](items ...T) *Array[T] {
return &Array[T]{items: items}
}
// Add appends values.
func (s *Array[T]) Add(values ...T) {
s.items = append(s.items, values...)
}
// AddUnique appends values only if not already present.
func (s *Array[T]) AddUnique(values ...T) {
for _, v := range values {
if !s.Contains(v) {
s.items = append(s.items, v)
}
}
}
// Contains returns true if the value is in the slice.
func (s *Array[T]) Contains(val T) bool {
for _, v := range s.items {
if v == val {
return true
}
}
return false
}
// Filter returns a new Array with elements matching the predicate.
func (s *Array[T]) Filter(fn func(T) bool) Result {
filtered := &Array[T]{}
for _, v := range s.items {
if fn(v) {
filtered.items = append(filtered.items, v)
}
}
return Result{filtered, true}
}
// Each runs a function on every element.
func (s *Array[T]) Each(fn func(T)) {
for _, v := range s.items {
fn(v)
}
}
// Remove removes the first occurrence of a value.
func (s *Array[T]) Remove(val T) {
for i, v := range s.items {
if v == val {
s.items = append(s.items[:i], s.items[i+1:]...)
return
}
}
}
// Deduplicate removes duplicate values, preserving order.
func (s *Array[T]) Deduplicate() {
seen := make(map[T]struct{})
result := make([]T, 0, len(s.items))
for _, v := range s.items {
if _, exists := seen[v]; !exists {
seen[v] = struct{}{}
result = append(result, v)
}
}
s.items = result
}
// Len returns the number of elements.
func (s *Array[T]) Len() int {
return len(s.items)
}
// Clear removes all elements.
func (s *Array[T]) Clear() {
s.items = nil
}
// AsSlice returns a copy of the underlying slice.
func (s *Array[T]) AsSlice() []T {
if s.items == nil {
return nil
}
out := make([]T, len(s.items))
copy(out, s.items)
return out
}

90
array_test.go Normal file
View file

@ -0,0 +1,90 @@
package core_test
import (
"testing"
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
// --- Array[T] ---
func TestArray_New_Good(t *testing.T) {
a := NewArray("a", "b", "c")
assert.Equal(t, 3, a.Len())
}
func TestArray_Add_Good(t *testing.T) {
a := NewArray[string]()
a.Add("x", "y")
assert.Equal(t, 2, a.Len())
assert.True(t, a.Contains("x"))
assert.True(t, a.Contains("y"))
}
func TestArray_AddUnique_Good(t *testing.T) {
a := NewArray("a", "b")
a.AddUnique("b", "c")
assert.Equal(t, 3, a.Len())
}
func TestArray_Contains_Good(t *testing.T) {
a := NewArray(1, 2, 3)
assert.True(t, a.Contains(2))
assert.False(t, a.Contains(99))
}
func TestArray_Filter_Good(t *testing.T) {
a := NewArray(1, 2, 3, 4, 5)
r := a.Filter(func(n int) bool { return n%2 == 0 })
assert.True(t, r.OK)
evens := r.Value.(*Array[int])
assert.Equal(t, 2, evens.Len())
assert.True(t, evens.Contains(2))
assert.True(t, evens.Contains(4))
}
func TestArray_Each_Good(t *testing.T) {
a := NewArray("a", "b", "c")
var collected []string
a.Each(func(s string) { collected = append(collected, s) })
assert.Equal(t, []string{"a", "b", "c"}, collected)
}
func TestArray_Remove_Good(t *testing.T) {
a := NewArray("a", "b", "c")
a.Remove("b")
assert.Equal(t, 2, a.Len())
assert.False(t, a.Contains("b"))
}
func TestArray_Remove_Bad(t *testing.T) {
a := NewArray("a", "b")
a.Remove("missing")
assert.Equal(t, 2, a.Len())
}
func TestArray_Deduplicate_Good(t *testing.T) {
a := NewArray("a", "b", "a", "c", "b")
a.Deduplicate()
assert.Equal(t, 3, a.Len())
}
func TestArray_Clear_Good(t *testing.T) {
a := NewArray(1, 2, 3)
a.Clear()
assert.Equal(t, 0, a.Len())
}
func TestArray_AsSlice_Good(t *testing.T) {
a := NewArray("x", "y")
s := a.AsSlice()
assert.Equal(t, []string{"x", "y"}, s)
}
func TestArray_Empty_Good(t *testing.T) {
a := NewArray[int]()
assert.Equal(t, 0, a.Len())
assert.False(t, a.Contains(0))
assert.Equal(t, []int(nil), a.AsSlice())
}

178
cli.go Normal file
View file

@ -0,0 +1,178 @@
// SPDX-License-Identifier: EUPL-1.2
// Cli is the CLI surface layer for the Core command tree.
//
// c := core.New(core.WithOption("name", "myapp")).Value.(*Core)
// c.Command("deploy", core.Command{Action: handler})
// c.Cli().Run()
package core
import (
"io"
"os"
)
// CliOptions holds configuration for the Cli service.
type CliOptions struct{}
// Cli is the CLI surface for the Core command tree.
type Cli struct {
*ServiceRuntime[CliOptions]
output io.Writer
banner func(*Cli) string
}
// Register creates a Cli service factory for core.WithService.
//
// core.New(core.WithService(core.CliRegister))
func CliRegister(c *Core) Result {
cl := &Cli{output: os.Stdout}
cl.ServiceRuntime = NewServiceRuntime[CliOptions](c, CliOptions{})
return c.RegisterService("cli", cl)
}
// Print writes to the CLI output (defaults to os.Stdout).
//
// c.Cli().Print("hello %s", "world")
func (cl *Cli) Print(format string, args ...any) {
Print(cl.output, format, args...)
}
// SetOutput sets the CLI output writer.
//
// c.Cli().SetOutput(os.Stderr)
func (cl *Cli) SetOutput(w io.Writer) {
cl.output = w
}
// Run resolves os.Args to a command path and executes it.
//
// c.Cli().Run()
// c.Cli().Run("deploy", "to", "homelab")
func (cl *Cli) Run(args ...string) Result {
if len(args) == 0 {
args = os.Args[1:]
}
clean := FilterArgs(args)
c := cl.Core()
if c == nil || c.commands == nil {
if cl.banner != nil {
cl.Print(cl.banner(cl))
}
return Result{}
}
c.commands.mu.RLock()
cmdCount := len(c.commands.commands)
c.commands.mu.RUnlock()
if cmdCount == 0 {
if cl.banner != nil {
cl.Print(cl.banner(cl))
}
return Result{}
}
// Resolve command path from args
var cmd *Command
var remaining []string
c.commands.mu.RLock()
for i := len(clean); i > 0; i-- {
path := JoinPath(clean[:i]...)
if found, ok := c.commands.commands[path]; ok {
cmd = found
remaining = clean[i:]
break
}
}
c.commands.mu.RUnlock()
if cmd == nil {
if cl.banner != nil {
cl.Print(cl.banner(cl))
}
cl.PrintHelp()
return Result{}
}
// Build options from remaining args
opts := NewOptions()
for _, arg := range remaining {
key, val, valid := ParseFlag(arg)
if valid {
if Contains(arg, "=") {
opts.Set(key, val)
} else {
opts.Set(key, true)
}
} else if !IsFlag(arg) {
opts.Set("_arg", arg)
}
}
if cmd.Action != nil {
return cmd.Run(opts)
}
if cmd.Lifecycle != nil {
return cmd.Start(opts)
}
return Result{E("core.Cli.Run", Concat("command \"", cmd.Path, "\" is not executable"), nil), false}
}
// PrintHelp prints available commands.
//
// c.Cli().PrintHelp()
func (cl *Cli) PrintHelp() {
c := cl.Core()
if c == nil || c.commands == nil {
return
}
name := ""
if c.app != nil {
name = c.app.Name
}
if name != "" {
cl.Print("%s commands:", name)
} else {
cl.Print("Commands:")
}
c.commands.mu.RLock()
defer c.commands.mu.RUnlock()
for path, cmd := range c.commands.commands {
if cmd.Hidden || (cmd.Action == nil && cmd.Lifecycle == nil) {
continue
}
tr := c.I18n().Translate(cmd.I18nKey())
desc, _ := tr.Value.(string)
if desc == "" || desc == cmd.I18nKey() {
cl.Print(" %s", path)
} else {
cl.Print(" %-30s %s", path, desc)
}
}
}
// SetBanner sets the banner function.
//
// c.Cli().SetBanner(func(_ *core.Cli) string { return "My App v1.0" })
func (cl *Cli) SetBanner(fn func(*Cli) string) {
cl.banner = fn
}
// Banner returns the banner string.
func (cl *Cli) Banner() string {
if cl.banner != nil {
return cl.banner(cl)
}
c := cl.Core()
if c != nil && c.app != nil && c.app.Name != "" {
return c.app.Name
}
return ""
}

85
cli_test.go Normal file
View file

@ -0,0 +1,85 @@
package core_test
import (
"bytes"
"testing"
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
// --- Cli Surface ---
func TestCli_Good(t *testing.T) {
c := New()
assert.NotNil(t, c.Cli())
}
func TestCli_Banner_Good(t *testing.T) {
c := New(WithOption("name", "myapp"))
assert.Equal(t, "myapp", c.Cli().Banner())
}
func TestCli_SetBanner_Good(t *testing.T) {
c := New()
c.Cli().SetBanner(func(_ *Cli) string { return "Custom Banner" })
assert.Equal(t, "Custom Banner", c.Cli().Banner())
}
func TestCli_Run_Good(t *testing.T) {
c := New()
executed := false
c.Command("hello", Command{Action: func(_ Options) Result {
executed = true
return Result{Value: "world", OK: true}
}})
r := c.Cli().Run("hello")
assert.True(t, r.OK)
assert.Equal(t, "world", r.Value)
assert.True(t, executed)
}
func TestCli_Run_Nested_Good(t *testing.T) {
c := New()
executed := false
c.Command("deploy/to/homelab", Command{Action: func(_ Options) Result {
executed = true
return Result{OK: true}
}})
r := c.Cli().Run("deploy", "to", "homelab")
assert.True(t, r.OK)
assert.True(t, executed)
}
func TestCli_Run_WithFlags_Good(t *testing.T) {
c := New()
var received Options
c.Command("serve", Command{Action: func(opts Options) Result {
received = opts
return Result{OK: true}
}})
c.Cli().Run("serve", "--port=8080", "--debug")
assert.Equal(t, "8080", received.String("port"))
assert.True(t, received.Bool("debug"))
}
func TestCli_Run_NoCommand_Good(t *testing.T) {
c := New()
r := c.Cli().Run()
assert.False(t, r.OK)
}
func TestCli_PrintHelp_Good(t *testing.T) {
c := New(WithOption("name", "myapp"))
c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }})
c.Command("serve", Command{Action: func(_ Options) Result { return Result{OK: true} }})
c.Cli().PrintHelp()
}
func TestCli_SetOutput_Good(t *testing.T) {
c := New()
var buf bytes.Buffer
c.Cli().SetOutput(&buf)
c.Cli().Print("hello %s", "world")
assert.Contains(t, buf.String(), "hello world")
}

208
command.go Normal file
View file

@ -0,0 +1,208 @@
// SPDX-License-Identifier: EUPL-1.2
// Command is a DTO representing an executable operation.
// Commands don't know if they're root, child, or nested — the tree
// structure comes from composition via path-based registration.
//
// Register a command:
//
// c.Command("deploy", func(opts core.Options) core.Result {
// return core.Result{"deployed", true}
// })
//
// Register a nested command:
//
// c.Command("deploy/to/homelab", handler)
//
// Description is an i18n key — derived from path if omitted:
//
// "deploy" → "cmd.deploy.description"
// "deploy/to/homelab" → "cmd.deploy.to.homelab.description"
package core
import (
"sync"
)
// CommandAction is the function signature for command handlers.
//
// func(opts core.Options) core.Result
type CommandAction func(Options) Result
// CommandLifecycle is implemented by commands that support managed lifecycle.
// Basic commands only need an action. Daemon commands implement Start/Stop/Signal
// via go-process.
type CommandLifecycle interface {
Start(Options) Result
Stop() Result
Restart() Result
Reload() Result
Signal(string) Result
}
// Command is the DTO for an executable operation.
type Command struct {
Name string
Description string // i18n key — derived from path if empty
Path string // "deploy/to/homelab"
Action CommandAction // business logic
Lifecycle CommandLifecycle // optional — provided by go-process
Flags Options // declared flags
Hidden bool
commands map[string]*Command // child commands (internal)
mu sync.RWMutex
}
// I18nKey returns the i18n key for this command's description.
//
// cmd with path "deploy/to/homelab" → "cmd.deploy.to.homelab.description"
func (cmd *Command) I18nKey() string {
if cmd.Description != "" {
return cmd.Description
}
path := cmd.Path
if path == "" {
path = cmd.Name
}
return Concat("cmd.", Replace(path, "/", "."), ".description")
}
// Run executes the command's action with the given options.
//
// result := cmd.Run(core.NewOptions(core.Option{Key: "target", Value: "homelab"}))
func (cmd *Command) Run(opts Options) Result {
if cmd.Action == nil {
return Result{E("core.Command.Run", Concat("command \"", cmd.Path, "\" is not executable"), nil), false}
}
return cmd.Action(opts)
}
// Start delegates to the lifecycle implementation if available.
func (cmd *Command) Start(opts Options) Result {
if cmd.Lifecycle != nil {
return cmd.Lifecycle.Start(opts)
}
return cmd.Run(opts)
}
// Stop delegates to the lifecycle implementation.
func (cmd *Command) Stop() Result {
if cmd.Lifecycle != nil {
return cmd.Lifecycle.Stop()
}
return Result{}
}
// Restart delegates to the lifecycle implementation.
func (cmd *Command) Restart() Result {
if cmd.Lifecycle != nil {
return cmd.Lifecycle.Restart()
}
return Result{}
}
// Reload delegates to the lifecycle implementation.
func (cmd *Command) Reload() Result {
if cmd.Lifecycle != nil {
return cmd.Lifecycle.Reload()
}
return Result{}
}
// Signal delegates to the lifecycle implementation.
func (cmd *Command) Signal(sig string) Result {
if cmd.Lifecycle != nil {
return cmd.Lifecycle.Signal(sig)
}
return Result{}
}
// --- Command Registry (on Core) ---
// commandRegistry holds the command tree.
type commandRegistry struct {
commands map[string]*Command
mu sync.RWMutex
}
// Command gets or registers a command by path.
//
// c.Command("deploy", Command{Action: handler})
// r := c.Command("deploy")
func (c *Core) Command(path string, command ...Command) Result {
if len(command) == 0 {
c.commands.mu.RLock()
cmd, ok := c.commands.commands[path]
c.commands.mu.RUnlock()
return Result{cmd, ok}
}
if path == "" || HasPrefix(path, "/") || HasSuffix(path, "/") || Contains(path, "//") {
return Result{E("core.Command", Concat("invalid command path: \"", path, "\""), nil), false}
}
c.commands.mu.Lock()
defer c.commands.mu.Unlock()
if existing, exists := c.commands.commands[path]; exists && (existing.Action != nil || existing.Lifecycle != nil) {
return Result{E("core.Command", Concat("command \"", path, "\" already registered"), nil), false}
}
cmd := &command[0]
cmd.Name = pathName(path)
cmd.Path = path
if cmd.commands == nil {
cmd.commands = make(map[string]*Command)
}
// Preserve existing subtree when overwriting a placeholder parent
if existing, exists := c.commands.commands[path]; exists {
for k, v := range existing.commands {
if _, has := cmd.commands[k]; !has {
cmd.commands[k] = v
}
}
}
c.commands.commands[path] = cmd
// Build parent chain — "deploy/to/homelab" creates "deploy" and "deploy/to" if missing
parts := Split(path, "/")
for i := len(parts) - 1; i > 0; i-- {
parentPath := JoinPath(parts[:i]...)
if _, exists := c.commands.commands[parentPath]; !exists {
c.commands.commands[parentPath] = &Command{
Name: parts[i-1],
Path: parentPath,
commands: make(map[string]*Command),
}
}
c.commands.commands[parentPath].commands[parts[i]] = cmd
cmd = c.commands.commands[parentPath]
}
return Result{OK: true}
}
// Commands returns all registered command paths.
//
// paths := c.Commands()
func (c *Core) Commands() []string {
if c.commands == nil {
return nil
}
c.commands.mu.RLock()
defer c.commands.mu.RUnlock()
var paths []string
for k := range c.commands.commands {
paths = append(paths, k)
}
return paths
}
// pathName extracts the last segment of a path.
// "deploy/to/homelab" → "homelab"
func pathName(path string) string {
parts := Split(path, "/")
return parts[len(parts)-1]
}

217
command_test.go Normal file
View file

@ -0,0 +1,217 @@
package core_test
import (
"testing"
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
// --- Command DTO ---
func TestCommand_Register_Good(t *testing.T) {
c := New()
r := c.Command("deploy", Command{Action: func(_ Options) Result {
return Result{Value: "deployed", OK: true}
}})
assert.True(t, r.OK)
}
func TestCommand_Get_Good(t *testing.T) {
c := New()
c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }})
r := c.Command("deploy")
assert.True(t, r.OK)
assert.NotNil(t, r.Value)
}
func TestCommand_Get_Bad(t *testing.T) {
c := New()
r := c.Command("nonexistent")
assert.False(t, r.OK)
}
func TestCommand_Run_Good(t *testing.T) {
c := New()
c.Command("greet", Command{Action: func(opts Options) Result {
return Result{Value: Concat("hello ", opts.String("name")), OK: true}
}})
cmd := c.Command("greet").Value.(*Command)
r := cmd.Run(NewOptions(Option{Key: "name", Value: "world"}))
assert.True(t, r.OK)
assert.Equal(t, "hello world", r.Value)
}
func TestCommand_Run_NoAction_Good(t *testing.T) {
c := New()
c.Command("empty", Command{Description: "no action"})
cmd := c.Command("empty").Value.(*Command)
r := cmd.Run(NewOptions())
assert.False(t, r.OK)
}
// --- Nested Commands ---
func TestCommand_Nested_Good(t *testing.T) {
c := New()
c.Command("deploy/to/homelab", Command{Action: func(_ Options) Result {
return Result{Value: "deployed to homelab", OK: true}
}})
r := c.Command("deploy/to/homelab")
assert.True(t, r.OK)
// Parent auto-created
assert.True(t, c.Command("deploy").OK)
assert.True(t, c.Command("deploy/to").OK)
}
func TestCommand_Paths_Good(t *testing.T) {
c := New()
c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }})
c.Command("serve", Command{Action: func(_ Options) Result { return Result{OK: true} }})
c.Command("deploy/to/homelab", Command{Action: func(_ Options) Result { return Result{OK: true} }})
paths := c.Commands()
assert.Contains(t, paths, "deploy")
assert.Contains(t, paths, "serve")
assert.Contains(t, paths, "deploy/to/homelab")
assert.Contains(t, paths, "deploy/to")
}
// --- I18n Key Derivation ---
func TestCommand_I18nKey_Good(t *testing.T) {
c := New()
c.Command("deploy/to/homelab", Command{})
cmd := c.Command("deploy/to/homelab").Value.(*Command)
assert.Equal(t, "cmd.deploy.to.homelab.description", cmd.I18nKey())
}
func TestCommand_I18nKey_Custom_Good(t *testing.T) {
c := New()
c.Command("deploy", Command{Description: "custom.deploy.key"})
cmd := c.Command("deploy").Value.(*Command)
assert.Equal(t, "custom.deploy.key", cmd.I18nKey())
}
func TestCommand_I18nKey_Simple_Good(t *testing.T) {
c := New()
c.Command("serve", Command{})
cmd := c.Command("serve").Value.(*Command)
assert.Equal(t, "cmd.serve.description", cmd.I18nKey())
}
// --- Lifecycle ---
func TestCommand_Lifecycle_NoImpl_Good(t *testing.T) {
c := New()
c.Command("serve", Command{Action: func(_ Options) Result {
return Result{Value: "running", OK: true}
}})
cmd := c.Command("serve").Value.(*Command)
r := cmd.Start(NewOptions())
assert.True(t, r.OK)
assert.Equal(t, "running", r.Value)
assert.False(t, cmd.Stop().OK)
assert.False(t, cmd.Restart().OK)
assert.False(t, cmd.Reload().OK)
assert.False(t, cmd.Signal("HUP").OK)
}
// --- Lifecycle with Implementation ---
type testLifecycle struct {
started bool
stopped bool
restarted bool
reloaded bool
signalled string
}
func (l *testLifecycle) Start(opts Options) Result {
l.started = true
return Result{Value: "started", OK: true}
}
func (l *testLifecycle) Stop() Result {
l.stopped = true
return Result{OK: true}
}
func (l *testLifecycle) Restart() Result {
l.restarted = true
return Result{OK: true}
}
func (l *testLifecycle) Reload() Result {
l.reloaded = true
return Result{OK: true}
}
func (l *testLifecycle) Signal(sig string) Result {
l.signalled = sig
return Result{Value: sig, OK: true}
}
func TestCommand_Lifecycle_WithImpl_Good(t *testing.T) {
c := New()
lc := &testLifecycle{}
c.Command("daemon", Command{Lifecycle: lc})
cmd := c.Command("daemon").Value.(*Command)
r := cmd.Start(NewOptions())
assert.True(t, r.OK)
assert.True(t, lc.started)
assert.True(t, cmd.Stop().OK)
assert.True(t, lc.stopped)
assert.True(t, cmd.Restart().OK)
assert.True(t, lc.restarted)
assert.True(t, cmd.Reload().OK)
assert.True(t, lc.reloaded)
r = cmd.Signal("HUP")
assert.True(t, r.OK)
assert.Equal(t, "HUP", lc.signalled)
}
func TestCommand_Duplicate_Bad(t *testing.T) {
c := New()
c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }})
r := c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }})
assert.False(t, r.OK)
}
func TestCommand_InvalidPath_Bad(t *testing.T) {
c := New()
assert.False(t, c.Command("/leading", Command{}).OK)
assert.False(t, c.Command("trailing/", Command{}).OK)
assert.False(t, c.Command("double//slash", Command{}).OK)
}
// --- Cli Run with Lifecycle ---
func TestCli_Run_Lifecycle_Good(t *testing.T) {
c := New()
lc := &testLifecycle{}
c.Command("serve", Command{Lifecycle: lc})
r := c.Cli().Run("serve")
assert.True(t, r.OK)
assert.True(t, lc.started)
}
func TestCli_Run_NoActionNoLifecycle_Bad(t *testing.T) {
c := New()
c.Command("empty", Command{})
r := c.Cli().Run("empty")
assert.False(t, r.OK)
}
// --- Empty path ---
func TestCommand_EmptyPath_Bad(t *testing.T) {
c := New()
r := c.Command("", Command{})
assert.False(t, r.OK)
}

144
config.go Normal file
View file

@ -0,0 +1,144 @@
// SPDX-License-Identifier: EUPL-1.2
// Settings, feature flags, and typed configuration for the Core framework.
package core
import (
"sync"
)
// ConfigVar is a variable that can be set, unset, and queried for its state.
type ConfigVar[T any] struct {
val T
set bool
}
func (v *ConfigVar[T]) Get() T { return v.val }
func (v *ConfigVar[T]) Set(val T) { v.val = val; v.set = true }
func (v *ConfigVar[T]) IsSet() bool { return v.set }
func (v *ConfigVar[T]) Unset() {
v.set = false
var zero T
v.val = zero
}
func NewConfigVar[T any](val T) ConfigVar[T] {
return ConfigVar[T]{val: val, set: true}
}
// ConfigOptions holds configuration data.
type ConfigOptions struct {
Settings map[string]any
Features map[string]bool
}
func (o *ConfigOptions) init() {
if o.Settings == nil {
o.Settings = make(map[string]any)
}
if o.Features == nil {
o.Features = make(map[string]bool)
}
}
// Config holds configuration settings and feature flags.
type Config struct {
*ConfigOptions
mu sync.RWMutex
}
// New initialises a Config with empty settings and features.
//
// cfg := (&core.Config{}).New()
func (e *Config) New() *Config {
e.ConfigOptions = &ConfigOptions{}
e.ConfigOptions.init()
return e
}
// Set stores a configuration value by key.
func (e *Config) Set(key string, val any) {
e.mu.Lock()
if e.ConfigOptions == nil {
e.ConfigOptions = &ConfigOptions{}
}
e.ConfigOptions.init()
e.Settings[key] = val
e.mu.Unlock()
}
// Get retrieves a configuration value by key.
func (e *Config) Get(key string) Result {
e.mu.RLock()
defer e.mu.RUnlock()
if e.ConfigOptions == nil || e.Settings == nil {
return Result{}
}
val, ok := e.Settings[key]
if !ok {
return Result{}
}
return Result{val, true}
}
func (e *Config) String(key string) string { return ConfigGet[string](e, key) }
func (e *Config) Int(key string) int { return ConfigGet[int](e, key) }
func (e *Config) Bool(key string) bool { return ConfigGet[bool](e, key) }
// ConfigGet retrieves a typed configuration value.
func ConfigGet[T any](e *Config, key string) T {
r := e.Get(key)
if !r.OK {
var zero T
return zero
}
typed, _ := r.Value.(T)
return typed
}
// --- Feature Flags ---
func (e *Config) Enable(feature string) {
e.mu.Lock()
if e.ConfigOptions == nil {
e.ConfigOptions = &ConfigOptions{}
}
e.ConfigOptions.init()
e.Features[feature] = true
e.mu.Unlock()
}
func (e *Config) Disable(feature string) {
e.mu.Lock()
if e.ConfigOptions == nil {
e.ConfigOptions = &ConfigOptions{}
}
e.ConfigOptions.init()
e.Features[feature] = false
e.mu.Unlock()
}
func (e *Config) Enabled(feature string) bool {
e.mu.RLock()
defer e.mu.RUnlock()
if e.ConfigOptions == nil || e.Features == nil {
return false
}
return e.Features[feature]
}
func (e *Config) EnabledFeatures() []string {
e.mu.RLock()
defer e.mu.RUnlock()
if e.ConfigOptions == nil || e.Features == nil {
return nil
}
var result []string
for k, v := range e.Features {
if v {
result = append(result, k)
}
}
return result
}

102
config_test.go Normal file
View file

@ -0,0 +1,102 @@
package core_test
import (
"testing"
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
// --- Config ---
func TestConfig_SetGet_Good(t *testing.T) {
c := New()
c.Config().Set("api_url", "https://api.lthn.ai")
c.Config().Set("max_agents", 5)
r := c.Config().Get("api_url")
assert.True(t, r.OK)
assert.Equal(t, "https://api.lthn.ai", r.Value)
}
func TestConfig_Get_Bad(t *testing.T) {
c := New()
r := c.Config().Get("missing")
assert.False(t, r.OK)
assert.Nil(t, r.Value)
}
func TestConfig_TypedAccessors_Good(t *testing.T) {
c := New()
c.Config().Set("url", "https://lthn.ai")
c.Config().Set("port", 8080)
c.Config().Set("debug", true)
assert.Equal(t, "https://lthn.ai", c.Config().String("url"))
assert.Equal(t, 8080, c.Config().Int("port"))
assert.True(t, c.Config().Bool("debug"))
}
func TestConfig_TypedAccessors_Bad(t *testing.T) {
c := New()
// Missing keys return zero values
assert.Equal(t, "", c.Config().String("missing"))
assert.Equal(t, 0, c.Config().Int("missing"))
assert.False(t, c.Config().Bool("missing"))
}
// --- Feature Flags ---
func TestConfig_Features_Good(t *testing.T) {
c := New()
c.Config().Enable("dark-mode")
c.Config().Enable("beta")
assert.True(t, c.Config().Enabled("dark-mode"))
assert.True(t, c.Config().Enabled("beta"))
assert.False(t, c.Config().Enabled("missing"))
}
func TestConfig_Features_Disable_Good(t *testing.T) {
c := New()
c.Config().Enable("feature")
assert.True(t, c.Config().Enabled("feature"))
c.Config().Disable("feature")
assert.False(t, c.Config().Enabled("feature"))
}
func TestConfig_Features_CaseSensitive(t *testing.T) {
c := New()
c.Config().Enable("Feature")
assert.True(t, c.Config().Enabled("Feature"))
assert.False(t, c.Config().Enabled("feature"))
}
func TestConfig_EnabledFeatures_Good(t *testing.T) {
c := New()
c.Config().Enable("a")
c.Config().Enable("b")
c.Config().Enable("c")
c.Config().Disable("b")
features := c.Config().EnabledFeatures()
assert.Contains(t, features, "a")
assert.Contains(t, features, "c")
assert.NotContains(t, features, "b")
}
// --- ConfigVar ---
func TestConfigVar_Good(t *testing.T) {
v := NewConfigVar("hello")
assert.True(t, v.IsSet())
assert.Equal(t, "hello", v.Get())
v.Set("world")
assert.Equal(t, "world", v.Get())
v.Unset()
assert.False(t, v.IsSet())
assert.Equal(t, "", v.Get())
}

228
contract.go Normal file
View file

@ -0,0 +1,228 @@
// SPDX-License-Identifier: EUPL-1.2
// Contracts, options, and type definitions for the Core framework.
package core
import (
"context"
"reflect"
)
// Message is the type for IPC broadcasts (fire-and-forget).
type Message any
// Query is the type for read-only IPC requests.
type Query any
// Task is the type for IPC requests that perform side effects.
type Task any
// TaskWithIdentifier is an optional interface for tasks that need to know their assigned identifier.
type TaskWithIdentifier interface {
Task
SetTaskIdentifier(id string)
GetTaskIdentifier() string
}
// QueryHandler handles Query requests. Returns Result{Value, OK}.
type QueryHandler func(*Core, Query) Result
// TaskHandler handles Task requests. Returns Result{Value, OK}.
type TaskHandler func(*Core, Task) Result
// Startable is implemented by services that need startup initialisation.
type Startable interface {
OnStartup(ctx context.Context) error
}
// Stoppable is implemented by services that need shutdown cleanup.
type Stoppable interface {
OnShutdown(ctx context.Context) error
}
// --- Action Messages ---
type ActionServiceStartup struct{}
type ActionServiceShutdown struct{}
type ActionTaskStarted struct {
TaskIdentifier string
Task Task
}
type ActionTaskProgress struct {
TaskIdentifier string
Task Task
Progress float64
Message string
}
type ActionTaskCompleted struct {
TaskIdentifier string
Task Task
Result any
Error error
}
// --- Constructor ---
// CoreOption is a functional option applied during Core construction.
// Returns Result — if !OK, New() stops and returns the error.
//
// core.New(
// core.WithService(agentic.Register),
// core.WithService(monitor.Register),
// core.WithServiceLock(),
// )
type CoreOption func(*Core) Result
// New initialises a Core instance by applying options in order.
// Services registered here form the application conclave — they share
// IPC access and participate in the lifecycle (ServiceStartup/ServiceShutdown).
//
// r := core.New(
// core.WithOptions(core.NewOptions(core.Option{Key: "name", Value: "myapp"})),
// core.WithService(auth.Register),
// core.WithServiceLock(),
// )
// if !r.OK { log.Fatal(r.Value) }
// c := r.Value.(*Core)
func New(opts ...CoreOption) *Core {
c := &Core{
app: &App{},
data: &Data{},
drive: &Drive{},
fs: (&Fs{}).New("/"),
config: (&Config{}).New(),
error: &ErrorPanic{},
log: &ErrorLog{},
lock: &Lock{},
ipc: &Ipc{},
info: systemInfo,
i18n: &I18n{},
services: &serviceRegistry{services: make(map[string]*Service)},
commands: &commandRegistry{commands: make(map[string]*Command)},
}
c.context, c.cancel = context.WithCancel(context.Background())
// Core services
CliRegister(c)
for _, opt := range opts {
if r := opt(c); !r.OK {
Error("core.New failed", "err", r.Value)
break
}
}
// Apply service lock after all opts — v0.3.3 parity
c.LockApply()
return c
}
// WithOptions applies key-value configuration to Core.
//
// core.WithOptions(core.NewOptions(core.Option{Key: "name", Value: "myapp"}))
func WithOptions(opts Options) CoreOption {
return func(c *Core) Result {
c.options = &opts
if name := opts.String("name"); name != "" {
c.app.Name = name
}
return Result{OK: true}
}
}
// WithService registers a service via its factory function.
// If the factory returns a non-nil Value, WithService auto-discovers the
// service name from the factory's package path (last path segment, lowercase,
// with any "_test" suffix stripped) and calls RegisterService on the instance.
// IPC handler auto-registration is handled by RegisterService.
//
// If the factory returns nil Value (it registered itself), WithService
// returns success without a second registration.
//
// core.WithService(agentic.Register)
// core.WithService(display.Register(nil))
func WithService(factory func(*Core) Result) CoreOption {
return func(c *Core) Result {
r := factory(c)
if !r.OK {
return r
}
if r.Value == nil {
// Factory self-registered — nothing more to do.
return Result{OK: true}
}
// Auto-discover the service name from the instance's package path.
instance := r.Value
typeOf := reflect.TypeOf(instance)
if typeOf.Kind() == reflect.Ptr {
typeOf = typeOf.Elem()
}
pkgPath := typeOf.PkgPath()
parts := Split(pkgPath, "/")
name := Lower(parts[len(parts)-1])
if name == "" {
return Result{E("core.WithService", Sprintf("service name could not be discovered for type %T", instance), nil), false}
}
// RegisterService handles Startable/Stoppable/HandleIPCEvents discovery
return c.RegisterService(name, instance)
}
}
// WithName registers a service with an explicit name (no reflect discovery).
//
// core.WithName("ws", func(c *Core) Result {
// return Result{Value: hub, OK: true}
// })
func WithName(name string, factory func(*Core) Result) CoreOption {
return func(c *Core) Result {
r := factory(c)
if !r.OK {
return r
}
if r.Value == nil {
return Result{E("core.WithName", Sprintf("failed to create service %q", name), nil), false}
}
return c.RegisterService(name, r.Value)
}
}
// WithOption is a convenience for setting a single key-value option.
//
// core.New(
// core.WithOption("name", "myapp"),
// core.WithOption("port", 8080),
// )
func WithOption(key string, value any) CoreOption {
return func(c *Core) Result {
if c.options == nil {
opts := NewOptions()
c.options = &opts
}
c.options.Set(key, value)
if key == "name" {
if s, ok := value.(string); ok {
c.app.Name = s
}
}
return Result{OK: true}
}
}
// WithServiceLock prevents further service registration after construction.
//
// core.New(
// core.WithService(auth.Register),
// core.WithServiceLock(),
// )
func WithServiceLock() CoreOption {
return func(c *Core) Result {
c.LockEnable()
return Result{OK: true}
}
}

133
contract_test.go Normal file
View file

@ -0,0 +1,133 @@
// SPDX-License-Identifier: EUPL-1.2
package core_test
import (
"context"
"testing"
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
// --- WithService ---
// stub service used only for name-discovery tests.
type stubNamedService struct{}
// stubFactory is a package-level factory so the runtime function name carries
// the package path "core_test.stubFactory" — last segment after '/' is
// "core_test", and after stripping a "_test" suffix we get "core".
// For a real service package such as "dappco.re/go/agentic" the discovered
// name would be "agentic".
func stubFactory(c *Core) Result {
return Result{Value: &stubNamedService{}, OK: true}
}
// TestWithService_NameDiscovery_Good verifies that WithService discovers the
// service name from the factory's package path and registers the instance via
// RegisterService, making it retrievable through c.Services().
//
// stubFactory lives in package "dappco.re/go/core_test", so the last path
// segment is "core_test" — WithService strips the "_test" suffix and registers
// the service under the name "core".
func TestWithService_NameDiscovery_Good(t *testing.T) {
c := New(WithService(stubFactory))
names := c.Services()
// Service should be auto-registered under a discovered name (not just "cli" which is built-in)
assert.Greater(t, len(names), 1, "expected auto-discovered service to be registered alongside built-in 'cli'")
}
// TestWithService_FactorySelfRegisters_Good verifies that when a factory
// returns Result{OK:true} with no Value (it registered itself), WithService
// does not attempt a second registration and returns success.
func TestWithService_FactorySelfRegisters_Good(t *testing.T) {
selfReg := func(c *Core) Result {
// Factory registers directly, returns no instance.
c.Service("self", Service{})
return Result{OK: true}
}
c := New(WithService(selfReg))
// "self" must be present and registered exactly once.
svc := c.Service("self")
assert.True(t, svc.OK, "expected self-registered service to be present")
}
// --- WithName ---
func TestWithName_Good(t *testing.T) {
c := New(
WithName("custom", func(c *Core) Result {
return Result{Value: &stubNamedService{}, OK: true}
}),
)
assert.Contains(t, c.Services(), "custom")
}
// --- Lifecycle ---
type lifecycleService struct {
started bool
}
func (s *lifecycleService) OnStartup(_ context.Context) error {
s.started = true
return nil
}
func TestWithService_Lifecycle_Good(t *testing.T) {
svc := &lifecycleService{}
c := New(
WithService(func(c *Core) Result {
return Result{Value: svc, OK: true}
}),
)
c.ServiceStartup(context.Background(), nil)
assert.True(t, svc.started)
}
// --- IPC Handler ---
type ipcService struct {
received Message
}
func (s *ipcService) HandleIPCEvents(c *Core, msg Message) Result {
s.received = msg
return Result{OK: true}
}
func TestWithService_IPCHandler_Good(t *testing.T) {
svc := &ipcService{}
c := New(
WithService(func(c *Core) Result {
return Result{Value: svc, OK: true}
}),
)
c.ACTION("ping")
assert.Equal(t, "ping", svc.received)
}
// --- Error ---
// TestWithService_FactoryError_Bad verifies that a failing factory
// stops further option processing (second service not registered).
func TestWithService_FactoryError_Bad(t *testing.T) {
secondCalled := false
c := New(
WithService(func(c *Core) Result {
return Result{Value: E("test", "factory failed", nil), OK: false}
}),
WithService(func(c *Core) Result {
secondCalled = true
return Result{OK: true}
}),
)
assert.NotNil(t, c)
assert.False(t, secondCalled, "second option should not run after first fails")
}

117
core.go Normal file
View file

@ -0,0 +1,117 @@
// SPDX-License-Identifier: EUPL-1.2
// Package core is a dependency injection and service lifecycle framework for Go.
// This file defines the Core struct, accessors, and IPC/error wrappers.
package core
import (
"context"
"os"
"sync"
"sync/atomic"
)
// --- Core Struct ---
// Core is the central application object that manages services, assets, and communication.
type Core struct {
options *Options // c.Options() — Input configuration used to create this Core
app *App // c.App() — Application identity + optional GUI runtime
data *Data // c.Data() — Embedded/stored content from packages
drive *Drive // c.Drive() — Resource handle registry (transports)
fs *Fs // c.Fs() — Local filesystem I/O (sandboxable)
config *Config // c.Config() — Configuration, settings, feature flags
error *ErrorPanic // c.Error() — Panic recovery and crash reporting
log *ErrorLog // c.Log() — Structured logging + error wrapping
// cli accessed via ServiceFor[*Cli](c, "cli")
commands *commandRegistry // c.Command("path") — Command tree
services *serviceRegistry // c.Service("name") — Service registry
lock *Lock // c.Lock("name") — Named mutexes
ipc *Ipc // c.IPC() — Message bus for IPC
info *SysInfo // c.Env("key") — Read-only system/environment information
i18n *I18n // c.I18n() — Internationalisation and locale collection
context context.Context
cancel context.CancelFunc
taskIDCounter atomic.Uint64
waitGroup sync.WaitGroup
shutdown atomic.Bool
}
// --- Accessors ---
func (c *Core) Options() *Options { return c.options }
func (c *Core) App() *App { return c.app }
func (c *Core) Data() *Data { return c.data }
func (c *Core) Drive() *Drive { return c.drive }
func (c *Core) Embed() Result { return c.data.Get("app") } // legacy — use Data()
func (c *Core) Fs() *Fs { return c.fs }
func (c *Core) Config() *Config { return c.config }
func (c *Core) Error() *ErrorPanic { return c.error }
func (c *Core) Log() *ErrorLog { return c.log }
func (c *Core) Cli() *Cli {
cl, _ := ServiceFor[*Cli](c, "cli")
return cl
}
func (c *Core) IPC() *Ipc { return c.ipc }
func (c *Core) I18n() *I18n { return c.i18n }
func (c *Core) Env(key string) string { return Env(key) }
func (c *Core) Context() context.Context { return c.context }
func (c *Core) Core() *Core { return c }
// --- Lifecycle ---
// Run starts all services, runs the CLI, then shuts down.
// This is the standard application lifecycle for CLI apps.
//
// c := core.New(core.WithService(myService.Register)).Value.(*Core)
// c.Run()
func (c *Core) Run() {
r := c.ServiceStartup(c.context, nil)
if !r.OK {
if err, ok := r.Value.(error); ok {
Error(err.Error())
}
os.Exit(1)
}
if cli := c.Cli(); cli != nil {
r = cli.Run()
}
c.ServiceShutdown(context.Background())
if !r.OK {
if err, ok := r.Value.(error); ok {
Error(err.Error())
}
os.Exit(1)
}
}
// --- IPC (uppercase aliases) ---
func (c *Core) ACTION(msg Message) Result { return c.Action(msg) }
func (c *Core) QUERY(q Query) Result { return c.Query(q) }
func (c *Core) QUERYALL(q Query) Result { return c.QueryAll(q) }
func (c *Core) PERFORM(t Task) Result { return c.Perform(t) }
// --- Error+Log ---
// LogError logs an error and returns the Result from ErrorLog.
func (c *Core) LogError(err error, op, msg string) Result {
return c.log.Error(err, op, msg)
}
// LogWarn logs a warning and returns the Result from ErrorLog.
func (c *Core) LogWarn(err error, op, msg string) Result {
return c.log.Warn(err, op, msg)
}
// Must logs and panics if err is not nil.
func (c *Core) Must(err error, op, msg string) {
c.log.Must(err, op, msg)
}
// --- Global Instance ---

145
core_test.go Normal file
View file

@ -0,0 +1,145 @@
package core_test
import (
"context"
"testing"
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
// --- New ---
func TestNew_Good(t *testing.T) {
c := New()
assert.NotNil(t, c)
}
func TestNew_WithOptions_Good(t *testing.T) {
c := New(WithOptions(NewOptions(Option{Key: "name", Value: "myapp"})))
assert.NotNil(t, c)
assert.Equal(t, "myapp", c.App().Name)
}
func TestNew_WithOptions_Bad(t *testing.T) {
// Empty options — should still create a valid Core
c := New(WithOptions(NewOptions()))
assert.NotNil(t, c)
}
func TestNew_WithService_Good(t *testing.T) {
started := false
c := New(
WithOptions(NewOptions(Option{Key: "name", Value: "myapp"})),
WithService(func(c *Core) Result {
c.Service("test", Service{
OnStart: func() Result { started = true; return Result{OK: true} },
})
return Result{OK: true}
}),
)
svc := c.Service("test")
assert.True(t, svc.OK)
c.ServiceStartup(context.Background(), nil)
assert.True(t, started)
}
func TestNew_WithServiceLock_Good(t *testing.T) {
c := New(
WithService(func(c *Core) Result {
c.Service("allowed", Service{})
return Result{OK: true}
}),
WithServiceLock(),
)
// Registration after lock should fail
reg := c.Service("blocked", Service{})
assert.False(t, reg.OK)
}
func TestNew_WithService_Bad_FailingOption(t *testing.T) {
secondCalled := false
_ = New(
WithService(func(c *Core) Result {
return Result{Value: E("test", "intentional failure", nil), OK: false}
}),
WithService(func(c *Core) Result {
secondCalled = true
return Result{OK: true}
}),
)
assert.False(t, secondCalled, "second option should not run after first fails")
}
// --- Accessors ---
func TestAccessors_Good(t *testing.T) {
c := New()
assert.NotNil(t, c.App())
assert.NotNil(t, c.Data())
assert.NotNil(t, c.Drive())
assert.NotNil(t, c.Fs())
assert.NotNil(t, c.Config())
assert.NotNil(t, c.Error())
assert.NotNil(t, c.Log())
assert.NotNil(t, c.Cli())
assert.NotNil(t, c.IPC())
assert.NotNil(t, c.I18n())
assert.Equal(t, c, c.Core())
}
func TestOptions_Accessor_Good(t *testing.T) {
c := New(WithOptions(NewOptions(
Option{Key: "name", Value: "testapp"},
Option{Key: "port", Value: 8080},
Option{Key: "debug", Value: true},
)))
opts := c.Options()
assert.NotNil(t, opts)
assert.Equal(t, "testapp", opts.String("name"))
assert.Equal(t, 8080, opts.Int("port"))
assert.True(t, opts.Bool("debug"))
}
func TestOptions_Accessor_Nil(t *testing.T) {
c := New()
// No options passed — Options() returns nil
assert.Nil(t, c.Options())
}
// --- Core Error/Log Helpers ---
func TestCore_LogError_Good(t *testing.T) {
c := New()
cause := assert.AnError
r := c.LogError(cause, "test.Operation", "something broke")
err, ok := r.Value.(error)
assert.True(t, ok)
assert.ErrorIs(t, err, cause)
}
func TestCore_LogWarn_Good(t *testing.T) {
c := New()
r := c.LogWarn(assert.AnError, "test.Operation", "heads up")
_, ok := r.Value.(error)
assert.True(t, ok)
}
func TestCore_Must_Ugly(t *testing.T) {
c := New()
assert.Panics(t, func() {
c.Must(assert.AnError, "test.Operation", "fatal")
})
}
func TestCore_Must_Nil_Good(t *testing.T) {
c := New()
assert.NotPanics(t, func() {
c.Must(nil, "test.Operation", "no error")
})
}

202
data.go Normal file
View file

@ -0,0 +1,202 @@
// SPDX-License-Identifier: EUPL-1.2
// Data is the embedded/stored content system for core packages.
// Packages mount their embedded content here and other packages
// read from it by path.
//
// Mount a package's assets:
//
// c.Data().New(core.NewOptions(
// core.Option{Key: "name", Value: "brain"},
// core.Option{Key: "source", Value: brainFS},
// core.Option{Key: "path", Value: "prompts"},
// ))
//
// Read from any mounted path:
//
// content := c.Data().ReadString("brain/coding.md")
// entries := c.Data().List("agent/flow")
//
// Extract a template directory:
//
// c.Data().Extract("agent/workspace/default", "/tmp/ws", data)
package core
import (
"io/fs"
"path/filepath"
"sync"
)
// Data manages mounted embedded filesystems from core packages.
type Data struct {
mounts map[string]*Embed
mu sync.RWMutex
}
// New registers an embedded filesystem under a named prefix.
//
// c.Data().New(core.NewOptions(
// core.Option{Key: "name", Value: "brain"},
// core.Option{Key: "source", Value: brainFS},
// core.Option{Key: "path", Value: "prompts"},
// ))
func (d *Data) New(opts Options) Result {
name := opts.String("name")
if name == "" {
return Result{}
}
r := opts.Get("source")
if !r.OK {
return r
}
fsys, ok := r.Value.(fs.FS)
if !ok {
return Result{E("data.New", "source is not fs.FS", nil), false}
}
path := opts.String("path")
if path == "" {
path = "."
}
d.mu.Lock()
defer d.mu.Unlock()
if d.mounts == nil {
d.mounts = make(map[string]*Embed)
}
mr := Mount(fsys, path)
if !mr.OK {
return mr
}
emb := mr.Value.(*Embed)
d.mounts[name] = emb
return Result{emb, true}
}
// Get returns the Embed for a named mount point.
//
// r := c.Data().Get("brain")
// if r.OK { emb := r.Value.(*Embed) }
func (d *Data) Get(name string) Result {
d.mu.RLock()
defer d.mu.RUnlock()
if d.mounts == nil {
return Result{}
}
emb, ok := d.mounts[name]
if !ok {
return Result{}
}
return Result{emb, true}
}
// resolve splits a path like "brain/coding.md" into mount name + relative path.
func (d *Data) resolve(path string) (*Embed, string) {
d.mu.RLock()
defer d.mu.RUnlock()
parts := SplitN(path, "/", 2)
if len(parts) < 2 {
return nil, ""
}
if d.mounts == nil {
return nil, ""
}
emb := d.mounts[parts[0]]
return emb, parts[1]
}
// ReadFile reads a file by full path.
//
// r := c.Data().ReadFile("brain/prompts/coding.md")
// if r.OK { data := r.Value.([]byte) }
func (d *Data) ReadFile(path string) Result {
emb, rel := d.resolve(path)
if emb == nil {
return Result{}
}
return emb.ReadFile(rel)
}
// ReadString reads a file as a string.
//
// r := c.Data().ReadString("agent/flow/deploy/to/homelab.yaml")
// if r.OK { content := r.Value.(string) }
func (d *Data) ReadString(path string) Result {
r := d.ReadFile(path)
if !r.OK {
return r
}
return Result{string(r.Value.([]byte)), true}
}
// List returns directory entries at a path.
//
// r := c.Data().List("agent/persona/code")
// if r.OK { entries := r.Value.([]fs.DirEntry) }
func (d *Data) List(path string) Result {
emb, rel := d.resolve(path)
if emb == nil {
return Result{}
}
r := emb.ReadDir(rel)
if !r.OK {
return r
}
return Result{r.Value, true}
}
// ListNames returns filenames (without extensions) at a path.
//
// r := c.Data().ListNames("agent/flow")
// if r.OK { names := r.Value.([]string) }
func (d *Data) ListNames(path string) Result {
r := d.List(path)
if !r.OK {
return r
}
entries := r.Value.([]fs.DirEntry)
var names []string
for _, e := range entries {
name := e.Name()
if !e.IsDir() {
name = TrimSuffix(name, filepath.Ext(name))
}
names = append(names, name)
}
return Result{names, true}
}
// Extract copies a template directory to targetDir.
//
// r := c.Data().Extract("agent/workspace/default", "/tmp/ws", templateData)
func (d *Data) Extract(path, targetDir string, templateData any) Result {
emb, rel := d.resolve(path)
if emb == nil {
return Result{}
}
r := emb.Sub(rel)
if !r.OK {
return r
}
return Extract(r.Value.(*Embed).FS(), targetDir, templateData)
}
// Mounts returns the names of all mounted content.
//
// names := c.Data().Mounts()
func (d *Data) Mounts() []string {
d.mu.RLock()
defer d.mu.RUnlock()
var names []string
for k := range d.mounts {
names = append(names, k)
}
return names
}

141
data_test.go Normal file
View file

@ -0,0 +1,141 @@
package core_test
import (
"embed"
"io"
"testing"
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
//go:embed testdata
var testFS embed.FS
// --- Data (Embedded Content Mounts) ---
func mountTestData(t *testing.T, c *Core, name string) {
t.Helper()
r := c.Data().New(NewOptions(
Option{Key: "name", Value: name},
Option{Key: "source", Value: testFS},
Option{Key: "path", Value: "testdata"},
))
assert.True(t, r.OK)
}
func TestData_New_Good(t *testing.T) {
c := New()
r := c.Data().New(NewOptions(
Option{Key: "name", Value: "test"},
Option{Key: "source", Value: testFS},
Option{Key: "path", Value: "testdata"},
))
assert.True(t, r.OK)
assert.NotNil(t, r.Value)
}
func TestData_New_Bad(t *testing.T) {
c := New()
r := c.Data().New(NewOptions(Option{Key: "source", Value: testFS}))
assert.False(t, r.OK)
r = c.Data().New(NewOptions(Option{Key: "name", Value: "test"}))
assert.False(t, r.OK)
r = c.Data().New(NewOptions(Option{Key: "name", Value: "test"}, Option{Key: "source", Value: "not-an-fs"}))
assert.False(t, r.OK)
}
func TestData_ReadString_Good(t *testing.T) {
c := New()
mountTestData(t, c, "app")
r := c.Data().ReadString("app/test.txt")
assert.True(t, r.OK)
assert.Equal(t, "hello from testdata\n", r.Value.(string))
}
func TestData_ReadString_Bad(t *testing.T) {
c := New()
r := c.Data().ReadString("nonexistent/file.txt")
assert.False(t, r.OK)
}
func TestData_ReadFile_Good(t *testing.T) {
c := New()
mountTestData(t, c, "app")
r := c.Data().ReadFile("app/test.txt")
assert.True(t, r.OK)
assert.Equal(t, "hello from testdata\n", string(r.Value.([]byte)))
}
func TestData_Get_Good(t *testing.T) {
c := New()
mountTestData(t, c, "brain")
gr := c.Data().Get("brain")
assert.True(t, gr.OK)
emb := gr.Value.(*Embed)
r := emb.Open("test.txt")
assert.True(t, r.OK)
file := r.Value.(io.ReadCloser)
defer file.Close()
content, _ := io.ReadAll(file)
assert.Equal(t, "hello from testdata\n", string(content))
}
func TestData_Get_Bad(t *testing.T) {
c := New()
r := c.Data().Get("nonexistent")
assert.False(t, r.OK)
}
func TestData_Mounts_Good(t *testing.T) {
c := New()
mountTestData(t, c, "a")
mountTestData(t, c, "b")
mounts := c.Data().Mounts()
assert.Len(t, mounts, 2)
}
func TestEmbed_Legacy_Good(t *testing.T) {
c := New()
mountTestData(t, c, "app")
assert.NotNil(t, c.Embed())
}
func TestData_List_Good(t *testing.T) {
c := New()
mountTestData(t, c, "app")
r := c.Data().List("app/.")
assert.True(t, r.OK)
}
func TestData_List_Bad(t *testing.T) {
c := New()
r := c.Data().List("nonexistent/path")
assert.False(t, r.OK)
}
func TestData_ListNames_Good(t *testing.T) {
c := New()
mountTestData(t, c, "app")
r := c.Data().ListNames("app/.")
assert.True(t, r.OK)
assert.Contains(t, r.Value.([]string), "test")
}
func TestData_Extract_Good(t *testing.T) {
c := New()
mountTestData(t, c, "app")
r := c.Data().Extract("app/.", t.TempDir(), nil)
assert.True(t, r.OK)
}
func TestData_Extract_Bad(t *testing.T) {
c := New()
r := c.Data().Extract("nonexistent/path", t.TempDir(), nil)
assert.False(t, r.OK)
}

View file

@ -1,100 +0,0 @@
# AI Examples
## Workflow Example
Complete task management workflow:
```bash
# 1. List available tasks
core ai tasks --status pending
# 2. Auto-select and claim a task
core ai task --auto --claim
# 3. Work on the task...
# 4. Update progress
core ai task:update abc123 --progress 75
# 5. Commit with task reference
core ai task:commit abc123 -m 'implement feature'
# 6. Create PR
core ai task:pr abc123
# 7. Mark complete
core ai task:complete abc123 --output 'Feature implemented and PR created'
```
## Task Filtering
```bash
# By status
core ai tasks --status pending
core ai tasks --status in_progress
# By priority
core ai tasks --priority critical
core ai tasks --priority high
# By labels
core ai tasks --labels bug,urgent
# Combined filters
core ai tasks --status pending --priority high --labels bug
```
## Task Updates
```bash
# Change status
core ai task:update abc123 --status in_progress
core ai task:update abc123 --status blocked
# Update progress
core ai task:update abc123 --progress 25
core ai task:update abc123 --progress 50 --notes 'Halfway done'
core ai task:update abc123 --progress 100
```
## Git Integration
```bash
# Commit with task reference
core ai task:commit abc123 -m 'add authentication'
# With scope
core ai task:commit abc123 -m 'fix login' --scope auth
# Commit and push
core ai task:commit abc123 -m 'complete feature' --push
# Create PR
core ai task:pr abc123
# Draft PR
core ai task:pr abc123 --draft
# PR with labels
core ai task:pr abc123 --labels 'enhancement,ready-for-review'
# PR to different base
core ai task:pr abc123 --base develop
```
## Configuration
### Environment Variables
```env
AGENTIC_TOKEN=your-api-token
AGENTIC_BASE_URL=https://agentic.example.com
```
### ~/.core/agentic.yaml
```yaml
token: your-api-token
base_url: https://agentic.example.com
default_project: my-project
```

View file

@ -1,262 +0,0 @@
# core ai
AI agent task management and Claude Code integration.
## Task Management Commands
| Command | Description |
|---------|-------------|
| `tasks` | List available tasks from core-agentic |
| `task` | View task details or auto-select |
| `task:update` | Update task status or progress |
| `task:complete` | Mark task as completed or failed |
| `task:commit` | Create git commit with task reference |
| `task:pr` | Create GitHub PR linked to task |
## Claude Integration
| Command | Description |
|---------|-------------|
| `claude run` | Run Claude Code in current directory |
| `claude config` | Manage Claude configuration |
---
## Configuration
Task commands load configuration from:
1. Environment variables (`AGENTIC_TOKEN`, `AGENTIC_BASE_URL`)
2. `.env` file in current directory
3. `~/.core/agentic.yaml`
---
## ai tasks
List available tasks from core-agentic.
```bash
core ai tasks [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--status` | Filter by status (`pending`, `in_progress`, `completed`, `blocked`) |
| `--priority` | Filter by priority (`critical`, `high`, `medium`, `low`) |
| `--labels` | Filter by labels (comma-separated) |
| `--project` | Filter by project |
| `--limit` | Max number of tasks to return (default: 20) |
### Examples
```bash
# List all pending tasks
core ai tasks
# Filter by status and priority
core ai tasks --status pending --priority high
# Filter by labels
core ai tasks --labels bug,urgent
```
---
## ai task
View task details or auto-select a task.
```bash
core ai task [task-id] [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--auto` | Auto-select highest priority pending task |
| `--claim` | Claim the task after showing details |
| `--context` | Show gathered context for AI collaboration |
### Examples
```bash
# Show task details
core ai task abc123
# Show and claim
core ai task abc123 --claim
# Show with context
core ai task abc123 --context
# Auto-select highest priority pending task
core ai task --auto
```
---
## ai task:update
Update a task's status, progress, or notes.
```bash
core ai task:update <task-id> [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--status` | New status (`pending`, `in_progress`, `completed`, `blocked`) |
| `--progress` | Progress percentage (0-100) |
| `--notes` | Notes about the update |
### Examples
```bash
# Set task to in progress
core ai task:update abc123 --status in_progress
# Update progress with notes
core ai task:update abc123 --progress 50 --notes 'Halfway done'
```
---
## ai task:complete
Mark a task as completed with optional output and artifacts.
```bash
core ai task:complete <task-id> [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--output` | Summary of the completed work |
| `--failed` | Mark the task as failed |
| `--error` | Error message if failed |
### Examples
```bash
# Complete successfully
core ai task:complete abc123 --output 'Feature implemented'
# Mark as failed
core ai task:complete abc123 --failed --error 'Build failed'
```
---
## ai task:commit
Create a git commit with a task reference and co-author attribution.
```bash
core ai task:commit <task-id> [flags]
```
Commit message format:
```
feat(scope): description
Task: #123
Co-Authored-By: Claude <noreply@anthropic.com>
```
### Flags
| Flag | Description |
|------|-------------|
| `-m`, `--message` | Commit message (without task reference) |
| `--scope` | Scope for the commit type (e.g., `auth`, `api`, `ui`) |
| `--push` | Push changes after committing |
### Examples
```bash
# Commit with message
core ai task:commit abc123 --message 'add user authentication'
# With scope
core ai task:commit abc123 -m 'fix login bug' --scope auth
# Commit and push
core ai task:commit abc123 -m 'update docs' --push
```
---
## ai task:pr
Create a GitHub pull request linked to a task.
```bash
core ai task:pr <task-id> [flags]
```
Requires the GitHub CLI (`gh`) to be installed and authenticated.
### Flags
| Flag | Description |
|------|-------------|
| `--title` | PR title (defaults to task title) |
| `--base` | Base branch (defaults to main) |
| `--draft` | Create as draft PR |
| `--labels` | Labels to add (comma-separated) |
### Examples
```bash
# Create PR with defaults
core ai task:pr abc123
# Custom title
core ai task:pr abc123 --title 'Add authentication feature'
# Draft PR with labels
core ai task:pr abc123 --draft --labels 'enhancement,needs-review'
# Target different base branch
core ai task:pr abc123 --base develop
```
---
## ai claude
Claude Code integration commands.
### ai claude run
Run Claude Code in the current directory.
```bash
core ai claude run
```
### ai claude config
Manage Claude configuration.
```bash
core ai claude config
```
---
## Workflow Example
See [Workflow Example](example.md#workflow-example) for a complete task management workflow.
## See Also
- [dev](../dev/) - Multi-repo workflow commands
- [Claude Code documentation](https://claude.ai/code)

View file

@ -1,83 +0,0 @@
# Build Examples
## Quick Start
```bash
# Auto-detect and build
core build
# Build for specific platforms
core build --targets linux/amd64,darwin/arm64
# CI mode
core build --ci
```
## Configuration
`.core/build.yaml`:
```yaml
version: 1
project:
name: myapp
binary: myapp
build:
main: ./cmd/myapp
ldflags:
- -s -w
- -X main.version={{.Version}}
targets:
- os: linux
arch: amd64
- os: linux
arch: arm64
- os: darwin
arch: arm64
```
## Cross-Platform Build
```bash
core build --targets linux/amd64,linux/arm64,darwin/arm64,windows/amd64
```
Output:
```
dist/
├── myapp-linux-amd64.tar.gz
├── myapp-linux-arm64.tar.gz
├── myapp-darwin-arm64.tar.gz
├── myapp-windows-amd64.zip
└── CHECKSUMS.txt
```
## Code Signing
```yaml
sign:
enabled: true
gpg:
key: $GPG_KEY_ID
macos:
identity: "Developer ID Application: Your Name (TEAM_ID)"
notarize: true
apple_id: $APPLE_ID
team_id: $APPLE_TEAM_ID
app_password: $APPLE_APP_PASSWORD
```
## Docker Build
```bash
core build --type docker --image ghcr.io/myorg/myapp
```
## Wails Desktop App
```bash
core build --type wails --targets darwin/arm64,windows/amd64
```

View file

@ -1,176 +0,0 @@
# core build
Build Go, Wails, Docker, and LinuxKit projects with automatic project detection.
## Subcommands
| Command | Description |
|---------|-------------|
| [sdk](sdk/) | Generate API SDKs from OpenAPI |
| `from-path` | Build from a local directory |
| `pwa` | Build from a live PWA URL |
## Usage
```bash
core build [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--type` | Project type: `go`, `wails`, `docker`, `linuxkit`, `taskfile` (auto-detected) |
| `--targets` | Build targets: `linux/amd64,darwin/arm64,windows/amd64` |
| `--output` | Output directory (default: `dist`) |
| `--ci` | CI mode - minimal output with JSON artifact list at the end |
| `--image` | Docker image name (for docker builds) |
| `--config` | Config file path (for linuxkit: YAML config, for docker: Dockerfile) |
| `--format` | Output format for linuxkit (iso-bios, qcow2-bios, raw, vmdk) |
| `--push` | Push Docker image after build (default: false) |
| `--archive` | Create archives (tar.gz for linux/darwin, zip for windows) - default: true |
| `--checksum` | Generate SHA256 checksums and CHECKSUMS.txt - default: true |
| `--no-sign` | Skip all code signing |
| `--notarize` | Enable macOS notarization (requires Apple credentials) |
## Examples
### Go Project
```bash
# Auto-detect and build
core build
# Build for specific platforms
core build --targets linux/amd64,linux/arm64,darwin/arm64
# CI mode
core build --ci
```
### Wails Project
```bash
# Build Wails desktop app
core build --type wails
# Build for all desktop platforms
core build --type wails --targets darwin/amd64,darwin/arm64,windows/amd64,linux/amd64
```
### Docker Image
```bash
# Build Docker image
core build --type docker
# With custom image name
core build --type docker --image ghcr.io/myorg/myapp
# Build and push to registry
core build --type docker --image ghcr.io/myorg/myapp --push
```
### LinuxKit Image
```bash
# Build LinuxKit ISO
core build --type linuxkit
# Build with specific format
core build --type linuxkit --config linuxkit.yml --format qcow2-bios
```
## Project Detection
Core automatically detects project type based on files:
| Files | Type |
|-------|------|
| `wails.json` | Wails |
| `go.mod` | Go |
| `Dockerfile` | Docker |
| `Taskfile.yml` | Taskfile |
| `composer.json` | PHP |
| `package.json` | Node |
## Output
Build artifacts are placed in `dist/` by default:
```
dist/
├── myapp-linux-amd64.tar.gz
├── myapp-linux-arm64.tar.gz
├── myapp-darwin-amd64.tar.gz
├── myapp-darwin-arm64.tar.gz
├── myapp-windows-amd64.zip
└── CHECKSUMS.txt
```
## Configuration
Optional `.core/build.yaml` - see [Configuration](example.md#configuration) for examples.
## Code Signing
Core supports GPG signing for checksums and native code signing for macOS.
### GPG Signing
Signs `CHECKSUMS.txt` with a detached ASCII signature (`.asc`):
```bash
# Build with GPG signing (default if key configured)
core build
# Skip signing
core build --no-sign
```
Users can verify:
```bash
gpg --verify CHECKSUMS.txt.asc CHECKSUMS.txt
sha256sum -c CHECKSUMS.txt
```
### macOS Code Signing
Signs Darwin binaries with your Developer ID and optionally notarizes with Apple:
```bash
# Build with codesign (automatic if identity configured)
core build
# Build with notarization (takes 1-5 minutes)
core build --notarize
```
### Environment Variables
| Variable | Purpose |
|----------|---------|
| `GPG_KEY_ID` | GPG key ID or fingerprint |
| `CODESIGN_IDENTITY` | macOS Developer ID (fallback) |
| `APPLE_ID` | Apple account email |
| `APPLE_TEAM_ID` | Apple Developer Team ID |
| `APPLE_APP_PASSWORD` | App-specific password for notarization |
## Building from PWAs and Static Sites
### Build from Local Directory
Build a desktop app from static web application files:
```bash
core build from-path --path ./dist
```
### Build from Live PWA
Build a desktop app from a live Progressive Web App URL:
```bash
core build pwa --url https://example.com
```

View file

@ -1,56 +0,0 @@
# SDK Build Examples
## Generate All SDKs
```bash
core build sdk
```
## Specific Language
```bash
core build sdk --lang typescript
core build sdk --lang php
core build sdk --lang go
```
## Custom Spec
```bash
core build sdk --spec ./api/openapi.yaml
```
## With Version
```bash
core build sdk --version v2.0.0
```
## Preview
```bash
core build sdk --dry-run
```
## Configuration
`.core/sdk.yaml`:
```yaml
version: 1
spec: ./api/openapi.yaml
languages:
- name: typescript
output: sdk/typescript
package: "@myorg/api-client"
- name: php
output: sdk/php
namespace: MyOrg\ApiClient
- name: go
output: sdk/go
module: github.com/myorg/api-client-go
```

View file

@ -1,27 +0,0 @@
# core build sdk
Generate typed API clients from OpenAPI specifications. Supports TypeScript, Python, Go, and PHP.
## Usage
```bash
core build sdk [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--spec` | Path to OpenAPI spec file |
| `--lang` | Generate only this language (typescript, python, go, php) |
| `--version` | Version to embed in generated SDKs |
| `--dry-run` | Show what would be generated without writing files |
## Examples
```bash
core build sdk # Generate all
core build sdk --lang typescript # TypeScript only
core build sdk --spec ./api.yaml # Custom spec
core build sdk --dry-run # Preview
```

View file

@ -1,36 +0,0 @@
# CI Changelog Examples
```bash
core ci changelog
```
## Output
```markdown
## v1.2.0
### Features
- Add user authentication (#123)
- Support dark mode (#124)
### Bug Fixes
- Fix memory leak in worker (#125)
### Performance
- Optimize database queries (#126)
```
## Configuration
`.core/release.yaml`:
```yaml
changelog:
include:
- feat
- fix
- perf
exclude:
- chore
- docs
```

View file

@ -1,28 +0,0 @@
# core ci changelog
Generate changelog from conventional commits.
## Usage
```bash
core ci changelog
```
## Output
Generates markdown changelog from git commits since last tag:
```markdown
## v1.2.0
### Features
- Add user authentication (#123)
- Support dark mode (#124)
### Bug Fixes
- Fix memory leak in worker (#125)
```
## Configuration
See [configuration.md](../../../configuration.md) for changelog configuration options.

View file

@ -1,90 +0,0 @@
# CI Examples
## Quick Start
```bash
# Build first
core build
# Preview release
core ci
# Publish
core ci --we-are-go-for-launch
```
## Configuration
`.core/release.yaml`:
```yaml
version: 1
project:
name: myapp
repository: host-uk/myapp
publishers:
- type: github
```
## Publisher Examples
### GitHub + Docker
```yaml
publishers:
- type: github
- type: docker
registry: ghcr.io
image: host-uk/myapp
platforms:
- linux/amd64
- linux/arm64
tags:
- latest
- "{{.Version}}"
```
### Full Stack (GitHub + npm + Homebrew)
```yaml
publishers:
- type: github
- type: npm
package: "@host-uk/myapp"
access: public
- type: homebrew
tap: host-uk/homebrew-tap
```
### LinuxKit Image
```yaml
publishers:
- type: linuxkit
config: .core/linuxkit/server.yml
formats:
- iso
- qcow2
platforms:
- linux/amd64
- linux/arm64
```
## Changelog Configuration
```yaml
changelog:
include:
- feat
- fix
- perf
exclude:
- chore
- docs
- test
```

View file

@ -1,79 +0,0 @@
# core ci
Publish releases to GitHub, Docker, npm, Homebrew, and more.
**Safety:** Dry-run by default. Use `--we-are-go-for-launch` to actually publish.
## Subcommands
| Command | Description |
|---------|-------------|
| [init](init/) | Initialize release config |
| [changelog](changelog/) | Generate changelog |
| [version](version/) | Show determined version |
## Usage
```bash
core ci [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--we-are-go-for-launch` | Actually publish (default is dry-run) |
| `--version` | Override version |
| `--draft` | Create as draft release |
| `--prerelease` | Mark as prerelease |
## Examples
```bash
# Preview what would be published (safe)
core ci
# Actually publish
core ci --we-are-go-for-launch
# Publish as draft
core ci --we-are-go-for-launch --draft
# Publish as prerelease
core ci --we-are-go-for-launch --prerelease
```
## Workflow
Build and publish are **separated** to prevent accidents:
```bash
# Step 1: Build artifacts
core build
core build sdk
# Step 2: Preview (dry-run by default)
core ci
# Step 3: Publish (explicit flag required)
core ci --we-are-go-for-launch
```
## Publishers
See [Publisher Examples](example.md#publisher-examples) for configuration.
| Type | Target |
|------|--------|
| `github` | GitHub Releases |
| `docker` | Container registries |
| `linuxkit` | LinuxKit images |
| `npm` | npm registry |
| `homebrew` | Homebrew tap |
| `scoop` | Scoop bucket |
| `aur` | Arch User Repository |
| `chocolatey` | Chocolatey |
## Changelog
Auto-generated from conventional commits. See [Changelog Configuration](example.md#changelog-configuration).

View file

@ -1,17 +0,0 @@
# CI Init Examples
```bash
core ci init
```
Creates `.core/release.yaml`:
```yaml
version: 1
project:
name: myapp
publishers:
- type: github
```

View file

@ -1,11 +0,0 @@
# core ci init
Initialize release configuration.
## Usage
```bash
core ci init
```
Creates `.core/release.yaml` with default configuration. See [Configuration](../example.md#configuration) for output format.

View file

@ -1,18 +0,0 @@
# CI Version Examples
```bash
core ci version
```
## Output
```
v1.2.0
```
## Version Resolution
1. `--version` flag (if provided)
2. Git tag on HEAD
3. Latest git tag + increment
4. `v0.0.1` (no tags)

View file

@ -1,21 +0,0 @@
# core ci version
Show the determined release version.
## Usage
```bash
core ci version
```
## Output
```
v1.2.0
```
Version is determined from:
1. `--version` flag (if provided)
2. Git tag on HEAD
3. Latest git tag + increment
4. `v0.0.1` (if no tags exist)

View file

@ -1,61 +0,0 @@
# core dev ci
Check CI status across all repositories.
Fetches GitHub Actions workflow status for all repos. Shows latest run status for each repo. Requires the `gh` CLI to be installed and authenticated.
## Usage
```bash
core dev ci [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
| `--branch` | Filter by branch (default: main) |
| `--failed` | Show only failed runs |
## Examples
```bash
# Check CI status for all repos
core dev ci
# Check specific branch
core dev ci --branch develop
# Show only failures
core dev ci --failed
```
## Output
```
core-php ✓ passing 2m ago
core-tenant ✓ passing 5m ago
core-admin ✗ failed 12m ago
core-api ⏳ running now
core-bio ✓ passing 1h ago
```
## Status Icons
| Symbol | Meaning |
|--------|---------|
| `✓` | Passing |
| `✗` | Failed |
| `⏳` | Running |
| `-` | No runs |
## Requirements
- GitHub CLI (`gh`) must be installed
- Must be authenticated: `gh auth login`
## See Also
- [issues command](../issues/) - List open issues
- [reviews command](../reviews/) - List PRs needing review

View file

@ -1,46 +0,0 @@
# core dev commit
Claude-assisted commits across repositories.
Uses Claude to create commits for dirty repos. Shows uncommitted changes and invokes Claude to generate commit messages.
## Usage
```bash
core dev commit [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
| `--all` | Commit all dirty repos without prompting |
## Examples
```bash
# Interactive commit (prompts for each repo)
core dev commit
# Commit all dirty repos automatically
core dev commit --all
# Use specific registry
core dev commit --registry ~/projects/repos.yaml
```
## How It Works
1. Scans all repositories for uncommitted changes
2. For each dirty repo:
- Shows the diff
- Invokes Claude to generate a commit message
- Creates the commit with `Co-Authored-By: Claude`
3. Reports success/failure for each repo
## See Also
- [health command](../health/) - Check repo status
- [push command](../push/) - Push commits after committing
- [work command](../work/) - Full workflow (status + commit + push)

View file

@ -1,203 +0,0 @@
# Dev Examples
## Multi-Repo Workflow
```bash
# Quick status
core dev health
# Detailed breakdown
core dev health --verbose
# Full workflow
core dev work
# Status only
core dev work --status
# Commit and push
core dev work --commit
# Commit dirty repos
core dev commit
# Commit all without prompting
core dev commit --all
# Push unpushed
core dev push
# Push without confirmation
core dev push --force
# Pull behind repos
core dev pull
# Pull all repos
core dev pull --all
```
## GitHub Integration
```bash
# Open issues
core dev issues
# Filter by assignee
core dev issues --assignee @me
# Limit results
core dev issues --limit 5
# PRs needing review
core dev reviews
# All PRs including drafts
core dev reviews --all
# Filter by author
core dev reviews --author username
# CI status
core dev ci
# Only failed runs
core dev ci --failed
# Specific branch
core dev ci --branch develop
```
## Dependency Analysis
```bash
# What depends on core-php?
core dev impact core-php
```
## Task Management
```bash
# List tasks
core ai tasks
# Filter by status and priority
core ai tasks --status pending --priority high
# Filter by labels
core ai tasks --labels bug,urgent
# Show task details
core ai task abc123
# Auto-select highest priority task
core ai task --auto
# Claim a task
core ai task abc123 --claim
# Update task status
core ai task:update abc123 --status in_progress
# Add progress notes
core ai task:update abc123 --progress 50 --notes 'Halfway done'
# Complete a task
core ai task:complete abc123 --output 'Feature implemented'
# Mark as failed
core ai task:complete abc123 --failed --error 'Build failed'
# Commit with task reference
core ai task:commit abc123 -m 'add user authentication'
# Commit with scope and push
core ai task:commit abc123 -m 'fix login bug' --scope auth --push
# Create PR for task
core ai task:pr abc123
# Create draft PR with labels
core ai task:pr abc123 --draft --labels 'enhancement,needs-review'
```
## Service API Management
```bash
# Synchronize public service APIs
core dev sync
# Or using the api command
core dev api sync
```
## Dev Environment
```bash
# First time setup
core dev install
core dev boot
# Open shell
core dev shell
# Mount and serve
core dev serve
# Run tests
core dev test
# Sandboxed Claude
core dev claude
```
## Configuration
### repos.yaml
```yaml
org: host-uk
repos:
core-php:
type: package
description: Foundation framework
core-tenant:
type: package
depends: [core-php]
```
### ~/.core/config.yaml
```yaml
version: 1
images:
source: auto # auto | github | registry | cdn
cdn:
url: https://images.example.com/core-devops
github:
repo: host-uk/core-images
registry:
image: ghcr.io/host-uk/core-devops
```
### .core/test.yaml
```yaml
version: 1
commands:
- name: unit
run: vendor/bin/pest --parallel
- name: types
run: vendor/bin/phpstan analyse
- name: lint
run: vendor/bin/pint --test
env:
APP_ENV: testing
DB_CONNECTION: sqlite
```

View file

@ -1,52 +0,0 @@
# core dev health
Quick health check across all repositories.
Shows a summary of repository health: total repos, dirty repos, unpushed commits, etc.
## Usage
```bash
core dev health [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
| `--verbose` | Show detailed breakdown |
## Examples
```bash
# Quick health summary
core dev health
# Detailed breakdown
core dev health --verbose
# Use specific registry
core dev health --registry ~/projects/repos.yaml
```
## Output
```
18 repos │ 2 dirty │ 1 ahead │ all synced
```
With `--verbose`:
```
Repos: 18
Dirty: 2 (core-php, core-admin)
Ahead: 1 (core-tenant)
Behind: 0
Synced: ✓
```
## See Also
- [work command](../work/) - Full workflow (status + commit + push)
- [commit command](../commit/) - Claude-assisted commits

View file

@ -1,65 +0,0 @@
# core dev impact
Show impact of changing a repository.
Analyses the dependency graph to show which repos would be affected by changes to the specified repo.
## Usage
```bash
core dev impact <repo-name> [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
## Examples
```bash
# Show what depends on core-php
core dev impact core-php
# Show what depends on core-tenant
core dev impact core-tenant
```
## Output
```
Impact of changes to core-php:
Direct dependents (5):
core-tenant
core-admin
core-api
core-mcp
core-commerce
Indirect dependents (12):
core-bio (via core-tenant)
core-social (via core-tenant)
core-analytics (via core-tenant)
core-notify (via core-tenant)
core-trust (via core-tenant)
core-support (via core-tenant)
core-content (via core-tenant)
core-developer (via core-tenant)
core-agentic (via core-mcp)
...
Total: 17 repos affected
```
## Use Cases
- Before making breaking changes, see what needs updating
- Plan release order based on dependency graph
- Understand the ripple effect of changes
## See Also
- [health command](../health/) - Quick repo status
- [setup command](../../setup/) - Clone repos with dependencies

View file

@ -1,388 +0,0 @@
# core dev
Multi-repo workflow and portable development environment.
## Multi-Repo Commands
| Command | Description |
|---------|-------------|
| [work](work/) | Full workflow: status + commit + push |
| `health` | Quick health check across repos |
| `commit` | Claude-assisted commits |
| `push` | Push repos with unpushed commits |
| `pull` | Pull repos that are behind |
| `issues` | List open issues |
| `reviews` | List PRs needing review |
| `ci` | Check CI status |
| `impact` | Show dependency impact |
| `api` | Tools for managing service APIs |
| `sync` | Synchronize public service APIs |
## Task Management Commands
> **Note:** Task management commands have moved to [`core ai`](../ai/).
| Command | Description |
|---------|-------------|
| [`ai tasks`](../ai/) | List available tasks from core-agentic |
| [`ai task`](../ai/) | Show task details or auto-select a task |
| [`ai task:update`](../ai/) | Update task status or progress |
| [`ai task:complete`](../ai/) | Mark a task as completed |
| [`ai task:commit`](../ai/) | Auto-commit changes with task reference |
| [`ai task:pr`](../ai/) | Create a pull request for a task |
## Dev Environment Commands
| Command | Description |
|---------|-------------|
| `install` | Download the core-devops image |
| `boot` | Start the environment |
| `stop` | Stop the environment |
| `status` | Show status |
| `shell` | Open shell |
| `serve` | Start dev server |
| `test` | Run tests |
| `claude` | Sandboxed Claude |
| `update` | Update image |
---
## Dev Environment Overview
Core DevOps provides a sandboxed, immutable development environment based on LinuxKit with 100+ embedded tools.
## Quick Start
```bash
# First time setup
core dev install
core dev boot
# Open shell
core dev shell
# Or mount current project and serve
core dev serve
```
## dev install
Download the core-devops image for your platform.
```bash
core dev install
```
Downloads the platform-specific dev environment image including Go, PHP, Node.js, Python, Docker, and Claude CLI. Downloads are cached at `~/.core/images/`.
### Examples
```bash
# Download image (auto-detects platform)
core dev install
```
## dev boot
Start the development environment.
```bash
core dev boot [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--memory` | Memory allocation in MB (default: 4096) |
| `--cpus` | Number of CPUs (default: 2) |
| `--fresh` | Stop existing and start fresh |
### Examples
```bash
# Start with defaults
core dev boot
# More resources
core dev boot --memory 8192 --cpus 4
# Fresh start
core dev boot --fresh
```
## dev shell
Open a shell in the running environment.
```bash
core dev shell [flags] [-- command]
```
Uses SSH by default, or serial console with `--console`.
### Flags
| Flag | Description |
|------|-------------|
| `--console` | Use serial console instead of SSH |
### Examples
```bash
# SSH into environment
core dev shell
# Serial console (for debugging)
core dev shell --console
# Run a command
core dev shell -- ls -la
```
## dev serve
Mount current directory and start the appropriate dev server.
```bash
core dev serve [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--port` | Port to expose (default: 8000) |
| `--path` | Subdirectory to serve |
### Auto-Detection
| Project | Server Command |
|---------|---------------|
| Laravel (`artisan`) | `php artisan octane:start` |
| Node (`package.json` with `dev` script) | `npm run dev` |
| PHP (`composer.json`) | `frankenphp php-server` |
| Other | `python -m http.server` |
### Examples
```bash
# Auto-detect and serve
core dev serve
# Custom port
core dev serve --port 3000
```
## dev test
Run tests inside the environment.
```bash
core dev test [flags] [-- custom command]
```
### Flags
| Flag | Description |
|------|-------------|
| `--name` | Run named test command from `.core/test.yaml` |
### Test Detection
Core auto-detects the test framework or uses `.core/test.yaml`:
1. `.core/test.yaml` - Custom config
2. `composer.json``composer test`
3. `package.json``npm test`
4. `go.mod``go test ./...`
5. `pytest.ini``pytest`
6. `Taskfile.yaml``task test`
### Examples
```bash
# Auto-detect and run tests
core dev test
# Run named test from config
core dev test --name integration
# Custom command
core dev test -- go test -v ./pkg/...
```
### Test Configuration
Create `.core/test.yaml` for custom test setup - see [Configuration](example.md#configuration) for examples.
## dev claude
Start a sandboxed Claude session with your project mounted.
```bash
core dev claude [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--model` | Model to use (`opus`, `sonnet`) |
| `--no-auth` | Don't forward any auth credentials |
| `--auth` | Selective auth forwarding (`gh`, `anthropic`, `ssh`, `git`) |
### What Gets Forwarded
By default, these are forwarded to the sandbox:
- `~/.anthropic/` or `ANTHROPIC_API_KEY`
- `~/.config/gh/` (GitHub CLI auth)
- SSH agent
- Git config (name, email)
### Examples
```bash
# Full auth forwarding (default)
core dev claude
# Use Opus model
core dev claude --model opus
# Clean sandbox
core dev claude --no-auth
# Only GitHub and Anthropic auth
core dev claude --auth gh,anthropic
```
### Why Use This?
- **Immutable base** - Reset anytime with `core dev boot --fresh`
- **Safe experimentation** - Claude can install packages, make mistakes
- **Host system untouched** - All changes stay in the sandbox
- **Real credentials** - Can still push code, create PRs
- **Full tooling** - 100+ tools available in the image
## dev status
Show the current state of the development environment.
```bash
core dev status
```
Output includes:
- Running/stopped state
- Resource usage (CPU, memory)
- Exposed ports
- Mounted directories
## dev update
Check for and apply updates.
```bash
core dev update [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--apply` | Download and apply the update |
### Examples
```bash
# Check for updates
core dev update
# Apply available update
core dev update --apply
```
## Embedded Tools
The core-devops image includes 100+ tools:
| Category | Tools |
|----------|-------|
| **AI/LLM** | claude, gemini, aider, ollama, llm |
| **VCS** | git, gh, glab, lazygit, delta, git-lfs |
| **Runtimes** | frankenphp, node, bun, deno, go, python3, rustc |
| **Package Mgrs** | composer, npm, pnpm, yarn, pip, uv, cargo |
| **Build** | task, make, just, nx, turbo |
| **Linting** | pint, phpstan, prettier, eslint, biome, golangci-lint, ruff |
| **Testing** | phpunit, pest, vitest, playwright, k6 |
| **Infra** | docker, kubectl, k9s, helm, terraform, ansible |
| **Databases** | sqlite3, mysql, psql, redis-cli, mongosh, usql |
| **HTTP/Net** | curl, httpie, xh, websocat, grpcurl, mkcert, ngrok |
| **Data** | jq, yq, fx, gron, miller, dasel |
| **Security** | age, sops, cosign, trivy, trufflehog, vault |
| **Files** | fd, rg, fzf, bat, eza, tree, zoxide, broot |
| **Editors** | nvim, helix, micro |
## Configuration
Global config in `~/.core/config.yaml` - see [Configuration](example.md#configuration) for examples.
## Image Storage
Images are stored in `~/.core/images/`:
```
~/.core/
├── config.yaml
└── images/
├── core-devops-darwin-arm64.qcow2
├── core-devops-linux-amd64.qcow2
└── manifest.json
```
## Multi-Repo Commands
See the [work](work/) page for detailed documentation on multi-repo commands.
### dev ci
Check GitHub Actions workflow status across all repos.
```bash
core dev ci [flags]
```
#### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
| `--branch` | Filter by branch (default: main) |
| `--failed` | Show only failed runs |
Requires the `gh` CLI to be installed and authenticated.
### dev api
Tools for managing service APIs.
```bash
core dev api sync
```
Synchronizes the public service APIs with their internal implementations.
### dev sync
Alias for `core dev api sync`. Synchronizes the public service APIs with their internal implementations.
```bash
core dev sync
```
This command scans the `pkg` directory for services and ensures that the top-level public API for each service is in sync with its internal implementation. It automatically generates the necessary Go files with type aliases.
## See Also
- [work](work/) - Multi-repo workflow commands (`core dev work`, `core dev health`, etc.)
- [ai](../ai/) - Task management commands (`core ai tasks`, `core ai task`, etc.)

View file

@ -1,57 +0,0 @@
# core dev issues
List open issues across all repositories.
Fetches open issues from GitHub for all repos in the registry. Requires the `gh` CLI to be installed and authenticated.
## Usage
```bash
core dev issues [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
| `--assignee` | Filter by assignee (use `@me` for yourself) |
| `--limit` | Max issues per repo (default 10) |
## Examples
```bash
# List all open issues
core dev issues
# Show issues assigned to you
core dev issues --assignee @me
# Limit to 5 issues per repo
core dev issues --limit 5
# Filter by specific assignee
core dev issues --assignee username
```
## Output
```
core-php (3 issues)
#42 Add retry logic to HTTP client bug
#38 Update documentation for v2 API docs
#35 Support custom serializers enhancement
core-tenant (1 issue)
#12 Workspace isolation bug bug, critical
```
## Requirements
- GitHub CLI (`gh`) must be installed
- Must be authenticated: `gh auth login`
## See Also
- [reviews command](../reviews/) - List PRs needing review
- [ci command](../ci/) - Check CI status

View file

@ -1,47 +0,0 @@
# core dev pull
Pull updates across all repositories.
Pulls updates for all repos. By default only pulls repos that are behind. Use `--all` to pull all repos.
## Usage
```bash
core dev pull [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
| `--all` | Pull all repos, not just those behind |
## Examples
```bash
# Pull only repos that are behind
core dev pull
# Pull all repos
core dev pull --all
# Use specific registry
core dev pull --registry ~/projects/repos.yaml
```
## Output
```
Pulling 2 repo(s) that are behind:
✓ core-php (3 commits)
✓ core-tenant (1 commit)
Done: 2 pulled
```
## See Also
- [push command](../push/) - Push local commits
- [health command](../health/) - Check sync status
- [work command](../work/) - Full workflow

View file

@ -1,52 +0,0 @@
# core dev push
Push commits across all repositories.
Pushes unpushed commits for all repos. Shows repos with commits to push and confirms before pushing.
## Usage
```bash
core dev push [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
| `--force` | Skip confirmation prompt |
## Examples
```bash
# Push with confirmation
core dev push
# Push without confirmation
core dev push --force
# Use specific registry
core dev push --registry ~/projects/repos.yaml
```
## Output
```
3 repo(s) with unpushed commits:
core-php: 2 commit(s)
core-admin: 1 commit(s)
core-tenant: 1 commit(s)
Push all? [y/N] y
✓ core-php
✓ core-admin
✓ core-tenant
```
## See Also
- [commit command](../commit/) - Create commits before pushing
- [pull command](../pull/) - Pull updates from remote
- [work command](../work/) - Full workflow (status + commit + push)

View file

@ -1,61 +0,0 @@
# core dev reviews
List PRs needing review across all repositories.
Fetches open PRs from GitHub for all repos in the registry. Shows review status (approved, changes requested, pending). Requires the `gh` CLI to be installed and authenticated.
## Usage
```bash
core dev reviews [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml (auto-detected if not specified) |
| `--all` | Show all PRs including drafts |
| `--author` | Filter by PR author |
## Examples
```bash
# List PRs needing review
core dev reviews
# Include draft PRs
core dev reviews --all
# Filter by author
core dev reviews --author username
```
## Output
```
core-php (2 PRs)
#45 feat: Add caching layer ✓ approved @alice
#43 fix: Memory leak in worker ⏳ pending @bob
core-admin (1 PR)
#28 refactor: Extract components ✗ changes @charlie
```
## Review Status
| Symbol | Meaning |
|--------|---------|
| `✓` | Approved |
| `⏳` | Pending review |
| `✗` | Changes requested |
## Requirements
- GitHub CLI (`gh`) must be installed
- Must be authenticated: `gh auth login`
## See Also
- [issues command](../issues/) - List open issues
- [ci command](../ci/) - Check CI status

View file

@ -1,33 +0,0 @@
# Dev Work Examples
```bash
# Full workflow: status → commit → push
core dev work
# Status only
core dev work --status
```
## Output
```
┌─────────────┬────────┬──────────┬─────────┐
│ Repo │ Branch │ Status │ Behind │
├─────────────┼────────┼──────────┼─────────┤
│ core-php │ main │ clean │ 0 │
│ core-tenant │ main │ 2 files │ 0 │
│ core-admin │ dev │ clean │ 3 │
└─────────────┴────────┴──────────┴─────────┘
```
## Registry
```yaml
repos:
- name: core
path: ./core
url: https://github.com/host-uk/core
- name: core-php
path: ./core-php
url: https://github.com/host-uk/core-php
```

View file

@ -1,293 +0,0 @@
# core dev work
Multi-repo git operations for managing the host-uk organization.
## Overview
The `core dev work` command and related subcommands help manage multiple repositories in the host-uk ecosystem simultaneously.
## Commands
| Command | Description |
|---------|-------------|
| `core dev work` | Full workflow: status + commit + push |
| `core dev work --status` | Status table only |
| `core dev work --commit` | Use Claude to commit dirty repos |
| `core dev health` | Quick health check across all repos |
| `core dev commit` | Claude-assisted commits across repos |
| `core dev push` | Push commits across all repos |
| `core dev pull` | Pull updates across all repos |
| `core dev issues` | List open issues across all repos |
| `core dev reviews` | List PRs needing review |
| `core dev ci` | Check CI status across all repos |
| `core dev impact` | Show impact of changing a repo |
## core dev work
Manage git status, commits, and pushes across multiple repositories.
```bash
core dev work [flags]
```
Reads `repos.yaml` to discover repositories and their relationships. Shows status, optionally commits with Claude, and pushes changes.
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
| `--status` | Show status only, don't push |
| `--commit` | Use Claude to commit dirty repos before pushing |
### Examples
```bash
# Full workflow
core dev work
# Status only
core dev work --status
# Commit and push
core dev work --commit
```
## core dev health
Quick health check showing summary of repository health across all repos.
```bash
core dev health [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
| `--verbose` | Show detailed breakdown |
Output shows:
- Total repos
- Dirty repos
- Unpushed commits
- Repos behind remote
### Examples
```bash
# Quick summary
core dev health
# Detailed breakdown
core dev health --verbose
```
## core dev issues
List open issues across all repositories.
```bash
core dev issues [flags]
```
Fetches open issues from GitHub for all repos in the registry. Requires the `gh` CLI to be installed and authenticated.
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
| `--assignee` | Filter by assignee (use `@me` for yourself) |
| `--limit` | Max issues per repo (default: 10) |
### Examples
```bash
# List all open issues
core dev issues
# Filter by assignee
core dev issues --assignee @me
# Limit results
core dev issues --limit 5
```
## core dev reviews
List pull requests needing review across all repos.
```bash
core dev reviews [flags]
```
Fetches open PRs from GitHub for all repos in the registry. Shows review status (approved, changes requested, pending). Requires the `gh` CLI to be installed and authenticated.
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
| `--all` | Show all PRs including drafts |
| `--author` | Filter by PR author |
### Examples
```bash
# List PRs needing review
core dev reviews
# Show all PRs including drafts
core dev reviews --all
# Filter by author
core dev reviews --author username
```
## core dev commit
Create commits across repos with Claude assistance.
```bash
core dev commit [flags]
```
Uses Claude to create commits for dirty repos. Shows uncommitted changes and invokes Claude to generate commit messages.
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
| `--all` | Commit all dirty repos without prompting |
### Examples
```bash
# Commit with prompts
core dev commit
# Commit all automatically
core dev commit --all
```
## core dev push
Push commits across all repos.
```bash
core dev push [flags]
```
Pushes unpushed commits for all repos. Shows repos with commits to push and confirms before pushing.
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
| `--force` | Skip confirmation prompt |
### Examples
```bash
# Push with confirmation
core dev push
# Skip confirmation
core dev push --force
```
## core dev pull
Pull updates across all repos.
```bash
core dev pull [flags]
```
Pulls updates for all repos. By default only pulls repos that are behind. Use `--all` to pull all repos.
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
| `--all` | Pull all repos, not just those behind |
### Examples
```bash
# Pull repos that are behind
core dev pull
# Pull all repos
core dev pull --all
```
## core dev ci
Check GitHub Actions workflow status across all repos.
```bash
core dev ci [flags]
```
Fetches GitHub Actions workflow status for all repos. Shows latest run status for each repo. Requires the `gh` CLI to be installed and authenticated.
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
| `--branch` | Filter by branch (default: main) |
| `--failed` | Show only failed runs |
### Examples
```bash
# Show CI status for all repos
core dev ci
# Show only failed runs
core dev ci --failed
# Check specific branch
core dev ci --branch develop
```
## core dev impact
Show the impact of changing a repository.
```bash
core dev impact <repo> [flags]
```
Analyzes the dependency graph to show which repos would be affected by changes to the specified repo.
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to `repos.yaml` (auto-detected if not specified) |
### Examples
```bash
# Show impact of changing core-php
core dev impact core-php
```
## Registry
These commands use `repos.yaml` to know which repos to manage. See [repos.yaml](../../../configuration.md#reposyaml) for format.
Use `core setup` to clone all repos from the registry.
## See Also
- [setup command](../../setup/) - Clone repos from registry
- [search command](../../pkg/search/) - Find and install repos

View file

@ -1,14 +0,0 @@
# Docs Examples
## List
```bash
core docs list
```
## Sync
```bash
core docs sync
core docs sync --output ./docs
```

View file

@ -1,110 +0,0 @@
# core docs
Documentation management across repositories.
## Usage
```bash
core docs <command> [flags]
```
## Commands
| Command | Description |
|---------|-------------|
| `list` | List documentation across repos |
| `sync` | Sync documentation to output directory |
## docs list
Show documentation coverage across all repos.
```bash
core docs list [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml |
### Output
```
Repo README CLAUDE CHANGELOG docs/
──────────────────────────────────────────────────────────────────────
core ✓ ✓ — 12 files
core-php ✓ ✓ ✓ 8 files
core-images ✓ — — —
Coverage: 3 with docs, 0 without
```
## docs sync
Sync documentation from all repos to an output directory.
```bash
core docs sync [flags]
```
### Flags
| Flag | Description |
|------|-------------|
| `--registry` | Path to repos.yaml |
| `--output` | Output directory (default: ./docs-build) |
| `--dry-run` | Show what would be synced |
### Output Structure
```
docs-build/
└── packages/
├── core/
│ ├── index.md # from README.md
│ ├── claude.md # from CLAUDE.md
│ ├── changelog.md # from CHANGELOG.md
│ ├── build.md # from docs/build.md
│ └── ...
└── core-php/
├── index.md
└── ...
```
### Example
```bash
# Preview what will be synced
core docs sync --dry-run
# Sync to default output
core docs sync
# Sync to custom directory
core docs sync --output ./site/content
```
## What Gets Synced
For each repo, the following files are collected:
| Source | Destination |
|--------|-------------|
| `README.md` | `index.md` |
| `CLAUDE.md` | `claude.md` |
| `CHANGELOG.md` | `changelog.md` |
| `docs/*.md` | `*.md` |
## Integration with core.help
The synced docs are used to build https://core.help:
1. Run `core docs sync --output ../core-php/docs/packages`
2. VitePress builds the combined documentation
3. Deploy to core.help
## See Also
- [Configuration](../../configuration.md) - Project configuration

View file

@ -1,20 +0,0 @@
# Doctor Examples
```bash
core doctor
```
## Output
```
✓ go 1.25.0
✓ git 2.43.0
✓ gh 2.40.0
✓ docker 24.0.7
✓ task 3.30.0
✓ golangci-lint 1.55.0
✗ wails (not installed)
✓ php 8.3.0
✓ composer 2.6.0
✓ node 20.10.0
```

View file

@ -1,81 +0,0 @@
# core doctor
Check your development environment for required tools and configuration.
## Usage
```bash
core doctor [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--verbose` | Show detailed version information |
## What It Checks
### Required Tools
| Tool | Purpose |
|------|---------|
| `git` | Version control |
| `go` | Go compiler |
| `gh` | GitHub CLI |
### Optional Tools
| Tool | Purpose |
|------|---------|
| `node` | Node.js runtime |
| `docker` | Container runtime |
| `wails` | Desktop app framework |
| `qemu` | VM runtime for LinuxKit |
| `gpg` | Code signing |
| `codesign` | macOS signing (macOS only) |
### Configuration
- Git user name and email
- GitHub CLI authentication
- Go workspace setup
## Output
```
Core Doctor
===========
Required:
[OK] git 2.43.0
[OK] go 1.23.0
[OK] gh 2.40.0
Optional:
[OK] node 20.10.0
[OK] docker 24.0.7
[--] wails (not installed)
[OK] qemu 8.2.0
[OK] gpg 2.4.3
[OK] codesign (available)
Configuration:
[OK] git user.name: Your Name
[OK] git user.email: you@example.com
[OK] gh auth status: Logged in
All checks passed!
```
## Exit Codes
| Code | Meaning |
|------|---------|
| 0 | All required checks passed |
| 1 | One or more required checks failed |
## See Also
- [setup command](../setup/) - Clone repos from registry
- [dev](../dev/) - Development environment

View file

@ -1,18 +0,0 @@
# Go Coverage Examples
```bash
# Summary
core go cov
# HTML report
core go cov --html
# Open in browser
core go cov --open
# Fail if below threshold
core go cov --threshold 80
# Specific package
core go cov --pkg ./pkg/release
```

View file

@ -1,28 +0,0 @@
# core go cov
Generate coverage report with thresholds.
## Usage
```bash
core go cov [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--pkg` | Package to test (default: `./...`) |
| `--html` | Generate HTML coverage report |
| `--open` | Generate and open HTML report in browser |
| `--threshold` | Minimum coverage percentage (exit 1 if below) |
## Examples
```bash
core go cov # Summary
core go cov --html # HTML report
core go cov --open # Open in browser
core go cov --threshold 80 # Fail if < 80%
core go cov --pkg ./pkg/release # Specific package
```

View file

@ -1,89 +0,0 @@
# Go Examples
## Testing
```bash
# Run all tests
core go test
# Specific package
core go test --pkg ./pkg/core
# Specific test
core go test --run TestHash
# With coverage
core go test --coverage
# Race detection
core go test --race
```
## Coverage
```bash
# Summary
core go cov
# HTML report
core go cov --html
# Open in browser
core go cov --open
# Fail if below threshold
core go cov --threshold 80
```
## Formatting
```bash
# Check
core go fmt
# Fix
core go fmt --fix
# Show diff
core go fmt --diff
```
## Linting
```bash
# Check
core go lint
# Auto-fix
core go lint --fix
```
## Installing
```bash
# Auto-detect cmd/
core go install
# Specific path
core go install ./cmd/myapp
# Pure Go (no CGO)
core go install --no-cgo
```
## Module Management
```bash
core go mod tidy
core go mod download
core go mod verify
core go mod graph
```
## Workspace
```bash
core go work sync
core go work init
core go work use ./pkg/mymodule
```

View file

@ -1,12 +0,0 @@
# Go Format Examples
```bash
# Check only
core go fmt
# Apply fixes
core go fmt --fix
# Show diff
core go fmt --diff
```

View file

@ -1,25 +0,0 @@
# core go fmt
Format Go code using goimports or gofmt.
## Usage
```bash
core go fmt [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--fix` | Fix formatting in place |
| `--diff` | Show diff of changes |
| `--check` | Check only, exit 1 if not formatted |
## Examples
```bash
core go fmt # Check formatting
core go fmt --fix # Fix formatting
core go fmt --diff # Show diff
```

View file

@ -1,15 +0,0 @@
# core go
Go development tools with enhanced output and environment setup.
## Subcommands
| Command | Description |
|---------|-------------|
| [test](test/) | Run tests with coverage |
| [cov](cov/) | Run tests with coverage report |
| [fmt](fmt/) | Format Go code |
| [lint](lint/) | Run golangci-lint |
| [install](install/) | Install Go binary |
| [mod](mod/) | Module management |
| [work](work/) | Workspace management |

View file

@ -1,15 +0,0 @@
# Go Install Examples
```bash
# Auto-detect cmd/
core go install
# Specific path
core go install ./cmd/myapp
# Pure Go (no CGO)
core go install --no-cgo
# Verbose
core go install -v
```

View file

@ -1,25 +0,0 @@
# core go install
Install Go binary with auto-detection.
## Usage
```bash
core go install [path] [flags]
```
## Flags
| Flag | Description |
|------|-------------|
| `--no-cgo` | Disable CGO |
| `-v` | Verbose |
## Examples
```bash
core go install # Install current module
core go install ./cmd/core # Install specific path
core go install --no-cgo # Pure Go (no C dependencies)
core go install -v # Verbose output
```

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