youmd 0.6.27 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ // T29: pre-publish guard — refuse to attempt `npm publish` if the version in
3
+ // package.json already exists on the npm registry. npm itself errors out with
4
+ // E_PUBLISH_CONFLICT, but only after build + tarball + 2FA dance — surfacing
5
+ // the conflict up-front saves the OTP round-trip every time someone forgets
6
+ // `npm version patch`.
7
+
8
+ import { readFileSync } from "node:fs";
9
+ import { fileURLToPath } from "node:url";
10
+ import { dirname, join } from "node:path";
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const pkgPath = join(__dirname, "..", "package.json");
14
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
15
+ const name = pkg.name;
16
+ const version = pkg.version;
17
+
18
+ const url = `https://registry.npmjs.org/${encodeURIComponent(name)}/${encodeURIComponent(version)}`;
19
+
20
+ try {
21
+ const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
22
+ if (res.status === 404) {
23
+ // unknown version — safe to publish
24
+ process.exit(0);
25
+ }
26
+ if (res.ok) {
27
+ console.error(
28
+ `\n ✗ ${name}@${version} is already published on npm.\n` +
29
+ ` bump the version before publishing:\n\n` +
30
+ ` cd cli && npm version patch --no-git-tag-version\n\n`
31
+ );
32
+ process.exit(1);
33
+ }
34
+ // 5xx or transient — don't block the publish on registry flakiness; warn and continue
35
+ console.error(` ! could not reach npm registry (HTTP ${res.status}) — proceeding without version check`);
36
+ process.exit(0);
37
+ } catch (err) {
38
+ console.error(` ! could not reach npm registry (${err?.message ?? err}) — proceeding without version check`);
39
+ process.exit(0);
40
+ }
@@ -0,0 +1,123 @@
1
+ # env-vault — Secure .env.local Backup & Restore
2
+
3
+ Backs up all `.env.local` files across your CODE_2025 projects into a single
4
+ encrypted archive, and restores them on a new Mac. No secret values ever appear
5
+ in plaintext outside the encrypted blob.
6
+
7
+ ---
8
+
9
+ ## New-Machine Flow
10
+
11
+ ### On the old Mac (backup)
12
+
13
+ ```bash
14
+ # via CLI (preferred — ships in the npm package)
15
+ youmd env backup
16
+
17
+ # or directly via bash (from the youmd repo root)
18
+ cd /Users/houstongolden/Desktop/CODE_2025/youmd
19
+ bash cli/scripts/env-vault/backup.sh
20
+ ```
21
+
22
+ This writes two files to `.env-vault/`:
23
+ - `env-vault-<date>.tar.<ext>` — encrypted archive (safe to transport)
24
+ - `manifest-<date>.txt` — variable names + counts only (safe to read, no values)
25
+
26
+ You will be prompted for a passphrase. Store it in **1Password** immediately.
27
+
28
+ ### Transfer to new Mac
29
+
30
+ Options (all safe — the file is encrypted):
31
+ 1. **AirDrop** the `.tar.<ext>` file directly
32
+ 2. **USB drive** — copy to a thumb drive, plug into new Mac
33
+ 3. **iCloud Drive** — upload briefly, download on new Mac, then delete from cloud
34
+
35
+ Do NOT transfer via unencrypted email or public Slack.
36
+
37
+ ### On the new Mac (restore)
38
+
39
+ ```bash
40
+ # Install dependencies first (e.g. Node, pnpm, etc.), then:
41
+ youmd env restore /path/to/env-vault-<date>.tar.<ext>
42
+
43
+ # If project dirs already exist with .env.local files:
44
+ youmd env restore --force /path/to/env-vault-<date>.tar.<ext>
45
+
46
+ # or directly via bash:
47
+ bash cli/scripts/env-vault/restore.sh /path/to/env-vault-<date>.tar.<ext>
48
+ bash cli/scripts/env-vault/restore.sh --force /path/to/env-vault-<date>.tar.<ext>
49
+ ```
50
+
51
+ The script creates `<CODE_2025>/<project>/.env.local` for each project.
52
+ Existing files are backed up to `.env.local.bak.<timestamp>` (or overwritten with `--force`).
53
+
54
+ ---
55
+
56
+ ## 3-Location Backup Rule
57
+
58
+ Your encrypted vault should live in **at least 3 locations**:
59
+
60
+ | Location | Example |
61
+ |----------|---------|
62
+ | Local machine | `.env-vault/` (already done by backup.sh) |
63
+ | Secure cloud | 1Password attachment, private iCloud folder |
64
+ | Physical backup | USB drive stored separately from your Mac |
65
+
66
+ The encrypted file itself is safe to store anywhere. The passphrase is the secret.
67
+
68
+ ---
69
+
70
+ ## Security Model
71
+
72
+ - **Encryption:** `age` (preferred) → `gpg AES-256` → `openssl AES-256-CBC PBKDF2`
73
+ - **What's in the encrypted archive:** full `.env.local` file contents
74
+ - **What's in plaintext:** only the project name + variable NAMES + count (the manifest)
75
+ - **Values:** never printed, echoed, logged, or written anywhere outside the encrypted blob
76
+ - **Passphrase:** stored nowhere by these scripts — you must store it in 1Password
77
+
78
+ ---
79
+
80
+ ## CLI Integration
81
+
82
+ These scripts ship inside the `youmd` npm package (`cli/scripts/env-vault/`)
83
+ and are exposed as first-class CLI commands:
84
+
85
+ ```bash
86
+ youmd env backup # back up all .env.local files
87
+ youmd env backup --root <dir> # custom search root
88
+ youmd env backup --out <dir> # custom output dir
89
+
90
+ youmd env restore <vault> # restore from an encrypted vault
91
+ youmd env restore --force # overwrite existing .env.local files
92
+ youmd env restore --root <dir> # restore into a custom root
93
+ ```
94
+
95
+ The CLI wrapper delegates directly to these bash scripts via `spawnSync` with
96
+ `stdio: "inherit"` so the passphrase prompt and all output pass through to
97
+ your terminal unchanged.
98
+
99
+ ---
100
+
101
+ ## Flags
102
+
103
+ ### backup.sh
104
+
105
+ | Flag | Default | Description |
106
+ |------|---------|-------------|
107
+ | `--root <dir>` | `~/Desktop/CODE_2025` | Where to discover `.env.local` files |
108
+ | `--out <dir>` | `youmd/.env-vault/` | Where to write encrypted vault + manifest |
109
+
110
+ ### restore.sh
111
+
112
+ | Flag | Default | Description |
113
+ |------|---------|-------------|
114
+ | `--root <dir>` | `~/Desktop/CODE_2025` | Where to restore project `.env.local` files |
115
+ | `--force` | off | Overwrite existing `.env.local` instead of skipping |
116
+
117
+ ---
118
+
119
+ ## .gitignore Note
120
+
121
+ The `.env-vault/` output directory is listed in `youmd/.gitignore`. Encrypted
122
+ vault files are **never committed to git**. Only the scripts and this README
123
+ are tracked.
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env bash
2
+ # env-vault/backup.sh — backs up all .env.local files under CODE_2025 into an
3
+ # encrypted vault. NEVER prints, echoes, logs, or writes any secret VALUE.
4
+ # Only variable NAMES and counts appear in plaintext output.
5
+ #
6
+ # Usage: bash backup.sh [--root <search-root>] [--out <output-dir>]
7
+ # Defaults:
8
+ # search-root: /Users/houstongolden/Desktop/CODE_2025
9
+ # output-dir: /Users/houstongolden/Desktop/CODE_2025/youmd/.env-vault
10
+
11
+ set -euo pipefail
12
+
13
+ # ── configurable defaults ──────────────────────────────────────────────────────
14
+ SEARCH_ROOT="${ENV_VAULT_ROOT:-/Users/houstongolden/Desktop/CODE_2025}"
15
+ OUTPUT_DIR="${ENV_VAULT_OUT:-/Users/houstongolden/Desktop/CODE_2025/youmd/.env-vault}"
16
+
17
+ # ── parse flags ────────────────────────────────────────────────────────────────
18
+ while [[ $# -gt 0 ]]; do
19
+ case $1 in
20
+ --root) SEARCH_ROOT="$2"; shift 2 ;;
21
+ --out) OUTPUT_DIR="$2"; shift 2 ;;
22
+ *) echo "Unknown flag: $1" >&2; exit 1 ;;
23
+ esac
24
+ done
25
+
26
+ # ── detect encryption tool ────────────────────────────────────────────────────
27
+ # Prefer openssl: it ships on every macOS install, so a vault encrypted with it
28
+ # can ALWAYS be restored on a brand-new Mac (which may not have age or gpg). The
29
+ # others are used only if openssl is somehow unavailable. Set ENV_VAULT_PASS to
30
+ # run non-interactively (e.g. from automation); otherwise you'll be prompted.
31
+ ENC_TOOL=""
32
+ ENC_EXT=""
33
+ if command -v openssl &>/dev/null; then
34
+ ENC_TOOL="openssl"
35
+ ENC_EXT="enc"
36
+ elif command -v age &>/dev/null; then
37
+ ENC_TOOL="age"
38
+ ENC_EXT="age"
39
+ elif command -v gpg &>/dev/null; then
40
+ ENC_TOOL="gpg"
41
+ ENC_EXT="gpg"
42
+ else
43
+ echo "ERROR: No encryption tool found. Install age, gpg, or openssl." >&2
44
+ exit 1
45
+ fi
46
+
47
+ echo "Encryption tool: ${ENC_TOOL}"
48
+
49
+ # ── discover .env.local files (handles spaces in dir names) ───────────────────
50
+ # bash 3.2 (macOS default) has no `mapfile`, so build the array with read -d ''.
51
+ ENV_FILES=()
52
+ while IFS= read -r -d '' _envf; do
53
+ ENV_FILES+=("${_envf}")
54
+ done < <(find "${SEARCH_ROOT}" -maxdepth 2 -name ".env.local" -print0 2>/dev/null)
55
+
56
+ if [[ ${#ENV_FILES[@]} -eq 0 ]]; then
57
+ echo "No .env.local files found under ${SEARCH_ROOT}" >&2
58
+ exit 1
59
+ fi
60
+
61
+ echo "Found ${#ENV_FILES[@]} .env.local file(s)."
62
+
63
+ # ── build plaintext manifest (names + counts, NO values) ─────────────────────
64
+ mkdir -p "${OUTPUT_DIR}"
65
+ UTC_DATE="$(date -u +%Y-%m-%dT%H%MZ)"
66
+ MANIFEST_PATH="${OUTPUT_DIR}/manifest-${UTC_DATE}.txt"
67
+
68
+ {
69
+ echo "env-vault manifest — ${UTC_DATE}"
70
+ echo "Encryption: ${ENC_TOOL}"
71
+ echo "Search root: ${SEARCH_ROOT}"
72
+ echo "---"
73
+ } > "${MANIFEST_PATH}"
74
+
75
+ # ── stage files into a temp dir preserving project-relative paths ─────────────
76
+ STAGING_DIR="$(mktemp -d)"
77
+ trap 'rm -rf "${STAGING_DIR}"' EXIT
78
+
79
+ for ENV_FILE in "${ENV_FILES[@]}"; do
80
+ PROJECT_DIR="$(dirname "${ENV_FILE}")"
81
+ PROJECT_NAME="$(basename "${PROJECT_DIR}")"
82
+
83
+ # Store as <project-name>/.env.local inside the archive
84
+ DEST="${STAGING_DIR}/${PROJECT_NAME}"
85
+ mkdir -p "${DEST}"
86
+ cp "${ENV_FILE}" "${DEST}/.env.local"
87
+
88
+ # Count variable names only — never read values
89
+ VAR_COUNT=$(grep -cE '^[A-Za-z_][A-Za-z0-9_]*=' "${ENV_FILE}" 2>/dev/null || true)
90
+ VAR_NAMES=$(grep -oE '^[A-Za-z_][A-Za-z0-9_]*' "${ENV_FILE}" 2>/dev/null \
91
+ | grep -v '^$' | sort | paste -sd ',' - || true)
92
+
93
+ echo "${PROJECT_NAME}/.env.local: ${VAR_COUNT} variable(s) [${VAR_NAMES}]" \
94
+ >> "${MANIFEST_PATH}"
95
+ done
96
+
97
+ echo "" >> "${MANIFEST_PATH}"
98
+ echo "SECURITY: The encrypted archive is safe to transport. Keep the passphrase private." \
99
+ >> "${MANIFEST_PATH}"
100
+
101
+ # ── create tar from staging dir ───────────────────────────────────────────────
102
+ # BSD mktemp (macOS) has no --suffix; the tar path is temporary and deleted
103
+ # after encryption, so a plain mktemp is fine.
104
+ TAR_PATH="$(mktemp)"
105
+ tar -cf "${TAR_PATH}" -C "${STAGING_DIR}" .
106
+
107
+ VAULT_PATH="${OUTPUT_DIR}/env-vault-${UTC_DATE}.tar.${ENC_EXT}"
108
+
109
+ # ── encrypt ───────────────────────────────────────────────────────────────────
110
+ echo ""
111
+ echo "Enter a strong passphrase to encrypt the vault."
112
+ echo "(You will need this passphrase to restore on a new machine — store it safely.)"
113
+ echo ""
114
+
115
+ case "${ENC_TOOL}" in
116
+ openssl)
117
+ if [[ -n "${ENV_VAULT_PASS:-}" ]]; then
118
+ openssl enc -aes-256-cbc -pbkdf2 -salt -pass env:ENV_VAULT_PASS \
119
+ -in "${TAR_PATH}" -out "${VAULT_PATH}"
120
+ else
121
+ openssl enc -aes-256-cbc -pbkdf2 -salt \
122
+ -in "${TAR_PATH}" -out "${VAULT_PATH}"
123
+ fi
124
+ ;;
125
+ age)
126
+ age -p -o "${VAULT_PATH}" "${TAR_PATH}"
127
+ ;;
128
+ gpg)
129
+ if [[ -n "${ENV_VAULT_PASS:-}" ]]; then
130
+ printf '%s' "${ENV_VAULT_PASS}" | gpg --batch --yes --symmetric \
131
+ --cipher-algo AES256 --passphrase-fd 0 --pinentry-mode loopback \
132
+ --output "${VAULT_PATH}" "${TAR_PATH}"
133
+ else
134
+ gpg --symmetric --cipher-algo AES256 --output "${VAULT_PATH}" "${TAR_PATH}"
135
+ fi
136
+ ;;
137
+ esac
138
+
139
+ # Securely remove the unencrypted tar
140
+ rm -f "${TAR_PATH}"
141
+
142
+ # ── final output ──────────────────────────────────────────────────────────────
143
+ VAULT_SIZE=$(du -sh "${VAULT_PATH}" | cut -f1)
144
+
145
+ echo ""
146
+ echo "Vault written: ${VAULT_PATH} (${VAULT_SIZE})"
147
+ echo "Manifest: ${MANIFEST_PATH}"
148
+ echo ""
149
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
150
+ echo " NEXT STEPS — 3-LOCATION BACKUP RULE"
151
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
152
+ echo " The encrypted vault is safe to move (secrets are encrypted)."
153
+ echo " Store it in at least 3 locations:"
154
+ echo " 1. ✓ Already here: ${OUTPUT_DIR}"
155
+ echo " 2. Copy to iCloud Drive / 1Password attachment / USB drive"
156
+ echo " 3. Copy to a second USB or private cloud vault"
157
+ echo ""
158
+ echo " Transfer to new Macs:"
159
+ echo " • AirDrop the .${ENC_EXT} file directly"
160
+ echo " • OR copy via USB drive"
161
+ echo " • OR store in private (non-public) cloud storage"
162
+ echo ""
163
+ echo " On the new Mac, run:"
164
+ echo " bash restore.sh <path-to-vault.tar.${ENC_EXT}>"
165
+ echo ""
166
+ echo " The passphrase is NOT stored anywhere — keep it in 1Password."
167
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env bash
2
+ # env-vault/restore.sh — restores .env.local files from an encrypted vault
3
+ # created by backup.sh. NEVER prints, echoes, or logs any secret VALUES.
4
+ # Only prints which file paths were restored or skipped.
5
+ #
6
+ # Usage: bash restore.sh [--force] [--root <target-root>] <vault-file>
7
+ # vault-file: path to env-vault-*.tar.age / .tar.gpg / .tar.enc
8
+ # --force: overwrite existing .env.local files without backing them up
9
+ # --root: destination root (default: /Users/houstongolden/Desktop/CODE_2025)
10
+
11
+ set -euo pipefail
12
+
13
+ # ── defaults ──────────────────────────────────────────────────────────────────
14
+ TARGET_ROOT="${ENV_VAULT_RESTORE_ROOT:-/Users/houstongolden/Desktop/CODE_2025}"
15
+ FORCE=false
16
+ VAULT_FILE=""
17
+
18
+ # ── parse args ─────────────────────────────────────────────────────────────────
19
+ while [[ $# -gt 0 ]]; do
20
+ case $1 in
21
+ --force) FORCE=true; shift ;;
22
+ --root) TARGET_ROOT="$2"; shift 2 ;;
23
+ -*) echo "Unknown flag: $1" >&2; exit 1 ;;
24
+ *)
25
+ if [[ -z "${VAULT_FILE}" ]]; then
26
+ VAULT_FILE="$1"
27
+ else
28
+ echo "Unexpected argument: $1" >&2
29
+ exit 1
30
+ fi
31
+ shift ;;
32
+ esac
33
+ done
34
+
35
+ if [[ -z "${VAULT_FILE}" ]]; then
36
+ echo "Usage: bash restore.sh [--force] [--root <path>] <vault-file>" >&2
37
+ echo " vault-file: env-vault-*.tar.age | .tar.gpg | .tar.enc" >&2
38
+ exit 1
39
+ fi
40
+
41
+ if [[ ! -f "${VAULT_FILE}" ]]; then
42
+ echo "ERROR: Vault file not found: ${VAULT_FILE}" >&2
43
+ exit 1
44
+ fi
45
+
46
+ # ── detect encryption tool from extension ─────────────────────────────────────
47
+ case "${VAULT_FILE}" in
48
+ *.tar.age) ENC_TOOL="age" ;;
49
+ *.tar.gpg) ENC_TOOL="gpg" ;;
50
+ *.tar.enc) ENC_TOOL="openssl" ;;
51
+ *)
52
+ echo "ERROR: Unrecognised vault extension. Expected .tar.age / .tar.gpg / .tar.enc" >&2
53
+ exit 1 ;;
54
+ esac
55
+
56
+ if ! command -v "${ENC_TOOL}" &>/dev/null && [[ "${ENC_TOOL}" != "openssl" ]]; then
57
+ echo "ERROR: ${ENC_TOOL} is not installed." >&2
58
+ exit 1
59
+ fi
60
+
61
+ echo "Encryption tool: ${ENC_TOOL}"
62
+ echo "Vault file: ${VAULT_FILE}"
63
+ echo "Target root: ${TARGET_ROOT}"
64
+ echo ""
65
+
66
+ # ── decrypt into a temp dir ────────────────────────────────────────────────────
67
+ WORK_DIR="$(mktemp -d)"
68
+ TAR_PATH="${WORK_DIR}/vault.tar"
69
+
70
+ trap 'rm -rf "${WORK_DIR}"' EXIT
71
+
72
+ echo "Decrypting vault…"
73
+
74
+ case "${ENC_TOOL}" in
75
+ openssl)
76
+ if [[ -n "${ENV_VAULT_PASS:-}" ]]; then
77
+ openssl enc -d -aes-256-cbc -pbkdf2 -pass env:ENV_VAULT_PASS \
78
+ -in "${VAULT_FILE}" -out "${TAR_PATH}"
79
+ else
80
+ openssl enc -d -aes-256-cbc -pbkdf2 \
81
+ -in "${VAULT_FILE}" -out "${TAR_PATH}"
82
+ fi
83
+ ;;
84
+ age)
85
+ age -d -o "${TAR_PATH}" "${VAULT_FILE}"
86
+ ;;
87
+ gpg)
88
+ if [[ -n "${ENV_VAULT_PASS:-}" ]]; then
89
+ printf '%s' "${ENV_VAULT_PASS}" | gpg --batch --yes --decrypt \
90
+ --passphrase-fd 0 --pinentry-mode loopback \
91
+ --output "${TAR_PATH}" "${VAULT_FILE}"
92
+ else
93
+ gpg --output "${TAR_PATH}" --decrypt "${VAULT_FILE}"
94
+ fi
95
+ ;;
96
+ esac
97
+
98
+ echo "Decryption complete."
99
+ echo ""
100
+
101
+ # ── extract into work dir ─────────────────────────────────────────────────────
102
+ EXTRACT_DIR="${WORK_DIR}/extracted"
103
+ mkdir -p "${EXTRACT_DIR}"
104
+ tar -xf "${TAR_PATH}" -C "${EXTRACT_DIR}"
105
+
106
+ # ── restore each .env.local ───────────────────────────────────────────────────
107
+ RESTORED=0
108
+ SKIPPED=0
109
+ BACKED_UP=0
110
+
111
+ while IFS= read -r -d '' ENV_FILE; do
112
+ # ENV_FILE is relative to EXTRACT_DIR, e.g. ./project-name/.env.local
113
+ REL_PATH="${ENV_FILE#${EXTRACT_DIR}/}" # project-name/.env.local
114
+ PROJECT_NAME="$(dirname "${REL_PATH}")" # project-name
115
+ DEST="${TARGET_ROOT}/${PROJECT_NAME}/.env.local"
116
+ DEST_DIR="$(dirname "${DEST}")"
117
+
118
+ if [[ -f "${DEST}" ]]; then
119
+ if [[ "${FORCE}" == "true" ]]; then
120
+ mkdir -p "${DEST_DIR}"
121
+ cp "${ENV_FILE}" "${DEST}"
122
+ echo " RESTORED (overwrite): ${DEST}"
123
+ (( RESTORED++ )) || true
124
+ else
125
+ TS="$(date -u +%Y%m%dT%H%M%SZ)"
126
+ BAK="${DEST}.bak.${TS}"
127
+ cp "${DEST}" "${BAK}"
128
+ cp "${ENV_FILE}" "${DEST}"
129
+ echo " RESTORED (backed up old → $(basename "${BAK}")): ${DEST}"
130
+ (( RESTORED++ )) || true
131
+ (( BACKED_UP++ )) || true
132
+ fi
133
+ else
134
+ # Target project dir may not exist yet on a fresh machine — create it
135
+ mkdir -p "${DEST_DIR}"
136
+ cp "${ENV_FILE}" "${DEST}"
137
+ echo " RESTORED (new): ${DEST}"
138
+ (( RESTORED++ )) || true
139
+ fi
140
+ done < <(find "${EXTRACT_DIR}" -name ".env.local" -print0)
141
+
142
+ echo ""
143
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
144
+ echo " Restore summary"
145
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
146
+ echo " Restored: ${RESTORED} | Skipped: ${SKIPPED} | Old files backed up: ${BACKED_UP}"
147
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
148
+ echo ""
149
+ echo "Secrets are now in place. Do not print or share .env.local files."
@@ -0,0 +1,166 @@
1
+ # skillstack-sync
2
+
3
+ Cross-machine sync toolkit for Houston Golden's agent skill/stack setup.
4
+
5
+ ## What this syncs
6
+
7
+ ### Sync plane: Skills & Stacks (this toolkit)
8
+
9
+ | What | How | Notes |
10
+ |---|---|---|
11
+ | `~/.agent-shared` | git push/pull to `houstongolden/agent-shared` | Canonical shared agent layer: AGENTS.md, learnings, preferences, STACK-MAP |
12
+ | `~/.claude/scistack` | git push/pull to `Hubify-Projects/scistack` | Science skills: astrostack, hubstack, extensions |
13
+ | 4 loose skills (see below) | rsync into agent-shared repo, then back out | Ride along in agent-shared without their own repo |
14
+
15
+ **Loose skills synced via agent-shared/claude-skills/:**
16
+ - `agent-runtime-guard`
17
+ - `agent-stack-sync`
18
+ - `continue`
19
+ - `skill-governor`
20
+
21
+ ### What does NOT sync here
22
+
23
+ | What | Why |
24
+ |---|---|
25
+ | `~/.claude/skills/gstack/` | Tracks upstream `garrytan/gstack` — updated via `/gstack-upgrade`, read-only to us |
26
+ | Project files / code | Synced via their own project repos (youmd, bamfaiapp, etc.) |
27
+ | Secrets / env vars | Use the env-vault (`cli/scripts/env-vault/`) |
28
+
29
+ ### Complementary sync planes
30
+
31
+ - **Identity plane:** `youmd sync` (Convex ↔ local profile, memories, preferences). Automated via `com.youmd.identity-sync` daemon.
32
+ - **Env-vault plane:** `cli/scripts/env-vault/restore.sh` for secrets (manual, invoked on new machine); also available as `youmd env restore <vault>`.
33
+
34
+ ---
35
+
36
+ ## Files
37
+
38
+ ```
39
+ cli/scripts/skillstack-sync/
40
+ ├── sync.sh Core syncer (bash 3.2 compatible)
41
+ ├── install-daemons.sh Installs both LaunchAgents
42
+ ├── bootstrap-new-mac.sh New machine setup script
43
+ ├── com.youmd.skillstack-sync.plist LaunchAgent: runs `youmd stack sync` every 5 min
44
+ ├── com.youmd.identity-sync.plist LaunchAgent: runs `youmd sync` every 5 min
45
+ └── README.md This file
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Sync logic (sync.sh)
51
+
52
+ ```
53
+ 1. Mirror loose skills: ~/.claude/skills/<name>/ → ~/.agent-shared/claude-skills/<name>/
54
+ 2. Sync ~/.agent-shared:
55
+ a. git add -A + commit "auto-sync: <host> <timestamp>" if dirty
56
+ b. git pull --no-rebase --no-edit (merge)
57
+ → on conflict: abort merge, log LOUD warning, skip push, continue
58
+ c. git push
59
+ 3. Sync ~/.claude/scistack: same a/b/c sequence
60
+ 4. Reverse-mirror loose skills: ~/.agent-shared/claude-skills/<name>/ → ~/.claude/skills/<name>/
61
+ → backs up existing local copy to <name>.bak.<timestamp> before overwriting
62
+ → skips if no changes detected
63
+ ```
64
+
65
+ All actions logged to `~/.youmd/logs/skillstack-sync.log`.
66
+
67
+ ### Flags
68
+
69
+ ```bash
70
+ youmd stack sync # live sync (all repos + loose skills)
71
+ youmd stack sync --dry-run # log what WOULD happen, write nothing
72
+
73
+ # or run the script directly:
74
+ ./sync.sh
75
+ ./sync.sh --dry-run
76
+ ```
77
+
78
+ ### Env overrides
79
+
80
+ ```bash
81
+ SKILLSTACK_REPOS="/path/to/repo1 /path/to/repo2" # override repo list
82
+ GIT_USER_NAME="Houston Golden" # commit author (default)
83
+ GIT_USER_EMAIL="houston@bamf.ai" # commit email (default)
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Daemon setup (existing machine)
89
+
90
+ The preferred way is via the CLI:
91
+
92
+ ```bash
93
+ youmd stack daemon install # installs both LaunchAgents
94
+ youmd stack daemon status # check loaded/not-loaded
95
+ youmd stack daemon uninstall # remove plists from ~/Library/LaunchAgents/
96
+ ```
97
+
98
+ Or run the raw script directly:
99
+
100
+ ```bash
101
+ bash cli/scripts/skillstack-sync/install-daemons.sh
102
+ ```
103
+
104
+ Installs two LaunchAgents that fire every 300 seconds (5 minutes):
105
+ - `com.youmd.skillstack-sync` → runs `youmd stack sync`
106
+ - `com.youmd.identity-sync` → runs `youmd sync`
107
+
108
+ Check status:
109
+ ```bash
110
+ youmd stack daemon status
111
+ # or directly:
112
+ launchctl list com.youmd.skillstack-sync
113
+ launchctl list com.youmd.identity-sync
114
+ ```
115
+
116
+ Check logs:
117
+ ```bash
118
+ tail -f ~/.youmd/logs/skillstack-sync.log
119
+ tail -f ~/.youmd/logs/skillstack-sync.out.log
120
+ tail -f ~/.youmd/logs/identity-sync.out.log
121
+ ```
122
+
123
+ ### Uninstall daemons
124
+
125
+ ```bash
126
+ youmd stack daemon uninstall
127
+ ```
128
+
129
+ Or manually:
130
+ ```bash
131
+ launchctl unload ~/Library/LaunchAgents/com.youmd.skillstack-sync.plist
132
+ launchctl unload ~/Library/LaunchAgents/com.youmd.identity-sync.plist
133
+ rm ~/Library/LaunchAgents/com.youmd.skillstack-sync.plist
134
+ rm ~/Library/LaunchAgents/com.youmd.identity-sync.plist
135
+ ```
136
+
137
+ ---
138
+
139
+ ## New Mac setup
140
+
141
+ ```bash
142
+ # 1. Install youmd CLI
143
+ curl -fsSL https://you.md/install.sh | bash
144
+
145
+ # 2. Clone this repo (youmd)
146
+ git clone https://github.com/houstongolden/youmd ~/Desktop/CODE_2025/youmd
147
+
148
+ # 3. Run bootstrap (clones agent-shared + scistack)
149
+ bash ~/Desktop/CODE_2025/youmd/cli/scripts/skillstack-sync/bootstrap-new-mac.sh
150
+
151
+ # 4. Restore secrets from env-vault (manual step — see env-vault/README)
152
+
153
+ # 5. Activate daemons
154
+ youmd stack daemon install
155
+ ```
156
+
157
+ The bootstrap script is idempotent (safe to re-run).
158
+
159
+ ---
160
+
161
+ ## Compatibility
162
+
163
+ Scripts target macOS with:
164
+ - bash 3.2.57 (default macOS shell)
165
+ - BSD coreutils (no GNU `mapfile`, no `mktemp --suffix`, BSD `date`/`find`/`rsync`)
166
+ - No external tools installed beyond: `git`, `rsync`, `bash` (all stock on macOS)