Compare commits
23 Commits
d086243fe1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ed335ed28 | ||
|
|
319f379129 | ||
|
|
b39c4b85d1 | ||
|
|
401768a552 | ||
|
|
8dbcde0e26 | ||
|
|
6af18d56bc | ||
|
|
39ef1f4eea | ||
|
|
edd60e905e | ||
|
|
195132a380 | ||
|
|
fb5e5c5dc5 | ||
|
|
a960b1809a | ||
|
|
ade77d6c98 | ||
|
|
d77456c250 | ||
|
|
9d9892a4a4 | ||
|
|
aedd24ba18 | ||
|
|
ef463785da | ||
|
|
2eeb236214 | ||
|
|
75479f6a40 | ||
|
|
e1020f0ec7 | ||
|
|
f8b09eade7 | ||
|
|
eddb974aaf | ||
|
|
f7426cf3a6 | ||
|
|
cf7f4d95fc |
140
README.md
140
README.md
@@ -1,65 +1,153 @@
|
||||
# Mock RTA Knowledge Base
|
||||
# Section 0 Shared Docs -- 0.1.8
|
||||
|
||||
This hosted Git repo is intentionally plain Markdown. It has no required RTA
|
||||
sidecars. Collaborators clone, edit, commit, and push it as the canonical
|
||||
documentation source. The projection operator reads a separate projector
|
||||
checkout and mirrors the Markdown into AFFiNE as read-only docs.
|
||||
These documents are available in two ways:
|
||||
|
||||
1. Read the projected copy in AFFiNE.
|
||||
AFFiNE is the shared reading and whiteboard space. Markdown from this repo
|
||||
appears there as a read-only projection.
|
||||
|
||||
2. Edit them in Git.
|
||||
The editable files live in this plain Markdown repo. Use your normal editor,
|
||||
then commit and push changes.
|
||||
|
||||
If you want to change one of these docs, edit the Markdown file. Do not edit the
|
||||
AFFiNE copy; it will be refreshed from Git.
|
||||
|
||||
## What To Edit
|
||||
|
||||
Edit Markdown files in this Git repo. Do not edit the projected AFFiNE copies
|
||||
directly; AFFiNE is the shared reading and canvas surface, not the source of
|
||||
truth for these files.
|
||||
Edit Markdown files in this Git repo.
|
||||
|
||||
Current projected docs:
|
||||
Current docs:
|
||||
|
||||
- `README.md`
|
||||
- `docs/source-models.md`
|
||||
- `rta/concept.md`
|
||||
- `rta/reports/current-status.md`
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
From any machine that can reach the lab mesh:
|
||||
From any machine that can reach the private lab network:
|
||||
|
||||
```sh
|
||||
curl -fsSL http://100.64.0.1:30087/virgil-admin/rta-mock-docs/raw/branch/main/scripts/setup-rta-mock-docs.sh | sh
|
||||
curl -fsSL http://100.64.0.1:30087/section0/rta-handbook/raw/branch/main/scripts/setup-section0-docs.sh | sh
|
||||
```
|
||||
|
||||
The script clones this repo into:
|
||||
|
||||
```text
|
||||
~/Developer/Section0/rta-mock-docs
|
||||
~/Developer/Section0/rta-handbook
|
||||
```
|
||||
|
||||
It also installs a small helper command at:
|
||||
|
||||
```text
|
||||
~/.local/bin/section0-docs
|
||||
```
|
||||
|
||||
Override the destination if you prefer:
|
||||
|
||||
```sh
|
||||
TARGET_DIR=~/work/rta-mock-docs \
|
||||
curl -fsSL http://100.64.0.1:30087/virgil-admin/rta-mock-docs/raw/branch/main/scripts/setup-rta-mock-docs.sh | sh
|
||||
TARGET_DIR=~/work/rta-handbook \
|
||||
curl -fsSL http://100.64.0.1:30087/section0/rta-handbook/raw/branch/main/scripts/setup-section0-docs.sh | sh
|
||||
```
|
||||
|
||||
## Daily Flow
|
||||
|
||||
```sh
|
||||
cd ~/Developer/Section0/rta-mock-docs
|
||||
git pull --ff-only
|
||||
section0-docs auth login
|
||||
section0-docs doctor
|
||||
section0-docs pull
|
||||
|
||||
# Edit Markdown files in your normal editor.
|
||||
|
||||
git status
|
||||
git add path/to/file.md
|
||||
git commit -m "Describe the doc change"
|
||||
git push
|
||||
section0-docs status
|
||||
MESSAGE="Describe the doc change" section0-docs commit
|
||||
section0-docs push
|
||||
section0-docs wait
|
||||
```
|
||||
|
||||
After a push, ask the projection operator to pull and project. For now that is
|
||||
manual. The current operator command lives in
|
||||
`docs/projection-operations.md`.
|
||||
`section0-docs push` updates Git. By default it also waits until the projection
|
||||
status says the pushed commit has reached AFFiNE. `section0-docs wait` can be
|
||||
run again any time to check convergence.
|
||||
|
||||
## Personal Markdown Mounts
|
||||
|
||||
Use this when you want to share a selected local Markdown or Obsidian folder
|
||||
without moving custody into the shared Git repo.
|
||||
|
||||
```sh
|
||||
section0-docs auth login
|
||||
section0-docs mount add research ~/Documents/Obsidian/Research
|
||||
section0-docs mount list
|
||||
section0-docs mount plan research
|
||||
section0-docs mount sync research
|
||||
section0-docs mount status research
|
||||
```
|
||||
|
||||
The mount declaration is local to your machine. That is intentional: your
|
||||
personal source files stay under your control. AFFiNE receives a read-only
|
||||
derived copy.
|
||||
|
||||
To stop publishing that folder:
|
||||
|
||||
```sh
|
||||
section0-docs mount unmount research
|
||||
section0-docs mount remove research
|
||||
```
|
||||
|
||||
## Contributor Setup
|
||||
|
||||
Anonymous clone/read works on the private lab network. Pushing requires
|
||||
Authentik login through the helper.
|
||||
|
||||
Run this once per checkout:
|
||||
|
||||
```sh
|
||||
section0-docs auth login
|
||||
```
|
||||
|
||||
The login opens Authentik in your browser, configures the Git author from your
|
||||
identity, and installs the repo Git credential. After that, normal Git tooling
|
||||
works too.
|
||||
|
||||
Prove write access without changing `main`:
|
||||
|
||||
```sh
|
||||
section0-docs push-test
|
||||
```
|
||||
|
||||
`push-test` does not commit files and does not change `main`. It pushes the
|
||||
current commit to a temporary scratch branch, then deletes that branch
|
||||
immediately. If Git prompts for a password, use a Gitea access token rather
|
||||
than your normal account password. If login is healthy, Git should usually get
|
||||
the credential from your local credential store.
|
||||
|
||||
## End-To-End Smoke Test
|
||||
|
||||
Use this when setting up a new machine:
|
||||
|
||||
```sh
|
||||
section0-docs pull
|
||||
|
||||
cd "$(section0-docs open)"
|
||||
printf '\nMBP sync smoke test: %s\n' "$(date -Is)" >> rta/reports/current-status.md
|
||||
|
||||
section0-docs status
|
||||
MESSAGE="MBP sync smoke test" section0-docs commit
|
||||
section0-docs push
|
||||
```
|
||||
|
||||
After projection completes, the smoke line should appear in:
|
||||
|
||||
```text
|
||||
Agent Workspace / Section 0 / Git Projections / RTA Handbook / RTA Status
|
||||
```
|
||||
|
||||
## Source Models
|
||||
|
||||
This repo is the Git-backed model: shared docs are canonical in Gitea.
|
||||
This repo is the shared Git model: shared docs are edited in Git, then copied
|
||||
into AFFiNE for reading.
|
||||
|
||||
Personal vault mounts are different: a person keeps custody of their own local
|
||||
Obsidian vault and only projects selected Markdown into AFFiNE. See
|
||||
`docs/source-models.md`.
|
||||
Obsidian vault and only shares selected Markdown into AFFiNE. See
|
||||
`docs/source-models.md` for that more advanced case.
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
# Projection Operations
|
||||
|
||||
This repo is projected into AFFiNE manually for now.
|
||||
|
||||
Canonical Gitea repo:
|
||||
|
||||
```text
|
||||
http://100.64.0.1:30087/virgil-admin/rta-mock-docs.git
|
||||
```
|
||||
|
||||
Projected AFFiNE path:
|
||||
|
||||
```text
|
||||
Agent Workspace / projected-markdown / mock-rta-docs
|
||||
```
|
||||
|
||||
## What A Collaborator Does
|
||||
|
||||
```sh
|
||||
cd ~/Developer/Section0/rta-mock-docs
|
||||
git pull --ff-only
|
||||
# edit Markdown
|
||||
git add .
|
||||
git commit -m "Update docs"
|
||||
git push
|
||||
```
|
||||
|
||||
## What The Projection Operator Does
|
||||
|
||||
The operator keeps a separate checkout so projection never depends on a
|
||||
collaborator's dirty working tree.
|
||||
|
||||
```sh
|
||||
git -C /Users/virgil/Developer/rta/tmp/markdown-projection-gitea/projector-checkout/rta-mock-docs pull --ff-only
|
||||
```
|
||||
|
||||
Then from `home-lab-v7`:
|
||||
|
||||
```sh
|
||||
cd /Users/virgil/Developer/Virgil-Info/home-lab-v7
|
||||
|
||||
nix develop --command bash -lc 'scripts/ops/sync-obsidian-affine.rb \
|
||||
--name rta-mock-docs \
|
||||
--source /Users/virgil/Developer/rta/tmp/markdown-projection-gitea/projector-checkout/rta-mock-docs \
|
||||
--username projection-bot \
|
||||
--authentik-sub rta-projection-bot \
|
||||
--affine-workspace "Agent Workspace" \
|
||||
--affine-workspace-id 53ea0a0b-eca7-4887-8e31-f5b2a8ab7744 \
|
||||
--affine-user-id ce42f50a-5367-4466-920b-7422c4e27de0 \
|
||||
--affine-namespace projected-markdown/mock-rta-docs \
|
||||
--include "**/*.md" \
|
||||
--apply'
|
||||
```
|
||||
|
||||
Expected healthy result:
|
||||
|
||||
```text
|
||||
docs: 3
|
||||
mark stale: 0
|
||||
mode: one-way read-only AFFiNE docs
|
||||
```
|
||||
|
||||
## Rule Of Thumb
|
||||
|
||||
If the source repo changes, pull the projector checkout and run projection.
|
||||
|
||||
If AFFiNE changes, treat it as a comment or sketch. Move durable edits back to
|
||||
Markdown before projecting again.
|
||||
@@ -52,7 +52,8 @@ Flow:
|
||||
```text
|
||||
personal Markdown or Obsidian vault
|
||||
-> selected include paths
|
||||
-> projection registry
|
||||
-> local mount registry
|
||||
-> authenticated projection request
|
||||
-> read-only AFFiNE docs
|
||||
```
|
||||
|
||||
@@ -62,6 +63,19 @@ Why this exists:
|
||||
- It avoids forcing everyone into one giant shared repo.
|
||||
- It lets people publish useful slices without adopting a new editor.
|
||||
|
||||
CLI flow:
|
||||
|
||||
```sh
|
||||
section0-docs auth login
|
||||
section0-docs mount add research ~/Documents/Obsidian/Research
|
||||
section0-docs mount plan research
|
||||
section0-docs mount sync research
|
||||
```
|
||||
|
||||
`mount plan` prints the exact payload before it is sent. `mount status` shows
|
||||
the local declaration and the last sync receipt. `mount unmount` removes the
|
||||
projected AFFiNE docs for that namespace without touching the source folder.
|
||||
|
||||
## Choosing Between Them
|
||||
|
||||
Use Git-backed docs when the document itself is a shared artifact.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# RTA Status
|
||||
|
||||
This is still a demo status, not an exhaustive status report.
|
||||
The current mock says RTA has a working Markdown projection slice with hosted Git as the source authority.
|
||||
This is a concise operator status, not an exhaustive status report.
|
||||
The current handbook records that RTA has a working Markdown projection slice with hosted Git as the source authority.
|
||||
Live AFFiNE writing remains the next adapter step.
|
||||
The rename demonstrates that Git history can preserve projection identity.
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
REPO_URL="${REPO_URL:-http://100.64.0.1:30087/virgil-admin/rta-mock-docs.git}"
|
||||
TARGET_DIR="${TARGET_DIR:-$HOME/Developer/Section0/rta-mock-docs}"
|
||||
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
echo "git is required but was not found on PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -d "$TARGET_DIR/.git" ]; then
|
||||
echo "Repo already exists: $TARGET_DIR"
|
||||
git -C "$TARGET_DIR" remote set-url origin "$REPO_URL"
|
||||
git -C "$TARGET_DIR" fetch origin
|
||||
git -C "$TARGET_DIR" checkout main
|
||||
git -C "$TARGET_DIR" pull --ff-only
|
||||
else
|
||||
mkdir -p "$(dirname "$TARGET_DIR")"
|
||||
git clone "$REPO_URL" "$TARGET_DIR"
|
||||
fi
|
||||
|
||||
git -C "$TARGET_DIR" config pull.ff only
|
||||
|
||||
cat <<EOF
|
||||
Ready.
|
||||
|
||||
Repo: $TARGET_DIR
|
||||
Remote: $REPO_URL
|
||||
|
||||
Daily flow:
|
||||
cd "$TARGET_DIR"
|
||||
git pull --ff-only
|
||||
# edit Markdown
|
||||
git add .
|
||||
git commit -m "Update docs"
|
||||
git push
|
||||
|
||||
AFFiNE updates after the projection operator pulls and runs projection.
|
||||
EOF
|
||||
837
scripts/setup-section0-docs.sh
Executable file
837
scripts/setup-section0-docs.sh
Executable file
@@ -0,0 +1,837 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
REPO_URL="${REPO_URL:-http://100.64.0.1:30087/section0/rta-handbook.git}"
|
||||
TARGET_DIR="${TARGET_DIR:-$HOME/Developer/Section0/rta-handbook}"
|
||||
COMMAND_DIR="${COMMAND_DIR:-$HOME/.local/bin}"
|
||||
COMMAND_NAME="${COMMAND_NAME:-section0-docs}"
|
||||
COMMAND_PATH="$COMMAND_DIR/$COMMAND_NAME"
|
||||
SERVER_URL="${SECTION0_SERVER_URL:-https://ops.virgil.info/md-to-section0-api}"
|
||||
STATUS_URL="${SECTION0_STATUS_URL:-https://ops.virgil.info/section0/projection-status.json}"
|
||||
|
||||
step() {
|
||||
printf "\n==> %s\n" "$*"
|
||||
}
|
||||
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
echo "git is required but was not found on PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
step "Preparing local Markdown workspace"
|
||||
if [ -d "$TARGET_DIR/.git" ]; then
|
||||
echo "Repo already exists: $TARGET_DIR"
|
||||
git -C "$TARGET_DIR" remote set-url origin "$REPO_URL"
|
||||
git -C "$TARGET_DIR" fetch origin
|
||||
git -C "$TARGET_DIR" checkout main
|
||||
git -C "$TARGET_DIR" pull --ff-only
|
||||
else
|
||||
mkdir -p "$(dirname "$TARGET_DIR")"
|
||||
git clone "$REPO_URL" "$TARGET_DIR"
|
||||
fi
|
||||
|
||||
git -C "$TARGET_DIR" config pull.ff only
|
||||
|
||||
step "Installing helper command"
|
||||
mkdir -p "$COMMAND_DIR"
|
||||
cat > "$COMMAND_PATH" <<EOF
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
REPO_DIR="$TARGET_DIR"
|
||||
SERVER_URL="$SERVER_URL"
|
||||
STATUS_URL="$STATUS_URL"
|
||||
SESSION_DIR="\${SECTION0_SESSION_DIR:-\$HOME/.config/section0-docs}"
|
||||
SESSION_PATH="\$SESSION_DIR/session.json"
|
||||
EVENTS_PATH="\$SESSION_DIR/events.jsonl"
|
||||
MOUNTS_PATH="\$SESSION_DIR/mounts.json"
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
section0-docs - helper for Section 0 shared Markdown docs
|
||||
|
||||
Commands:
|
||||
auth login open Authentik and install Git credentials
|
||||
auth finish finish login from a printed device code
|
||||
auth status show current saved login
|
||||
configure set Git author name/email for this repo
|
||||
doctor check clone, author, remote, and read access
|
||||
open print the repo path
|
||||
pull pull latest Markdown with --ff-only
|
||||
status show Git status
|
||||
commit commit all current changes with MESSAGE
|
||||
push push current branch
|
||||
wait show AFFiNE projection state for a pushed commit
|
||||
push-test prove write access with a temporary remote branch
|
||||
trace show local/origin state and recent helper events
|
||||
mount list
|
||||
list local personal Markdown projections
|
||||
mount add NAME PATH
|
||||
remember a local Markdown folder as a personal read-only projection
|
||||
mount plan NAME
|
||||
print the projection plan that would be sent to the home-lab
|
||||
mount sync NAME
|
||||
send the projection plan to the authenticated home-lab endpoint
|
||||
mount status NAME
|
||||
show the local projection declaration and last sync receipt
|
||||
mount unmount NAME
|
||||
ask the home-lab to remove projected AFFiNE docs for this mount
|
||||
mount remove NAME
|
||||
remove the local projection declaration
|
||||
help show this help
|
||||
|
||||
Examples:
|
||||
section0-docs auth login
|
||||
section0-docs auth finish DEVICE_CODE
|
||||
section0-docs auth status
|
||||
section0-docs doctor
|
||||
section0-docs configure
|
||||
section0-docs pull
|
||||
section0-docs status
|
||||
section0-docs push-test
|
||||
section0-docs trace
|
||||
section0-docs mount add research ~/Documents/Obsidian/Research
|
||||
section0-docs mount sync research
|
||||
MESSAGE="Update concept notes" section0-docs commit
|
||||
section0-docs push
|
||||
section0-docs wait
|
||||
|
||||
AFFiNE is read-only for these docs. Make lasting changes in Markdown.
|
||||
USAGE
|
||||
}
|
||||
|
||||
need_python() {
|
||||
command -v python3 >/dev/null 2>&1 || {
|
||||
echo "python3 is required for Authentik login JSON handling" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
json_get() {
|
||||
need_python
|
||||
python3 -c 'import json,sys; data=json.load(sys.stdin); cur=data
|
||||
for part in sys.argv[1].split("."):
|
||||
cur = cur.get(part, "") if isinstance(cur, dict) else ""
|
||||
print(cur if cur is not None else "")' "\$1"
|
||||
}
|
||||
|
||||
now_utc() {
|
||||
date -u +%Y-%m-%dT%H:%M:%SZ
|
||||
}
|
||||
|
||||
run_id() {
|
||||
if command -v uuidgen >/dev/null 2>&1; then
|
||||
uuidgen | tr '[:upper:]' '[:lower:]'
|
||||
else
|
||||
date -u +%Y%m%d%H%M%S
|
||||
fi
|
||||
}
|
||||
|
||||
json_log() {
|
||||
need_python
|
||||
mkdir -p "\$SESSION_DIR"
|
||||
event="\$1"
|
||||
detail="\${2:-}"
|
||||
current_head="\$(git -C "\$REPO_DIR" rev-parse --short HEAD 2>/dev/null || true)"
|
||||
current_branch="\$(git -C "\$REPO_DIR" branch --show-current 2>/dev/null || true)"
|
||||
remote="\$(git -C "\$REPO_DIR" remote get-url origin 2>/dev/null || true)"
|
||||
python3 -c 'import json,sys
|
||||
print(json.dumps({
|
||||
"ts": sys.argv[1],
|
||||
"event": sys.argv[2],
|
||||
"detail": sys.argv[3],
|
||||
"repo": sys.argv[4],
|
||||
"branch": sys.argv[5],
|
||||
"head": sys.argv[6],
|
||||
"remote": sys.argv[7],
|
||||
}, separators=(",", ":")))' "\$(now_utc)" "\$event" "\$detail" "\$REPO_DIR" "\$current_branch" "\$current_head" "\$remote" >> "\$EVENTS_PATH"
|
||||
chmod 600 "\$EVENTS_PATH"
|
||||
}
|
||||
|
||||
open_url() {
|
||||
if command -v open >/dev/null 2>&1; then
|
||||
open "\$1" >/dev/null 2>&1 || true
|
||||
elif command -v xdg-open >/dev/null 2>&1; then
|
||||
xdg-open "\$1" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
credential_host() {
|
||||
need_python
|
||||
python3 -c 'from urllib.parse import urlparse; import sys
|
||||
url=urlparse(sys.argv[1])
|
||||
print(url.netloc)' "\$1"
|
||||
}
|
||||
|
||||
http_json() {
|
||||
curl --connect-timeout 5 --max-time 15 -fsS "\$@"
|
||||
}
|
||||
|
||||
projection_status() {
|
||||
http_json "\$STATUS_URL"
|
||||
}
|
||||
|
||||
wait_for_projection() {
|
||||
expected="\${1:-}"
|
||||
[ -n "\$expected" ] || expected="\$(git -C "\$REPO_DIR" rev-parse --short HEAD)"
|
||||
timeout_seconds="\${SECTION0_WAIT_TIMEOUT_SECONDS:-180}"
|
||||
started_at="\$(date +%s)"
|
||||
echo "Waiting for AFFiNE projection:"
|
||||
echo " commit: \$expected"
|
||||
echo " status: \$STATUS_URL"
|
||||
while :; do
|
||||
status_json="\$(projection_status 2>/dev/null || true)"
|
||||
if [ -n "\$status_json" ]; then
|
||||
state="\$(printf "%s" "\$status_json" | json_get state 2>/dev/null || true)"
|
||||
origin_commit="\$(printf "%s" "\$status_json" | json_get source.originCommit 2>/dev/null || true)"
|
||||
projected_commit="\$(printf "%s" "\$status_json" | json_get projected.commit 2>/dev/null || true)"
|
||||
completed_at="\$(printf "%s" "\$status_json" | json_get projected.completedAt 2>/dev/null || true)"
|
||||
backend_ok="\$(printf "%s" "\$status_json" | json_get verification.backend.ok 2>/dev/null || true)"
|
||||
native_ok="\$(printf "%s" "\$status_json" | json_get verification.nativeRead.ok 2>/dev/null || true)"
|
||||
browser_status="\$(printf "%s" "\$status_json" | json_get verification.browserFreshness.status 2>/dev/null || true)"
|
||||
writer_status="\$(printf "%s" "\$status_json" | json_get writer.status 2>/dev/null || true)"
|
||||
writer_note="\$(printf "%s" "\$status_json" | json_get writer.note 2>/dev/null || true)"
|
||||
readme_url="\$(printf "%s" "\$status_json" | python3 -c 'import json,sys
|
||||
data=json.load(sys.stdin)
|
||||
for doc in (data.get("projected") or {}).get("docs") or []:
|
||||
if doc.get("sourcePath") == "README.md":
|
||||
print(doc.get("url",""))
|
||||
break' 2>/dev/null || true)"
|
||||
printf " origin=%s projected=%s state=%s%s\n" "\${origin_commit:-unknown}" "\${projected_commit:-none}" "\${state:-unknown}" "\${completed_at:+ completed=\$completed_at}"
|
||||
printf " writer: %s\n" "\${writer_status:-unknown}"
|
||||
printf " verification: backend=%s native-read=%s browser=%s\n" "\${backend_ok:-unknown}" "\${native_ok:-unknown}" "\${browser_status:-not_verified}"
|
||||
if [ "\$state" = "writer-frozen" ] || [ "\$writer_status" = "frozen" ]; then
|
||||
echo "AFFiNE projection is paused. Your Git push is still the source of truth."
|
||||
[ -z "\$writer_note" ] || echo "Reason: \$writer_note"
|
||||
json_log "wait.writer_frozen" "\$expected"
|
||||
return 2
|
||||
fi
|
||||
if [ "\$projected_commit" = "\$expected" ] && { [ "\$backend_ok" = "True" ] || [ "\$backend_ok" = "true" ]; } && { [ "\$native_ok" = "True" ] || [ "\$native_ok" = "true" ]; }; then
|
||||
echo "Projected and verified through AFFiNE backend and native read."
|
||||
[ -z "\$readme_url" ] || echo "Doc: \$readme_url"
|
||||
echo "If a browser tab still shows old content, reload the AFFiNE tab or open the doc URL in a fresh tab."
|
||||
json_log "wait.complete" "\$expected"
|
||||
return 0
|
||||
elif [ "\$projected_commit" = "\$expected" ]; then
|
||||
echo " commit is projected, but verification is not complete yet"
|
||||
fi
|
||||
else
|
||||
echo " projection status not reachable yet"
|
||||
fi
|
||||
now="\$(date +%s)"
|
||||
elapsed=\$((now - started_at))
|
||||
if [ "\$elapsed" -ge "\$timeout_seconds" ]; then
|
||||
echo "Timed out after \${timeout_seconds}s waiting for projection of \$expected." >&2
|
||||
echo "Run again later: section0-docs wait" >&2
|
||||
json_log "wait.timeout" "\$expected"
|
||||
return 1
|
||||
fi
|
||||
sleep "\${SECTION0_WAIT_INTERVAL_SECONDS:-5}"
|
||||
done
|
||||
}
|
||||
|
||||
install_git_credential() {
|
||||
remote="\$1"
|
||||
username="\$2"
|
||||
password="\$3"
|
||||
host="\$(credential_host "\$remote")"
|
||||
protocol="\$(printf "%s" "\$remote" | sed -n "s#^\\([^:/]*\\)://.*#\\1#p")"
|
||||
[ -n "\$protocol" ] || protocol="http"
|
||||
mkdir -p "\$SESSION_DIR"
|
||||
credential_path="\$SESSION_DIR/git-credentials"
|
||||
touch "\$credential_path"
|
||||
chmod 600 "\$credential_path"
|
||||
git -C "\$REPO_DIR" config --local --replace-all credential.helper ""
|
||||
git -C "\$REPO_DIR" config --local --add credential.helper "store --file=\$credential_path"
|
||||
printf "protocol=%s\nhost=%s\nusername=%s\npassword=%s\n\n" "\$protocol" "\$host" "\$username" "\$password" \
|
||||
| git -C "\$REPO_DIR" credential approve
|
||||
chmod 600 "\$credential_path"
|
||||
}
|
||||
|
||||
auth_login() {
|
||||
need_python
|
||||
mkdir -p "\$SESSION_DIR"
|
||||
login_run_id="\$(run_id)"
|
||||
json_log "auth.login.start" "\$login_run_id"
|
||||
device_json="\$(curl -fsS -X POST "\$SERVER_URL/device")"
|
||||
code="\$(printf "%s" "\$device_json" | json_get code)"
|
||||
auth_url="\$(printf "%s" "\$device_json" | json_get authUrl)"
|
||||
[ -n "\$code" ] && [ -n "\$auth_url" ] || {
|
||||
echo "login server did not return a device code" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "Opening Authentik login:"
|
||||
echo " \$auth_url"
|
||||
echo "Device code:"
|
||||
echo " \$code"
|
||||
echo "Run id:"
|
||||
echo " \$login_run_id"
|
||||
echo "Token check:"
|
||||
echo " \$SERVER_URL/device/\$code/token"
|
||||
open_url "\$auth_url"
|
||||
if [ "\${SECTION0_AUTH_POLL:-}" != "1" ]; then
|
||||
echo "When the browser says login succeeded, return here and press Enter."
|
||||
printf "Press Enter to finish login: "
|
||||
IFS= read -r _section0_continue
|
||||
auth_finish "\$code"
|
||||
return
|
||||
fi
|
||||
echo "Waiting for login..."
|
||||
token_json=""
|
||||
i=0
|
||||
while [ "\$i" -lt 90 ]; do
|
||||
token_json="\$(http_json "\$SERVER_URL/device/\$code/token" 2>/dev/null || true)"
|
||||
access_token="\$(printf "%s" "\${token_json:-{}}" | json_get accessToken 2>/dev/null || true)"
|
||||
[ -n "\$access_token" ] && break
|
||||
status="\$(printf "%s" "\${token_json:-{}}" | json_get status 2>/dev/null || true)"
|
||||
if [ \$((i % 5)) -eq 0 ]; then
|
||||
printf "\n still waiting%s\n" "\${status:+ (\$status)}"
|
||||
else
|
||||
printf "."
|
||||
fi
|
||||
i=\$((i + 1))
|
||||
sleep 2
|
||||
done
|
||||
printf "\n"
|
||||
[ -n "\${access_token:-}" ] || {
|
||||
echo "timed out waiting for Authentik login" >&2
|
||||
echo "If the browser says login succeeded, run:" >&2
|
||||
echo " section0-docs auth finish \$code" >&2
|
||||
exit 1
|
||||
}
|
||||
complete_login "\$token_json" "\$access_token"
|
||||
}
|
||||
|
||||
auth_finish() {
|
||||
need_python
|
||||
mkdir -p "\$SESSION_DIR"
|
||||
code="\${1:-}"
|
||||
[ -n "\$code" ] || {
|
||||
echo "usage: section0-docs auth finish DEVICE_CODE" >&2
|
||||
exit 1
|
||||
}
|
||||
token_json="\$(http_json "\$SERVER_URL/device/\$code/token")"
|
||||
access_token="\$(printf "%s" "\$token_json" | json_get accessToken 2>/dev/null || true)"
|
||||
[ -n "\$access_token" ] || {
|
||||
echo "No completed login token for code: \$code" >&2
|
||||
printf "%s\n" "\$token_json" >&2
|
||||
exit 1
|
||||
}
|
||||
complete_login "\$token_json" "\$access_token"
|
||||
}
|
||||
|
||||
complete_login() {
|
||||
token_json="\$1"
|
||||
access_token="\$2"
|
||||
echo "Login accepted; requesting Section 0 Git access..."
|
||||
access_json="\$(http_json -X POST -H "Authorization: Bearer \$access_token" "\$SERVER_URL/section0/git/access")"
|
||||
ok="\$(printf "%s" "\$access_json" | json_get ok)"
|
||||
[ "\$ok" = "True" ] || [ "\$ok" = "true" ] || {
|
||||
echo "Git access broker did not return credentials:" >&2
|
||||
printf "%s\n" "\$access_json" >&2
|
||||
exit 1
|
||||
}
|
||||
remote="\$(printf "%s" "\$access_json" | json_get remote)"
|
||||
git_username="\$(printf "%s" "\$access_json" | json_get git.username)"
|
||||
git_password="\$(printf "%s" "\$access_json" | json_get git.password)"
|
||||
author_name="\$(printf "%s" "\$access_json" | json_get author.name)"
|
||||
author_email="\$(printf "%s" "\$access_json" | json_get author.email)"
|
||||
[ -n "\$remote" ] && [ -n "\$git_username" ] && [ -n "\$git_password" ] || {
|
||||
echo "Git access broker response was incomplete" >&2
|
||||
exit 1
|
||||
}
|
||||
git -C "\$REPO_DIR" remote set-url origin "\$remote"
|
||||
git -C "\$REPO_DIR" config user.name "\$author_name"
|
||||
git -C "\$REPO_DIR" config user.email "\$author_email"
|
||||
echo "Storing Git credential..."
|
||||
install_git_credential "\$remote" "\$git_username" "\$git_password"
|
||||
python3 -c 'import json,sys
|
||||
token=json.loads(sys.argv[1])
|
||||
access=json.loads(sys.argv[2])
|
||||
print(json.dumps({
|
||||
"authenticated": True,
|
||||
"serverUrl": sys.argv[3],
|
||||
"accessToken": token.get("accessToken", ""),
|
||||
"authentik": token.get("authentik", {}),
|
||||
"affine": token.get("affine", {}),
|
||||
"author": access.get("author", {}),
|
||||
"remote": access.get("remote", ""),
|
||||
"credentialUser": access.get("git", {}).get("username", "")
|
||||
}, indent=2))' "\$token_json" "\$access_json" "\$SERVER_URL" > "\$SESSION_PATH"
|
||||
chmod 600 "\$SESSION_PATH"
|
||||
echo "Authenticated: \$author_email"
|
||||
echo "Git remote: \$remote"
|
||||
echo "Git user: \$git_username"
|
||||
echo "Session: \$SESSION_PATH"
|
||||
json_log "auth.login.complete" "\$author_email"
|
||||
}
|
||||
|
||||
require_session() {
|
||||
if [ ! -f "\$SESSION_PATH" ]; then
|
||||
echo "not authenticated; run: section0-docs auth login" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
session_value() {
|
||||
require_session
|
||||
json_get "\$1" < "\$SESSION_PATH"
|
||||
}
|
||||
|
||||
mounts_init() {
|
||||
mkdir -p "\$SESSION_DIR"
|
||||
if [ ! -f "\$MOUNTS_PATH" ]; then
|
||||
printf '{\n "mounts": {}\n}\n' > "\$MOUNTS_PATH"
|
||||
chmod 600 "\$MOUNTS_PATH"
|
||||
fi
|
||||
}
|
||||
|
||||
mount_add() {
|
||||
need_python
|
||||
require_session
|
||||
mounts_init
|
||||
name="\${1:-}"
|
||||
source="\${2:-}"
|
||||
[ -n "\$name" ] && [ -n "\$source" ] || {
|
||||
echo "usage: section0-docs mount add NAME PATH" >&2
|
||||
exit 1
|
||||
}
|
||||
if [ ! -d "\$source" ]; then
|
||||
echo "mount source is not a directory: \$source" >&2
|
||||
exit 1
|
||||
fi
|
||||
abs_source="\$(cd "\$source" && pwd)"
|
||||
username="\$(session_value authentik.username)"
|
||||
[ -n "\$username" ] || username="\$(session_value authentik.email | sed 's/@.*//')"
|
||||
workspace_id="\$(session_value affine.workspaceId)"
|
||||
namespace="mount/\$username/\$name"
|
||||
python3 - "\$MOUNTS_PATH" "\$name" "\$abs_source" "\$namespace" "\$workspace_id" <<'PY'
|
||||
import datetime, json, sys
|
||||
path, name, source, namespace, workspace_id = sys.argv[1:6]
|
||||
now = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
mounts = data.setdefault("mounts", {})
|
||||
mounts[name] = {
|
||||
"name": name,
|
||||
"source": source,
|
||||
"namespace": namespace,
|
||||
"workspaceId": workspace_id,
|
||||
"include": ["**/*.md"],
|
||||
"exclude": [".git/**", ".obsidian/**", ".trash/**", "node_modules/**"],
|
||||
"createdAt": mounts.get(name, {}).get("createdAt") or now,
|
||||
"updatedAt": now,
|
||||
}
|
||||
with open(path, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
PY
|
||||
chmod 600 "\$MOUNTS_PATH"
|
||||
echo "Mount saved: \$name"
|
||||
echo "Source: \$abs_source"
|
||||
echo "AFFiNE namespace: \$namespace"
|
||||
json_log "mount.add" "\$name"
|
||||
}
|
||||
|
||||
mount_list() {
|
||||
need_python
|
||||
mounts_init
|
||||
python3 - "\$MOUNTS_PATH" <<'PY'
|
||||
import json, sys
|
||||
with open(sys.argv[1]) as f:
|
||||
mounts = json.load(f).get("mounts", {})
|
||||
if not mounts:
|
||||
print("no personal Markdown mounts; add one with: section0-docs mount add NAME PATH")
|
||||
raise SystemExit
|
||||
for name, mount in sorted(mounts.items()):
|
||||
print(f"{name}\t{mount.get('source','')}\t{mount.get('namespace','')}")
|
||||
PY
|
||||
}
|
||||
|
||||
mount_status() {
|
||||
need_python
|
||||
mounts_init
|
||||
name="\${1:-}"
|
||||
[ -n "\$name" ] || {
|
||||
echo "usage: section0-docs mount status NAME" >&2
|
||||
exit 1
|
||||
}
|
||||
python3 - "\$MOUNTS_PATH" "\$name" <<'PY'
|
||||
import json, sys
|
||||
with open(sys.argv[1]) as f:
|
||||
mount = json.load(f).get("mounts", {}).get(sys.argv[2])
|
||||
if not mount:
|
||||
print(f"mount not found: {sys.argv[2]}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
print(json.dumps(mount, indent=2))
|
||||
PY
|
||||
}
|
||||
|
||||
mount_remove() {
|
||||
need_python
|
||||
mounts_init
|
||||
name="\${1:-}"
|
||||
[ -n "\$name" ] || {
|
||||
echo "usage: section0-docs mount remove NAME" >&2
|
||||
exit 1
|
||||
}
|
||||
python3 - "\$MOUNTS_PATH" "\$name" <<'PY'
|
||||
import json, sys
|
||||
path, name = sys.argv[1:3]
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
if name not in data.get("mounts", {}):
|
||||
print(f"mount not found: {name}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
del data["mounts"][name]
|
||||
with open(path, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
PY
|
||||
echo "Removed local mount declaration: \$name"
|
||||
json_log "mount.remove" "\$name"
|
||||
}
|
||||
|
||||
mount_plan() {
|
||||
need_python
|
||||
require_session
|
||||
mounts_init
|
||||
name="\${1:-}"
|
||||
[ -n "\$name" ] || {
|
||||
echo "usage: section0-docs mount plan NAME" >&2
|
||||
exit 1
|
||||
}
|
||||
python3 - "\$MOUNTS_PATH" "\$SESSION_PATH" "\$name" <<'PY'
|
||||
import fnmatch, hashlib, json, os, pathlib, sys
|
||||
mounts_path, session_path, name = sys.argv[1:4]
|
||||
with open(mounts_path) as f:
|
||||
mount = json.load(f).get("mounts", {}).get(name)
|
||||
if not mount:
|
||||
print(f"mount not found: {name}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
with open(session_path) as f:
|
||||
session = json.load(f)
|
||||
source = pathlib.Path(mount["source"])
|
||||
if not source.is_dir():
|
||||
print(f"mount source is not a directory: {source}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
include = mount.get("include") or ["**/*.md"]
|
||||
exclude = mount.get("exclude") or []
|
||||
docs = []
|
||||
def matches(patterns, rel):
|
||||
for pattern in patterns:
|
||||
if fnmatch.fnmatch(rel, pattern):
|
||||
return True
|
||||
if pattern.startswith("**/") and fnmatch.fnmatch(rel, pattern[3:]):
|
||||
return True
|
||||
return False
|
||||
|
||||
for path in sorted(source.rglob("*.md")):
|
||||
if not path.is_file():
|
||||
continue
|
||||
rel = path.relative_to(source).as_posix()
|
||||
if not matches(include, rel):
|
||||
continue
|
||||
if any(fnmatch.fnmatch(rel, pattern) or rel.startswith(pattern.removesuffix("/**") + "/") for pattern in exclude):
|
||||
continue
|
||||
markdown = path.read_text(encoding="utf-8")
|
||||
digest = hashlib.sha256(markdown.encode("utf-8")).hexdigest()
|
||||
stable = hashlib.sha256(f"{session.get('authentik', {}).get('subject','')}:{mount['namespace']}:{rel}".encode("utf-8")).hexdigest()[:24]
|
||||
title = path.stem.replace("_", " ").replace("-", " ").strip() or rel
|
||||
docs.append({
|
||||
"stableSourceId": stable,
|
||||
"operation": "update",
|
||||
"sourcePath": rel,
|
||||
"sourceSha256": digest,
|
||||
"title": title,
|
||||
"markdown": markdown,
|
||||
"affine": {
|
||||
"docId": f"obs-{stable}",
|
||||
"namespace": mount["namespace"],
|
||||
},
|
||||
})
|
||||
plan = {
|
||||
"metadata": {
|
||||
"name": name,
|
||||
"sourceModel": "personal-local-markdown",
|
||||
"custody": "local-user",
|
||||
"readOnlyIntent": True,
|
||||
},
|
||||
"owner": {
|
||||
"authentik": session.get("authentik", {}),
|
||||
"affine": session.get("affine", {}),
|
||||
},
|
||||
"target": {
|
||||
"workspaceId": mount.get("workspaceId") or session.get("affine", {}).get("workspaceId"),
|
||||
"namespace": mount["namespace"],
|
||||
},
|
||||
"summary": {
|
||||
"documents": len(docs),
|
||||
"source": str(source),
|
||||
},
|
||||
"projectedDocuments": docs,
|
||||
}
|
||||
print(json.dumps({"plan": plan}, indent=2))
|
||||
PY
|
||||
}
|
||||
|
||||
mount_sync() {
|
||||
need_python
|
||||
require_session
|
||||
name="\${1:-}"
|
||||
[ -n "\$name" ] || {
|
||||
echo "usage: section0-docs mount sync NAME" >&2
|
||||
exit 1
|
||||
}
|
||||
access_token="\$(session_value accessToken)"
|
||||
[ -n "\$access_token" ] || {
|
||||
echo "saved session has no access token; run: section0-docs auth login" >&2
|
||||
exit 1
|
||||
}
|
||||
tmp_plan="\$(mktemp)"
|
||||
mount_plan "\$name" > "\$tmp_plan"
|
||||
doc_count="\$(python3 -c 'import json,sys; print(len(json.load(open(sys.argv[1]))["plan"].get("projectedDocuments", [])))' "\$tmp_plan")"
|
||||
echo "Submitting personal Markdown projection:"
|
||||
echo " mount: \$name"
|
||||
echo " docs: \$doc_count"
|
||||
response="\$(curl -fsS -X POST -H "Authorization: Bearer \$access_token" -H "Content-Type: application/json" --data-binary "@\$tmp_plan" "\$SERVER_URL/sync")"
|
||||
rm -f "\$tmp_plan"
|
||||
printf "%s\n" "\$response"
|
||||
python3 - "\$MOUNTS_PATH" "\$name" "\$response" <<'PY'
|
||||
import datetime, json, sys
|
||||
path, name, response = sys.argv[1:4]
|
||||
now = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
mount = data.setdefault("mounts", {}).setdefault(name, {"name": name})
|
||||
mount["lastSyncAt"] = now
|
||||
try:
|
||||
mount["lastSync"] = json.loads(response)
|
||||
except Exception:
|
||||
mount["lastSync"] = {"raw": response}
|
||||
with open(path, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
PY
|
||||
json_log "mount.sync" "\$name"
|
||||
}
|
||||
|
||||
mount_unmount() {
|
||||
need_python
|
||||
require_session
|
||||
mounts_init
|
||||
name="\${1:-}"
|
||||
[ -n "\$name" ] || {
|
||||
echo "usage: section0-docs mount unmount NAME" >&2
|
||||
exit 1
|
||||
}
|
||||
access_token="\$(session_value accessToken)"
|
||||
[ -n "\$access_token" ] || {
|
||||
echo "saved session has no access token; run: section0-docs auth login" >&2
|
||||
exit 1
|
||||
}
|
||||
payload="\$(python3 - "\$MOUNTS_PATH" "\$SESSION_PATH" "\$name" <<'PY'
|
||||
import json, sys
|
||||
with open(sys.argv[1]) as f:
|
||||
mount = json.load(f).get("mounts", {}).get(sys.argv[3])
|
||||
if not mount:
|
||||
print(f"mount not found: {sys.argv[3]}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
with open(sys.argv[2]) as f:
|
||||
session = json.load(f)
|
||||
print(json.dumps({
|
||||
"workspaceId": mount.get("workspaceId") or session.get("affine", {}).get("workspaceId"),
|
||||
"namespace": mount["namespace"],
|
||||
}))
|
||||
PY
|
||||
)"
|
||||
response="\$(printf "%s" "\$payload" | curl -fsS -X POST -H "Authorization: Bearer \$access_token" -H "Content-Type: application/json" --data-binary @- "\$SERVER_URL/unmount")"
|
||||
printf "%s\n" "\$response"
|
||||
json_log "mount.unmount" "\$name"
|
||||
}
|
||||
|
||||
case "\${1:-help}" in
|
||||
auth)
|
||||
case "\${2:-}" in
|
||||
login)
|
||||
auth_login
|
||||
;;
|
||||
finish)
|
||||
auth_finish "\${3:-}"
|
||||
;;
|
||||
status)
|
||||
if [ -f "\$SESSION_PATH" ]; then
|
||||
cat "\$SESSION_PATH"
|
||||
else
|
||||
echo "not authenticated; run: section0-docs auth login" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "usage: section0-docs auth <login|status>" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
configure)
|
||||
current_name="\$(git -C "\$REPO_DIR" config user.name || true)"
|
||||
current_email="\$(git -C "\$REPO_DIR" config user.email || true)"
|
||||
printf "Git author name [%s]: " "\$current_name"
|
||||
IFS= read -r author_name
|
||||
printf "Git author email [%s]: " "\$current_email"
|
||||
IFS= read -r author_email
|
||||
if [ -n "\$author_name" ]; then
|
||||
git -C "\$REPO_DIR" config user.name "\$author_name"
|
||||
elif [ -z "\$current_name" ]; then
|
||||
echo "Git author name is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "\$author_email" ]; then
|
||||
git -C "\$REPO_DIR" config user.email "\$author_email"
|
||||
elif [ -z "\$current_email" ]; then
|
||||
echo "Git author email is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
doctor)
|
||||
remote="\$(git -C "\$REPO_DIR" remote get-url origin)"
|
||||
branch="\$(git -C "\$REPO_DIR" branch --show-current)"
|
||||
author_name="\$(git -C "\$REPO_DIR" config user.name || true)"
|
||||
author_email="\$(git -C "\$REPO_DIR" config user.email || true)"
|
||||
echo "Repo: \$REPO_DIR"
|
||||
echo "Remote: \$remote"
|
||||
echo "Branch: \$branch"
|
||||
if [ -n "\$author_name" ] && [ -n "\$author_email" ]; then
|
||||
echo "Git author: \$author_name <\$author_email>"
|
||||
else
|
||||
echo "Git author: missing; run section0-docs configure"
|
||||
fi
|
||||
git -C "\$REPO_DIR" ls-remote --heads origin main >/dev/null
|
||||
echo "Read access: ok"
|
||||
;;
|
||||
open)
|
||||
printf "%s\n" "\$REPO_DIR"
|
||||
;;
|
||||
pull)
|
||||
git -C "\$REPO_DIR" pull --ff-only
|
||||
;;
|
||||
status)
|
||||
git -C "\$REPO_DIR" status --short --branch
|
||||
;;
|
||||
commit)
|
||||
: "\${MESSAGE:?Set MESSAGE before running commit}"
|
||||
json_log "commit.start" "\$MESSAGE"
|
||||
git -C "\$REPO_DIR" add .
|
||||
git -C "\$REPO_DIR" commit -m "\$MESSAGE"
|
||||
json_log "commit.complete" "\$(git -C "\$REPO_DIR" rev-parse --short HEAD)"
|
||||
;;
|
||||
push)
|
||||
before="\$(git -C "\$REPO_DIR" rev-parse --short HEAD)"
|
||||
json_log "push.start" "\$before"
|
||||
git -C "\$REPO_DIR" push
|
||||
git -C "\$REPO_DIR" fetch origin >/dev/null 2>&1 || true
|
||||
origin_head="\$(git -C "\$REPO_DIR" rev-parse --short origin/main 2>/dev/null || true)"
|
||||
json_log "push.complete" "\${origin_head:-\$before}"
|
||||
echo "Pushed commit: \${origin_head:-\$before}"
|
||||
if [ "\${SECTION0_WAIT_AFTER_PUSH:-1}" = "1" ]; then
|
||||
wait_for_projection "\${origin_head:-\$before}"
|
||||
else
|
||||
echo "AFFiNE updates after the operator refresh runs."
|
||||
echo "To watch it: section0-docs wait"
|
||||
fi
|
||||
;;
|
||||
wait)
|
||||
wait_for_projection "\${2:-}"
|
||||
;;
|
||||
push-test)
|
||||
branch="section0-smoke-\${USER:-user}-\$(date +%Y%m%d%H%M%S)"
|
||||
git -C "\$REPO_DIR" push origin HEAD:refs/heads/"\$branch"
|
||||
git -C "\$REPO_DIR" push origin :refs/heads/"\$branch"
|
||||
echo "Write access: ok"
|
||||
json_log "push-test.complete" "\$branch"
|
||||
;;
|
||||
trace)
|
||||
echo "Repo: \$REPO_DIR"
|
||||
echo "Remote: \$(git -C "\$REPO_DIR" remote get-url origin)"
|
||||
echo "Projection status: \$STATUS_URL"
|
||||
echo "Branch: \$(git -C "\$REPO_DIR" branch --show-current)"
|
||||
echo "Local HEAD: \$(git -C "\$REPO_DIR" rev-parse --short HEAD)"
|
||||
git -C "\$REPO_DIR" fetch origin >/dev/null 2>&1 || true
|
||||
echo "Origin HEAD: \$(git -C "\$REPO_DIR" rev-parse --short origin/main 2>/dev/null || true)"
|
||||
echo "Status:"
|
||||
git -C "\$REPO_DIR" status --short --branch
|
||||
echo
|
||||
echo "Recent helper events: \$EVENTS_PATH"
|
||||
if [ -f "\$EVENTS_PATH" ]; then
|
||||
tail -20 "\$EVENTS_PATH"
|
||||
else
|
||||
echo "no events yet"
|
||||
fi
|
||||
;;
|
||||
mount)
|
||||
case "\${2:-}" in
|
||||
list)
|
||||
mount_list
|
||||
;;
|
||||
add)
|
||||
mount_add "\${3:-}" "\${4:-}"
|
||||
;;
|
||||
plan)
|
||||
mount_plan "\${3:-}"
|
||||
;;
|
||||
sync)
|
||||
mount_sync "\${3:-}"
|
||||
;;
|
||||
status)
|
||||
mount_status "\${3:-}"
|
||||
;;
|
||||
unmount)
|
||||
mount_unmount "\${3:-}"
|
||||
;;
|
||||
remove)
|
||||
mount_remove "\${3:-}"
|
||||
;;
|
||||
*)
|
||||
echo "usage: section0-docs mount <list|add|plan|sync|status|unmount|remove>" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
help|--help|-h)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "$COMMAND_PATH"
|
||||
|
||||
cat <<EOF
|
||||
Ready.
|
||||
|
||||
Repo: $TARGET_DIR
|
||||
Remote: $REPO_URL
|
||||
Helper: $COMMAND_PATH
|
||||
|
||||
Daily flow:
|
||||
$COMMAND_NAME auth login
|
||||
$COMMAND_NAME doctor
|
||||
$COMMAND_NAME pull
|
||||
# edit Markdown
|
||||
$COMMAND_NAME status
|
||||
MESSAGE="Update docs" $COMMAND_NAME commit
|
||||
$COMMAND_NAME push
|
||||
$COMMAND_NAME wait
|
||||
|
||||
Contributor setup:
|
||||
$COMMAND_NAME configure
|
||||
$COMMAND_NAME push-test
|
||||
|
||||
By default, push waits until the operator projection status says the commit has
|
||||
been projected and verified in AFFiNE.
|
||||
|
||||
If $COMMAND_DIR is not on PATH, add this to your shell profile:
|
||||
export PATH="$COMMAND_DIR:\$PATH"
|
||||
EOF
|
||||
Reference in New Issue
Block a user