-
Notifications
You must be signed in to change notification settings - Fork 314
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·268 lines (246 loc) · 11.3 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·268 lines (246 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
#!/usr/bin/env bash
set -euo pipefail
# claude-blog installer
# Installs the blog skill ecosystem to ~/.claude/skills/ and ~/.claude/agents/
#
# One-command install:
# curl -sL https://raw.githubusercontent.com/AgriciDaniel/claude-blog/main/install.sh | bash
# Declared outside main() so the EXIT trap can access it after main() returns
TEMP_DIR=""
readonly CLAUDE_BLOG_VERSION="2.2.0"
copy_tree() {
local src="$1"
local dest="$2"
[ -d "${src}" ] || return 0
mkdir -p "${dest}"
while IFS= read -r -d '' rel_path; do
mkdir -p "${dest}/$(dirname "${rel_path}")"
cp "${src}/${rel_path}" "${dest}/${rel_path}"
done < <(
cd "${src}" &&
find . -type d -name '__pycache__' -prune -o -type f ! -name '*.pyc' -print0
)
}
count_files() {
local path="$1"
[ -d "${path}" ] || {
echo 0
return
}
find "${path}" -type d -name '__pycache__' -prune -o -type f ! -name '*.pyc' -print | wc -l | tr -d ' '
}
record_install() {
local manifest="$1"
local path="$2"
printf '%s\n' "${path}" >>"${manifest}"
}
print_commands() {
local skill_md="$1"
if [ ! -f "${skill_md}" ]; then
return
fi
awk -F'|' '
/^\| `\/blog / {
cmd=$2
desc=$3
gsub(/`/, "", cmd)
gsub(/\\\|/, "|", cmd)
gsub(/^[ \t]+|[ \t]+$/, "", cmd)
gsub(/^[ \t]+|[ \t]+$/, "", desc)
printf " %-38s %s\n", cmd, desc
}
' "${skill_md}"
}
main() {
local SKILL_DIR="${HOME}/.claude/skills"
local AGENT_DIR="${HOME}/.claude/agents"
local CLAUDE_DIR="${HOME}/.claude"
local MANIFEST="${CLAUDE_DIR}/claude-blog-manifest.txt"
local SCRIPT_DIR
echo ""
echo " ╔══════════════════════════════════════╗"
echo " ║ claude-blog Installer ║"
echo " ║ Blog Content Engine for Claude Code ║"
echo " ╚══════════════════════════════════════╝"
echo ""
echo " Release: ${CLAUDE_BLOG_VERSION}"
echo ""
# Determine source directory (local clone or piped from curl)
if [ -f "${BASH_SOURCE[0]:-}" ] && [ -d "$(dirname "${BASH_SOURCE[0]}")/skills/blog" ]; then
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
else
local repo="${CLAUDE_BLOG_REPO:-AgriciDaniel/claude-blog}"
local ref="${CLAUDE_BLOG_REF:-main}"
local url="${CLAUDE_BLOG_URL:-https://github.com/${repo}.git}"
echo "→ Cloning claude-blog from ${repo} (${ref})..."
TEMP_DIR="$(mktemp -d)"
trap 'rm -rf "${TEMP_DIR}"' EXIT
if ! git clone --depth 1 --branch "${ref}" "${url}" "${TEMP_DIR}/claude-blog" 2>/dev/null; then
git clone "${url}" "${TEMP_DIR}/claude-blog" 2>/dev/null
git -C "${TEMP_DIR}/claude-blog" checkout --detach "${ref}" >/dev/null 2>&1
fi
SCRIPT_DIR="${TEMP_DIR}/claude-blog"
echo " + checked out $(git -C "${SCRIPT_DIR}" rev-parse --short HEAD)"
if [ "${ref}" = "main" ]; then
echo " Tip: set CLAUDE_BLOG_REF to a tag or commit SHA for a pinned install."
fi
fi
# Check prerequisites
if ! command -v python3 &>/dev/null; then
echo "WARNING: python3 not found. The scripts require Python 3.11+."
echo " Install with: sudo apt install python3"
echo ""
elif ! python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)' >/dev/null 2>&1; then
local python3_version
python3_version="$(python3 -c 'import sys; print("%d.%d.%d" % sys.version_info[:3])' 2>/dev/null || echo unknown)"
echo "WARNING: python3 ${python3_version} found. The scripts require Python 3.11+."
echo " Install Python 3.11+ before running scoring, preflight, render, hero, and lint helpers."
echo ""
fi
# Create directories
echo "→ Creating directories..."
mkdir -p "${CLAUDE_DIR}"
mkdir -p "${SKILL_DIR}/blog/references"
mkdir -p "${SKILL_DIR}/blog/templates"
mkdir -p "${SKILL_DIR}/blog/scripts"
mkdir -p "${AGENT_DIR}"
: >"${MANIFEST}.tmp"
# Copy main skill
echo "→ Installing main skill: blog..."
cp "${SCRIPT_DIR}/skills/blog/SKILL.md" "${SKILL_DIR}/blog/SKILL.md"
record_install "${MANIFEST}.tmp" "${SKILL_DIR}/blog"
# Copy references
echo "→ Installing reference files..."
copy_tree "${SCRIPT_DIR}/skills/blog/references" "${SKILL_DIR}/blog/references"
# Copy templates
if [ -d "${SCRIPT_DIR}/skills/blog/templates" ]; then
echo "→ Installing content templates..."
copy_tree "${SCRIPT_DIR}/skills/blog/templates" "${SKILL_DIR}/blog/templates"
fi
# Ship the reviewed Google update ledger with the main skill. The source
# stays at data/google-updates.json in the repository; standalone installs
# receive it under the self-contained blog skill directory.
if [ -f "${SCRIPT_DIR}/data/google-updates.json" ]; then
echo "→ Installing Google update ledger..."
mkdir -p "${SKILL_DIR}/blog/data"
cp "${SCRIPT_DIR}/data/google-updates.json" "${SKILL_DIR}/blog/data/google-updates.json"
fi
# Copy sub-skills (auto-discovers all skill directories)
echo "→ Installing sub-skills..."
local sub_skill_count=0
local expected_sub_skill_count=0
for skill_dir in "${SCRIPT_DIR}/skills/"*/; do
skill_name="$(basename "${skill_dir}")"
[ "$skill_name" = "blog" ] && continue
[ -f "${skill_dir}SKILL.md" ] && expected_sub_skill_count=$((expected_sub_skill_count + 1))
# VULN-IAC-003 (v1.9.1): defense-in-depth name validation. The
# repo is single-owner and a clean clone cannot produce odd names,
# but a tampered repo with a symlink like `skills/../../etc` would
# hand us '..' here. Refuse anything outside the expected charset
# rather than mkdir + cp into the parent. Spell out the accepted
# characters instead of using a locale-sensitive regex range.
case "$skill_name" in
''|*[!abcdefghijklmnopqrstuvwxyz0123456789-]*)
echo " ! refusing skill with unexpected name: ${skill_name}" >&2
continue
;;
esac
mkdir -p "${SKILL_DIR}/${skill_name}"
if [ -f "${skill_dir}SKILL.md" ]; then
cp "${skill_dir}SKILL.md" "${SKILL_DIR}/${skill_name}/SKILL.md"
echo " + ${skill_name}"
record_install "${MANIFEST}.tmp" "${SKILL_DIR}/${skill_name}"
sub_skill_count=$((sub_skill_count + 1))
fi
for payload_dir in references scripts assets templates; do
copy_tree "${skill_dir}${payload_dir}" "${SKILL_DIR}/${skill_name}/${payload_dir}"
done
if [ -d "${SKILL_DIR}/${skill_name}/scripts" ]; then
find "${SKILL_DIR}/${skill_name}/scripts" -type f -name '*.py' -exec chmod +x {} +
fi
done
if [ "${sub_skill_count}" -ne "${expected_sub_skill_count}" ]; then
echo "ERROR: installed ${sub_skill_count} of ${expected_sub_skill_count} sub-skills." >&2
echo " Installation is incomplete; review the refusal messages above." >&2
return 1
fi
# Create personas directory for blog-persona
mkdir -p "${SKILL_DIR}/blog/references/personas"
# Copy agents
echo "→ Installing agents..."
local agent_count=0
for agent_file in "${SCRIPT_DIR}/agents/"*.md; do
if [ -f "${agent_file}" ]; then
agent_name="$(basename "${agent_file}")"
cp "${agent_file}" "${AGENT_DIR}/${agent_name}"
record_install "${MANIFEST}.tmp" "${AGENT_DIR}/${agent_name}"
echo " + ${agent_name%.md}"
agent_count=$((agent_count + 1))
fi
done
# Copy scripts (v1.8.6: ALL root-level scripts, not just analyze_blog.py).
# Before v1.8.6 the installer only copied analyze_blog.py, leaving the
# v1.8.0+ helpers (cognitive_load, discourse_research, load_untrusted_root,
# lint_prose, sync_flow) absent on the user's machine. This broke the
# v1.8.3 "code-enforced" untrusted-data contract for every marketplace
# / curl-pipe install (closes 7TH-AUDIT-001).
echo "→ Installing scripts..."
mkdir -p "${SKILL_DIR}/blog/scripts"
mkdir -p "${HOME}/.claude/scripts"
local script_name
local root_script_count=0
for script_path in "${SCRIPT_DIR}/scripts/"*.py; do
[ -f "${script_path}" ] || continue
script_name="$(basename "${script_path}")"
# Copy to ~/.claude/scripts/ (canonical install location) AND to the
# blog-skill scripts dir (legacy callers of analyze_blog.py).
cp "${script_path}" "${HOME}/.claude/scripts/${script_name}"
chmod +x "${HOME}/.claude/scripts/${script_name}"
record_install "${MANIFEST}.tmp" "${HOME}/.claude/scripts/${script_name}"
if [ "${script_name}" = "analyze_blog.py" ]; then
cp "${script_path}" "${SKILL_DIR}/blog/scripts/${script_name}"
chmod +x "${SKILL_DIR}/blog/scripts/${script_name}"
fi
echo " + scripts/${script_name}"
root_script_count=$((root_script_count + 1))
done
mv "${MANIFEST}.tmp" "${MANIFEST}"
# Install Python dependencies (closes audit VULN-507/804: capture stderr
# to a logfile instead of swallowing it. Operator can diagnose failures.)
if [ -f "${SCRIPT_DIR}/requirements.txt" ] && command -v pip3 &>/dev/null; then
echo "→ Installing Python dependencies..."
local pip_log
pip_log="$(mktemp -t claude-blog-pip-XXXXXX.log)"
if pip3 install --quiet -r "${SCRIPT_DIR}/requirements.txt" 2>"${pip_log}"; then
rm -f "${pip_log}"
else
echo " WARNING: pip install failed."
echo " See log: ${pip_log}"
echo " First error: $(head -n1 "${pip_log}" 2>/dev/null || echo '(empty)')"
echo " Manual install: pip3 install -r requirements.txt"
fi
echo " Tip: Consider using a virtual environment: python3 -m venv .venv && source .venv/bin/activate"
fi
echo ""
echo " ╔══════════════════════════════════════╗"
echo " ║ Installation Complete! ║"
echo " ╚══════════════════════════════════════╝"
echo ""
echo " Installed:"
echo " Main skill: blog/ (orchestrator + $(count_files "${SKILL_DIR}/blog/references") references + $(count_files "${SKILL_DIR}/blog/templates") templates)"
echo " Sub-skills: ${sub_skill_count} installed"
echo " Agents: ${agent_count} specialists"
echo " Scripts: ${root_script_count} root-level + per-skill scripts"
echo " Manifest: ${MANIFEST}"
echo ""
echo " Commands available:"
print_commands "${SCRIPT_DIR}/skills/blog/SKILL.md"
echo ""
echo " Optional: AI Features (same API key for both)"
echo " /blog image setup Configure Gemini image generation"
echo " /blog audio setup Configure Gemini TTS audio narration"
echo " Requires: Google AI API key (free at https://aistudio.google.com/apikey)"
echo ""
echo " Restart Claude Code to activate the new skill."
}
main "$@"