chore: add Entire CLI, .claude config, CLAUDE.md auto-generation
Enable auto-commit tracking, git-sync hooks, session recovery, and anonymous identity for the new repo. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
102
.claude/settings.json
Normal file
102
.claude/settings.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PostToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Task",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "entire hooks claude-code post-task"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "TodoWrite",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "entire hooks claude-code post-todo"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PreToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Task",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "entire hooks claude-code pre-task"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"SessionEnd": [
|
||||||
|
{
|
||||||
|
"matcher": "",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "entire hooks claude-code session-end"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "bash .entire/git-sync.sh push 2>/dev/null || true"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"SessionStart": [
|
||||||
|
{
|
||||||
|
"matcher": "compact",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "bash .entire/git-sync.sh pull 2>&1 || true; echo '=== CONTEXT RESTORED AFTER COMPACTION ==='; echo \"Project: $(basename $(git rev-parse --show-toplevel 2>/dev/null || pwd))\"; echo 'Strategy: auto-commit'; echo \"Remote: $(git remote get-url $(git remote | head -1) 2>/dev/null || echo 'not set')\"; echo '=== Recent commits ==='; git log --oneline -10 2>/dev/null; echo '=== END CONTEXT ==='"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "bash .entire/git-sync.sh pull 2>&1 || true; bash .entire/generate-claude-md.sh . 2>/dev/null; entire hooks claude-code session-start"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Stop": [
|
||||||
|
{
|
||||||
|
"matcher": "",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "entire hooks claude-code stop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "bash .entire/git-sync.sh push 2>/dev/null || true"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"UserPromptSubmit": [
|
||||||
|
{
|
||||||
|
"matcher": "",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "entire hooks claude-code user-prompt-submit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"defaultMode": "bypassPermissions",
|
||||||
|
"deny": [
|
||||||
|
"Read(./.entire/metadata/**)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
4
.entire/.gitignore
vendored
Normal file
4
.entire/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
tmp/
|
||||||
|
settings.local.json
|
||||||
|
metadata/
|
||||||
|
logs/
|
||||||
356
.entire/generate-claude-md.sh
Executable file
356
.entire/generate-claude-md.sh
Executable file
@@ -0,0 +1,356 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# generate-claude-md.sh — Auto-generate and update CLAUDE.md from project state
|
||||||
|
#
|
||||||
|
# Called by SessionStart hook. Detects project type, dependencies, structure,
|
||||||
|
# git info and writes/updates CLAUDE.md so Claude Code has full context.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROJECT_DIR="${1:-.}"
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
|
||||||
|
CLAUDE_MD="CLAUDE.md"
|
||||||
|
MARKER="<!-- auto-generated by generate-claude-md.sh -->"
|
||||||
|
|
||||||
|
# --- Ensure anonymous git identity ---
|
||||||
|
ensure_anonymous_identity() {
|
||||||
|
local current_email
|
||||||
|
current_email=$(git config --global user.email 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Check if identity looks anonymous (ends with @device.local)
|
||||||
|
if echo "$current_email" | grep -q '@device.local$'; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try to generate/restore device ID
|
||||||
|
local device_id=""
|
||||||
|
local device_id_file="${XDG_CONFIG_HOME:-$HOME/.config}/entire/device-id"
|
||||||
|
|
||||||
|
if [ -f "$device_id_file" ]; then
|
||||||
|
device_id=$(cat "$device_id_file")
|
||||||
|
elif [ -f ".entire/generate-device-id.sh" ]; then
|
||||||
|
device_id=$(bash ".entire/generate-device-id.sh" 2>/dev/null)
|
||||||
|
elif [ -f "$(dirname "$0")/generate-device-id.sh" ]; then
|
||||||
|
device_id=$(bash "$(dirname "$0")/generate-device-id.sh" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$device_id" ]; then
|
||||||
|
git config --global user.name "$device_id"
|
||||||
|
git config --global user.email "${device_id}@device.local"
|
||||||
|
echo "[generate-claude-md] Fixed git identity: $device_id (was: ${current_email:-not set})"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_anonymous_identity
|
||||||
|
|
||||||
|
# --- Helpers ---
|
||||||
|
|
||||||
|
detect_project_name() {
|
||||||
|
if [ -f "package.json" ]; then
|
||||||
|
node -e "console.log(require('./package.json').name || '')" 2>/dev/null
|
||||||
|
elif [ -f "Cargo.toml" ]; then
|
||||||
|
grep '^name' Cargo.toml | head -1 | sed 's/.*= *"\(.*\)"/\1/'
|
||||||
|
elif [ -f "pyproject.toml" ]; then
|
||||||
|
grep '^name' pyproject.toml | head -1 | sed 's/.*= *"\(.*\)"/\1/'
|
||||||
|
elif [ -f "go.mod" ]; then
|
||||||
|
head -1 go.mod | awk '{print $2}' | sed 's|.*/||'
|
||||||
|
else
|
||||||
|
basename "$PWD"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_project_description() {
|
||||||
|
if [ -f "package.json" ]; then
|
||||||
|
node -e "console.log(require('./package.json').description || '')" 2>/dev/null
|
||||||
|
elif [ -f "pyproject.toml" ]; then
|
||||||
|
grep '^description' pyproject.toml | head -1 | sed 's/.*= *"\(.*\)"/\1/'
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_tech_stack() {
|
||||||
|
local stack=()
|
||||||
|
|
||||||
|
# JavaScript/TypeScript ecosystem
|
||||||
|
if [ -f "package.json" ]; then
|
||||||
|
local deps
|
||||||
|
deps=$(cat package.json)
|
||||||
|
|
||||||
|
# Runtime
|
||||||
|
if echo "$deps" | grep -q '"typescript"'; then stack+=("TypeScript"); else stack+=("JavaScript"); fi
|
||||||
|
|
||||||
|
# Frameworks
|
||||||
|
if echo "$deps" | grep -q '"next"'; then stack+=("Next.js"); fi
|
||||||
|
if echo "$deps" | grep -q '"react"'; then stack+=("React"); fi
|
||||||
|
if echo "$deps" | grep -q '"vue"'; then stack+=("Vue"); fi
|
||||||
|
if echo "$deps" | grep -q '"svelte"'; then stack+=("Svelte"); fi
|
||||||
|
if echo "$deps" | grep -q '"express"'; then stack+=("Express"); fi
|
||||||
|
if echo "$deps" | grep -q '"fastify"'; then stack+=("Fastify"); fi
|
||||||
|
if echo "$deps" | grep -q '"hono"'; then stack+=("Hono"); fi
|
||||||
|
if echo "$deps" | grep -q '"astro"'; then stack+=("Astro"); fi
|
||||||
|
|
||||||
|
# Build tools
|
||||||
|
if echo "$deps" | grep -q '"vite"'; then stack+=("Vite"); fi
|
||||||
|
if echo "$deps" | grep -q '"webpack"'; then stack+=("Webpack"); fi
|
||||||
|
if echo "$deps" | grep -q '"esbuild"'; then stack+=("esbuild"); fi
|
||||||
|
if echo "$deps" | grep -q '"turbo"'; then stack+=("Turborepo"); fi
|
||||||
|
|
||||||
|
# CSS
|
||||||
|
if echo "$deps" | grep -q '"tailwindcss"'; then stack+=("Tailwind CSS"); fi
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
if echo "$deps" | grep -q '"vitest"'; then stack+=("Vitest"); fi
|
||||||
|
if echo "$deps" | grep -q '"jest"'; then stack+=("Jest"); fi
|
||||||
|
if echo "$deps" | grep -q '"playwright"'; then stack+=("Playwright"); fi
|
||||||
|
if echo "$deps" | grep -q '"cypress"'; then stack+=("Cypress"); fi
|
||||||
|
|
||||||
|
# DB/ORM
|
||||||
|
if echo "$deps" | grep -q '"prisma"'; then stack+=("Prisma"); fi
|
||||||
|
if echo "$deps" | grep -q '"drizzle-orm"'; then stack+=("Drizzle"); fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Python
|
||||||
|
if [ -f "requirements.txt" ] || [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
|
||||||
|
stack+=("Python")
|
||||||
|
if [ -f "requirements.txt" ]; then
|
||||||
|
if grep -qi "django" requirements.txt; then stack+=("Django"); fi
|
||||||
|
if grep -qi "flask" requirements.txt; then stack+=("Flask"); fi
|
||||||
|
if grep -qi "fastapi" requirements.txt; then stack+=("FastAPI"); fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rust
|
||||||
|
if [ -f "Cargo.toml" ]; then stack+=("Rust"); fi
|
||||||
|
|
||||||
|
# Go
|
||||||
|
if [ -f "go.mod" ]; then stack+=("Go"); fi
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
if [ -f "Dockerfile" ] || [ -f "docker-compose.yml" ] || [ -f "docker-compose.yaml" ]; then
|
||||||
|
stack+=("Docker")
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${stack[*]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_scripts() {
|
||||||
|
if [ -f "package.json" ]; then
|
||||||
|
node -e "
|
||||||
|
const pkg = require('./package.json');
|
||||||
|
const scripts = pkg.scripts || {};
|
||||||
|
Object.entries(scripts).forEach(([k, v]) => console.log('- \`npm run ' + k + '\` — \`' + v + '\`'));
|
||||||
|
" 2>/dev/null
|
||||||
|
elif [ -f "Makefile" ]; then
|
||||||
|
grep -E '^[a-zA-Z_-]+:' Makefile | sed 's/:.*//' | while read -r target; do
|
||||||
|
echo "- \`make $target\`"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_git_info() {
|
||||||
|
if [ -d ".git" ]; then
|
||||||
|
local branch remote
|
||||||
|
branch=$(git branch --show-current 2>/dev/null || echo "unknown")
|
||||||
|
echo "- **Branch**: $branch"
|
||||||
|
git remote -v 2>/dev/null | grep '(push)' | while read -r name url _; do
|
||||||
|
# Strip credentials from URL
|
||||||
|
local clean_url
|
||||||
|
clean_url=$(echo "$url" | sed 's|://[^@]*@|://|')
|
||||||
|
echo "- **Remote \`$name\`**: $clean_url"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_project_structure() {
|
||||||
|
# Show top-level dirs and key files
|
||||||
|
local items=()
|
||||||
|
for f in src/ app/ pages/ components/ lib/ utils/ api/ server/ public/ assets/ tests/ test/ docs/ .github/; do
|
||||||
|
if [ -d "$f" ]; then
|
||||||
|
items+=("$f")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for f in index.html index.ts index.js main.ts main.js App.tsx App.vue; do
|
||||||
|
if [ -f "src/$f" ] || [ -f "$f" ]; then
|
||||||
|
items+=("$f")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "${items[*]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_entire_status() {
|
||||||
|
if command -v entire &>/dev/null; then
|
||||||
|
local status
|
||||||
|
status=$(entire status 2>/dev/null | head -1)
|
||||||
|
echo "$status"
|
||||||
|
else
|
||||||
|
echo "not installed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Generate ---
|
||||||
|
|
||||||
|
PROJECT_NAME=$(detect_project_name)
|
||||||
|
PROJECT_DESC=$(detect_project_description)
|
||||||
|
TECH_STACK=$(detect_tech_stack)
|
||||||
|
GIT_INFO=$(detect_git_info)
|
||||||
|
SCRIPTS=$(detect_scripts)
|
||||||
|
STRUCTURE=$(detect_project_structure)
|
||||||
|
ENTIRE_STATUS=$(detect_entire_status)
|
||||||
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
# --- Check if update is needed ---
|
||||||
|
|
||||||
|
if [ -f "$CLAUDE_MD" ]; then
|
||||||
|
# Check if file was auto-generated by us
|
||||||
|
if ! grep -q "$MARKER" "$CLAUDE_MD" 2>/dev/null; then
|
||||||
|
# User-created CLAUDE.md — don't overwrite, just append status section if missing
|
||||||
|
if ! grep -q "## Auto-detected Project Info" "$CLAUDE_MD"; then
|
||||||
|
cat >> "$CLAUDE_MD" << APPEND
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Auto-detected Project Info
|
||||||
|
$MARKER
|
||||||
|
_Last updated: ${TIMESTAMP}_
|
||||||
|
|
||||||
|
**Tech stack**: $TECH_STACK
|
||||||
|
**Entire CLI**: $ENTIRE_STATUS
|
||||||
|
|
||||||
|
### Git
|
||||||
|
$GIT_INFO
|
||||||
|
|
||||||
|
### Available scripts
|
||||||
|
$SCRIPTS
|
||||||
|
APPEND
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Write CLAUDE.md (to temp file first for idempotency check) ---
|
||||||
|
|
||||||
|
TMPFILE=$(mktemp)
|
||||||
|
trap 'rm -f "$TMPFILE"' EXIT
|
||||||
|
|
||||||
|
cat > "$TMPFILE" << EOF
|
||||||
|
# $PROJECT_NAME
|
||||||
|
$MARKER
|
||||||
|
_Last updated: ${TIMESTAMP}_
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ -n "$PROJECT_DESC" ]; then
|
||||||
|
cat >> "$TMPFILE" << EOF
|
||||||
|
$PROJECT_DESC
|
||||||
|
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat >> "$TMPFILE" << EOF
|
||||||
|
## Tech Stack
|
||||||
|
$TECH_STACK
|
||||||
|
|
||||||
|
## Git
|
||||||
|
$GIT_INFO
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
Key directories: $STRUCTURE
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
$SCRIPTS
|
||||||
|
|
||||||
|
## Entire CLI
|
||||||
|
- **Status**: $ENTIRE_STATUS
|
||||||
|
- **Strategy**: auto-commit
|
||||||
|
- **Telemetry**: disabled
|
||||||
|
- **Push sessions**: enabled
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# --- Multi-machine sync section ---
|
||||||
|
if [ -f ".entire/git-sync.sh" ]; then
|
||||||
|
cat >> "$TMPFILE" << 'EOF'
|
||||||
|
|
||||||
|
## Multi-machine Sync
|
||||||
|
This project uses automatic git synchronization between machines:
|
||||||
|
- **SessionStart**: `git-sync.sh pull` runs before session — fetches and rebases from remote
|
||||||
|
- **post-commit**: `git-sync.sh push` runs in background — pushes to remote after each commit
|
||||||
|
- **SessionEnd**: `git-sync.sh push` runs — final push when session ends
|
||||||
|
- **Scope**: only main/master branch; feature branches are not synced
|
||||||
|
- **Conflicts**: if rebase fails, it aborts and creates a backup tag `sync/backup/pre-rebase/TIMESTAMP`
|
||||||
|
- **Diagnostics**: `bash .entire/git-sync.sh status`
|
||||||
|
- **Backup recovery**: `git tag -l 'sync/backup/*'` then `git reset --hard <tag>`
|
||||||
|
|
||||||
|
**Important for agents**: Do NOT manually push/pull on main/master — sync is automatic.
|
||||||
|
If you see "[git-sync] Behind remote by N commits" at session start, rebase already happened.
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Patcher section (claude_code_patcher specific) ---
|
||||||
|
if [ -f "update_patcher.py" ] && [ -d "updater" ]; then
|
||||||
|
cat >> "$TMPFILE" << 'EOF'
|
||||||
|
|
||||||
|
## Claude Code Patcher
|
||||||
|
This project patches Claude Code CLI (`cli.js`) to work with custom API endpoints.
|
||||||
|
|
||||||
|
### Quick reference
|
||||||
|
- **Check for new version**: `python3 update_patcher.py --check`
|
||||||
|
- **Full auto-update**: `python3 update_patcher.py --auto` (check → download → validate → patch → release)
|
||||||
|
- **Skill**: `/update-patcher` — runs the full pipeline with agent guidance
|
||||||
|
- **Releases**: `releases/` directory — versioned patched cli.js files, `releases/latest` symlink
|
||||||
|
- **Validation**: 13 patch targets, GREEN/YELLOW/RED classification
|
||||||
|
- **Config**: `patcher.config.json` — target version and settings
|
||||||
|
|
||||||
|
### If YELLOW/RED patches found
|
||||||
|
1. Read `.update_work/v<VERSION>/validation_report.json`
|
||||||
|
2. Find anchor strings in downloaded `cli.js`
|
||||||
|
3. Update regex in `claude_code_patcher.py`
|
||||||
|
4. Re-validate: `python3 update_patcher.py --validate --version <VERSION>`
|
||||||
|
|
||||||
|
### Key files
|
||||||
|
- `claude_code_patcher.py` — main patcher with all 13 regex patterns
|
||||||
|
- `update_patcher.py` — CLI for update pipeline
|
||||||
|
- `updater/` — modules: version_checker, downloader, pattern_validator, patch_applier, release_manager, changelog_fetcher
|
||||||
|
- `releases/` — versioned releases with metadata, changelogs, patched cli.js
|
||||||
|
- `.claude/commands/update-patcher.md` — skill definition
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat >> "$TMPFILE" << 'EOF'
|
||||||
|
|
||||||
|
## Anonymous Identity
|
||||||
|
Git commits MUST use anonymous device ID, never real name/email/hostname.
|
||||||
|
- Correct: `delta-cloud-208e <delta-cloud-208e@device.local>`
|
||||||
|
- Wrong: `root <root@hostname.example.com>` or `John <john@gmail.com>`
|
||||||
|
- Identity is auto-set by `generate-claude-md.sh` on each SessionStart
|
||||||
|
- If you see real name/email in `git config user.name` or `git log`, run: `bash .entire/generate-device-id.sh` and apply result via `git config --global user.name/email`
|
||||||
|
- Remote URLs must NOT contain credentials inline (use `git credential store`)
|
||||||
|
|
||||||
|
## Session Recovery
|
||||||
|
When context is compacted, the SessionStart hook restores:
|
||||||
|
- Project name, tech stack, and git info
|
||||||
|
- Last 10 commits via `git log --oneline -10`
|
||||||
|
- This file is re-read automatically
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
- Use conventional commits (feat:, fix:, refactor:, docs:, chore:)
|
||||||
|
- All Claude Code sessions are tracked by Entire CLI
|
||||||
|
- Run `entire status` to verify tracking is active
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# --- Idempotency check: compare content without timestamp ---
|
||||||
|
if [ -f "$CLAUDE_MD" ]; then
|
||||||
|
# Strip the timestamp line for comparison (it changes every run)
|
||||||
|
OLD_CONTENT=$(sed '/_Last updated:/d' "$CLAUDE_MD")
|
||||||
|
NEW_CONTENT=$(sed '/_Last updated:/d' "$TMPFILE")
|
||||||
|
|
||||||
|
if [ "$OLD_CONTENT" = "$NEW_CONTENT" ]; then
|
||||||
|
echo "[generate-claude-md] No changes detected, skipping update"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv "$TMPFILE" "$CLAUDE_MD"
|
||||||
|
echo "[generate-claude-md] Updated $CLAUDE_MD ($TIMESTAMP)"
|
||||||
81
.entire/generate-device-id.sh
Executable file
81
.entire/generate-device-id.sh
Executable file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# generate-device-id.sh — Generate anonymous device nickname for git commits
|
||||||
|
#
|
||||||
|
# Creates a persistent device identity like "phantom-fox-a3b2" that:
|
||||||
|
# - Hides real username, hostname, and IP
|
||||||
|
# - Stays the same across sessions on the same machine
|
||||||
|
# - Is unique per device (based on hardware ID hash)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/entire"
|
||||||
|
DEVICE_ID_FILE="$CONFIG_DIR/device-id"
|
||||||
|
|
||||||
|
# If already generated, just output it
|
||||||
|
if [ -f "$DEVICE_ID_FILE" ]; then
|
||||||
|
cat "$DEVICE_ID_FILE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Generate hardware fingerprint ---
|
||||||
|
get_machine_id() {
|
||||||
|
# Try multiple sources for a stable machine ID
|
||||||
|
if [ -f "/etc/machine-id" ]; then
|
||||||
|
cat /etc/machine-id
|
||||||
|
elif [ -f "/var/lib/dbus/machine-id" ]; then
|
||||||
|
cat /var/lib/dbus/machine-id
|
||||||
|
elif command -v ioreg &>/dev/null; then
|
||||||
|
# macOS: IOPlatformSerialNumber
|
||||||
|
ioreg -rd1 -c IOPlatformExpertDevice 2>/dev/null | grep -o '"IOPlatformSerialNumber" = "[^"]*"' | sed 's/.*= "//;s/"//'
|
||||||
|
elif command -v wmic &>/dev/null; then
|
||||||
|
# Windows
|
||||||
|
wmic csproduct get uuid 2>/dev/null | tail -1 | tr -d ' \r\n'
|
||||||
|
else
|
||||||
|
# Fallback: hostname + username hash
|
||||||
|
echo "$(hostname)$(whoami)$(date +%s)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Word lists for readable nicknames ---
|
||||||
|
ADJECTIVES=(
|
||||||
|
phantom silent cosmic rapid swift
|
||||||
|
frozen arctic lunar solar neon
|
||||||
|
hidden shadow cipher binary quantum
|
||||||
|
iron steel chrome cobalt silver
|
||||||
|
alpha delta omega sigma theta
|
||||||
|
rogue ghost echo pulse drift
|
||||||
|
)
|
||||||
|
|
||||||
|
NOUNS=(
|
||||||
|
fox wolf hawk bear lynx
|
||||||
|
node core byte mesh grid
|
||||||
|
spark flame pulse wave beam
|
||||||
|
tower vault forge nexus axis
|
||||||
|
rover scout pilot agent proxy
|
||||||
|
cloud storm frost ember flare
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Deterministic selection from hardware ID ---
|
||||||
|
MACHINE_ID=$(get_machine_id)
|
||||||
|
if command -v sha256sum &>/dev/null; then
|
||||||
|
HASH=$(echo -n "$MACHINE_ID" | sha256sum | cut -c1-8)
|
||||||
|
elif command -v shasum &>/dev/null; then
|
||||||
|
HASH=$(echo -n "$MACHINE_ID" | shasum -a 256 | cut -c1-8)
|
||||||
|
else
|
||||||
|
HASH=$(echo -n "$MACHINE_ID" | md5sum 2>/dev/null | cut -c1-8 || echo "00000000")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Convert first 4 hex chars to indices
|
||||||
|
ADJ_IDX=$(( 16#${HASH:0:2} % ${#ADJECTIVES[@]} ))
|
||||||
|
NOUN_IDX=$(( 16#${HASH:2:2} % ${#NOUNS[@]} ))
|
||||||
|
SUFFIX="${HASH:4:4}"
|
||||||
|
|
||||||
|
DEVICE_NAME="${ADJECTIVES[$ADJ_IDX]}-${NOUNS[$NOUN_IDX]}-${SUFFIX}"
|
||||||
|
|
||||||
|
# --- Persist ---
|
||||||
|
mkdir -p "$CONFIG_DIR"
|
||||||
|
echo "$DEVICE_NAME" > "$DEVICE_ID_FILE"
|
||||||
|
|
||||||
|
echo "$DEVICE_NAME"
|
||||||
583
.entire/git-sync.sh
Executable file
583
.entire/git-sync.sh
Executable file
@@ -0,0 +1,583 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# git-sync.sh — Multi-machine git sync for Entire CLI projects
|
||||||
|
# =============================================================================
|
||||||
|
# Usage:
|
||||||
|
# bash .entire/git-sync.sh pull # SessionStart: fetch + rebase
|
||||||
|
# bash .entire/git-sync.sh push # post-commit: push to remote (background)
|
||||||
|
# bash .entire/git-sync.sh status # Diagnostics
|
||||||
|
# bash .entire/git-sync.sh cleanup # Remove old backup tags
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
SYNC_DIR=".entire/tmp"
|
||||||
|
SYNC_LOG="$SYNC_DIR/sync.log"
|
||||||
|
SYNC_LOCK="$SYNC_DIR/sync.lock"
|
||||||
|
NETWORK_TIMEOUT=30
|
||||||
|
BACKUP_TAG_PREFIX="sync/backup"
|
||||||
|
DAILY_TAG_PREFIX="sync/daily"
|
||||||
|
BACKUP_MAX_AGE_DAYS=7
|
||||||
|
DAILY_MAX_AGE_DAYS=30
|
||||||
|
ARCHIVE_INTERVAL_DAYS=3
|
||||||
|
ARCHIVE_BRANCH="archive/snapshots"
|
||||||
|
ARCHIVE_LOCAL_DIR=".entire/tmp/archives"
|
||||||
|
STALE_LOCK_SECONDS=120
|
||||||
|
|
||||||
|
# --- Logging ---
|
||||||
|
log() {
|
||||||
|
mkdir -p "$SYNC_DIR"
|
||||||
|
local ts
|
||||||
|
ts=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
echo "[$ts] $*" >> "$SYNC_LOG"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_and_echo() {
|
||||||
|
log "$@"
|
||||||
|
echo "[git-sync] $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Lock management ---
|
||||||
|
acquire_lock() {
|
||||||
|
mkdir -p "$SYNC_DIR"
|
||||||
|
|
||||||
|
# Remove stale lock
|
||||||
|
if [ -f "$SYNC_LOCK" ]; then
|
||||||
|
local lock_pid lock_age
|
||||||
|
lock_pid=$(cat "$SYNC_LOCK" 2>/dev/null || echo "")
|
||||||
|
lock_age=$(( $(date +%s) - $(stat -c %Y "$SYNC_LOCK" 2>/dev/null || stat -f %m "$SYNC_LOCK" 2>/dev/null || echo 0) ))
|
||||||
|
|
||||||
|
if [ -n "$lock_pid" ] && ! kill -0 "$lock_pid" 2>/dev/null; then
|
||||||
|
log "Removing lock from dead process (PID: $lock_pid)"
|
||||||
|
rm -f "$SYNC_LOCK"
|
||||||
|
elif [ "$lock_age" -gt "$STALE_LOCK_SECONDS" ]; then
|
||||||
|
log "Removing stale lock (age: ${lock_age}s)"
|
||||||
|
rm -f "$SYNC_LOCK"
|
||||||
|
else
|
||||||
|
log "Lock held by process $lock_pid (age: ${lock_age}s), skipping"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo $$ > "$SYNC_LOCK"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
release_lock() {
|
||||||
|
rm -f "$SYNC_LOCK"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Detect current branch ---
|
||||||
|
current_branch() {
|
||||||
|
git symbolic-ref --short HEAD 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Check if we should sync (only main/master) ---
|
||||||
|
should_sync() {
|
||||||
|
local branch
|
||||||
|
branch=$(current_branch)
|
||||||
|
if [ -z "$branch" ]; then
|
||||||
|
log "Detached HEAD, skipping sync"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
case "$branch" in
|
||||||
|
main|master) return 0 ;;
|
||||||
|
*)
|
||||||
|
log "Branch '$branch' is not main/master, skipping sync"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Detect remote for current branch ---
|
||||||
|
detect_remote() {
|
||||||
|
local branch
|
||||||
|
branch=$(current_branch)
|
||||||
|
local remote
|
||||||
|
remote=$(git config "branch.${branch}.remote" 2>/dev/null)
|
||||||
|
if [ -n "$remote" ]; then
|
||||||
|
echo "$remote"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback: try first available remote
|
||||||
|
remote=$(git remote | head -1)
|
||||||
|
if [ -n "$remote" ]; then
|
||||||
|
echo "$remote"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Create backup tag ---
|
||||||
|
create_backup_tag() {
|
||||||
|
local ts
|
||||||
|
ts=$(date '+%Y%m%d-%H%M%S')
|
||||||
|
local tag="${BACKUP_TAG_PREFIX}/pre-rebase/${ts}"
|
||||||
|
git tag "$tag" HEAD 2>/dev/null && log "Backup tag: $tag"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Daily snapshot tag with summary ---
|
||||||
|
create_daily_tag() {
|
||||||
|
local today
|
||||||
|
today=$(date '+%Y-%m-%d')
|
||||||
|
local tag="${DAILY_TAG_PREFIX}/${today}"
|
||||||
|
|
||||||
|
# Skip if today's tag already exists
|
||||||
|
if git tag -l "$tag" 2>/dev/null | grep -q .; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find yesterday's tag or the most recent daily tag
|
||||||
|
local prev_ref=""
|
||||||
|
prev_ref=$(git tag -l "${DAILY_TAG_PREFIX}/*" 2>/dev/null | sort -r | head -1)
|
||||||
|
|
||||||
|
# Build summary of commits since last daily tag (or last 24h)
|
||||||
|
local summary=""
|
||||||
|
if [ -n "$prev_ref" ]; then
|
||||||
|
summary=$(git log --oneline "${prev_ref}..HEAD" 2>/dev/null | head -20)
|
||||||
|
else
|
||||||
|
summary=$(git log --oneline --since="yesterday" 2>/dev/null | head -20)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$summary" ]; then
|
||||||
|
summary="(no new commits)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local commit_count
|
||||||
|
if [ -n "$prev_ref" ]; then
|
||||||
|
commit_count=$(git rev-list --count "${prev_ref}..HEAD" 2>/dev/null || echo 0)
|
||||||
|
else
|
||||||
|
commit_count=$(git log --oneline --since="yesterday" 2>/dev/null | wc -l)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create annotated tag with summary as message
|
||||||
|
local msg
|
||||||
|
msg=$(printf "Daily snapshot %s (%s commits)\n\n%s" "$today" "$commit_count" "$summary")
|
||||||
|
git tag -a "$tag" -m "$msg" HEAD 2>/dev/null && log "Daily tag: $tag ($commit_count commits)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Periodic archive: full snapshot to archive/snapshots branch ---
|
||||||
|
create_periodic_archive() {
|
||||||
|
local today
|
||||||
|
today=$(date '+%Y-%m-%d')
|
||||||
|
local branch
|
||||||
|
branch=$(current_branch)
|
||||||
|
local project_name
|
||||||
|
project_name=$(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
|
||||||
|
|
||||||
|
# Check if archive branch exists, get last archive date
|
||||||
|
local last_archive_date="0000-00-00"
|
||||||
|
if git rev-parse --verify "$ARCHIVE_BRANCH" &>/dev/null; then
|
||||||
|
# Get date from last commit message on archive branch
|
||||||
|
last_archive_date=$(git log "$ARCHIVE_BRANCH" -1 --format="%s" 2>/dev/null | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' | head -1 || echo "0000-00-00")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check interval: skip if last archive is less than ARCHIVE_INTERVAL_DAYS ago
|
||||||
|
if [ "$last_archive_date" != "0000-00-00" ]; then
|
||||||
|
local last_epoch today_epoch
|
||||||
|
last_epoch=$(date -d "$last_archive_date" +%s 2>/dev/null || date -j -f '%Y-%m-%d' "$last_archive_date" +%s 2>/dev/null || echo 0)
|
||||||
|
today_epoch=$(date +%s)
|
||||||
|
local diff_days=$(( (today_epoch - last_epoch) / 86400 ))
|
||||||
|
if [ "$diff_days" -lt "$ARCHIVE_INTERVAL_DAYS" ]; then
|
||||||
|
log "Archive skipped: last archive $last_archive_date ($diff_days days ago, interval=${ARCHIVE_INTERVAL_DAYS})"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if there are new commits since last archive
|
||||||
|
local new_commits=0
|
||||||
|
if git rev-parse --verify "$ARCHIVE_BRANCH" &>/dev/null; then
|
||||||
|
# Find the source commit hash stored in last archive
|
||||||
|
local last_source_hash
|
||||||
|
last_source_hash=$(git log "$ARCHIVE_BRANCH" -1 --format="%b" 2>/dev/null | sed -n 's/^Source: \([a-f0-9]*\).*/\1/p' | head -1)
|
||||||
|
if [ -n "$last_source_hash" ] && git rev-parse --verify "$last_source_hash" &>/dev/null; then
|
||||||
|
new_commits=$(git rev-list --count "${last_source_hash}..HEAD" 2>/dev/null || echo 0)
|
||||||
|
else
|
||||||
|
new_commits=1 # Can't determine, assume changes
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
new_commits=$(git rev-list --count HEAD 2>/dev/null || echo 1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$new_commits" -eq 0 ]; then
|
||||||
|
log "Archive skipped: 0 new commits since last archive"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_and_echo "Creating archive ($new_commits new commits since last snapshot)..."
|
||||||
|
|
||||||
|
local short_hash
|
||||||
|
short_hash=$(git rev-parse --short HEAD 2>/dev/null)
|
||||||
|
local archive_name="${project_name}_${today}_${short_hash}.tar.gz"
|
||||||
|
|
||||||
|
# Create archive in temp
|
||||||
|
local tmpdir
|
||||||
|
tmpdir=$(mktemp -d)
|
||||||
|
|
||||||
|
if ! git archive --format=tar.gz --prefix="${project_name}/" -o "${tmpdir}/${archive_name}" HEAD 2>>"$SYNC_LOG"; then
|
||||||
|
log "Archive creation failed"
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local size
|
||||||
|
size=$(du -h "${tmpdir}/${archive_name}" | cut -f1)
|
||||||
|
|
||||||
|
# Build changelog
|
||||||
|
local changelog="${tmpdir}/CHANGELOG.md"
|
||||||
|
local prev_tag
|
||||||
|
prev_tag=$(git tag -l "${DAILY_TAG_PREFIX}/*" 2>/dev/null | sort -r | head -1)
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Archive ${today} (${branch}@${short_hash})"
|
||||||
|
echo ""
|
||||||
|
echo "**Project**: ${project_name}"
|
||||||
|
echo "**Branch**: ${branch}"
|
||||||
|
echo "**Commit**: ${short_hash} ($(git log -1 --format='%s' HEAD 2>/dev/null))"
|
||||||
|
echo "**Size**: ${size}"
|
||||||
|
echo "**Date**: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||||
|
echo ""
|
||||||
|
echo "## Commits since last archive"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if git rev-parse --verify "$ARCHIVE_BRANCH" &>/dev/null; then
|
||||||
|
local last_src
|
||||||
|
last_src=$(git log "$ARCHIVE_BRANCH" -1 --format="%b" 2>/dev/null | sed -n 's/^Source: \([a-f0-9]*\).*/\1/p' | head -1)
|
||||||
|
if [ -n "$last_src" ] && git rev-parse --verify "$last_src" &>/dev/null; then
|
||||||
|
git log --oneline "${last_src}..HEAD" 2>/dev/null | while read -r line; do
|
||||||
|
echo "- ${line}"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
git log --oneline -20 HEAD 2>/dev/null | while read -r line; do
|
||||||
|
echo "- ${line}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
git log --oneline -50 HEAD 2>/dev/null | while read -r line; do
|
||||||
|
echo "- ${line}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
} > "$changelog"
|
||||||
|
|
||||||
|
# Save local copy
|
||||||
|
mkdir -p "$ARCHIVE_LOCAL_DIR"
|
||||||
|
cp "${tmpdir}/${archive_name}" "$ARCHIVE_LOCAL_DIR/"
|
||||||
|
cp "${tmpdir}/CHANGELOG.md" "${ARCHIVE_LOCAL_DIR}/CHANGELOG_${today}.md"
|
||||||
|
log "Local archive copy: ${ARCHIVE_LOCAL_DIR}/${archive_name}"
|
||||||
|
|
||||||
|
# Cleanup local archives older than 30 days
|
||||||
|
local cutoff
|
||||||
|
cutoff=$(date -d "-30 days" '+%Y-%m-%d' 2>/dev/null || \
|
||||||
|
date -v "-30d" '+%Y-%m-%d' 2>/dev/null || echo "0000-00-00")
|
||||||
|
for f in "${ARCHIVE_LOCAL_DIR}/${project_name}_"*.tar.gz; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
local fdate
|
||||||
|
fdate=$(echo "$f" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' | tail -1)
|
||||||
|
if [ -n "$fdate" ] && [ "$fdate" \< "$cutoff" ]; then
|
||||||
|
rm -f "$f"
|
||||||
|
# Also remove matching changelog
|
||||||
|
rm -f "${ARCHIVE_LOCAL_DIR}/CHANGELOG_${fdate}.md"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Switch to archive branch (orphan if new), commit, switch back
|
||||||
|
local original_branch
|
||||||
|
original_branch=$(current_branch)
|
||||||
|
local archive_stashed=false
|
||||||
|
|
||||||
|
# Safety trap: restore branch on interrupt
|
||||||
|
archive_cleanup() {
|
||||||
|
if [ "$(current_branch 2>/dev/null)" != "$original_branch" ]; then
|
||||||
|
git checkout "$original_branch" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [ "$archive_stashed" = true ]; then
|
||||||
|
git stash pop 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap archive_cleanup INT TERM
|
||||||
|
|
||||||
|
if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
|
||||||
|
git stash push -m "archive-stash" 2>/dev/null && archive_stashed=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git rev-parse --verify "$ARCHIVE_BRANCH" &>/dev/null; then
|
||||||
|
git checkout "$ARCHIVE_BRANCH" 2>>"$SYNC_LOG"
|
||||||
|
else
|
||||||
|
git checkout --orphan "$ARCHIVE_BRANCH" 2>>"$SYNC_LOG"
|
||||||
|
git rm -rf . 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy archive and changelog
|
||||||
|
cp "${tmpdir}/${archive_name}" .
|
||||||
|
cp "${tmpdir}/CHANGELOG.md" .
|
||||||
|
git add "${archive_name}" CHANGELOG.md 2>/dev/null
|
||||||
|
|
||||||
|
# Commit with source hash in body for tracking
|
||||||
|
local full_hash
|
||||||
|
full_hash=$(git rev-parse "$original_branch" 2>/dev/null || echo "$short_hash")
|
||||||
|
git commit -m "snapshot ${today} (${branch}@${short_hash}, ${size})" \
|
||||||
|
-m "Source: ${full_hash}" \
|
||||||
|
2>>"$SYNC_LOG"
|
||||||
|
|
||||||
|
# Push archive branch
|
||||||
|
local remote
|
||||||
|
remote=$(detect_remote) || true
|
||||||
|
if [ -n "$remote" ]; then
|
||||||
|
timeout "$NETWORK_TIMEOUT" git push "$remote" "$ARCHIVE_BRANCH" 2>>"$SYNC_LOG" || \
|
||||||
|
log "Archive branch push failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Switch back and remove trap
|
||||||
|
git checkout "$original_branch" 2>>"$SYNC_LOG"
|
||||||
|
if [ "$archive_stashed" = true ]; then
|
||||||
|
git stash pop 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
trap - INT TERM
|
||||||
|
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
log_and_echo "Archive committed to ${ARCHIVE_BRANCH}: ${archive_name} (${size}, ${new_commits} commits)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Push tags to remote ---
|
||||||
|
push_tags() {
|
||||||
|
local remote
|
||||||
|
remote=$(detect_remote) || return 0
|
||||||
|
|
||||||
|
# Push only sync/* tags (not all tags)
|
||||||
|
local tag
|
||||||
|
while IFS= read -r tag; do
|
||||||
|
[ -z "$tag" ] && continue
|
||||||
|
timeout "$NETWORK_TIMEOUT" git push "$remote" "refs/tags/$tag" 2>>"$SYNC_LOG" || \
|
||||||
|
log "Failed to push tag: $tag"
|
||||||
|
done < <(git tag -l "sync/*" 2>/dev/null)
|
||||||
|
log "Sync tags pushed to $remote"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- PULL: fetch + rebase ---
|
||||||
|
do_pull() {
|
||||||
|
if ! should_sync; then
|
||||||
|
echo "[git-sync] Skipped (not on main/master)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local remote
|
||||||
|
remote=$(detect_remote) || {
|
||||||
|
echo "[git-sync] No remote configured, skipping pull"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
local branch
|
||||||
|
branch=$(current_branch)
|
||||||
|
|
||||||
|
if ! acquire_lock; then
|
||||||
|
echo "[git-sync] Another sync in progress, skipping"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
trap release_lock EXIT
|
||||||
|
|
||||||
|
# Fetch with timeout
|
||||||
|
log "Fetching from $remote..."
|
||||||
|
if ! timeout "$NETWORK_TIMEOUT" git fetch "$remote" "$branch" 2>>"$SYNC_LOG"; then
|
||||||
|
log_and_echo "Fetch failed (network timeout or error), continuing with local state"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create daily snapshot tag and periodic archive after fetch (has latest remote state)
|
||||||
|
create_daily_tag
|
||||||
|
create_periodic_archive
|
||||||
|
|
||||||
|
# Check if we're behind
|
||||||
|
local behind
|
||||||
|
behind=$(git rev-list --count "HEAD..${remote}/${branch}" 2>/dev/null || echo 0)
|
||||||
|
if [ "$behind" -eq 0 ]; then
|
||||||
|
log_and_echo "Already up to date with ${remote}/${branch}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_and_echo "Behind ${remote}/${branch} by $behind commit(s), syncing..."
|
||||||
|
|
||||||
|
# Create backup tag before rebase
|
||||||
|
create_backup_tag
|
||||||
|
|
||||||
|
# Stash dirty changes if any
|
||||||
|
local stashed=false
|
||||||
|
if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
|
||||||
|
log "Stashing dirty working tree..."
|
||||||
|
git stash push -m "git-sync auto-stash $(date '+%Y%m%d-%H%M%S')" 2>>"$SYNC_LOG" && stashed=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rebase
|
||||||
|
if git rebase "${remote}/${branch}" 2>>"$SYNC_LOG"; then
|
||||||
|
log_and_echo "Rebased successfully onto ${remote}/${branch} (+$behind commits)"
|
||||||
|
else
|
||||||
|
log_and_echo "WARNING: Rebase conflict! Aborting rebase, backup tag preserved"
|
||||||
|
git rebase --abort 2>/dev/null
|
||||||
|
if [ "$stashed" = true ]; then
|
||||||
|
git stash pop 2>>"$SYNC_LOG" || true
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restore stashed changes
|
||||||
|
if [ "$stashed" = true ]; then
|
||||||
|
if git stash pop 2>>"$SYNC_LOG"; then
|
||||||
|
log "Stash restored successfully"
|
||||||
|
else
|
||||||
|
log_and_echo "WARNING: Stash pop conflict! Your changes are saved in git stash"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- PUSH: push to remote ---
|
||||||
|
do_push() {
|
||||||
|
if ! should_sync; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local remote
|
||||||
|
remote=$(detect_remote) || return 0
|
||||||
|
|
||||||
|
local branch
|
||||||
|
branch=$(current_branch)
|
||||||
|
|
||||||
|
if ! acquire_lock; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
trap release_lock EXIT
|
||||||
|
|
||||||
|
log "Pushing to ${remote}/${branch}..."
|
||||||
|
if timeout "$NETWORK_TIMEOUT" git push "$remote" "$branch" 2>>"$SYNC_LOG"; then
|
||||||
|
log "Push successful"
|
||||||
|
# Push tags to remote after successful push
|
||||||
|
push_tags
|
||||||
|
else
|
||||||
|
log "Push failed (likely behind remote). Will reconcile at next session pull."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- STATUS: diagnostics ---
|
||||||
|
do_status() {
|
||||||
|
local branch
|
||||||
|
branch=$(current_branch)
|
||||||
|
echo "[git-sync] Status"
|
||||||
|
echo " Branch: ${branch:-DETACHED}"
|
||||||
|
|
||||||
|
if ! should_sync; then
|
||||||
|
echo " Sync: DISABLED (not on main/master)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local remote
|
||||||
|
remote=$(detect_remote) || {
|
||||||
|
echo " Remote: NONE"
|
||||||
|
echo " Sync: DISABLED (no remote)"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
echo " Remote: $remote"
|
||||||
|
|
||||||
|
# Fetch silently for status check
|
||||||
|
timeout "$NETWORK_TIMEOUT" git fetch "$remote" "$branch" 2>/dev/null
|
||||||
|
|
||||||
|
local ahead behind
|
||||||
|
ahead=$(git rev-list --count "${remote}/${branch}..HEAD" 2>/dev/null || echo "?")
|
||||||
|
behind=$(git rev-list --count "HEAD..${remote}/${branch}" 2>/dev/null || echo "?")
|
||||||
|
echo " Ahead: $ahead | Behind: $behind"
|
||||||
|
|
||||||
|
local backup_count daily_count
|
||||||
|
backup_count=$(git tag -l "${BACKUP_TAG_PREFIX}/*" 2>/dev/null | wc -l)
|
||||||
|
daily_count=$(git tag -l "${DAILY_TAG_PREFIX}/*" 2>/dev/null | wc -l)
|
||||||
|
echo " Backup tags: $backup_count"
|
||||||
|
echo " Daily snapshots: $daily_count"
|
||||||
|
|
||||||
|
# Show recent daily tags with summaries
|
||||||
|
local latest_daily
|
||||||
|
latest_daily=$(git tag -l "${DAILY_TAG_PREFIX}/*" 2>/dev/null | sort -r | head -3)
|
||||||
|
if [ -n "$latest_daily" ]; then
|
||||||
|
echo " Recent daily snapshots:"
|
||||||
|
echo "$latest_daily" | while read -r dtag; do
|
||||||
|
local dmsg
|
||||||
|
dmsg=$(git tag -n1 "$dtag" 2>/dev/null | sed "s|^${dtag}\s*||")
|
||||||
|
echo " $dtag — $dmsg"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Show archive branch info
|
||||||
|
if git rev-parse --verify "$ARCHIVE_BRANCH" &>/dev/null; then
|
||||||
|
local archive_count
|
||||||
|
archive_count=$(git log "$ARCHIVE_BRANCH" --oneline 2>/dev/null | wc -l)
|
||||||
|
echo " Archive branch: $ARCHIVE_BRANCH ($archive_count snapshots)"
|
||||||
|
echo " Recent archives:"
|
||||||
|
git log "$ARCHIVE_BRANCH" --oneline -3 2>/dev/null | sed 's/^/ /'
|
||||||
|
else
|
||||||
|
echo " Archive branch: not created yet (interval: every ${ARCHIVE_INTERVAL_DAYS} days)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$SYNC_LOG" ]; then
|
||||||
|
echo " Last log entries:"
|
||||||
|
tail -5 "$SYNC_LOG" | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Sync: ENABLED"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- CLEANUP: remove old backup tags ---
|
||||||
|
do_cleanup() {
|
||||||
|
local cutoff
|
||||||
|
cutoff=$(date -d "-${BACKUP_MAX_AGE_DAYS} days" '+%Y%m%d' 2>/dev/null || \
|
||||||
|
date -v "-${BACKUP_MAX_AGE_DAYS}d" '+%Y%m%d' 2>/dev/null || echo "00000000")
|
||||||
|
|
||||||
|
local count=0
|
||||||
|
while IFS= read -r tag; do
|
||||||
|
[ -z "$tag" ] && continue
|
||||||
|
# Extract date from tag: sync/backup/pre-rebase/YYYYMMDD-HHMMSS
|
||||||
|
local tag_date
|
||||||
|
tag_date=$(echo "$tag" | grep -oE '[0-9]{8}' | head -1)
|
||||||
|
if [ -n "$tag_date" ] && [ "$tag_date" -lt "$cutoff" ] 2>/dev/null; then
|
||||||
|
git tag -d "$tag" 2>/dev/null
|
||||||
|
count=$((count + 1))
|
||||||
|
fi
|
||||||
|
done < <(git tag -l "${BACKUP_TAG_PREFIX}/*" 2>/dev/null)
|
||||||
|
|
||||||
|
# Clean old daily tags (older than DAILY_MAX_AGE_DAYS)
|
||||||
|
local daily_cutoff
|
||||||
|
daily_cutoff=$(date -d "-${DAILY_MAX_AGE_DAYS} days" '+%Y-%m-%d' 2>/dev/null || \
|
||||||
|
date -v "-${DAILY_MAX_AGE_DAYS}d" '+%Y-%m-%d' 2>/dev/null || echo "0000-00-00")
|
||||||
|
|
||||||
|
while IFS= read -r tag; do
|
||||||
|
[ -z "$tag" ] && continue
|
||||||
|
local tag_date
|
||||||
|
tag_date=$(echo "$tag" | sed "s|${DAILY_TAG_PREFIX}/||")
|
||||||
|
if [ "$tag_date" \< "$daily_cutoff" ] 2>/dev/null; then
|
||||||
|
git tag -d "$tag" 2>/dev/null
|
||||||
|
count=$((count + 1))
|
||||||
|
fi
|
||||||
|
done < <(git tag -l "${DAILY_TAG_PREFIX}/*" 2>/dev/null)
|
||||||
|
|
||||||
|
log_and_echo "Cleaned up $count old backup/daily tag(s)"
|
||||||
|
|
||||||
|
# Trim sync log if > 1000 lines
|
||||||
|
if [ -f "$SYNC_LOG" ]; then
|
||||||
|
local lines
|
||||||
|
lines=$(wc -l < "$SYNC_LOG")
|
||||||
|
if [ "$lines" -gt 1000 ]; then
|
||||||
|
tail -500 "$SYNC_LOG" > "${SYNC_LOG}.tmp" && mv "${SYNC_LOG}.tmp" "$SYNC_LOG"
|
||||||
|
log "Trimmed sync log from $lines to 500 lines"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Main ---
|
||||||
|
case "${1:-}" in
|
||||||
|
pull) do_pull ;;
|
||||||
|
push) do_push ;;
|
||||||
|
status) do_status ;;
|
||||||
|
cleanup) do_cleanup ;;
|
||||||
|
*)
|
||||||
|
echo "Usage: bash .entire/git-sync.sh {pull|push|status|cleanup}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
11
.entire/settings.json
Normal file
11
.entire/settings.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"strategy": "auto-commit",
|
||||||
|
"enabled": true,
|
||||||
|
"telemetry": false,
|
||||||
|
"strategy_options": {
|
||||||
|
"push_sessions": true,
|
||||||
|
"summarize": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
CLAUDE.md
Normal file
54
CLAUDE.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# gitea-token-access
|
||||||
|
<!-- auto-generated by generate-claude-md.sh -->
|
||||||
|
_Last updated: 2026-02-21T14:51:31Z_
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
|
||||||
|
## Git
|
||||||
|
- **Branch**: master
|
||||||
|
- **Remote `sensey`**: https://git.sensey24.ru/aibot777/gitea-token-access.git
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
Key directories: docs/
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
|
||||||
|
## Entire CLI
|
||||||
|
- **Status**: Enabled (auto-commit)
|
||||||
|
- **Strategy**: auto-commit
|
||||||
|
- **Telemetry**: disabled
|
||||||
|
- **Push sessions**: enabled
|
||||||
|
|
||||||
|
## Multi-machine Sync
|
||||||
|
This project uses automatic git synchronization between machines:
|
||||||
|
- **SessionStart**: `git-sync.sh pull` runs before session — fetches and rebases from remote
|
||||||
|
- **post-commit**: `git-sync.sh push` runs in background — pushes to remote after each commit
|
||||||
|
- **SessionEnd**: `git-sync.sh push` runs — final push when session ends
|
||||||
|
- **Scope**: only main/master branch; feature branches are not synced
|
||||||
|
- **Conflicts**: if rebase fails, it aborts and creates a backup tag `sync/backup/pre-rebase/TIMESTAMP`
|
||||||
|
- **Diagnostics**: `bash .entire/git-sync.sh status`
|
||||||
|
- **Backup recovery**: `git tag -l 'sync/backup/*'` then `git reset --hard <tag>`
|
||||||
|
|
||||||
|
**Important for agents**: Do NOT manually push/pull on main/master — sync is automatic.
|
||||||
|
If you see "[git-sync] Behind remote by N commits" at session start, rebase already happened.
|
||||||
|
|
||||||
|
## Anonymous Identity
|
||||||
|
Git commits MUST use anonymous device ID, never real name/email/hostname.
|
||||||
|
- Correct: `delta-cloud-208e <delta-cloud-208e@device.local>`
|
||||||
|
- Wrong: `root <root@hostname.example.com>` or `John <john@gmail.com>`
|
||||||
|
- Identity is auto-set by `generate-claude-md.sh` on each SessionStart
|
||||||
|
- If you see real name/email in `git config user.name` or `git log`, run: `bash .entire/generate-device-id.sh` and apply result via `git config --global user.name/email`
|
||||||
|
- Remote URLs must NOT contain credentials inline (use `git credential store`)
|
||||||
|
|
||||||
|
## Session Recovery
|
||||||
|
When context is compacted, the SessionStart hook restores:
|
||||||
|
- Project name, tech stack, and git info
|
||||||
|
- Last 10 commits via `git log --oneline -10`
|
||||||
|
- This file is re-read automatically
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
- Use conventional commits (feat:, fix:, refactor:, docs:, chore:)
|
||||||
|
- All Claude Code sessions are tracked by Entire CLI
|
||||||
|
- Run `entire status` to verify tracking is active
|
||||||
Reference in New Issue
Block a user