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.
- package/dist/commands/env.d.ts +9 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/env.js +105 -0
- package/dist/commands/env.js.map +1 -0
- package/dist/commands/stack.d.ts.map +1 -1
- package/dist/commands/stack.js +98 -0
- package/dist/commands/stack.js.map +1 -1
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/scripts/check-version-exists.mjs +40 -0
- package/scripts/env-vault/README.md +123 -0
- package/scripts/env-vault/backup.sh +167 -0
- package/scripts/env-vault/restore.sh +149 -0
- package/scripts/skillstack-sync/README.md +166 -0
- package/scripts/skillstack-sync/bootstrap-new-mac.sh +141 -0
- package/scripts/skillstack-sync/com.youmd.identity-sync.plist +36 -0
- package/scripts/skillstack-sync/com.youmd.skillstack-sync.plist +38 -0
- package/scripts/skillstack-sync/install-daemons.sh +75 -0
- package/scripts/skillstack-sync/sync.sh +320 -0
|
@@ -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)
|