[agent/codex] Review PR #28. Read CLAUDE.md first. Check: 1) API contract ... #29
735 changed files with 13676 additions and 126183 deletions
5
.claude/settings.json
Normal file
5
.claude/settings.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"enabledPlugins": {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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
28
.core/build.yaml
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}'
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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
24
.core/release.yaml
Normal 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
12
.editorconfig
Normal 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
23
.gitattributes
vendored
Normal 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
4
.githooks/pre-commit
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
exec core go qa full --fix
|
||||
58
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
58
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -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
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -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
|
||||
58
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
58
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
|
@ -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
|
||||
24
.github/dependabot.yml
vendored
24
.github/dependabot.yml
vendored
|
|
@ -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):"
|
||||
|
||||
133
.github/workflows/agent-verify.yml
vendored
133
.github/workflows/agent-verify.yml
vendored
|
|
@ -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']
|
||||
});
|
||||
115
.github/workflows/auto-label.yml
vendored
115
.github/workflows/auto-label.yml
vendored
|
|
@ -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(', ')}`);
|
||||
}
|
||||
}
|
||||
30
.github/workflows/auto-project.yml
vendored
30
.github/workflows/auto-project.yml
vendored
|
|
@ -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 }}
|
||||
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
36
.github/workflows/codeql.yml
vendored
36
.github/workflows/codeql.yml
vendored
|
|
@ -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"
|
||||
|
||||
36
.github/workflows/codescan.yml
vendored
36
.github/workflows/codescan.yml
vendored
|
|
@ -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
|
||||
46
.github/workflows/coverage.yml
vendored
46
.github/workflows/coverage.yml
vendored
|
|
@ -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
|
||||
94
.github/workflows/dev-release.yml
vendored
94
.github/workflows/dev-release.yml
vendored
|
|
@ -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/*
|
||||
86
.github/workflows/release.yml
vendored
86
.github/workflows/release.yml
vendored
|
|
@ -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
11
.gitignore
vendored
|
|
@ -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
9
.mcp.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"core": {
|
||||
"type": "stdio",
|
||||
"command": "core-agent",
|
||||
"args": ["mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
134
CLAUDE.md
134
CLAUDE.md
|
|
@ -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.
|
||||
|
|
|
|||
55
GEMINI.md
55
GEMINI.md
|
|
@ -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.
|
||||
20
Makefile
20
Makefile
|
|
@ -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
447
README.md
|
|
@ -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
|
||||
|
|
|
|||
132
Taskfile.yml
132
Taskfile.yml
|
|
@ -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
|
||||
53
app.go
Normal file
53
app.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
// Application identity for the Core framework.
|
||||
// Based on leaanthony/sail — Name, Filename, Path.
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// App holds the application identity and optional GUI runtime.
|
||||
type App struct {
|
||||
// Name is the human-readable application name (e.g., "Core CLI").
|
||||
Name string
|
||||
|
||||
// Version is the application version string (e.g., "1.2.3").
|
||||
Version string
|
||||
|
||||
// Description is a short description of the application.
|
||||
Description string
|
||||
|
||||
// Filename is the executable filename (e.g., "core").
|
||||
Filename string
|
||||
|
||||
// Path is the absolute path to the executable.
|
||||
Path string
|
||||
|
||||
// Runtime is the GUI runtime (e.g., Wails App).
|
||||
// Nil for CLI-only applications.
|
||||
Runtime any
|
||||
}
|
||||
|
||||
// Find locates a program on PATH and returns a Result containing the App.
|
||||
//
|
||||
// r := core.Find("node", "Node.js")
|
||||
// if r.OK { app := r.Value.(*App) }
|
||||
func 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}
|
||||
}
|
||||
39
app_test.go
Normal file
39
app_test.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "dappco.re/go/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// --- App ---
|
||||
|
||||
func TestApp_Good(t *testing.T) {
|
||||
c := New(WithOptions(Options{{Key: "name", Value: "myapp"}})).Value.(*Core)
|
||||
assert.Equal(t, "myapp", c.App().Name)
|
||||
}
|
||||
|
||||
func TestApp_Empty_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
assert.NotNil(t, c.App())
|
||||
assert.Equal(t, "", c.App().Name)
|
||||
}
|
||||
|
||||
func TestApp_Runtime_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
c.App().Runtime = &struct{ Name string }{Name: "wails"}
|
||||
assert.NotNil(t, c.App().Runtime)
|
||||
}
|
||||
|
||||
func TestApp_Find_Good(t *testing.T) {
|
||||
r := 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 := Find("nonexistent-binary-xyz", "test")
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
101
array.go
Normal file
101
array.go
Normal 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
90
array_test.go
Normal 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())
|
||||
}
|
||||
169
cli.go
Normal file
169
cli.go
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
// Cli is the CLI surface layer for the Core command tree.
|
||||
// It reads commands from Core's registry and wires them to terminal I/O.
|
||||
//
|
||||
// Run the CLI:
|
||||
//
|
||||
// c := core.New(core.Options{{Key: "name", Value: "myapp"}})
|
||||
// c.Command("deploy", handler)
|
||||
// c.Cli().Run()
|
||||
//
|
||||
// The Cli resolves os.Args to a command path, parses flags,
|
||||
// and calls the command's action with parsed options.
|
||||
package core
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Cli is the CLI surface for the Core command tree.
|
||||
type Cli struct {
|
||||
core *Core
|
||||
output io.Writer
|
||||
banner func(*Cli) string
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
if cl.core == nil || cl.core.commands == nil {
|
||||
if cl.banner != nil {
|
||||
cl.Print(cl.banner(cl))
|
||||
}
|
||||
return Result{}
|
||||
}
|
||||
|
||||
cl.core.commands.mu.RLock()
|
||||
cmdCount := len(cl.core.commands.commands)
|
||||
cl.core.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
|
||||
|
||||
cl.core.commands.mu.RLock()
|
||||
for i := len(clean); i > 0; i-- {
|
||||
path := JoinPath(clean[:i]...)
|
||||
if c, ok := cl.core.commands.commands[path]; ok {
|
||||
cmd = c
|
||||
remaining = clean[i:]
|
||||
break
|
||||
}
|
||||
}
|
||||
cl.core.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 := Options{}
|
||||
for _, arg := range remaining {
|
||||
key, val, valid := ParseFlag(arg)
|
||||
if valid {
|
||||
if Contains(arg, "=") {
|
||||
opts = append(opts, Option{Key: key, Value: val})
|
||||
} else {
|
||||
opts = append(opts, Option{Key: key, Value: true})
|
||||
}
|
||||
} else if !IsFlag(arg) {
|
||||
opts = append(opts, Option{Key: "_arg", Value: 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() {
|
||||
if cl.core == nil || cl.core.commands == nil {
|
||||
return
|
||||
}
|
||||
|
||||
name := ""
|
||||
if cl.core.app != nil {
|
||||
name = cl.core.app.Name
|
||||
}
|
||||
if name != "" {
|
||||
cl.Print("%s commands:", name)
|
||||
} else {
|
||||
cl.Print("Commands:")
|
||||
}
|
||||
|
||||
cl.core.commands.mu.RLock()
|
||||
defer cl.core.commands.mu.RUnlock()
|
||||
|
||||
for path, cmd := range cl.core.commands.commands {
|
||||
if cmd.Hidden || (cmd.Action == nil && cmd.Lifecycle == nil) {
|
||||
continue
|
||||
}
|
||||
tr := cl.core.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)
|
||||
}
|
||||
if cl.core != nil && cl.core.app != nil && cl.core.app.Name != "" {
|
||||
return cl.core.app.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
85
cli_test.go
Normal file
85
cli_test.go
Normal 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().Value.(*Core)
|
||||
assert.NotNil(t, c.Cli())
|
||||
}
|
||||
|
||||
func TestCli_Banner_Good(t *testing.T) {
|
||||
c := New(WithOptions(Options{{Key: "name", Value: "myapp"}})).Value.(*Core)
|
||||
assert.Equal(t, "myapp", c.Cli().Banner())
|
||||
}
|
||||
|
||||
func TestCli_SetBanner_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
r := c.Cli().Run()
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestCli_PrintHelp_Good(t *testing.T) {
|
||||
c := New(WithOptions(Options{{Key: "name", Value: "myapp"}})).Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
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
208
command.go
Normal 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.Options{{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
217
command_test.go
Normal 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().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
r := c.Command("nonexistent")
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestCommand_Run_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
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(Options{{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().Value.(*Core)
|
||||
c.Command("empty", Command{Description: "no action"})
|
||||
cmd := c.Command("empty").Value.(*Command)
|
||||
r := cmd.Run(Options{})
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
// --- Nested Commands ---
|
||||
|
||||
func TestCommand_Nested_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
c.Command("serve", Command{Action: func(_ Options) Result {
|
||||
return Result{Value: "running", OK: true}
|
||||
}})
|
||||
cmd := c.Command("serve").Value.(*Command)
|
||||
|
||||
r := cmd.Start(Options{})
|
||||
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().Value.(*Core)
|
||||
lc := &testLifecycle{}
|
||||
c.Command("daemon", Command{Lifecycle: lc})
|
||||
cmd := c.Command("daemon").Value.(*Command)
|
||||
|
||||
r := cmd.Start(Options{})
|
||||
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().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
r := c.Command("", Command{})
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
135
config.go
Normal file
135
config.go
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
// 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
|
||||
}
|
||||
|
||||
// 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
102
config_test.go
Normal 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().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
r := c.Config().Get("missing")
|
||||
assert.False(t, r.OK)
|
||||
assert.Nil(t, r.Value)
|
||||
}
|
||||
|
||||
func TestConfig_TypedAccessors_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
// 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().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
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())
|
||||
}
|
||||
157
contract.go
Normal file
157
contract.go
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
// Contracts, options, and type definitions for the Core framework.
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// 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.Options{{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) Result {
|
||||
c := &Core{
|
||||
app: &App{},
|
||||
data: &Data{},
|
||||
drive: &Drive{},
|
||||
fs: &Fs{root: "/"},
|
||||
config: &Config{ConfigOptions: &ConfigOptions{}},
|
||||
error: &ErrorPanic{},
|
||||
log: &ErrorLog{log: Default()},
|
||||
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())
|
||||
c.cli = &Cli{core: c}
|
||||
|
||||
for _, opt := range opts {
|
||||
if r := opt(c); !r.OK {
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
return Result{c, true}
|
||||
}
|
||||
|
||||
// WithOptions applies key-value configuration to Core.
|
||||
//
|
||||
// core.WithOptions(core.Options{{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.
|
||||
// The factory receives *Core so the service can wire IPC handlers
|
||||
// and access other subsystems during construction.
|
||||
// Service name is auto-discovered from the package path.
|
||||
// If the service implements HandleIPCEvents, it is auto-registered.
|
||||
//
|
||||
// core.WithService(agentic.Register)
|
||||
// core.WithService(display.Register(nil))
|
||||
func WithService(factory func(*Core) Result) CoreOption {
|
||||
return func(c *Core) Result {
|
||||
return factory(c)
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
c.LockApply()
|
||||
return Result{OK: true}
|
||||
}
|
||||
}
|
||||
83
core.go
Normal file
83
core.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// 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"
|
||||
"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 *Cli // c.Cli() — CLI surface layer
|
||||
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 { return c.cli }
|
||||
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 }
|
||||
|
||||
// --- 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 ---
|
||||
135
core_test.go
Normal file
135
core_test.go
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
. "dappco.re/go/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// --- New ---
|
||||
|
||||
func TestNew_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
assert.NotNil(t, c)
|
||||
}
|
||||
|
||||
func TestNew_WithOptions_Good(t *testing.T) {
|
||||
c := New(WithOptions(Options{{Key: "name", Value: "myapp"}})).Value.(*Core)
|
||||
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(Options{})).Value.(*Core)
|
||||
assert.NotNil(t, c)
|
||||
}
|
||||
|
||||
func TestNew_WithService_Good(t *testing.T) {
|
||||
started := false
|
||||
r := New(
|
||||
WithOptions(Options{{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}
|
||||
}),
|
||||
)
|
||||
assert.True(t, r.OK)
|
||||
c := r.Value.(*Core)
|
||||
|
||||
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) {
|
||||
r := New(
|
||||
WithService(func(c *Core) Result {
|
||||
c.Service("allowed", Service{})
|
||||
return Result{OK: true}
|
||||
}),
|
||||
WithServiceLock(),
|
||||
)
|
||||
assert.True(t, r.OK)
|
||||
c := r.Value.(*Core)
|
||||
|
||||
// Registration after lock should fail
|
||||
reg := c.Service("blocked", Service{})
|
||||
assert.False(t, reg.OK)
|
||||
}
|
||||
|
||||
// --- Accessors ---
|
||||
|
||||
func TestAccessors_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
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(Options{
|
||||
{Key: "name", Value: "testapp"},
|
||||
{Key: "port", Value: 8080},
|
||||
{Key: "debug", Value: true},
|
||||
})).Value.(*Core)
|
||||
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().Value.(*Core)
|
||||
// No options passed — Options() returns nil
|
||||
assert.Nil(t, c.Options())
|
||||
}
|
||||
|
||||
// --- Core Error/Log Helpers ---
|
||||
|
||||
func TestCore_LogError_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
cause := assert.AnError
|
||||
r := c.LogError(cause, "test.Operation", "something broke")
|
||||
assert.False(t, r.OK)
|
||||
err, ok := r.Value.(error)
|
||||
assert.True(t, ok)
|
||||
assert.ErrorIs(t, err, cause)
|
||||
}
|
||||
|
||||
func TestCore_LogWarn_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
r := c.LogWarn(assert.AnError, "test.Operation", "heads up")
|
||||
assert.False(t, r.OK)
|
||||
_, ok := r.Value.(error)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestCore_Must_Ugly(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
assert.Panics(t, func() {
|
||||
c.Must(assert.AnError, "test.Operation", "fatal")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCore_Must_Nil_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
assert.NotPanics(t, func() {
|
||||
c.Must(nil, "test.Operation", "no error")
|
||||
})
|
||||
}
|
||||
202
data.go
Normal file
202
data.go
Normal 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.Options{
|
||||
// {Key: "name", Value: "brain"},
|
||||
// {Key: "source", Value: brainFS},
|
||||
// {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.Options{
|
||||
// {Key: "name", Value: "brain"},
|
||||
// {Key: "source", Value: brainFS},
|
||||
// {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
|
||||
}
|
||||
130
data_test.go
Normal file
130
data_test.go
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
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 TestData_New_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
r := c.Data().New(Options{
|
||||
{Key: "name", Value: "test"},
|
||||
{Key: "source", Value: testFS},
|
||||
{Key: "path", Value: "testdata"},
|
||||
})
|
||||
assert.True(t, r.OK)
|
||||
assert.NotNil(t, r.Value)
|
||||
}
|
||||
|
||||
func TestData_New_Bad(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
|
||||
r := c.Data().New(Options{{Key: "source", Value: testFS}})
|
||||
assert.False(t, r.OK)
|
||||
|
||||
r = c.Data().New(Options{{Key: "name", Value: "test"}})
|
||||
assert.False(t, r.OK)
|
||||
|
||||
r = c.Data().New(Options{{Key: "name", Value: "test"}, {Key: "source", Value: "not-an-fs"}})
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestData_ReadString_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
|
||||
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().Value.(*Core)
|
||||
r := c.Data().ReadString("nonexistent/file.txt")
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestData_ReadFile_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
|
||||
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().Value.(*Core)
|
||||
c.Data().New(Options{{Key: "name", Value: "brain"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
|
||||
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().Value.(*Core)
|
||||
r := c.Data().Get("nonexistent")
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestData_Mounts_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
c.Data().New(Options{{Key: "name", Value: "a"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
|
||||
c.Data().New(Options{{Key: "name", Value: "b"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
|
||||
mounts := c.Data().Mounts()
|
||||
assert.Len(t, mounts, 2)
|
||||
}
|
||||
|
||||
func TestEmbed_Legacy_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
|
||||
assert.NotNil(t, c.Embed())
|
||||
}
|
||||
|
||||
func TestData_List_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "."}})
|
||||
r := c.Data().List("app/testdata")
|
||||
assert.True(t, r.OK)
|
||||
}
|
||||
|
||||
func TestData_List_Bad(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
r := c.Data().List("nonexistent/path")
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestData_ListNames_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "."}})
|
||||
r := c.Data().ListNames("app/testdata")
|
||||
assert.True(t, r.OK)
|
||||
assert.Contains(t, r.Value.([]string), "test")
|
||||
}
|
||||
|
||||
func TestData_Extract_Good(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "."}})
|
||||
r := c.Data().Extract("app/testdata", t.TempDir(), nil)
|
||||
assert.True(t, r.OK)
|
||||
}
|
||||
|
||||
func TestData_Extract_Bad(t *testing.T) {
|
||||
c := New().Value.(*Core)
|
||||
r := c.Data().Extract("nonexistent/path", t.TempDir(), nil)
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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).
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
# CI Init Examples
|
||||
|
||||
```bash
|
||||
core ci init
|
||||
```
|
||||
|
||||
Creates `.core/release.yaml`:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
project:
|
||||
name: myapp
|
||||
|
||||
publishers:
|
||||
- type: github
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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.)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
# Docs Examples
|
||||
|
||||
## List
|
||||
|
||||
```bash
|
||||
core docs list
|
||||
```
|
||||
|
||||
## Sync
|
||||
|
||||
```bash
|
||||
core docs sync
|
||||
core docs sync --output ./docs
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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 |
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# Go Lint Examples
|
||||
|
||||
```bash
|
||||
# Check
|
||||
core go lint
|
||||
|
||||
# Auto-fix
|
||||
core go lint --fix
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
`.golangci.yml`:
|
||||
|
||||
```yaml
|
||||
linters:
|
||||
enable:
|
||||
- gofmt
|
||||
- govet
|
||||
- errcheck
|
||||
- staticcheck
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue