bootstrap: projects management repo
This commit is contained in:
0
.projects.lock
Normal file
0
.projects.lock
Normal file
18
PROJECTS_INDEX.md
Normal file
18
PROJECTS_INDEX.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Projects Index
|
||||||
|
|
||||||
|
This file is generated from projects.json. Do not edit manually.
|
||||||
|
|
||||||
|
Last Updated: 2026-02-28
|
||||||
|
|
||||||
|
## Active Projects
|
||||||
|
|
||||||
|
- (none)
|
||||||
|
|
||||||
|
## Paused Projects
|
||||||
|
|
||||||
|
- (none)
|
||||||
|
|
||||||
|
## Archived Projects
|
||||||
|
|
||||||
|
- (none)
|
||||||
|
|
||||||
45
README.md
Normal file
45
README.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
Projects Management Repo (auto-updated by OpenClaw)
|
||||||
|
|
||||||
|
Goal
|
||||||
|
- OpenClaw updates this repo automatically (on main) to keep a durable project index.
|
||||||
|
- Humans read it offline. Do not hand-edit PROJECTS_INDEX.md.
|
||||||
|
|
||||||
|
Files
|
||||||
|
- projects.json Source of truth (machine-edited)
|
||||||
|
- PROJECTS_INDEX.md Generated from projects.json (human-readable)
|
||||||
|
- tools/projectsctl The ONLY allowed writer (adds/updates + commits + pushes)
|
||||||
|
- tools/render_projects_index.py Generator/validator
|
||||||
|
|
||||||
|
Prereqs (Debian/Ubuntu)
|
||||||
|
- sudo apt-get update
|
||||||
|
- sudo apt-get install -y git jq python3 util-linux
|
||||||
|
|
||||||
|
Bootstrap
|
||||||
|
1) Generate the index:
|
||||||
|
./tools/projectsctl render
|
||||||
|
|
||||||
|
2) Add a project (example):
|
||||||
|
PROJREPO=$(pwd) ./tools/projectsctl add "CognitiveOS" active \
|
||||||
|
"https://git.molloyhome.net/mmolloy/Cognition-OS" \
|
||||||
|
"/home/openclaw/.openclaw/workspace/repo-Cognition-OS" \
|
||||||
|
"Deterministic microkernel-style control plane for LLM/agent systems."
|
||||||
|
|
||||||
|
3) Change status:
|
||||||
|
PROJREPO=$(pwd) ./tools/projectsctl set-status "CognitiveOS" paused
|
||||||
|
|
||||||
|
4) Update notes:
|
||||||
|
PROJREPO=$(pwd) ./tools/projectsctl update-notes "CognitiveOS" "New notes here..."
|
||||||
|
|
||||||
|
Making OpenClaw read this reliably
|
||||||
|
- Symlink PROJECTS_INDEX.md into OpenClaw’s indexed memory folder:
|
||||||
|
|
||||||
|
ln -sf /opt/projects/projects-mgmt/PROJECTS_INDEX.md ~/.openclaw/memory/projects_index.md
|
||||||
|
sudo systemctl restart openclaw-gateway
|
||||||
|
|
||||||
|
Then confirm it’s indexed:
|
||||||
|
sqlite3 ~/.openclaw/memory/main.sqlite "SELECT path FROM files WHERE path LIKE '%projects_index%';"
|
||||||
|
|
||||||
|
Operational contract (recommended)
|
||||||
|
- OpenClaw MUST use tools/projectsctl for any changes.
|
||||||
|
- Never edit PROJECTS_INDEX.md directly.
|
||||||
|
- If push fails, OpenClaw must report the error (no silent drift).
|
||||||
4
projects.json
Normal file
4
projects.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"last_updated": "2026-02-28",
|
||||||
|
"projects": []
|
||||||
|
}
|
||||||
124
tools/projectsctl
Executable file
124
tools/projectsctl
Executable file
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO="${PROJREPO:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||||
|
LOCK="$REPO/.projects.lock"
|
||||||
|
TS="$(date +%F_%H%M%S)"
|
||||||
|
cd "$REPO"
|
||||||
|
|
||||||
|
need() { command -v "$1" >/dev/null 2>&1 || { echo "ERROR: missing $1"; exit 1; }; }
|
||||||
|
need jq
|
||||||
|
need git
|
||||||
|
need python3
|
||||||
|
need flock
|
||||||
|
|
||||||
|
exec 9>"$LOCK"
|
||||||
|
flock -n 9 || { echo "ERROR: projectsctl is already running"; exit 2; }
|
||||||
|
|
||||||
|
backup() {
|
||||||
|
mkdir -p "$REPO/backups"
|
||||||
|
cp -a "$REPO/projects.json" "$REPO/backups/projects.json.$TS"
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
./tools/render_projects_index.py >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
git_sync() {
|
||||||
|
git rev-parse --is-inside-work-tree >/dev/null
|
||||||
|
git checkout main >/dev/null 2>&1 || true
|
||||||
|
git pull --rebase --autostash >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
git_commit_push() {
|
||||||
|
if git diff --quiet && git diff --cached --quiet; then
|
||||||
|
echo "No changes to commit."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git add projects.json PROJECTS_INDEX.md
|
||||||
|
git commit -m "projects: update ($TS)" >/dev/null
|
||||||
|
git push origin main >/dev/null
|
||||||
|
echo "OK: committed and pushed to main."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd="${1:-}"; shift || true
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
add)
|
||||||
|
name="${1:-}"; status="${2:-active}"; shift 2 || true
|
||||||
|
repo="${1:-}"; local_path="${2:-}"; shift 2 || true
|
||||||
|
notes="${*:-}"
|
||||||
|
[[ -n "$name" ]] || { echo "usage: projectsctl add <name> [status] [repo] [local_path] [notes...]"; exit 1; }
|
||||||
|
|
||||||
|
git_sync
|
||||||
|
backup
|
||||||
|
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
jq --arg name "$name" --arg status "$status" --arg repo "$repo" --arg lp "$local_path" --arg notes "$notes" '
|
||||||
|
.projects |= ( . + [ {name:$name,status:$status,type:"",repo:$repo,local_path:$lp,notes:$notes} ] )
|
||||||
|
' projects.json > "$tmp"
|
||||||
|
mv "$tmp" projects.json
|
||||||
|
|
||||||
|
render
|
||||||
|
git_commit_push
|
||||||
|
;;
|
||||||
|
|
||||||
|
set-status)
|
||||||
|
name="${1:-}"; status="${2:-}"
|
||||||
|
[[ -n "$name" && -n "$status" ]] || { echo "usage: projectsctl set-status <name> <active|paused|archived>"; exit 1; }
|
||||||
|
|
||||||
|
git_sync
|
||||||
|
backup
|
||||||
|
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
jq --arg name "$name" --arg status "$status" '
|
||||||
|
.projects |= (map(if .name==$name then .status=$status else . end))
|
||||||
|
' projects.json > "$tmp"
|
||||||
|
mv "$tmp" projects.json
|
||||||
|
|
||||||
|
render
|
||||||
|
git_commit_push
|
||||||
|
;;
|
||||||
|
|
||||||
|
update-notes)
|
||||||
|
name="${1:-}"; shift || true
|
||||||
|
notes="${*:-}"
|
||||||
|
[[ -n "$name" && -n "$notes" ]] || { echo "usage: projectsctl update-notes <name> <notes...>"; exit 1; }
|
||||||
|
|
||||||
|
git_sync
|
||||||
|
backup
|
||||||
|
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
jq --arg name "$name" --arg notes "$notes" '
|
||||||
|
.projects |= (map(if .name==$name then .notes=$notes else . end))
|
||||||
|
' projects.json > "$tmp"
|
||||||
|
mv "$tmp" projects.json
|
||||||
|
|
||||||
|
render
|
||||||
|
git_commit_push
|
||||||
|
;;
|
||||||
|
|
||||||
|
list)
|
||||||
|
python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
p=json.load(open("projects.json"))
|
||||||
|
for pr in p.get("projects",[]):
|
||||||
|
print(f"{pr.get('status',''):8} {pr.get('name','')}")
|
||||||
|
PY
|
||||||
|
;;
|
||||||
|
|
||||||
|
render)
|
||||||
|
render
|
||||||
|
echo "OK: rendered PROJECTS_INDEX.md"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "usage:"
|
||||||
|
echo " projectsctl add <name> [status] [repo] [local_path] [notes...]"
|
||||||
|
echo " projectsctl set-status <name> <active|paused|archived>"
|
||||||
|
echo " projectsctl update-notes <name> <notes...>"
|
||||||
|
echo " projectsctl list"
|
||||||
|
echo " projectsctl render"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
54
tools/render_projects_index.py
Executable file
54
tools/render_projects_index.py
Executable file
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json, datetime, sys
|
||||||
|
|
||||||
|
p = json.load(open("projects.json"))
|
||||||
|
today = datetime.date.today().isoformat()
|
||||||
|
|
||||||
|
def norm(s): return (s or "").strip()
|
||||||
|
|
||||||
|
projects = p.get("projects", [])
|
||||||
|
if not isinstance(projects, list):
|
||||||
|
print("ERROR: projects.json must contain a top-level 'projects' array", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
for i, pr in enumerate(projects):
|
||||||
|
if not isinstance(pr, dict):
|
||||||
|
raise SystemExit(f"ERROR: project[{i}] is not an object")
|
||||||
|
if not norm(pr.get("name")):
|
||||||
|
raise SystemExit(f"ERROR: project[{i}] missing name")
|
||||||
|
st = norm(pr.get("status"))
|
||||||
|
if st not in ("active","paused","archived"):
|
||||||
|
raise SystemExit(f"ERROR: invalid status '{st}' for {pr.get('name')} (allowed: active|paused|archived)")
|
||||||
|
|
||||||
|
p["last_updated"] = today
|
||||||
|
|
||||||
|
def section(title, items):
|
||||||
|
out = [f"## {title}\n\n"]
|
||||||
|
if not items:
|
||||||
|
out.append("- (none)\n\n")
|
||||||
|
return out
|
||||||
|
for pr in sorted(items, key=lambda x: x["name"].lower()):
|
||||||
|
out.append(f"- Project: {pr['name']}\n")
|
||||||
|
out.append(f" Status: {pr.get('status','')}\n")
|
||||||
|
if pr.get("type"): out.append(f" Type: {pr['type']}\n")
|
||||||
|
if pr.get("repo"): out.append(f" Repo: {pr['repo']}\n")
|
||||||
|
if pr.get("local_path"): out.append(f" Local Path: {pr['local_path']}\n")
|
||||||
|
if pr.get("notes"): out.append(f" Notes: {pr['notes']}\n")
|
||||||
|
out.append("\n")
|
||||||
|
return out
|
||||||
|
|
||||||
|
active = [x for x in projects if x.get("status") == "active"]
|
||||||
|
paused = [x for x in projects if x.get("status") == "paused"]
|
||||||
|
archived = [x for x in projects if x.get("status") == "archived"]
|
||||||
|
|
||||||
|
md = []
|
||||||
|
md.append("# Projects Index\n\n")
|
||||||
|
md.append("This file is generated from projects.json. Do not edit manually.\n\n")
|
||||||
|
md.append(f"Last Updated: {p['last_updated']}\n\n")
|
||||||
|
md += section("Active Projects", active)
|
||||||
|
md += section("Paused Projects", paused)
|
||||||
|
md += section("Archived Projects", archived)
|
||||||
|
|
||||||
|
open("PROJECTS_INDEX.md","w").write("".join(md))
|
||||||
|
json.dump(p, open("projects.json","w"), indent=2, sort_keys=False)
|
||||||
|
print("OK: rendered PROJECTS_INDEX.md")
|
||||||
Reference in New Issue
Block a user