Compare commits
44 Commits
6d9144830b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 95cf7b85fa | |||
| 1e7af09245 | |||
| 7745bc8bbc | |||
| 8fc95aff22 | |||
| 0c5e6b8688 | |||
| 9bd1452042 | |||
| 82e287b265 | |||
| 597282cbd7 | |||
| 060b95f699 | |||
| 436c723a2f | |||
| 0b0f7d2a9f | |||
| d63f187bf2 | |||
| 87e75b77e8 | |||
| a86b80e83a | |||
| 0da1703140 | |||
| f0cb155fa4 | |||
| b14d3acbba | |||
| f0b27d5a1a | |||
| ae90cd7e0b | |||
| 5c405ff5fa | |||
| d20e827a5c | |||
| f3c19ba6e2 | |||
| 7c0b857a2f | |||
| bd84a50e98 | |||
| 5c8d1d3529 | |||
| 50fe0d87d7 | |||
| 39edaf3b83 | |||
| 8fed12b3ab | |||
| 457b989c1f | |||
| c8642368e9 | |||
| 5a5a2498c0 | |||
| fc720ac05a | |||
| 7f2ca44ef6 | |||
| db89b9e88e | |||
| f20bfeea80 | |||
| 87c98b26b8 | |||
| f6b13246d8 | |||
| 2e9be8375c | |||
| 8f3055be62 | |||
| cd88440361 | |||
| f73fd68e49 | |||
| 1d8427422a | |||
| 4d98d83fbb | |||
| dc5ce61bf7 |
@@ -33,15 +33,18 @@ Generate an access token for the current user.
|
||||
{baseDir}/scripts/git-ctrl generate-access-token
|
||||
```
|
||||
|
||||
### Create Repository
|
||||
### Repository Operations
|
||||
|
||||
Create a new git repository on git.hangman-lab.top.
|
||||
Manage repositories via `git-ctrl repo`:
|
||||
|
||||
```bash
|
||||
{baseDir}/scripts/git-ctrl create-repo <repo-name>
|
||||
{baseDir}/scripts/git-ctrl repo create <repo-name>
|
||||
{baseDir}/scripts/git-ctrl repo add-collaborators --user <user> --repo <repo>
|
||||
{baseDir}/scripts/git-ctrl repo list-all
|
||||
{baseDir}/scripts/git-ctrl repo config --repo-path <path> [--recursive]
|
||||
```
|
||||
|
||||
> **Note**: The repository will be created at `${AGENT_WORKSPACE}/${repo-name}` (default: `/root/.openclaw/workspace/workspace-mentor`)
|
||||
> `create` creates the repo at `${AGENT_WORKSPACE}/${repo-name}`
|
||||
|
||||
### Pull Request Operations
|
||||
|
||||
@@ -67,22 +70,6 @@ Link Keycloak account with Gitea (for OAuth binding).
|
||||
{baseDir}/scripts/git-ctrl link-keycloak
|
||||
```
|
||||
|
||||
### Add Repository Collaborator
|
||||
|
||||
Add a collaborator to a repository.
|
||||
|
||||
```bash
|
||||
{baseDir}/scripts/git-ctrl repo-add-collaborators --user <user> --repo <repo>
|
||||
```
|
||||
|
||||
### Repository Config
|
||||
|
||||
When you clone a repository from git.hangman-lab.top and are ready to develop, or after creating a new local repo with git init, run:
|
||||
|
||||
```bash
|
||||
{baseDir}/scripts/git-ctrl repo-config --repo-path <path to your repo>
|
||||
```
|
||||
|
||||
### External Login Control
|
||||
|
||||
Enable or disable local login on Gitea.
|
||||
@@ -98,4 +85,4 @@ Reset password for the current user (reads username from secret-mgr).
|
||||
|
||||
```bash
|
||||
{baseDir}/scripts/git-ctrl reset-password
|
||||
```
|
||||
```
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
if [[ -z "${AGENT_WORKSPACE:-}" ]]; then
|
||||
echo "Error: script must be executed by pcexec"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <repo-name>"
|
||||
echo ""
|
||||
echo "Create a new git repository on git.hangman-lab.top"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " repo-name Name of the repository to create"
|
||||
exit 2
|
||||
}
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
REPO_NAME="$1"
|
||||
|
||||
# Validate repo name (alphanumeric, hyphens, underscores only)
|
||||
if ! [[ "$REPO_NAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
||||
echo "Error: Invalid repository name '$REPO_NAME'"
|
||||
echo "Only alphanumeric characters, hyphens, and underscores are allowed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_DIR="${AGENT_WORKSPACE}/${REPO_NAME}"
|
||||
|
||||
# Step 1: Create directory
|
||||
echo "Creating directory: ${REPO_DIR}"
|
||||
mkdir -p "${REPO_DIR}"
|
||||
|
||||
# Step 2: cd to directory
|
||||
cd "${REPO_DIR}"
|
||||
|
||||
# Step 3: git init
|
||||
echo "Initializing git repository..."
|
||||
git init
|
||||
|
||||
# Step 4: git remote add origin
|
||||
echo "Adding remote origin..."
|
||||
USERNAME="$(secret-mgr get-username --key git)"
|
||||
REMOTE_URL="https://git.hangman-lab.top/${USERNAME}/${REPO_NAME}.git"
|
||||
git remote add origin "${REMOTE_URL}"
|
||||
echo " Remote: ${REMOTE_URL}"
|
||||
|
||||
# Step 5: Run repo-config
|
||||
echo "Configuring repository..."
|
||||
"$SCRIPT_DIR/repo-config" --repo-path "${REPO_DIR}"
|
||||
|
||||
echo ""
|
||||
echo "Done! Repository created at: ${REPO_DIR}"
|
||||
echo "Remote: ${REMOTE_URL}"
|
||||
@@ -1,17 +1,47 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Verify git credentials first
|
||||
"$SCRIPT_DIR/check-git-cred"
|
||||
|
||||
username=$(secret-mgr get-username --key git)
|
||||
token_output=$("$SCRIPT_DIR/gitea" admin user generate-access-token --username "$username" --token-name "$username")
|
||||
USERNAME=$(ego-mgr get default-username)
|
||||
PASSWORD=$(secret-mgr get-secret --key git)
|
||||
|
||||
# Extract token from output (format: "Access token was successfully created: <token>")
|
||||
token=$(echo "$token_output" | awk '{print $NF}')
|
||||
# Check if token already exists
|
||||
EXISTING=$(curl -s -u "${USERNAME}:${PASSWORD}" \
|
||||
"https://git.hangman-lab.top/api/v1/users/${USERNAME}/tokens")
|
||||
|
||||
secret-mgr set --key git-access-token --username "$username" --secret "$token"
|
||||
EXISTING_ID=$(echo "$EXISTING" | python3 -c "
|
||||
import sys, json
|
||||
tokens = json.load(sys.stdin)
|
||||
name = '$USERNAME'
|
||||
for t in tokens:
|
||||
if t.get('name') == name:
|
||||
print(t.get('id'))
|
||||
" 2>/dev/null)
|
||||
|
||||
if [[ -n "$EXISTING_ID" ]]; then
|
||||
curl -s -u "${USERNAME}:${PASSWORD}" -X DELETE \
|
||||
"https://git.hangman-lab.top/api/v1/users/${USERNAME}/tokens/${EXISTING_ID}" > /dev/null || true
|
||||
fi
|
||||
|
||||
# Create new token
|
||||
RESP=$(curl -s -u "${USERNAME}:${PASSWORD}" -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\": \"${USERNAME}\", \"scopes\": [\"all\"]}" \
|
||||
"https://git.hangman-lab.top/api/v1/users/${USERNAME}/tokens")
|
||||
|
||||
TOKEN=$(echo "$RESP" | python3 -c "
|
||||
import sys, json
|
||||
d = json.load(sys.stdin)
|
||||
print(d.get('sha1', ''))
|
||||
" 2>/dev/null)
|
||||
|
||||
if [[ -z "$TOKEN" ]]; then
|
||||
echo "Failed to generate access token: $RESP"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
secret-mgr set --key git-access-token --username "$USERNAME" --secret "$TOKEN"
|
||||
echo "Access token generated and stored successfully"
|
||||
|
||||
@@ -1,65 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Check if subcommand is provided
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "Usage: $0 <command> [options]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " check-git-cred Verify git credentials"
|
||||
echo " create-git-account Create a new git account"
|
||||
echo " create-repo Create a new repository"
|
||||
echo " pr Pull request operations (create/list/commits/merge)"
|
||||
echo " pr Pull request operations (create/list/commits/merge/show)"
|
||||
echo " generate-access-token Generate access token for current user"
|
||||
echo " link-keycloak Link Keycloak account with Gitea"
|
||||
echo " repo-add-collaborators Add collaborator to repository"
|
||||
echo " repo-config Configure repository"
|
||||
echo " repo Repository operations (create/add-collaborators/list-all/config)"
|
||||
echo " external-login-ctrl Enable/disable local login"
|
||||
echo " reset-password Reset user password"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get subcommand
|
||||
subcommand="$1"
|
||||
shift
|
||||
subcommand="$1"; shift
|
||||
|
||||
# Route to appropriate script
|
||||
case "$subcommand" in
|
||||
check-git-cred)
|
||||
"$SCRIPT_DIR/check-git-cred" "$@"
|
||||
;;
|
||||
create-git-account)
|
||||
"$SCRIPT_DIR/create-git-account" "$@"
|
||||
;;
|
||||
create-repo)
|
||||
"$SCRIPT_DIR/create-repo" "$@"
|
||||
;;
|
||||
pr)
|
||||
"$SCRIPT_DIR/pr" "$@"
|
||||
;;
|
||||
generate-access-token)
|
||||
"$SCRIPT_DIR/generate-access-token" "$@"
|
||||
;;
|
||||
link-keycloak)
|
||||
"$SCRIPT_DIR/link-keycloak" "$@"
|
||||
;;
|
||||
repo-add-collaborators)
|
||||
"$SCRIPT_DIR/repo-add-collaborators" "$@"
|
||||
;;
|
||||
repo-config)
|
||||
"$SCRIPT_DIR/repo-config" "$@"
|
||||
;;
|
||||
external-login-ctrl)
|
||||
"$SCRIPT_DIR/external-login-ctrl" "$@"
|
||||
;;
|
||||
reset-password)
|
||||
"$SCRIPT_DIR/reset-password" "$@"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: $subcommand"
|
||||
echo "Run '$0' for usage information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
check-git-cred) "$SCRIPT_DIR/check-git-cred" "$@" ;;
|
||||
create-git-account) "$SCRIPT_DIR/create-git-account" "$@" ;;
|
||||
pr) "$SCRIPT_DIR/pr" "$@" ;;
|
||||
generate-access-token) "$SCRIPT_DIR/generate-access-token" "$@" ;;
|
||||
link-keycloak) "$SCRIPT_DIR/link-keycloak" "$@" ;;
|
||||
repo) "$SCRIPT_DIR/repo" "$@" ;;
|
||||
external-login-ctrl) "$SCRIPT_DIR/external-login-ctrl" "$@" ;;
|
||||
reset-password) "$SCRIPT_DIR/reset-password" "$@" ;;
|
||||
*) echo "Unknown command: $subcommand"; exit 1 ;;
|
||||
esac
|
||||
207
git-hangman-lab/scripts/publish-package
Executable file
207
git-hangman-lab/scripts/publish-package
Executable file
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
COMMAND=""
|
||||
REGISTRY=""
|
||||
IMAGE=""
|
||||
TAG=""
|
||||
PACKAGE_FILE=""
|
||||
REPO=""
|
||||
|
||||
usage() {
|
||||
echo "Usage:"
|
||||
echo " publish-package docker <registry> <image> <tag> --proj <repo>"
|
||||
echo " publish-package nuget <source> <package-file> --proj <repo>"
|
||||
echo " publish-package pypi <package-file> --proj <repo>"
|
||||
echo " publish-package npm --proj <repo>"
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
docker|nuget|pypi|npm)
|
||||
COMMAND="$1"
|
||||
shift
|
||||
;;
|
||||
--proj)
|
||||
REPO="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$COMMAND" ]]; then
|
||||
usage
|
||||
fi
|
||||
case $COMMAND in
|
||||
docker)
|
||||
[[ -z "$REGISTRY" ]] && REGISTRY="$1" && shift && continue
|
||||
[[ -z "$IMAGE" ]] && IMAGE="$1" && shift && continue
|
||||
[[ -z "$TAG" ]] && TAG="$1" && shift && continue
|
||||
;;
|
||||
nuget)
|
||||
[[ -z "$SOURCE" ]] && SOURCE="$1" && shift && continue
|
||||
[[ -z "$PACKAGE_FILE" ]] && PACKAGE_FILE="$1" && shift && continue
|
||||
;;
|
||||
pypi)
|
||||
[[ -z "$PACKAGE_FILE" ]] && PACKAGE_FILE="$1" && shift && continue
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$COMMAND" ]] || [[ -z "$REPO" ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
do_docker() {
|
||||
if [[ -z "$REGISTRY" ]] || [[ -z "$IMAGE" ]] || [[ -z "$TAG" ]]; then
|
||||
echo "Error: docker requires <registry> <image> <tag>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OWNER=$(ego-mgr get default-username)
|
||||
if [[ -z "$OWNER" ]]; then
|
||||
echo "Error: cannot get username from ego-mgr"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
KEY=$(python3 -c "import uuid; print(uuid.uuid4())")
|
||||
|
||||
LOCKFILE="$HOME/.openclaw/.docker"
|
||||
lock-mgr acquire "$LOCKFILE" "$KEY"
|
||||
|
||||
# Push via SSH tunnel to bypass Cloudflare's 100MB request body limit.
|
||||
# Tunnel forwards 127.0.0.1:$TUNNEL_PORT on this host to Gitea's HTTP port on vps.git.
|
||||
TUNNEL_HOST="root@vps.git"
|
||||
TUNNEL_PORT="5000"
|
||||
TUNNEL_LOCAL="127.0.0.1:${TUNNEL_PORT}"
|
||||
TUNNEL_CTL="$HOME/.openclaw/.docker-tunnel.sock"
|
||||
rm -f "$TUNNEL_CTL"
|
||||
ssh -fN -o ExitOnForwardFailure=yes -o ControlMaster=yes -o ControlPath="$TUNNEL_CTL" \
|
||||
-L "${TUNNEL_LOCAL}:127.0.0.1:3000" "$TUNNEL_HOST"
|
||||
|
||||
cleanup() {
|
||||
ssh -O exit -o ControlPath="$TUNNEL_CTL" "$TUNNEL_HOST" 2>/dev/null || true
|
||||
rm -f "$TUNNEL_CTL"
|
||||
docker logout "$REGISTRY" 2>/dev/null || true
|
||||
docker logout "$TUNNEL_LOCAL" 2>/dev/null || true
|
||||
lock-mgr release "$LOCKFILE" "$KEY" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "Logging in to $REGISTRY (via tunnel $TUNNEL_LOCAL)..."
|
||||
docker login "$TUNNEL_LOCAL" -u "$OWNER" -p "$(secret-mgr get-secret --key git)" 2>/dev/null
|
||||
|
||||
FULL_IMAGE="${REGISTRY}/${OWNER}/${IMAGE}:${TAG}"
|
||||
TUNNEL_IMAGE="${TUNNEL_LOCAL}/${OWNER}/${IMAGE}:${TAG}"
|
||||
echo "Building: $FULL_IMAGE"
|
||||
|
||||
cd "$REPO"
|
||||
docker build -t "$FULL_IMAGE" .
|
||||
docker tag "$FULL_IMAGE" "$TUNNEL_IMAGE"
|
||||
|
||||
echo "Pushing via tunnel: $TUNNEL_IMAGE"
|
||||
docker push "$TUNNEL_IMAGE"
|
||||
docker rmi "$TUNNEL_IMAGE" 2>/dev/null || true
|
||||
|
||||
# Link package to repository
|
||||
TOKEN=$(secret-mgr get-secret --key git-access-token)
|
||||
REPO_NAME=$(basename "$REPO")
|
||||
|
||||
# Determine the actual owner of the git repository
|
||||
REPO_OWNER="$OWNER"
|
||||
if [[ -d "$REPO/.git" ]]; then
|
||||
REMOTE_URL=$(git -C "$REPO" remote get-url origin 2>/dev/null || true)
|
||||
if [[ "$REMOTE_URL" =~ git\.hangman-lab\.top/([^/]+)/ ]]; then
|
||||
REPO_OWNER="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Gitea's link/unlink endpoints are not idempotent: POST /-/link/{repo}
|
||||
# returns HTTP 400 "invalid argument" when the package already has ANY link
|
||||
# (even to the same repo), and POST /-/unlink returns 400 when nothing is
|
||||
# linked. So read the current link first and branch accordingly.
|
||||
TARGET_LINK="${REPO_OWNER}/${REPO_NAME}"
|
||||
STATE_RESP=$(curl -s -w "\n%{http_code}" -u "${OWNER}:${TOKEN}" \
|
||||
"https://git.hangman-lab.top/api/v1/packages/${OWNER}/container/${IMAGE}/${TAG}")
|
||||
STATE_STATUS=$(printf "%s" "$STATE_RESP" | tail -n1)
|
||||
STATE_BODY=$(printf "%s" "$STATE_RESP" | sed '$d')
|
||||
|
||||
CURRENT_LINK=""
|
||||
if [[ "$STATE_STATUS" == "200" ]]; then
|
||||
CURRENT_LINK=$(printf "%s" "$STATE_BODY" | python3 -c \
|
||||
'import sys, json; p=json.load(sys.stdin); r=p.get("repository") or {}; print(r.get("full_name") or "")' \
|
||||
2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
if [[ "$CURRENT_LINK" == "$TARGET_LINK" ]]; then
|
||||
echo "Package already linked to ${TARGET_LINK}, skipping link step."
|
||||
else
|
||||
if [[ -n "$CURRENT_LINK" ]]; then
|
||||
echo "Package currently linked to ${CURRENT_LINK}, unlinking first..."
|
||||
UNLINK_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST -u "${OWNER}:${TOKEN}" \
|
||||
"https://git.hangman-lab.top/api/v1/packages/${OWNER}/container/${IMAGE}/-/unlink")
|
||||
if [[ "$UNLINK_STATUS" != "204" ]]; then
|
||||
echo "Warning: unlink returned HTTP $UNLINK_STATUS, proceeding with link attempt anyway..."
|
||||
fi
|
||||
fi
|
||||
|
||||
LINK_RESP=$(curl -s -w "%{http_code}" -X POST \
|
||||
-u "${OWNER}:${TOKEN}" \
|
||||
"https://git.hangman-lab.top/api/v1/packages/${OWNER}/container/${IMAGE}/-/link/${REPO_NAME}")
|
||||
LINK_STATUS="${LINK_RESP: -3}"
|
||||
LINK_BODY="${LINK_RESP:0:-3}"
|
||||
|
||||
if [[ "$LINK_STATUS" != "200" && "$LINK_STATUS" != "201" ]]; then
|
||||
if echo "$LINK_BODY" | grep -q '"message".*repository does not exist'; then
|
||||
echo "Warning: repository '$REPO_NAME' is not owned by '$OWNER' — skipping link (requires site admin or matching owner)."
|
||||
else
|
||||
echo "Warning: package link failed (HTTP $LINK_STATUS): $LINK_BODY"
|
||||
fi
|
||||
else
|
||||
echo "Linked package to ${TARGET_LINK}."
|
||||
fi
|
||||
fi
|
||||
|
||||
lock-mgr release "$LOCKFILE" "$KEY"
|
||||
echo "Done: $FULL_IMAGE"
|
||||
}
|
||||
|
||||
do_nuget() {
|
||||
echo "publish-package nuget: not yet implemented"
|
||||
exit 1
|
||||
}
|
||||
|
||||
do_pypi() {
|
||||
echo "publish-package pypi: not yet implemented"
|
||||
exit 1
|
||||
}
|
||||
|
||||
do_npm() {
|
||||
echo "publish-package npm: not yet implemented"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# For docker, determine the actual repo owner via search API and switch to that owner's agent-id
|
||||
if [[ "$COMMAND" == "docker" ]]; then
|
||||
REPO_NAME=$(basename "$REPO")
|
||||
SCRIPT_DIR_CALLER=$(cd "$(dirname "$0")" && pwd)
|
||||
search_result=$("$SCRIPT_DIR_CALLER/repo" search "$REPO_NAME" 2>&1) || true
|
||||
if [[ -n "$search_result" ]] && echo "$search_result" | python3 -q -c "import sys,json; sys.exit(0 if json.load(sys.stdin) else 1)" 2>/dev/null; then
|
||||
repo_owner=$(echo "$search_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('owner',''))" 2>/dev/null)
|
||||
if [[ -n "$repo_owner" ]]; then
|
||||
owner_agent_id=$(ego-mgr lookup "$repo_owner" 2>/dev/null || echo "")
|
||||
if [[ -n "$owner_agent_id" ]]; then
|
||||
export AGENT_ID="$owner_agent_id"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$COMMAND" in
|
||||
docker) do_docker ;;
|
||||
nuget) do_nuget ;;
|
||||
pypi) do_pypi ;;
|
||||
npm) do_npm ;;
|
||||
esac
|
||||
461
git-hangman-lab/scripts/repo
Executable file
461
git-hangman-lab/scripts/repo
Executable file
@@ -0,0 +1,461 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
MYSQL_CONTAINER="git-kc-mysql"
|
||||
MYSQL_USER="root"
|
||||
MYSQL_DB="giteadb"
|
||||
MYSQL_ROOT_PASS="K0DprNKJ^vAu3Mx32hMZ%LCzWKElFRfA"
|
||||
GIT_HOST="root@vps.git"
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# create
|
||||
# ─────────────────────────────────────────────
|
||||
do_create() {
|
||||
local REPO_NAME="" VISIBILITY="private"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--visibility)
|
||||
VISIBILITY="$2"; shift 2 ;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 create <repo-name> [--visibility <public|private>]"
|
||||
exit 0 ;;
|
||||
*)
|
||||
if [[ -z "$REPO_NAME" ]]; then
|
||||
REPO_NAME="$1"; shift
|
||||
else
|
||||
echo "Error: unexpected argument '$1'"
|
||||
exit 1
|
||||
fi ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$REPO_NAME" ]]; then
|
||||
echo "Usage: $0 create <repo-name> [--visibility <public|private>]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$VISIBILITY" != "public" && "$VISIBILITY" != "private" ]]; then
|
||||
echo "Error: --visibility must be 'public' or 'private' (got '$VISIBILITY')"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${AGENT_WORKSPACE:-}" ]]; then
|
||||
echo "Error: script must be executed by pcexec"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate repo name
|
||||
if ! [[ "$REPO_NAME" =~ ^[a-zA-Z0-9_.-]+$ ]]; then
|
||||
echo "Error: Invalid repository name '$REPO_NAME'"
|
||||
echo "Only alphanumeric, hyphens, underscores, and dots are allowed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_DIR="${AGENT_WORKSPACE}/${REPO_NAME}"
|
||||
|
||||
# Pre-create the repo on Gitea so --visibility is honoured. Relying on
|
||||
# push-to-create always yields a private repo regardless of flag.
|
||||
if ! secret-mgr list | grep -q "git-access-token"; then
|
||||
echo "Error: git-access-token not found. Generate one first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local USERNAME TOKEN PRIVATE_JSON STATUS BODY
|
||||
USERNAME="$(secret-mgr get-username --key git)"
|
||||
TOKEN="$(secret-mgr get-secret --key git-access-token)"
|
||||
if [[ "$VISIBILITY" == "public" ]]; then
|
||||
PRIVATE_JSON="false"
|
||||
else
|
||||
PRIVATE_JSON="true"
|
||||
fi
|
||||
|
||||
echo "Creating ${USERNAME}/${REPO_NAME} on git.hangman-lab.top (visibility: ${VISIBILITY})..."
|
||||
RESP=$(curl -s -w "
|
||||
%{http_code}" -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\": \"${REPO_NAME}\", \"private\": ${PRIVATE_JSON}, \"auto_init\": false}" \
|
||||
"https://git.hangman-lab.top/api/v1/user/repos")
|
||||
STATUS=$(printf "%s" "$RESP" | tail -n1)
|
||||
BODY=$(printf "%s" "$RESP" | sed '$d')
|
||||
|
||||
case "$STATUS" in
|
||||
201)
|
||||
echo " Remote repository created."
|
||||
;;
|
||||
409)
|
||||
echo " Remote repository already exists — ensuring visibility..."
|
||||
PATCH_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"private\": ${PRIVATE_JSON}}" \
|
||||
"https://git.hangman-lab.top/api/v1/repos/${USERNAME}/${REPO_NAME}")
|
||||
if [[ "$PATCH_STATUS" != "200" ]]; then
|
||||
echo "Warning: failed to update visibility (HTTP $PATCH_STATUS)"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Error: remote repo creation failed (HTTP $STATUS): $BODY"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "${REPO_DIR}"
|
||||
cd "${REPO_DIR}"
|
||||
if [[ ! -d .git ]]; then
|
||||
git init
|
||||
fi
|
||||
|
||||
REMOTE_URL="https://git.hangman-lab.top/${USERNAME}/${REPO_NAME}.git"
|
||||
if ! git remote get-url origin >/dev/null 2>&1; then
|
||||
git remote add origin "${REMOTE_URL}"
|
||||
else
|
||||
git remote set-url origin "${REMOTE_URL}"
|
||||
fi
|
||||
|
||||
do_config --repo-path "${REPO_DIR}"
|
||||
|
||||
echo "Done! Repository created at: ${REPO_DIR}"
|
||||
echo "Remote: ${REMOTE_URL}"
|
||||
}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# add-collaborators
|
||||
# ─────────────────────────────────────────────
|
||||
do_add_collaborators() {
|
||||
local user="" repo=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--user) user="$2"; shift 2 ;;
|
||||
--repo) repo="$2"; shift 2 ;;
|
||||
*) echo "Unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$user" || -z "$repo" ]]; then
|
||||
echo "Usage: $0 add-collaborators --user <user> --repo <repo>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! secret-mgr list | grep -q "git-access-token"; then
|
||||
echo "Error: git-access-token not found. Generate one first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
owner=$(secret-mgr get-username --key git)
|
||||
token=$(secret-mgr get-secret --key git-access-token)
|
||||
|
||||
curl -s -X PUT \
|
||||
-H "Authorization: token $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"permission":"write"}' \
|
||||
"https://git.hangman-lab.top/api/v1/repos/$owner/$repo/collaborators/$user"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# list-all
|
||||
# ─────────────────────────────────────────────
|
||||
do_list_all() {
|
||||
USERNAME=$(ego-mgr get default-username)
|
||||
if [[ -z "$USERNAME" ]]; then
|
||||
echo "Error: cannot get username from ego-mgr" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
QUERY="
|
||||
SELECT r.name, u.name as owner, r.is_private,
|
||||
COALESCE((r.owner_id = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci
|
||||
OR a.user_id = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci
|
||||
OR EXISTS (SELECT 1 FROM team_user tu JOIN team t ON t.id = tu.team_id
|
||||
WHERE tu.uid = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci
|
||||
AND (t.includes_all_repositories = 1
|
||||
OR EXISTS (SELECT 1 FROM team_repo tr WHERE tr.team_id = t.id AND tr.repo_id = r.id)))), 0) as can_write
|
||||
FROM repository r
|
||||
JOIN user u ON r.owner_id = u.id
|
||||
LEFT JOIN access a ON a.repo_id = r.id AND a.user_id = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci
|
||||
WHERE r.is_archived = 0
|
||||
AND (r.owner_id = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci
|
||||
OR r.is_private = 0
|
||||
OR a.user_id = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci
|
||||
OR EXISTS (SELECT 1 FROM team_user tu WHERE tu.uid = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci))
|
||||
ORDER BY r.name
|
||||
"
|
||||
|
||||
RESULT=$(ssh -o StrictHostKeyChecking=no "$GIT_HOST" \
|
||||
"docker exec $MYSQL_CONTAINER mysql -u $MYSQL_USER -p'$MYSQL_ROOT_PASS' -N -e \"$QUERY\" $MYSQL_DB" 2>/dev/null)
|
||||
|
||||
echo "| proj-name | owner | url | can-write |"
|
||||
echo "|------------|-------|-----|-----------|"
|
||||
|
||||
[[ -z "$RESULT" ]] && exit 0
|
||||
|
||||
echo "$RESULT" | while IFS=$'\t' read -r name owner is_private can_write; do
|
||||
can_write_val=$([[ "$can_write" == "1" ]] && echo "yes" || echo "no")
|
||||
echo "| $name | $owner | https://git.hangman-lab.top/$owner/$name.git | $can_write_val |"
|
||||
done
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# config
|
||||
# ─────────────────────────────────────────────
|
||||
do_config() {
|
||||
local REPO_PATH=""
|
||||
local RECURSIVE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--repo-path) REPO_PATH="${2:-}"; shift 2 ;;
|
||||
--recursive) RECURSIVE=true; shift ;;
|
||||
*) echo "Usage: $0 config --repo-path <path> [--recursive]"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$REPO_PATH" ]]; then
|
||||
echo "Usage: $0 config --repo-path <path> [--recursive]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EMAIL=$(ego-mgr get email)
|
||||
if [[ -z "$EMAIL" ]]; then
|
||||
echo "Error: email not set in ego-mgr"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
is_git_repo() {
|
||||
if [[ -d "$1/.git" ]]; then return 0; fi
|
||||
if [[ -f "$1/.git" ]]; then
|
||||
gitdir=$(grep -m1 "gitdir:" "$1/.git" | cut -d' ' -f2 | tr -d ' ')
|
||||
[[ -n "$gitdir" ]]; return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! is_git_repo "$REPO_PATH"; then
|
||||
echo "Not a git repo: $REPO_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
USER="$(secret-mgr get-username --key git)"
|
||||
PASS="$(secret-mgr get-secret --key git)"
|
||||
ENC_USER="$(U="$USER" python3 - <<'PY'
|
||||
import os, urllib.parse
|
||||
print(urllib.parse.quote(os.environ['U'], safe=''))
|
||||
PY
|
||||
)"
|
||||
|
||||
configure_repo() {
|
||||
local repo="$1" relative="${2:-}"
|
||||
local name="${relative:-$repo}"
|
||||
echo "Configuring: $name"
|
||||
( cd "$repo" && git config user.name "$USER" )
|
||||
( cd "$repo" && git config user.email "$EMAIL" )
|
||||
local git_dir
|
||||
git_dir="$(cd "$repo" && git rev-parse --absolute-git-dir)"
|
||||
local cred_file="${git_dir}/credentials"
|
||||
( cd "$repo" && git config credential.helper "store --file ${cred_file}" )
|
||||
( cd "$repo" && GIT_ASKPASS=true git credential-store --file "${cred_file}" store <<EOF
|
||||
protocol=https
|
||||
host=git.hangman-lab.top
|
||||
username=${ENC_USER}
|
||||
password=${PASS}
|
||||
EOF
|
||||
)
|
||||
}
|
||||
|
||||
configure_repo "$REPO_PATH"
|
||||
|
||||
if [[ "$RECURSIVE" == "true" ]]; then
|
||||
submodules=$(cd "$REPO_PATH" && git submodule status --recursive 2>/dev/null | awk '{print $2}' || true)
|
||||
for sm in $submodules; do
|
||||
sm_path="$REPO_PATH/$sm"
|
||||
if is_git_repo "$sm_path"; then
|
||||
configure_repo "$sm_path" "$sm"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "OK"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# get-latest
|
||||
# ─────────────────────────────────────────────
|
||||
do_get_latest() {
|
||||
local REPO_NAME="" BRANCH="main" RECURSIVE=false FORCE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--recursive) RECURSIVE=true; shift ;;
|
||||
--force) FORCE=true; shift ;;
|
||||
-*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Usage: $0 get-latest <repo-name> [branch] [--recursive] [--force]"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
[[ -z "$REPO_NAME" ]] && REPO_NAME="$1" && shift && continue
|
||||
[[ -z "$BRANCH" ]] && BRANCH="$1" && shift && continue
|
||||
echo "Unknown argument: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$REPO_NAME" ]]; then
|
||||
echo "Usage: $0 get-latest <repo-name> [branch] [--recursive] [--force]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${AGENT_WORKSPACE:-}" ]]; then
|
||||
echo "Error: script must be executed by pcexec"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local REPO_DIR="${AGENT_WORKSPACE}/${REPO_NAME}"
|
||||
|
||||
if [[ -d "$REPO_DIR" ]]; then
|
||||
# Repo exists locally — update in place
|
||||
if ! git -C "$REPO_DIR" rev-parse --is-inside-work-tree 2>/dev/null; then
|
||||
echo "Error: $REPO_DIR is not a git repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local dirty
|
||||
dirty=$(git -C "$REPO_DIR" status --porcelain 2>/dev/null)
|
||||
if [[ -n "$dirty" ]] && [[ "$FORCE" != "true" ]]; then
|
||||
echo "Error: $REPO_DIR has uncommitted changes. Use --force to discard them."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$dirty" ]]; then
|
||||
echo "Discarding uncommitted changes..."
|
||||
git -C "$REPO_DIR" checkout -- . 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "Fetching: $REPO_NAME ($BRANCH)"
|
||||
git -C "$REPO_DIR" fetch origin "$BRANCH" 2>/dev/null || true
|
||||
git -C "$REPO_DIR" checkout "$BRANCH" 2>/dev/null || git -C "$REPO_DIR" checkout -b "$BRANCH" "origin/$BRANCH" 2>/dev/null || true
|
||||
|
||||
if [[ "$RECURSIVE" == "true" ]]; then
|
||||
echo "Updating submodules..."
|
||||
git -C "$REPO_DIR" submodule update --init --recursive 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "Done: $REPO_DIR updated."
|
||||
else
|
||||
# Repo does not exist locally — search via API then clone
|
||||
echo "Repo not found locally. Searching..."
|
||||
local search_result=""
|
||||
search_result=$(bash "$SCRIPT_DIR/repo" search "$REPO_NAME" 2>&1)
|
||||
local search_exit=$?
|
||||
|
||||
if [[ $search_exit -ne 0 ]]; then
|
||||
echo "Error: repository '$REPO_NAME' not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local url=""
|
||||
url=$(echo "$search_result" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
print(data.get('clone-url', ''))
|
||||
" 2>/dev/null)
|
||||
|
||||
if [[ -z "$url" ]]; then
|
||||
echo "Error: failed to parse clone URL for '$REPO_NAME'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Cloning: $url"
|
||||
git clone "$url" "$REPO_DIR" 2>/dev/null || {
|
||||
echo "Error: git clone failed"
|
||||
exit 1
|
||||
}
|
||||
|
||||
do_config --repo-path "$REPO_DIR"
|
||||
|
||||
if [[ "$RECURSIVE" == "true" ]]; then
|
||||
echo "Updating submodules..."
|
||||
git -C "$REPO_DIR" submodule update --init --recursive 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "Done: $REPO_NAME cloned to $REPO_DIR."
|
||||
fi
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# search
|
||||
# ─────────────────────────────────────────────
|
||||
do_search() {
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: $0 search <exact-repo-name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_NAME="$1"
|
||||
|
||||
TOKEN=$(secret-mgr get-secret --key git-access-token 2>/dev/null || secret-mgr get-secret --key git)
|
||||
|
||||
RESP=$(curl -s -H "Authorization: token $TOKEN" \
|
||||
"https://git.hangman-lab.top/api/v1/repos/search?q=${REPO_NAME}")
|
||||
|
||||
RESULT=$(echo "$RESP" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
results = data.get('data', []) if isinstance(data, dict) else []
|
||||
for r in results:
|
||||
if isinstance(r, dict) and r.get('name') == '${REPO_NAME}':
|
||||
owner = r.get('owner', {})
|
||||
login = owner.get('login', '') if isinstance(owner, dict) else ''
|
||||
out = {
|
||||
'name': r.get('name', ''),
|
||||
'id': r.get('id'),
|
||||
'owner': login,
|
||||
'clone-url': r.get('clone_url', '')
|
||||
}
|
||||
print(json.dumps(out))
|
||||
sys.exit(0)
|
||||
print('NOT_FOUND')
|
||||
" 2>/dev/null)
|
||||
|
||||
if [[ "$RESULT" == "NOT_FOUND" ]]; then
|
||||
echo "Error: repository '$REPO_NAME' not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$RESULT"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Dispatch
|
||||
# ─────────────────────────────────────────────
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: $0 <create|add-collaborators|list-all|config|search> [args...]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " create <repo-name> Create a new repository"
|
||||
echo " add-collaborators --user <u> --repo <r> Add collaborator"
|
||||
echo " list-all List all visible repositories"
|
||||
echo " config --repo-path <path> [--recursive] Configure repo credentials"
|
||||
echo " get-latest <repo-name> [branch] [--recursive] [--force] Pull latest or clone if missing"
|
||||
echo " search <exact-repo-name> Search for a repository by exact name"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
subcommand="$1"; shift
|
||||
|
||||
case "$subcommand" in
|
||||
create) do_create "$@" ;;
|
||||
add-collaborators) do_add_collaborators "$@" ;;
|
||||
list-all) do_list_all "$@" ;;
|
||||
config) do_config "$@" ;;
|
||||
get-latest) do_get_latest "$@" ;;
|
||||
search) do_search "$@" ;;
|
||||
*) echo "Unknown command: $subcommand"; exit 1 ;;
|
||||
esac
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Get username from ego-mgr
|
||||
username=$(ego-mgr get default-username)
|
||||
|
||||
# Check if username is provided
|
||||
if [[ -z "$username" ]]; then
|
||||
echo "Error: default-username not set in ego-mgr, please contact ard"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--repo)
|
||||
repo="$2"
|
||||
shift 2
|
||||
;;
|
||||
--user)
|
||||
user="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check if user and repo are provided
|
||||
if [[ -z "$user" || -z "$repo" ]]; then
|
||||
echo "Usage: $0 --user <user> --repo <repo>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if git-access-token exists
|
||||
if ! secret-mgr list | grep -q "git-access-token"; then
|
||||
echo "generate your access token first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
owner=$(secret-mgr get-username --key git)
|
||||
token=$(secret-mgr get-secret --key git-access-token)
|
||||
|
||||
# Execute
|
||||
curl -X PUT -H "Authorization: token $token" -H "Content-Type: application/json" -d '{"permission":"write"}' "https://git.hangman-lab.top/api/v1/repos/$owner/$repo/collaborators/$user"
|
||||
@@ -1,135 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 --repo-path <path> [--recursive]"
|
||||
echo " --repo-path: Path to the git repository"
|
||||
echo " --recursive: Also configure all submodules (recursive)"
|
||||
exit 2
|
||||
}
|
||||
|
||||
REPO_PATH=""
|
||||
RECURSIVE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--repo-path)
|
||||
REPO_PATH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--recursive)
|
||||
RECURSIVE=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$REPO_PATH" ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
# Get email from ego-mgr
|
||||
EMAIL=$(ego-mgr get email)
|
||||
|
||||
# Check if email is provided
|
||||
if [[ -z "$EMAIL" ]]; then
|
||||
echo "Error: email not set in ego-mgr, please contact ard"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if it's a git repo (either .git is a directory or a file with gitdir: reference)
|
||||
is_git_repo() {
|
||||
local repo="$1"
|
||||
if [[ -d "$repo/.git" ]]; then
|
||||
return 0
|
||||
elif [[ -f "$repo/.git" ]]; then
|
||||
local gitdir
|
||||
gitdir=$(grep -m1 "gitdir:" "$repo/.git" | cut -d' ' -f2 | tr -d ' ')
|
||||
if [[ -n "$gitdir" ]]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! is_git_repo "$REPO_PATH"; then
|
||||
echo "Not a git repo: $REPO_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
USER="$(secret-mgr get-username --key git)"
|
||||
PASS="$(secret-mgr get-secret --key git)"
|
||||
|
||||
if [[ -z "$USER" || -z "$PASS" ]]; then
|
||||
echo "Missing credentials from secret-mgr (key: git)"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# URL-encode username for credential URL
|
||||
ENC_USER="$(U="$USER" python3 - <<'PY'
|
||||
import os, urllib.parse
|
||||
print(urllib.parse.quote(os.environ['U'], safe=''))
|
||||
PY
|
||||
)"
|
||||
|
||||
# Function to configure a single repo
|
||||
configure_repo() {
|
||||
local repo="$1"
|
||||
local relative="${2:-}"
|
||||
|
||||
# Get relative path name for display
|
||||
local name="${relative:-$repo}"
|
||||
|
||||
echo "Configuring: $name"
|
||||
|
||||
# Set local user.name / user.email
|
||||
( cd "$repo" && git config user.name "$USER" )
|
||||
( cd "$repo" && git config user.email "$EMAIL" )
|
||||
|
||||
# Resolve the real git dir (works for normal repos and submodules)
|
||||
local git_dir
|
||||
git_dir="$(cd "$repo" && git rev-parse --absolute-git-dir)"
|
||||
local cred_file="${git_dir}/credentials"
|
||||
|
||||
( cd "$repo" && git config credential.helper "store --file ${cred_file}" )
|
||||
( cd "$repo" && GIT_ASKPASS=true git credential-store --file "${cred_file}" store <<EOF
|
||||
protocol=https
|
||||
host=git.hangman-lab.top
|
||||
username=${ENC_USER}
|
||||
password=${PASS}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo " - user.name: $USER"
|
||||
echo " - user.email: $EMAIL"
|
||||
echo " - credential.helper: configured"
|
||||
}
|
||||
|
||||
# Configure main repo
|
||||
configure_repo "$REPO_PATH"
|
||||
|
||||
# Configure submodules if --recursive is specified
|
||||
if [[ "$RECURSIVE" == "true" ]]; then
|
||||
echo ""
|
||||
echo "Configuring submodules..."
|
||||
|
||||
# Get submodules list
|
||||
submodules=$(cd "$REPO_PATH" && git submodule status --recursive 2>/dev/null | awk '{print $2}' || true)
|
||||
|
||||
if [[ -z "$submodules" ]]; then
|
||||
echo "No submodules found"
|
||||
else
|
||||
for sm in $submodules; do
|
||||
sm_path="$REPO_PATH/$sm"
|
||||
if is_git_repo "$sm_path"; then
|
||||
configure_repo "$sm_path" "$sm"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "OK"
|
||||
32
learn.sh
Executable file
32
learn.sh
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -z "${AGENT_WORKSPACE:-}" ]]; then
|
||||
echo "Error: script must be executed by pcexec"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SKILL_LIST="${AGENT_WORKSPACE}/.skill-list"
|
||||
TARGET_DIR="${AGENT_WORKSPACE}/skills"
|
||||
CLAW_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
if [[ ! -f "$SKILL_LIST" ]]; then
|
||||
echo "Error: .skill-list not found at $SKILL_LIST"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$TARGET_DIR"
|
||||
|
||||
while IFS= read -r skill_name || [[ -n "$skill_name" ]]; do
|
||||
[[ -z "$skill_name" || "$skill_name" == \#* ]] && continue
|
||||
|
||||
skill_dir="${CLAW_DIR}/${skill_name}"
|
||||
if [[ -d "$skill_dir" ]]; then
|
||||
echo "Installing: $skill_name"
|
||||
cp -r "$skill_dir" "${TARGET_DIR}/"
|
||||
else
|
||||
echo "Skipping (not found): $skill_name"
|
||||
fi
|
||||
done < "$SKILL_LIST"
|
||||
|
||||
echo "Done."
|
||||
72
recruitment/SKILL.md
Normal file
72
recruitment/SKILL.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
name: recruitment
|
||||
description: Onboard new agents into OpenClaw. Use when creating a new agent, adding an agent to the system, or setting up a new agent workspace. Triggers on requests like "new agent", "add agent", "create agent", "recruit agent", "onboard agent".
|
||||
---
|
||||
|
||||
# Recruitment Skill
|
||||
|
||||
Onboard new agents into OpenClaw with binding configuration.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
new-agent --type openclaw --agent-id <agent-id> [--model <primary-model>]
|
||||
|
||||
new-agent --type contractor --contractor-provider <claude|gemini> --agent-id <agent-id>
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
When a request to create a new agent is received, follow this workflow:
|
||||
|
||||
### Step 1 — Gather Requirements
|
||||
|
||||
Communicate with the requester to collect the following:
|
||||
- New agent's `agent-id`
|
||||
- New agent's primary model (`--model`)
|
||||
- New agent's role and position
|
||||
- Whether the agent is a contractor agent (default: not a contractor)
|
||||
|
||||
### Step 2 — Create Agent
|
||||
|
||||
Execute `{baseDir}/scripts/new-agent` with the gathered parameters:
|
||||
```bash
|
||||
# openclaw type
|
||||
new-agent --type openclaw --agent-id <agent-id> --model <primary-model>
|
||||
|
||||
# contractor type
|
||||
new-agent --type contractor --contractor-provider <claude|gemini> --agent-id <agent-id>
|
||||
```
|
||||
|
||||
### Step 3 — Interview
|
||||
|
||||
Use the `create-discussion-channel` tool to start an interview with the new agent:
|
||||
- Participant: `interviewee`
|
||||
- Discussion guide: "<@YOUR_DISCORD_USER_ID> please refer to {baseDir}/workflow/interview.md"
|
||||
|
||||
> Note: Replace `YOUR_DISCORD_USER_ID` with your actual Discord user ID. If unsure, use `ego-mgr get discord-id` (via pcexec) to look it up.
|
||||
|
||||
After receiving the discussion callback, review the discussion summary:
|
||||
- If it contains the agent's **name** and **gender** → proceed to Step 4
|
||||
- If either field is missing → attempt to gather the missing information via another `create-discussion-channel` call
|
||||
- If still unavailable → notify the requester and proceed without it
|
||||
|
||||
### Step 4 — Onboard
|
||||
|
||||
Use `proxy-pcexec` to call `{baseDir}/scripts/onboard`:
|
||||
- `proxy-for`: new agent's `agent-id`
|
||||
- Parameters: `--name`, `--role`, `--position`, `--gender`, `--bot-token`
|
||||
|
||||
### Step 5 — Report
|
||||
|
||||
Notify the requester that onboarding is complete.
|
||||
|
||||
## Scripts
|
||||
|
||||
### new-agent
|
||||
|
||||
Creates and registers the agent, then configures the `interviewee` Discord account binding.
|
||||
|
||||
### onboard
|
||||
|
||||
Sets up ego-mgr identity fields and configures the agent's Discord account using the name/gender collected during interview.
|
||||
122
recruitment/scripts/new-agent
Executable file
122
recruitment/scripts/new-agent
Executable file
@@ -0,0 +1,122 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
TYPE=""
|
||||
AGENT_ID=""
|
||||
MODEL=""
|
||||
CONTRACTOR_PROVIDER=""
|
||||
|
||||
usage() {
|
||||
echo "Usage: new-agent --type <openclaw|contractor> --agent-id <agent-id> [--model <primary-model>] [--contractor-provider <claude|gemini>]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--type)
|
||||
TYPE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--agent-id)
|
||||
AGENT_ID="$2"
|
||||
shift 2
|
||||
;;
|
||||
--model)
|
||||
MODEL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--contractor-provider)
|
||||
CONTRACTOR_PROVIDER="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$TYPE" ]] || [[ -z "$AGENT_ID" ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
WORKSPACE_DIR="$HOME/.openclaw/workspace/workspace-$AGENT_ID"
|
||||
STATE_DIR="$HOME/.openclaw/states"
|
||||
BINDINGS_LOCK="$STATE_DIR/bindings.lock"
|
||||
|
||||
# Step 1: Register agent
|
||||
if [[ "$TYPE" == "openclaw" ]]; then
|
||||
MODEL_ARG=""
|
||||
if [[ -n "$MODEL" ]]; then
|
||||
MODEL_ARG="--model $MODEL"
|
||||
fi
|
||||
openclaw agents add "$AGENT_ID" $MODEL_ARG --workspace "$WORKSPACE_DIR" --non-interactive
|
||||
elif [[ "$TYPE" == "contractor" ]]; then
|
||||
# Check contractor-agent plugin
|
||||
if ! openclaw plugins list 2>/dev/null | grep -q "contractor-agent"; then
|
||||
echo "Error: contractor-agent plugin is not installed" >&2
|
||||
exit 1
|
||||
fi
|
||||
openclaw contractor-agents add --agent-id "$AGENT_ID" --workspace "$WORKSPACE_DIR" --contractor "$CONTRACTOR_PROVIDER"
|
||||
else
|
||||
echo "Error: unknown type '$TYPE'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 2: Wait for bindings
|
||||
echo "Checking bindings..."
|
||||
|
||||
wait_for_bindings() {
|
||||
local bindings
|
||||
bindings=$(openclaw config get bindings 2>/dev/null || echo "{}")
|
||||
if echo "$bindings" | grep -q '"interviewee"'; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_lock() {
|
||||
if [[ -f "$BINDINGS_LOCK" ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
while wait_for_bindings; do
|
||||
echo "another interview is ongoing, waiting..."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
while wait_for_lock; do
|
||||
echo "bindings locked, waiting..."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
# Step 3: Set bindings
|
||||
mkdir -p "$STATE_DIR"
|
||||
touch "$BINDINGS_LOCK"
|
||||
|
||||
CURRENT_BINDINGS=$(openclaw config get bindings 2>/dev/null || echo "{}")
|
||||
|
||||
NEW_ENTRY="{\"agentId\": \"$AGENT_ID\", \"match\": {\"channel\": \"discord\", \"accountId\": \"interviewee\"}}"
|
||||
|
||||
if [[ "$CURRENT_BINDINGS" == "{}" ]] || [[ -z "$CURRENT_BINDINGS" ]]; then
|
||||
NEW_BINDINGS="[$NEW_ENTRY]"
|
||||
else
|
||||
NEW_BINDINGS=$(echo "$CURRENT_BINDINGS" | sed 's/\(\[.*\)\]/\1,/' | sed 's/\]$//' | sed 's/\([{,]\)$/\1/')" $NEW_ENTRY]"
|
||||
fi
|
||||
|
||||
openclaw config set bindings "$NEW_BINDINGS"
|
||||
|
||||
rm -f "$BINDINGS_LOCK"
|
||||
|
||||
# Step 4: Configure interviewee account
|
||||
TOKEN=$(secret-mgr get-secret --key interviewee-discord-token --public 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$TOKEN" ]]; then
|
||||
echo "Error: failed to get interviewee-discord-token from secret-mgr" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
openclaw config set channels.discord.accounts.interviewee "{\"enabled\": true, \"token\": \"$TOKEN\", \"groupPolicy\": \"open\", \"streaming\": {\"mode\": \"off\"}}"
|
||||
|
||||
echo "Agent $AGENT_ID recruited successfully."
|
||||
130
recruitment/scripts/onboard
Executable file
130
recruitment/scripts/onboard
Executable file
@@ -0,0 +1,130 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
NAME=""
|
||||
ROLE=""
|
||||
POSITION=""
|
||||
GENDER=""
|
||||
BOT_TOKEN=""
|
||||
|
||||
usage() {
|
||||
echo "Usage: onboard --name <name> --role <role> --position <position> --gender <gender> --bot-token <token>"
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--name)
|
||||
NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--role)
|
||||
ROLE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--position)
|
||||
POSITION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--gender)
|
||||
GENDER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--bot-token)
|
||||
BOT_TOKEN="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$NAME" ]] || [[ -z "$ROLE" ]] || [[ -z "$POSITION" ]] || [[ -z "$GENDER" ]] || [[ -z "$BOT_TOKEN" ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ "$PCEXEC_PROXIED" != "true" ]]; then
|
||||
echo "Error: this script must be executed with tool proxy-pcexec" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
|
||||
# Resolve discord-id from bot token
|
||||
DISCORD_ID=$(curl -s "https://discord.com/api/v10/users/@me" \
|
||||
-H "Authorization: Bot $BOT_TOKEN" \
|
||||
-H "Content-Type: application/json" | \
|
||||
python3 -c "import sys,json; print(json.load(sys.stdin).get('id', ''))" 2>/dev/null)
|
||||
|
||||
if [[ -z "$DISCORD_ID" ]]; then
|
||||
echo "Error: failed to resolve discord-id from bot token" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
AGENT_ID="agent-$NAME"
|
||||
|
||||
ego-mgr set name "$NAME"
|
||||
ego-mgr set default-username "$NAME"
|
||||
ego-mgr set discord-id "$DISCORD_ID"
|
||||
ego-mgr set role "$ROLE"
|
||||
ego-mgr set position "$POSITION"
|
||||
ego-mgr set gender "$GENDER"
|
||||
ego-mgr set email "${NAME}@${ROLE}.hangman-lab.top"
|
||||
ego-mgr set agent-id "$AGENT_ID"
|
||||
ego-mgr set date-of-birth "$TODAY"
|
||||
|
||||
BINDINGS_LOCK="$HOME/.openclaw/states/bindings.lock"
|
||||
STATE_DIR="$HOME/.openclaw/states"
|
||||
|
||||
# Wait for bindings.lock to disappear
|
||||
while [[ -f "$BINDINGS_LOCK" ]]; do
|
||||
echo "bindings locked, waiting..."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
# Create bindings.lock and update bindings
|
||||
mkdir -p "$STATE_DIR"
|
||||
touch "$BINDINGS_LOCK"
|
||||
|
||||
CURRENT_BINDINGS=$(openclaw config get bindings 2>/dev/null || echo "[]")
|
||||
|
||||
# Replace accountId "interviewee" -> "{name}" for this agent
|
||||
UPDATED_BINDINGS=$(echo "$CURRENT_BINDINGS" | python3 -c "
|
||||
import sys, json
|
||||
bindings = json.load(sys.stdin)
|
||||
for b in bindings:
|
||||
if b.get('agentId') == '$AGENT_ID' and b.get('match', {}).get('accountId') == 'interviewee':
|
||||
b['match']['accountId'] = '$NAME'
|
||||
print(json.dumps(bindings))
|
||||
" 2>/dev/null)
|
||||
|
||||
openclaw config set bindings "$UPDATED_BINDINGS"
|
||||
|
||||
rm -f "$BINDINGS_LOCK"
|
||||
|
||||
# Configure discord account for {name}
|
||||
openclaw config set channels.discord.accounts."$NAME".enabled true
|
||||
openclaw config set channels.discord.accounts."$NAME".commands.native true
|
||||
openclaw config set channels.discord.accounts."$NAME".commands.nativeSkills true
|
||||
openclaw config set channels.discord.accounts."$NAME".token "$BOT_TOKEN"
|
||||
openclaw config set channels.discord.accounts."$NAME".allowBots true
|
||||
openclaw config set channels.discord.accounts."$NAME".groupPolicy "open"
|
||||
openclaw config set channels.discord.accounts."$NAME".streaming.mode "off"
|
||||
openclaw config set channels.discord.accounts."$NAME".actions.messages true
|
||||
openclaw config set channels.discord.accounts."$NAME".actions.search true
|
||||
openclaw config set channels.discord.accounts."$NAME".actions.roles true
|
||||
openclaw config set channels.discord.accounts."$NAME".actions.channelInfo true
|
||||
openclaw config set channels.discord.accounts."$NAME".actions.events true
|
||||
openclaw config set channels.discord.accounts."$NAME".actions.channels true
|
||||
openclaw config set channels.discord.accounts."$NAME".dmPolicy "open"
|
||||
openclaw config set channels.discord.accounts."$NAME".allowFrom '["*"]'
|
||||
openclaw config set channels.discord.accounts."$NAME".dm.enabled true
|
||||
openclaw config set channels.discord.accounts."$NAME".execApprovals.enabled false
|
||||
openclaw config set channels.discord.accounts."$NAME".execApprovals.approvers '["*"]'
|
||||
|
||||
~/.openclaw/skills/keycloak-hangman-lab/scripts/create-keycloak-account
|
||||
~/.openclaw/skills/git-hangman-lab/scripts/create-git-account
|
||||
~/.openclaw/skills/git-hangman-lab/scripts/link-keycloak
|
||||
|
||||
echo "Onboarding complete for $NAME."
|
||||
0
recruitment/workflows/interviewer.md
Normal file
0
recruitment/workflows/interviewer.md
Normal file
Reference in New Issue
Block a user