mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 23:37:53 +01:00
Bins, careful/freeze hooks, setup, upgrade migrations, browse/src, design, ios-qa daemon, lib and scripts resolve the state root through bin/gstack-state-root.sh (bash) or lib/state-root.ts (TS). Bins source the twin and stop with a reinstall message when it is missing; hooks source it and never spawn gstack-paths. browse/src/config.ts and lib/cso/state.ts delegate to resolveStateRoot. Analytics writers and readers move together so the usage log stays one file. gstack-uninstall deletes state only at ~/.gstack, refuses (exit 2) when it resolves to /, $HOME or an ancestor, the checkout or the git root, and leaves any other resolved root in place with the removal command. Fixtures that copy single bins now copy the twin.
526 lines
21 KiB
Bash
Executable File
526 lines
21 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack-artifacts-init — set up ~/.gstack/ as a git repo synced to a private
|
|
# git host (GitHub or GitLab) so a remote gbrain can ingest your artifacts
|
|
# (CEO plans, designs, /investigate reports) as a federated source.
|
|
#
|
|
# Replaces gstack-brain-init in v1.27.0.0 (per D4 hard-delete; no compat
|
|
# shim). Existing users are migrated by gstack-upgrade/migrations/v1.27.0.0.sh.
|
|
#
|
|
# Usage:
|
|
# gstack-artifacts-init [--remote <url>] [--host github|gitlab|manual]
|
|
# [--push-protocol auto|https|ssh]
|
|
# [--url-form-supported true|false]
|
|
#
|
|
# Interactive by default. Pass --remote to skip the host prompt.
|
|
#
|
|
# Idempotent: safe to re-run. If ~/.gstack/.git already exists AND points at
|
|
# the same remote, reconfigures drivers/hooks/attributes without clobbering
|
|
# history. If it points at a DIFFERENT remote, refuses.
|
|
#
|
|
# What it does:
|
|
# 1. git init ~/.gstack/ (or verify existing repo points at the right remote)
|
|
# 2. Write .gitignore = "*" (ignore everything; allowlist is explicit)
|
|
# 3. Write .brain-allowlist (canonical paths to sync)
|
|
# 4. Write .brain-privacy-map.json (paths → privacy class)
|
|
# 5. Write .gitattributes (register JSONL + union merge drivers)
|
|
# 6. git config merge.jsonl-append.driver + merge.union.driver
|
|
# 7. Install .git/hooks/pre-commit (defense-in-depth secret scan)
|
|
# 8. Provider-aware repo create (gh / glab) OR manual URL paste
|
|
# 9. Initial commit + push
|
|
# 10. Write ~/.gstack-artifacts-remote.txt (HTTPS URL — canonical form)
|
|
# 11. Print "Send this to your brain admin" hookup command
|
|
#
|
|
# Env:
|
|
# GSTACK_HOME — override ~/.gstack
|
|
# USER — fallback for repo naming if $USER is unset
|
|
|
|
# Heredoc delivery guard. bash 5.2+ writes a heredoc body <=64KiB through a
|
|
# pipe in the forked child before exec, with no reader on the other end. On
|
|
# macOS under pipe-KVA pressure a fresh pipe gets a 512-byte buffer, so any
|
|
# body >=512B blocks write() forever and the script hangs at startup with no
|
|
# output. Compat level 50 restores the tempfile path. These scripts are
|
|
# bash-3.2-clean, so the compat level costs them nothing. Not exported: the
|
|
# guard is per-script, and it survives `bash script.sh` call sites that
|
|
# bypass the shebang.
|
|
BASH_COMPAT=50
|
|
|
|
set -euo pipefail
|
|
|
|
. "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; }
|
|
gstack_state_root_select
|
|
GSTACK_HOME="$_gstack_sr_root"
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
URL_BIN="$SCRIPT_DIR/gstack-artifacts-url"
|
|
REMOTE_FILE="$HOME/.gstack-artifacts-remote.txt"
|
|
|
|
# Egress receipt helpers (_receipted_git): fail-open for user-directed
|
|
# git ops against the user's own artifacts remote.
|
|
. "$SCRIPT_DIR/gstack-egress-lib.sh"
|
|
|
|
# remote host for receipt records (github.com etc). Set once PUSH_URL exists.
|
|
_artifacts_host() {
|
|
local h="${PUSH_URL#*://}"; h="${h#*@}"; h="${h%%[/:]*}"
|
|
echo "${h:-unknown}"
|
|
}
|
|
|
|
REMOTE_URL=""
|
|
HOST_PREF=""
|
|
PUSH_PROTOCOL="auto"
|
|
REMOTE_SOURCE="provider"
|
|
URL_FORM_SUPPORTED="false"
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--remote) REMOTE_URL="$2"; REMOTE_SOURCE="explicit"; shift 2 ;;
|
|
--host) HOST_PREF="$2"; shift 2 ;;
|
|
--push-protocol) PUSH_PROTOCOL="$2"; shift 2 ;;
|
|
--url-form-supported) URL_FORM_SUPPORTED="$2"; shift 2 ;;
|
|
--help|-h) sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
*) echo "Unknown flag: $1" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
case "$PUSH_PROTOCOL" in
|
|
auto|https|ssh) ;;
|
|
*) echo "Invalid --push-protocol: $PUSH_PROTOCOL (expected auto|https|ssh)" >&2; exit 1 ;;
|
|
esac
|
|
|
|
# ---- preconditions ----
|
|
mkdir -p "$GSTACK_HOME"
|
|
|
|
EXISTING_REMOTE=""
|
|
if [ -d "$GSTACK_HOME/.git" ]; then
|
|
EXISTING_REMOTE=$(git -C "$GSTACK_HOME" remote get-url origin 2>/dev/null || echo "")
|
|
if [ -n "$EXISTING_REMOTE" ] && [ -n "$REMOTE_URL" ]; then
|
|
# Compare at the canonical level. The stored remote is SSH (for git push),
|
|
# the input is usually HTTPS — same logical repo, different surface form.
|
|
EXISTING_HTTPS=$("$URL_BIN" --to https "$EXISTING_REMOTE" 2>/dev/null || echo "$EXISTING_REMOTE")
|
|
INPUT_HTTPS=$("$URL_BIN" --to https "$REMOTE_URL" 2>/dev/null || echo "$REMOTE_URL")
|
|
if [ "$EXISTING_HTTPS" != "$INPUT_HTTPS" ]; then
|
|
cat >&2 <<EOF
|
|
gstack-artifacts-init: ~/.gstack/ is already a git repo pointing at:
|
|
$EXISTING_REMOTE (canonical: $EXISTING_HTTPS)
|
|
|
|
You asked to init with:
|
|
$REMOTE_URL (canonical: $INPUT_HTTPS)
|
|
|
|
Refusing to overwrite. To switch remotes, edit manually:
|
|
git -C ~/.gstack remote set-url origin <url>
|
|
EOF
|
|
exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# ---- detect available providers ----
|
|
gh_ok=false
|
|
glab_ok=false
|
|
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then gh_ok=true; fi
|
|
if command -v glab >/dev/null 2>&1 && glab auth status >/dev/null 2>&1; then glab_ok=true; fi
|
|
|
|
# ---- choose remote URL ----
|
|
if [ -z "$REMOTE_URL" ] && [ -n "$EXISTING_REMOTE" ]; then
|
|
REMOTE_URL="$EXISTING_REMOTE"
|
|
REMOTE_SOURCE="existing"
|
|
echo "Using existing remote: $REMOTE_URL"
|
|
fi
|
|
|
|
REPO_NAME="gstack-artifacts-${USER:-$(whoami)}"
|
|
DESCRIPTION="gstack artifacts (CEO plans, designs, reports) — synced from ~/.gstack/projects/"
|
|
|
|
# Decide host preference if not pinned by --host.
|
|
if [ -z "$REMOTE_URL" ] && [ -z "$HOST_PREF" ]; then
|
|
if $gh_ok && $glab_ok; then
|
|
cat >&2 <<EOF
|
|
|
|
gstack-artifacts-init: which git host?
|
|
1) GitHub (gh CLI authenticated)
|
|
2) GitLab (glab CLI authenticated)
|
|
3) Other / paste a private git URL
|
|
|
|
EOF
|
|
printf "Choice [1]: " >&2
|
|
read -r CH || CH=""
|
|
case "$CH" in
|
|
""|1) HOST_PREF="github" ;;
|
|
2) HOST_PREF="gitlab" ;;
|
|
3) HOST_PREF="manual" ;;
|
|
*) echo "Invalid choice: $CH" >&2; exit 1 ;;
|
|
esac
|
|
elif $gh_ok; then
|
|
HOST_PREF="github"
|
|
echo "Using GitHub (gh CLI authenticated; glab not available)" >&2
|
|
elif $glab_ok; then
|
|
HOST_PREF="gitlab"
|
|
echo "Using GitLab (glab CLI authenticated; gh not available)" >&2
|
|
else
|
|
HOST_PREF="manual"
|
|
echo "(Neither gh nor glab CLI authenticated — falling through to manual URL)" >&2
|
|
fi
|
|
fi
|
|
|
|
# ---- create repo on chosen host ----
|
|
if [ -z "$REMOTE_URL" ]; then
|
|
case "$HOST_PREF" in
|
|
github)
|
|
echo "Creating GitHub repo: $REPO_NAME ..."
|
|
if ! gh repo create "$REPO_NAME" --private --description "$DESCRIPTION" 2>/dev/null; then
|
|
# Maybe already exists; try to fetch its URL.
|
|
REMOTE_URL=$(gh repo view "$REPO_NAME" --json url -q .url 2>/dev/null || echo "")
|
|
if [ -z "$REMOTE_URL" ]; then
|
|
echo "Failed to create or find '$REPO_NAME'. Try --remote <url>." >&2
|
|
exit 1
|
|
fi
|
|
echo "Repo already exists; using $REMOTE_URL"
|
|
else
|
|
REMOTE_URL=$(gh repo view "$REPO_NAME" --json url -q .url 2>/dev/null || echo "")
|
|
fi
|
|
;;
|
|
gitlab)
|
|
echo "Creating GitLab repo: $REPO_NAME ..."
|
|
if ! glab repo create "$REPO_NAME" --private --description "$DESCRIPTION" 2>/dev/null; then
|
|
REMOTE_URL=$(glab repo view "$REPO_NAME" -F json 2>/dev/null | jq -r '.web_url // empty' 2>/dev/null || echo "")
|
|
if [ -z "$REMOTE_URL" ]; then
|
|
echo "Failed to create or find '$REPO_NAME'. Try --remote <url>." >&2
|
|
exit 1
|
|
fi
|
|
echo "Repo already exists; using $REMOTE_URL"
|
|
else
|
|
REMOTE_URL=$(glab repo view "$REPO_NAME" -F json 2>/dev/null | jq -r '.web_url // empty' 2>/dev/null || echo "")
|
|
fi
|
|
;;
|
|
manual)
|
|
echo "(provide a private git URL)"
|
|
printf "Paste an HTTPS git URL (e.g. https://github.com/you/gstack-artifacts.git): " >&2
|
|
read -r REMOTE_URL || REMOTE_URL=""
|
|
if [ -z "$REMOTE_URL" ]; then
|
|
echo "No URL provided. Aborting." >&2
|
|
exit 1
|
|
fi
|
|
REMOTE_SOURCE="manual"
|
|
;;
|
|
*) echo "Unknown --host: $HOST_PREF (expected github|gitlab|manual)" >&2; exit 1 ;;
|
|
esac
|
|
fi
|
|
|
|
# ---- canonicalize to HTTPS form ----
|
|
# We store HTTPS in ~/.gstack-artifacts-remote.txt (codex Finding #10:
|
|
# canonical form, derive the configured push form via gstack-artifacts-url).
|
|
# Unrecognized forms (local bare paths, file:// URLs, self-hosted gitea, etc.)
|
|
# pass through verbatim so unusual remotes still work.
|
|
CANONICAL_HTTPS=$("$URL_BIN" --to https "$REMOTE_URL" 2>/dev/null || echo "")
|
|
if [ -z "$CANONICAL_HTTPS" ]; then
|
|
CANONICAL_HTTPS="$REMOTE_URL"
|
|
fi
|
|
|
|
# Choose the push protocol without overriding an explicit URL. Provider-created
|
|
# remotes honor the provider CLI's git protocol; GitHub CLI defaults to HTTPS.
|
|
# Unknown/local URL forms pass through unchanged.
|
|
RESOLVED_PUSH_PROTOCOL="$PUSH_PROTOCOL"
|
|
if [ "$RESOLVED_PUSH_PROTOCOL" = "auto" ]; then
|
|
case "$REMOTE_SOURCE" in
|
|
explicit|existing|manual)
|
|
case "$REMOTE_URL" in
|
|
git@*|ssh://*) RESOLVED_PUSH_PROTOCOL="ssh" ;;
|
|
http://*|https://*) RESOLVED_PUSH_PROTOCOL="https" ;;
|
|
*) RESOLVED_PUSH_PROTOCOL="preserve" ;;
|
|
esac
|
|
;;
|
|
provider)
|
|
CONFIGURED_PROTOCOL=""
|
|
case "$HOST_PREF" in
|
|
github) CONFIGURED_PROTOCOL=$(gh config get git_protocol 2>/dev/null || echo "") ;;
|
|
gitlab) CONFIGURED_PROTOCOL=$(glab config get git_protocol 2>/dev/null || echo "") ;;
|
|
esac
|
|
case "$CONFIGURED_PROTOCOL" in
|
|
ssh|https) RESOLVED_PUSH_PROTOCOL="$CONFIGURED_PROTOCOL" ;;
|
|
*) RESOLVED_PUSH_PROTOCOL="https" ;;
|
|
esac
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
if [ "$RESOLVED_PUSH_PROTOCOL" = "preserve" ]; then
|
|
PUSH_URL="$REMOTE_URL"
|
|
else
|
|
PUSH_URL=$("$URL_BIN" --to "$RESOLVED_PUSH_PROTOCOL" "$CANONICAL_HTTPS" 2>/dev/null || echo "$CANONICAL_HTTPS")
|
|
fi
|
|
|
|
# ---- verify push URL is reachable ----
|
|
echo "Verifying remote connectivity: $PUSH_URL"
|
|
if ! _receipted_git open artifacts-init "$(_artifacts_host)" artifacts-remote-ls-remote "user ran gstack-artifacts-init" \
|
|
bash -c 'git ls-remote "$1" >/dev/null 2>&1' _ "$PUSH_URL"; then
|
|
cat >&2 <<EOF
|
|
Remote not reachable via $RESOLVED_PUSH_PROTOCOL: $PUSH_URL
|
|
This could mean:
|
|
- Wrong URL
|
|
- Credentials for $RESOLVED_PUSH_PROTOCOL are not configured for your git host
|
|
- Network issue
|
|
Fix and re-run gstack-artifacts-init, or choose --push-protocol https|ssh.
|
|
EOF
|
|
exit 1
|
|
fi
|
|
|
|
# ---- git init ----
|
|
if [ ! -d "$GSTACK_HOME/.git" ]; then
|
|
git -C "$GSTACK_HOME" init -q -b main 2>/dev/null || git -C "$GSTACK_HOME" init -q
|
|
git -C "$GSTACK_HOME" branch -M main 2>/dev/null || true
|
|
fi
|
|
|
|
if [ -z "$(git -C "$GSTACK_HOME" remote 2>/dev/null)" ]; then
|
|
git -C "$GSTACK_HOME" remote add origin "$PUSH_URL"
|
|
else
|
|
git -C "$GSTACK_HOME" remote set-url origin "$PUSH_URL"
|
|
fi
|
|
|
|
# ---- write canonical files (idempotent) ----
|
|
cat > "$GSTACK_HOME/.gitignore" <<'EOF'
|
|
# gstack-artifacts sync: ignore-everything base. Paths are included explicitly via
|
|
# .brain-allowlist and `git add -f` from gstack-brain-sync. Do not edit.
|
|
*
|
|
EOF
|
|
|
|
ALLOWLIST="$GSTACK_HOME/.brain-allowlist"
|
|
ALLOWLIST_TMP=$(mktemp "$GSTACK_HOME/.brain-allowlist.XXXXXX")
|
|
ALLOWLIST_ASSEMBLED=$(mktemp "$GSTACK_HOME/.brain-allowlist.XXXXXX")
|
|
trap 'rm -f "$ALLOWLIST_TMP" "$ALLOWLIST_ASSEMBLED"' EXIT
|
|
ALLOWLIST_MARKER='# ---- USER ADDITIONS BELOW ---- (survives re-init; above is managed)'
|
|
|
|
cat > "$ALLOWLIST_TMP" <<'EOF'
|
|
# Canonical allowlist of paths that gstack-brain-sync will publish.
|
|
# One glob per line. Anything not matching stays local.
|
|
# Do not edit directly; managed by gstack-artifacts-init. User additions go
|
|
# below the marker and survive re-init.
|
|
projects/*/learnings.jsonl
|
|
projects/*/*-reviews.jsonl
|
|
projects/*/ceo-plans/*.md
|
|
projects/*/ceo-plans/*/*.md
|
|
projects/*/designs/*.md
|
|
projects/*/designs/*/*.md
|
|
# Project-root design / test-plan artifacts written by /office-hours,
|
|
# /plan-eng-review, and /autoplan. The skills emit
|
|
# `{user}-{branch}-design-{datetime}.md`,
|
|
# `{user}-{branch}-test-plan-{datetime}.md`, and
|
|
# `{user}-{branch}-eng-review-test-plan-{datetime}.md` at the project
|
|
# root (not under designs/), so the existing `designs/*.md` patterns
|
|
# miss them. Without these the cross-machine pull on machine B gets
|
|
# the referencing CEO plan but not the underlying design / test plan
|
|
# (#1452).
|
|
projects/*/*-design-*.md
|
|
projects/*/*-test-plan-*.md
|
|
projects/*/*-eng-review-test-plan-*.md
|
|
projects/*/timeline.jsonl
|
|
# The decision store. gstack-decision-log enqueues projects/<slug>/decisions.jsonl
|
|
# after EVERY write, but no glob above matched it, so compute_paths_to_stage rejected
|
|
# all of them at its "must match at least one allowlist glob" check -- a writer
|
|
# enqueueing a path the syncer is guaranteed to drop. Without these the durable
|
|
# decision ledger never leaves the machine, on any platform.
|
|
projects/*/decisions.jsonl
|
|
projects/*/decisions.active.json
|
|
projects/*/decisions.archive.jsonl
|
|
retros/*.md
|
|
developer-profile.json
|
|
builder-journey.md
|
|
builder-profile.jsonl
|
|
# Transcripts staged in remote-http MCP mode (per plan D11 split-engine).
|
|
# gstack-memory-ingest persists per-run dirs here when local gbrain import
|
|
# is skipped; brain admin pulls + indexes into the remote brain.
|
|
transcripts/run-*/*.md
|
|
transcripts/run-*/**/*.md
|
|
# NOT synced (machine-local UX state):
|
|
# projects/*/question-preferences.json (per-machine UX preferences)
|
|
# projects/*/question-log.jsonl (audit/derivation log stays with preferences)
|
|
# projects/*/question-events.jsonl (same)
|
|
# ---- USER ADDITIONS BELOW ---- (survives re-init; above is managed)
|
|
EOF
|
|
|
|
if [ -s "$ALLOWLIST" ]; then
|
|
if marker_count=$(grep -Fxc "$ALLOWLIST_MARKER" "$ALLOWLIST"); then
|
|
:
|
|
else
|
|
grep_status=$?
|
|
if [ "$grep_status" -gt 1 ]; then
|
|
echo "gstack-artifacts-init: could not read $ALLOWLIST; refusing to replace it" >&2
|
|
exit 1
|
|
fi
|
|
marker_count=0
|
|
fi
|
|
if [ "$marker_count" -ne 1 ]; then
|
|
if [ "$marker_count" -eq 0 ]; then reason="has no managed marker"; else reason="has multiple managed markers"; fi
|
|
echo "gstack-artifacts-init: $ALLOWLIST $reason; refusing to replace ambiguous user data" >&2
|
|
exit 1
|
|
fi
|
|
marker_line=$(grep -nFx "$ALLOWLIST_MARKER" "$ALLOWLIST" | cut -d: -f1)
|
|
suffix_start=$(head -n "$marker_line" "$ALLOWLIST" | wc -c)
|
|
cp -p "$ALLOWLIST" "$ALLOWLIST_ASSEMBLED"
|
|
: > "$ALLOWLIST_ASSEMBLED"
|
|
cat "$ALLOWLIST_TMP" >> "$ALLOWLIST_ASSEMBLED"
|
|
tail -c +"$((suffix_start + 1))" "$ALLOWLIST" >> "$ALLOWLIST_ASSEMBLED"
|
|
else
|
|
cat "$ALLOWLIST_TMP" > "$ALLOWLIST_ASSEMBLED"
|
|
fi
|
|
mv -f "$ALLOWLIST_ASSEMBLED" "$ALLOWLIST"
|
|
|
|
cat > "$GSTACK_HOME/.brain-privacy-map.json" <<'EOF'
|
|
[
|
|
{"pattern": "projects/*/learnings.jsonl", "class": "artifact"},
|
|
{"pattern": "projects/*/*-reviews.jsonl", "class": "artifact"},
|
|
{"pattern": "projects/*/ceo-plans/*.md", "class": "artifact"},
|
|
{"pattern": "projects/*/ceo-plans/*/*.md", "class": "artifact"},
|
|
{"pattern": "projects/*/designs/*.md", "class": "artifact"},
|
|
{"pattern": "projects/*/designs/*/*.md", "class": "artifact"},
|
|
{"pattern": "projects/*/*-design-*.md", "class": "artifact"},
|
|
{"pattern": "projects/*/*-test-plan-*.md", "class": "artifact"},
|
|
{"pattern": "projects/*/*-eng-review-test-plan-*.md", "class": "artifact"},
|
|
{"pattern": "projects/*/decisions.jsonl", "class": "artifact"},
|
|
{"pattern": "projects/*/decisions.active.json", "class": "artifact"},
|
|
{"pattern": "projects/*/decisions.archive.jsonl", "class": "artifact"},
|
|
{"pattern": "retros/*.md", "class": "artifact"},
|
|
{"pattern": "builder-journey.md", "class": "artifact"},
|
|
{"pattern": "projects/*/timeline.jsonl", "class": "behavioral"},
|
|
{"pattern": "developer-profile.json", "class": "behavioral"},
|
|
{"pattern": "builder-profile.jsonl", "class": "behavioral"},
|
|
{"pattern": "transcripts/run-*/*.md", "class": "behavioral"},
|
|
{"pattern": "transcripts/run-*/**/*.md", "class": "behavioral"}
|
|
]
|
|
EOF
|
|
|
|
cat > "$GSTACK_HOME/.gitattributes" <<'EOF'
|
|
# gstack-artifacts: merge drivers for cross-machine sync conflicts.
|
|
*.jsonl merge=jsonl-append
|
|
retros/*.md merge=union
|
|
projects/*/designs/**/*.md merge=union
|
|
projects/*/ceo-plans/**/*.md merge=union
|
|
projects/*/*-design-*.md merge=union
|
|
projects/*/*-test-plan-*.md merge=union
|
|
EOF
|
|
|
|
# ---- register merge drivers in local git config ----
|
|
git -C "$GSTACK_HOME" config merge.jsonl-append.driver "$SCRIPT_DIR/gstack-jsonl-merge %O %A %B"
|
|
git -C "$GSTACK_HOME" config merge.jsonl-append.name "gstack JSONL append-only merger"
|
|
git -C "$GSTACK_HOME" config merge.union.driver "cat %A %B > %A.merged && mv %A.merged %A"
|
|
git -C "$GSTACK_HOME" config merge.union.name "union concat"
|
|
|
|
# ---- install pre-commit hook (defense-in-depth) ----
|
|
HOOK="$GSTACK_HOME/.git/hooks/pre-commit"
|
|
mkdir -p "$(dirname "$HOOK")"
|
|
cat > "$HOOK" <<'HOOK_EOF'
|
|
#!/usr/bin/env bash
|
|
# gstack-artifacts pre-commit hook — secret-scan defense-in-depth.
|
|
# The primary scanner runs inside gstack-brain-sync BEFORE staging. This hook
|
|
# catches any manual `git commit` a user might accidentally run against the
|
|
# artifacts repo.
|
|
set -uo pipefail
|
|
|
|
python3 -c "
|
|
import sys, re, subprocess
|
|
try:
|
|
out = subprocess.check_output(['git', 'diff', '--cached'], stderr=subprocess.DEVNULL).decode('utf-8', 'replace')
|
|
except Exception:
|
|
sys.exit(0)
|
|
|
|
patterns = [
|
|
('aws-access-key', re.compile(r'AKIA[0-9A-Z]{16}')),
|
|
('github-token', re.compile(r'\b(gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})')),
|
|
('openai-key', re.compile(r'\bsk-[A-Za-z0-9_-]{20,}')),
|
|
('pem-block', re.compile(r'-----BEGIN [A-Z ]{3,}-----')),
|
|
('jwt', re.compile(r'\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b')),
|
|
('bearer-token-json',
|
|
re.compile(r'\"(authorization|api[_-]?key|apikey|token|secret|password)\"\s*:\s*\"[A-Za-z0-9_./+=-]{16,}\"',
|
|
re.IGNORECASE)),
|
|
]
|
|
for name, rx in patterns:
|
|
if rx.search(out):
|
|
sys.stderr.write(f'gstack-artifacts pre-commit: refusing commit — {name} detected in staged diff.\n')
|
|
sys.stderr.write('Either edit the offending file, or if intentional, run:\n')
|
|
sys.stderr.write(' gstack-brain-sync --skip-file <path> (to permanently exclude)\n')
|
|
sys.exit(1)
|
|
sys.exit(0)
|
|
"
|
|
HOOK_EOF
|
|
chmod +x "$HOOK"
|
|
|
|
# ---- initial commit (idempotent) ----
|
|
cd "$GSTACK_HOME"
|
|
git add -f .gitignore .brain-allowlist .brain-privacy-map.json .gitattributes
|
|
if git rev-parse HEAD >/dev/null 2>&1; then
|
|
if ! git diff --cached --quiet 2>/dev/null; then
|
|
git -c user.email="gstack@localhost" -c user.name="gstack-artifacts-init" \
|
|
commit -q -m "chore: gstack-artifacts-init (refresh sync config)"
|
|
fi
|
|
else
|
|
git -c user.email="gstack@localhost" -c user.name="gstack-artifacts-init" \
|
|
commit -q -m "chore: gstack-artifacts-init"
|
|
fi
|
|
|
|
# ---- initial push ----
|
|
if ! _receipted_git open artifacts-init "$(_artifacts_host)" artifacts-initial-push "user ran gstack-artifacts-init" \
|
|
bash -c 'git push -q -u origin main 2>/dev/null'; then
|
|
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
if _receipted_git open artifacts-init "$(_artifacts_host)" artifacts-fetch "user ran gstack-artifacts-init" \
|
|
bash -c 'git fetch origin 2>/dev/null' \
|
|
&& _receipted_git open artifacts-init "$(_artifacts_host)" artifacts-pull "user ran gstack-artifacts-init" \
|
|
bash -c 'git pull --ff-only origin "$1" 2>/dev/null' _ "$CURRENT_BRANCH"; then
|
|
_receipted_git open artifacts-init "$(_artifacts_host)" artifacts-initial-push "user ran gstack-artifacts-init" \
|
|
git push -q -u origin "$CURRENT_BRANCH" || {
|
|
echo "Push to $PUSH_URL failed. The remote may have divergent content." >&2
|
|
echo "Try: cd ~/.gstack && git pull --rebase origin $CURRENT_BRANCH && git push origin $CURRENT_BRANCH" >&2
|
|
exit 1
|
|
}
|
|
else
|
|
echo "Push to $PUSH_URL failed and fetch/merge didn't help." >&2
|
|
echo "Manual recovery: cd ~/.gstack && git status, then push once conflicts are resolved." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# ---- write the remote-url helper file (HTTPS canonical) ----
|
|
echo "$CANONICAL_HTTPS" > "$REMOTE_FILE"
|
|
chmod 600 "$REMOTE_FILE"
|
|
|
|
# ---- print brain-admin hookup command (always print, never auto-execute;
|
|
# codex Finding #3) ----
|
|
SOURCE_ID="gstack-artifacts-${USER:-$(whoami)}"
|
|
cat <<EOF
|
|
|
|
gstack-artifacts-init complete.
|
|
Repo: $GSTACK_HOME (git)
|
|
Remote: $CANONICAL_HTTPS (canonical form, in ~/.gstack-artifacts-remote.txt)
|
|
Push: $PUSH_URL ($RESOLVED_PUSH_PROTOCOL form for git push)
|
|
|
|
EOF
|
|
|
|
cat <<EOF
|
|
─────────────────────────────────────────────────────────────────────────
|
|
Send this to your brain admin (the person who runs your gbrain server)
|
|
─────────────────────────────────────────────────────────────────────────
|
|
EOF
|
|
|
|
if [ "$URL_FORM_SUPPORTED" = "true" ]; then
|
|
cat <<EOF
|
|
On the brain host, run:
|
|
|
|
gbrain sources add $SOURCE_ID --url $CANONICAL_HTTPS --federated
|
|
|
|
EOF
|
|
else
|
|
cat <<EOF
|
|
On the brain host (gbrain v0.26.x doesn't accept URLs directly yet), run:
|
|
|
|
git clone $CANONICAL_HTTPS ~/$SOURCE_ID
|
|
gbrain sources add $SOURCE_ID --path ~/$SOURCE_ID --federated
|
|
|
|
When gbrain ships --url support, this becomes a one-liner:
|
|
gbrain sources add $SOURCE_ID --url $CANONICAL_HTTPS --federated
|
|
|
|
EOF
|
|
fi
|
|
|
|
cat <<EOF
|
|
After that, your CEO plans / designs / reports become searchable via
|
|
'gbrain search' from any machine pointing at this brain.
|
|
─────────────────────────────────────────────────────────────────────────
|
|
|
|
New machine? Put a copy of $REMOTE_FILE in that machine's home directory,
|
|
then run: gstack-artifacts-init (it'll detect the remote and re-init).
|
|
EOF
|