@adityaaria/spark 6.0.15 → 6.0.17
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/README.md +27 -3
- package/bin/spark-install.sh +861 -0
- package/package.json +1 -1
- package/skills/using-spark/SKILL.md +7 -3
- package/src/cli/index.js +6 -12
- package/src/cli/install.js +38 -166
- package/src/cli/output.js +6 -3
- package/src/cli/parse-args.js +0 -46
- package/src/cli/prompt.js +0 -11
- package/src/installer/adapters/claude-staging.js +0 -68
- package/src/installer/adapters/codex-staging.js +0 -77
- package/src/installer/adapters/common.js +0 -158
- package/src/installer/adapters/cursor-staging.js +0 -42
- package/src/installer/adapters/extension-style.js +0 -93
- package/src/installer/adapters/opencode-staging.js +0 -61
- package/src/installer/adapters/shell-hook.js +0 -138
- package/src/installer/adapters/vscode-staging.js +0 -75
- package/src/installer/detect.js +0 -257
- package/src/installer/errors.js +0 -7
- package/src/installer/registry.js +0 -37
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# spark-install.sh — Native self-installer for SPARK
|
|
3
|
+
# Installs skills + hooks for detected coding agents.
|
|
4
|
+
# Zero external runtime dependencies (pure bash + common unix tools).
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# bash bin/spark-install.sh # project-scope install
|
|
8
|
+
# bash bin/spark-install.sh -g # global-scope install
|
|
9
|
+
# bash bin/spark-install.sh --force # re-install even if already installed
|
|
10
|
+
# bash bin/spark-install.sh --help # show usage
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# =============================================================================
|
|
16
|
+
# Constants & Colors
|
|
17
|
+
# =============================================================================
|
|
18
|
+
|
|
19
|
+
readonly VERSION="1.0.0"
|
|
20
|
+
readonly LOCK_FILE_NAME=".spark-lock.json"
|
|
21
|
+
|
|
22
|
+
# Colors (disabled if not a TTY)
|
|
23
|
+
if [ -t 1 ]; then
|
|
24
|
+
readonly C_RESET='\033[0m'
|
|
25
|
+
readonly C_BOLD='\033[1m'
|
|
26
|
+
readonly C_GREEN='\033[38;5;46m' # 8-bit neon green
|
|
27
|
+
readonly C_YELLOW='\033[38;5;226m' # 8-bit bright yellow
|
|
28
|
+
readonly C_RED='\033[38;5;196m' # 8-bit bright red
|
|
29
|
+
readonly C_CYAN='\033[38;5;51m' # 8-bit cyan
|
|
30
|
+
readonly C_MAGENTA='\033[38;5;201m' # 8-bit magenta
|
|
31
|
+
readonly C_DIM='\033[2m'
|
|
32
|
+
else
|
|
33
|
+
readonly C_RESET=''
|
|
34
|
+
readonly C_BOLD=''
|
|
35
|
+
readonly C_GREEN=''
|
|
36
|
+
readonly C_YELLOW=''
|
|
37
|
+
readonly C_RED=''
|
|
38
|
+
readonly C_CYAN=''
|
|
39
|
+
readonly C_MAGENTA=''
|
|
40
|
+
readonly C_DIM=''
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Exit codes
|
|
44
|
+
readonly EXIT_SUCCESS=0
|
|
45
|
+
readonly EXIT_NO_AGENTS=1
|
|
46
|
+
readonly EXIT_PERMISSION=2
|
|
47
|
+
readonly EXIT_NO_REPO=3
|
|
48
|
+
readonly EXIT_INSTALL_FAILED=4
|
|
49
|
+
|
|
50
|
+
# =============================================================================
|
|
51
|
+
# Output helpers (8-Bit Theme)
|
|
52
|
+
# =============================================================================
|
|
53
|
+
|
|
54
|
+
info() { printf "${C_CYAN}►${C_RESET} %s\n" "$*"; }
|
|
55
|
+
success() { printf "${C_GREEN}★${C_RESET} %s\n" "$*"; }
|
|
56
|
+
warn() { printf "${C_YELLOW}▲${C_RESET} %s\n" "$*" >&2; }
|
|
57
|
+
error() { printf "${C_RED}[X]${C_RESET} %s\n" "$*" >&2; }
|
|
58
|
+
step() { printf "${C_MAGENTA}[LVL %s/%s]${C_RESET} %s\n" "$1" "$2" "$3"; }
|
|
59
|
+
header() {
|
|
60
|
+
printf "\n${C_MAGENTA}▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄${C_RESET}\n"
|
|
61
|
+
printf "${C_MAGENTA}█${C_RESET} ${C_BOLD}%-35s${C_RESET} ${C_MAGENTA}█${C_RESET}\n" " $1 "
|
|
62
|
+
printf "${C_MAGENTA}▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀${C_RESET}\n\n"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# =============================================================================
|
|
66
|
+
# Argument parsing
|
|
67
|
+
# =============================================================================
|
|
68
|
+
|
|
69
|
+
SCOPE="project"
|
|
70
|
+
FORCE=false
|
|
71
|
+
HELP=false
|
|
72
|
+
DRY_RUN=false
|
|
73
|
+
|
|
74
|
+
parse_args() {
|
|
75
|
+
while [ $# -gt 0 ]; do
|
|
76
|
+
case "$1" in
|
|
77
|
+
-g|--global) SCOPE="global" ;;
|
|
78
|
+
--force) FORCE=true ;;
|
|
79
|
+
--dry-run) DRY_RUN=true ;;
|
|
80
|
+
-h|--help) HELP=true ;;
|
|
81
|
+
*)
|
|
82
|
+
error "Unknown argument: $1"
|
|
83
|
+
echo "Run with --help for usage."
|
|
84
|
+
exit 1
|
|
85
|
+
;;
|
|
86
|
+
esac
|
|
87
|
+
shift
|
|
88
|
+
done
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
print_help() {
|
|
92
|
+
cat <<'EOF'
|
|
93
|
+
|
|
94
|
+
SPARK Native Installer
|
|
95
|
+
|
|
96
|
+
Usage:
|
|
97
|
+
bash bin/spark-install.sh [options]
|
|
98
|
+
|
|
99
|
+
Options:
|
|
100
|
+
-g, --global Install to global agent config (~/.agent/skills/)
|
|
101
|
+
Default: project scope (./.agent/skills/)
|
|
102
|
+
--force Re-install even if already installed
|
|
103
|
+
--dry-run Show what would be done without making changes
|
|
104
|
+
-h, --help Show this help message
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
bash bin/spark-install.sh # install for detected agents (project scope)
|
|
108
|
+
bash bin/spark-install.sh -g # install globally
|
|
109
|
+
bash bin/spark-install.sh --force -g # force reinstall globally
|
|
110
|
+
|
|
111
|
+
One-liner from scratch:
|
|
112
|
+
git clone https://github.com/adityaaria/SPARK.git && cd SPARK && bash bin/spark-install.sh
|
|
113
|
+
|
|
114
|
+
EOF
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# =============================================================================
|
|
118
|
+
# Repo resolution & version detection
|
|
119
|
+
# =============================================================================
|
|
120
|
+
|
|
121
|
+
SPARK_ROOT=""
|
|
122
|
+
SPARK_VERSION=""
|
|
123
|
+
SPARK_COMMIT=""
|
|
124
|
+
|
|
125
|
+
resolve_spark_root() {
|
|
126
|
+
# Determine repo root from the script's own location
|
|
127
|
+
local script_dir
|
|
128
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
129
|
+
|
|
130
|
+
# Script lives in bin/, so repo root is one level up
|
|
131
|
+
SPARK_ROOT="$(cd "$script_dir/.." && pwd)"
|
|
132
|
+
|
|
133
|
+
# Validate: skills/ directory must exist
|
|
134
|
+
if [ ! -d "$SPARK_ROOT/skills" ]; then
|
|
135
|
+
error "Cannot find skills/ directory at $SPARK_ROOT"
|
|
136
|
+
error "Are you running this from within the SPARK repo?"
|
|
137
|
+
exit $EXIT_NO_REPO
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# Validate: hooks/ directory must exist
|
|
141
|
+
if [ ! -d "$SPARK_ROOT/hooks" ]; then
|
|
142
|
+
error "Cannot find hooks/ directory at $SPARK_ROOT"
|
|
143
|
+
exit $EXIT_NO_REPO
|
|
144
|
+
fi
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
detect_version() {
|
|
148
|
+
# Try git tag first
|
|
149
|
+
if command -v git >/dev/null 2>&1 && [ -d "$SPARK_ROOT/.git" ]; then
|
|
150
|
+
SPARK_COMMIT="$(git -C "$SPARK_ROOT" rev-parse HEAD 2>/dev/null || echo "unknown")"
|
|
151
|
+
SPARK_VERSION="$(git -C "$SPARK_ROOT" describe --tags --always 2>/dev/null || echo "")"
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# Fallback: parse version from package.json without jq
|
|
155
|
+
if [ -z "$SPARK_VERSION" ] && [ -f "$SPARK_ROOT/package.json" ]; then
|
|
156
|
+
# Simple grep-based extraction — no jq required
|
|
157
|
+
SPARK_VERSION="$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$SPARK_ROOT/package.json" | head -1 | grep -o '"[^"]*"$' | tr -d '"')"
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
if [ -z "$SPARK_VERSION" ]; then SPARK_VERSION="unknown"; fi
|
|
161
|
+
if [ -z "$SPARK_COMMIT" ]; then SPARK_COMMIT="unknown"; fi
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# =============================================================================
|
|
165
|
+
# Skill & hook discovery
|
|
166
|
+
# =============================================================================
|
|
167
|
+
|
|
168
|
+
# Parallel arrays for discovered skills
|
|
169
|
+
SKILL_NAMES=()
|
|
170
|
+
SKILL_PATHS=()
|
|
171
|
+
|
|
172
|
+
discover_skills() {
|
|
173
|
+
local count=0
|
|
174
|
+
for skill_dir in "$SPARK_ROOT"/skills/*/; do
|
|
175
|
+
[ ! -d "$skill_dir" ] && continue
|
|
176
|
+
local skill_md="$skill_dir/SKILL.md"
|
|
177
|
+
if [ ! -f "$skill_md" ]; then
|
|
178
|
+
warn "Skill directory $(basename "$skill_dir") missing SKILL.md — skipping"
|
|
179
|
+
continue
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
# Validate frontmatter: must have name: and description:
|
|
183
|
+
local has_name=false has_desc=false in_frontmatter=false
|
|
184
|
+
while IFS= read -r line; do
|
|
185
|
+
if [ "$line" = "---" ]; then
|
|
186
|
+
if $in_frontmatter; then
|
|
187
|
+
break # end of frontmatter
|
|
188
|
+
else
|
|
189
|
+
in_frontmatter=true
|
|
190
|
+
continue
|
|
191
|
+
fi
|
|
192
|
+
fi
|
|
193
|
+
if $in_frontmatter; then
|
|
194
|
+
case "$line" in
|
|
195
|
+
name:*) has_name=true ;;
|
|
196
|
+
description:*) has_desc=true ;;
|
|
197
|
+
esac
|
|
198
|
+
fi
|
|
199
|
+
done < "$skill_md"
|
|
200
|
+
|
|
201
|
+
if ! $has_name || ! $has_desc; then
|
|
202
|
+
warn "Skill $(basename "$skill_dir") has invalid frontmatter (missing name/description) — skipping"
|
|
203
|
+
continue
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
SKILL_NAMES+=("$(basename "$skill_dir")")
|
|
207
|
+
SKILL_PATHS+=("$skill_dir")
|
|
208
|
+
count=$((count + 1))
|
|
209
|
+
done
|
|
210
|
+
|
|
211
|
+
if [ $count -eq 0 ]; then
|
|
212
|
+
error "No valid skills found in $SPARK_ROOT/skills/"
|
|
213
|
+
exit $EXIT_NO_REPO
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
info "Discovered $count skills: ${SKILL_NAMES[*]}"
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# =============================================================================
|
|
220
|
+
# Agent detection
|
|
221
|
+
# =============================================================================
|
|
222
|
+
|
|
223
|
+
# Agent registry: parallel arrays
|
|
224
|
+
AGENT_IDS=()
|
|
225
|
+
AGENT_LABELS=()
|
|
226
|
+
AGENT_DETECTED=() # "true" / "false"
|
|
227
|
+
AGENT_REASONS=()
|
|
228
|
+
|
|
229
|
+
register_agent() {
|
|
230
|
+
local id="$1" label="$2" detected="$3" reason="$4"
|
|
231
|
+
AGENT_IDS+=("$id")
|
|
232
|
+
AGENT_LABELS+=("$label")
|
|
233
|
+
AGENT_DETECTED+=("$detected")
|
|
234
|
+
AGENT_REASONS+=("$reason")
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
command_exists() {
|
|
238
|
+
command -v "$1" >/dev/null 2>&1
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
detect_agents() {
|
|
242
|
+
local home="${HOME:-}"
|
|
243
|
+
[ -z "$home" ] && home="$(eval echo ~)"
|
|
244
|
+
|
|
245
|
+
# Claude Code: ~/.claude/ or claude binary or CLAUDE_PLUGIN_ROOT env
|
|
246
|
+
if [ -d "$home/.claude" ] || [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] || command_exists claude; then
|
|
247
|
+
local reason=""
|
|
248
|
+
[ -d "$home/.claude" ] && reason="config:~/.claude"
|
|
249
|
+
[ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && reason="${reason:+$reason, }env:CLAUDE_PLUGIN_ROOT"
|
|
250
|
+
command_exists claude && reason="${reason:+$reason, }path:claude"
|
|
251
|
+
register_agent "claude" "Claude Code" "true" "$reason"
|
|
252
|
+
else
|
|
253
|
+
register_agent "claude" "Claude Code" "false" ""
|
|
254
|
+
fi
|
|
255
|
+
|
|
256
|
+
# Codex CLI: ~/.codex/ or codex binary
|
|
257
|
+
if [ -d "$home/.codex" ] || command_exists codex; then
|
|
258
|
+
local reason=""
|
|
259
|
+
[ -d "$home/.codex" ] && reason="config:~/.codex"
|
|
260
|
+
command_exists codex && reason="${reason:+$reason, }path:codex"
|
|
261
|
+
register_agent "codex" "Codex CLI" "true" "$reason"
|
|
262
|
+
else
|
|
263
|
+
register_agent "codex" "Codex CLI" "false" ""
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
# Cursor: ~/.cursor/ or CURSOR_PLUGIN_ROOT env or cursor binary
|
|
267
|
+
if [ -d "$home/.cursor" ] || [ -n "${CURSOR_PLUGIN_ROOT:-}" ] || command_exists cursor; then
|
|
268
|
+
local reason=""
|
|
269
|
+
[ -d "$home/.cursor" ] && reason="config:~/.cursor"
|
|
270
|
+
[ -n "${CURSOR_PLUGIN_ROOT:-}" ] && reason="${reason:+$reason, }env:CURSOR_PLUGIN_ROOT"
|
|
271
|
+
command_exists cursor && reason="${reason:+$reason, }path:cursor"
|
|
272
|
+
register_agent "cursor" "Cursor" "true" "$reason"
|
|
273
|
+
else
|
|
274
|
+
register_agent "cursor" "Cursor" "false" ""
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
# Kimi: ~/.kimi/ or kimi binary
|
|
278
|
+
if [ -d "$home/.kimi" ] || command_exists kimi; then
|
|
279
|
+
local reason=""
|
|
280
|
+
[ -d "$home/.kimi" ] && reason="config:~/.kimi"
|
|
281
|
+
command_exists kimi && reason="${reason:+$reason, }path:kimi"
|
|
282
|
+
register_agent "kimi" "Kimi Code" "true" "$reason"
|
|
283
|
+
else
|
|
284
|
+
register_agent "kimi" "Kimi Code" "false" ""
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
# OpenCode: ~/.config/opencode/ or OPENCODE_CONFIG_DIR or opencode binary
|
|
288
|
+
local oc_config="${OPENCODE_CONFIG_DIR:-$home/.config/opencode}"
|
|
289
|
+
if [ -d "$oc_config" ] || command_exists opencode; then
|
|
290
|
+
local reason=""
|
|
291
|
+
[ -d "$oc_config" ] && reason="config:$oc_config"
|
|
292
|
+
[ -n "${OPENCODE_CONFIG_DIR:-}" ] && reason="${reason:+$reason, }env:OPENCODE_CONFIG_DIR"
|
|
293
|
+
command_exists opencode && reason="${reason:+$reason, }path:opencode"
|
|
294
|
+
register_agent "opencode" "OpenCode" "true" "$reason"
|
|
295
|
+
else
|
|
296
|
+
register_agent "opencode" "OpenCode" "false" ""
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
# Pi: PI_HOME env or pi binary
|
|
300
|
+
if [ -n "${PI_HOME:-}" ] || command_exists pi; then
|
|
301
|
+
local reason=""
|
|
302
|
+
[ -n "${PI_HOME:-}" ] && reason="env:PI_HOME"
|
|
303
|
+
command_exists pi && reason="${reason:+$reason, }path:pi"
|
|
304
|
+
register_agent "pi" "Pi" "true" "$reason"
|
|
305
|
+
else
|
|
306
|
+
register_agent "pi" "Pi" "false" ""
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
# Count detected
|
|
310
|
+
local detected_count=0
|
|
311
|
+
for i in "${!AGENT_IDS[@]}"; do
|
|
312
|
+
[ "${AGENT_DETECTED[$i]}" = "true" ] && detected_count=$((detected_count + 1))
|
|
313
|
+
done
|
|
314
|
+
|
|
315
|
+
if [ $detected_count -gt 0 ]; then
|
|
316
|
+
info "Detected $detected_count agent(s):"
|
|
317
|
+
for i in "${!AGENT_IDS[@]}"; do
|
|
318
|
+
if [ "${AGENT_DETECTED[$i]}" = "true" ]; then
|
|
319
|
+
printf " ${C_GREEN}■${C_RESET} %-15s ${C_DIM}(%s)${C_RESET}\n" "${AGENT_LABELS[$i]}" "${AGENT_REASONS[$i]}"
|
|
320
|
+
fi
|
|
321
|
+
done
|
|
322
|
+
fi
|
|
323
|
+
|
|
324
|
+
return $detected_count
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
# =============================================================================
|
|
328
|
+
# Interactive agent selection (when none detected)
|
|
329
|
+
# =============================================================================
|
|
330
|
+
|
|
331
|
+
SELECTED_AGENTS=()
|
|
332
|
+
|
|
333
|
+
prompt_agent_selection() {
|
|
334
|
+
# Check if stdin is a terminal
|
|
335
|
+
if [ ! -t 0 ]; then
|
|
336
|
+
error "No agents detected and stdin is not a terminal."
|
|
337
|
+
error "Run interactively or set agent config directories manually."
|
|
338
|
+
exit $EXIT_NO_AGENTS
|
|
339
|
+
fi
|
|
340
|
+
|
|
341
|
+
echo ""
|
|
342
|
+
warn "No coding agents detected automatically."
|
|
343
|
+
echo ""
|
|
344
|
+
echo "Select which agents to install SPARK for:"
|
|
345
|
+
echo ""
|
|
346
|
+
|
|
347
|
+
for i in "${!AGENT_IDS[@]}"; do
|
|
348
|
+
printf " ${C_BOLD}%d)${C_RESET} %s\n" "$((i + 1))" "${AGENT_LABELS[$i]}"
|
|
349
|
+
done
|
|
350
|
+
|
|
351
|
+
echo ""
|
|
352
|
+
printf "Enter numbers separated by spaces (e.g. '1 3 5'), or 'q' to quit: "
|
|
353
|
+
read -r selection
|
|
354
|
+
|
|
355
|
+
if [ "$selection" = "q" ] || [ "$selection" = "Q" ] || [ -z "$selection" ]; then
|
|
356
|
+
info "No agents selected. Exiting."
|
|
357
|
+
exit $EXIT_NO_AGENTS
|
|
358
|
+
fi
|
|
359
|
+
|
|
360
|
+
for num in $selection; do
|
|
361
|
+
# Validate: must be a number in range
|
|
362
|
+
if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -ge 1 ] && [ "$num" -le "${#AGENT_IDS[@]}" ]; then
|
|
363
|
+
local idx=$((num - 1))
|
|
364
|
+
SELECTED_AGENTS+=("${AGENT_IDS[$idx]}")
|
|
365
|
+
else
|
|
366
|
+
warn "Ignoring invalid selection: $num"
|
|
367
|
+
fi
|
|
368
|
+
done
|
|
369
|
+
|
|
370
|
+
if [ ${#SELECTED_AGENTS[@]} -eq 0 ]; then
|
|
371
|
+
error "No valid agents selected."
|
|
372
|
+
exit $EXIT_NO_AGENTS
|
|
373
|
+
fi
|
|
374
|
+
|
|
375
|
+
info "Selected: ${SELECTED_AGENTS[*]}"
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
build_selected_agents() {
|
|
379
|
+
# If agents were detected, use those. Otherwise, use manual selection.
|
|
380
|
+
local detected_count=0
|
|
381
|
+
for i in "${!AGENT_IDS[@]}"; do
|
|
382
|
+
[ "${AGENT_DETECTED[$i]}" = "true" ] && detected_count=$((detected_count + 1))
|
|
383
|
+
done
|
|
384
|
+
|
|
385
|
+
if [ $detected_count -eq 0 ]; then
|
|
386
|
+
prompt_agent_selection
|
|
387
|
+
else
|
|
388
|
+
# Use all detected agents
|
|
389
|
+
for i in "${!AGENT_IDS[@]}"; do
|
|
390
|
+
if [ "${AGENT_DETECTED[$i]}" = "true" ]; then
|
|
391
|
+
SELECTED_AGENTS+=("${AGENT_IDS[$i]}")
|
|
392
|
+
fi
|
|
393
|
+
done
|
|
394
|
+
fi
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
# =============================================================================
|
|
398
|
+
# Installation logic
|
|
399
|
+
# =============================================================================
|
|
400
|
+
|
|
401
|
+
INSTALL_RESULTS=() # "agent_id:method" pairs
|
|
402
|
+
INSTALL_ERRORS=()
|
|
403
|
+
|
|
404
|
+
# Determine target directory for a given agent
|
|
405
|
+
get_target_dir() {
|
|
406
|
+
local agent_id="$1"
|
|
407
|
+
local home="${HOME:-$(eval echo ~)}"
|
|
408
|
+
|
|
409
|
+
if [ "$SCOPE" = "global" ]; then
|
|
410
|
+
case "$agent_id" in
|
|
411
|
+
claude) echo "$home/.claude" ;;
|
|
412
|
+
codex) echo "$home/.codex" ;;
|
|
413
|
+
cursor) echo "$home/.cursor/plugins/spark" ;;
|
|
414
|
+
kimi) echo "$home/.kimi" ;;
|
|
415
|
+
opencode) echo "${OPENCODE_CONFIG_DIR:-$home/.config/opencode}" ;;
|
|
416
|
+
pi) echo "${PI_HOME:-$home/.pi}" ;;
|
|
417
|
+
*) echo "$home/.$agent_id" ;;
|
|
418
|
+
esac
|
|
419
|
+
else
|
|
420
|
+
# Project scope: relative to current working directory
|
|
421
|
+
case "$agent_id" in
|
|
422
|
+
claude) echo "$(pwd)/.claude" ;;
|
|
423
|
+
codex) echo "$(pwd)/.codex" ;;
|
|
424
|
+
cursor) echo "$(pwd)/.cursor" ;;
|
|
425
|
+
kimi) echo "$(pwd)/.kimi" ;;
|
|
426
|
+
opencode) echo "$(pwd)/.opencode" ;;
|
|
427
|
+
pi) echo "$(pwd)/.pi" ;;
|
|
428
|
+
*) echo "$(pwd)/.$agent_id" ;;
|
|
429
|
+
esac
|
|
430
|
+
fi
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
# Try to create a symlink, fallback to copy
|
|
434
|
+
# Returns: "symlink" or "copy"
|
|
435
|
+
link_or_copy() {
|
|
436
|
+
local source="$1"
|
|
437
|
+
local target="$2"
|
|
438
|
+
|
|
439
|
+
# If target already exists and points to same source, skip
|
|
440
|
+
if [ -L "$target" ]; then
|
|
441
|
+
local existing_target
|
|
442
|
+
existing_target="$(readlink "$target" 2>/dev/null || true)"
|
|
443
|
+
if [ "$existing_target" = "$source" ]; then
|
|
444
|
+
return 0 # Already linked correctly
|
|
445
|
+
fi
|
|
446
|
+
# Remove stale symlink
|
|
447
|
+
rm -f "$target"
|
|
448
|
+
elif [ -e "$target" ]; then
|
|
449
|
+
if $FORCE; then
|
|
450
|
+
rm -rf "$target"
|
|
451
|
+
else
|
|
452
|
+
warn "Target already exists (not a symlink): $target"
|
|
453
|
+
warn "Use --force to overwrite"
|
|
454
|
+
return 1
|
|
455
|
+
fi
|
|
456
|
+
fi
|
|
457
|
+
|
|
458
|
+
# Create parent directory
|
|
459
|
+
mkdir -p "$(dirname "$target")"
|
|
460
|
+
|
|
461
|
+
# Try symlink first
|
|
462
|
+
if ln -s "$source" "$target" 2>/dev/null; then
|
|
463
|
+
echo "symlink"
|
|
464
|
+
return 0
|
|
465
|
+
fi
|
|
466
|
+
|
|
467
|
+
# Fallback to copy
|
|
468
|
+
if [ -d "$source" ]; then
|
|
469
|
+
cp -R "$source" "$target"
|
|
470
|
+
else
|
|
471
|
+
cp "$source" "$target"
|
|
472
|
+
fi
|
|
473
|
+
echo "copy"
|
|
474
|
+
return 0
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
# Copy a file preserving permissions
|
|
478
|
+
copy_file() {
|
|
479
|
+
local source="$1"
|
|
480
|
+
local target="$2"
|
|
481
|
+
|
|
482
|
+
mkdir -p "$(dirname "$target")"
|
|
483
|
+
|
|
484
|
+
if [ -e "$target" ] && ! $FORCE; then
|
|
485
|
+
# Check if content is identical
|
|
486
|
+
if cmp -s "$source" "$target" 2>/dev/null; then
|
|
487
|
+
return 0 # Already identical
|
|
488
|
+
fi
|
|
489
|
+
fi
|
|
490
|
+
|
|
491
|
+
cp "$source" "$target"
|
|
492
|
+
# Preserve executable permission if source has it
|
|
493
|
+
if [ -x "$source" ]; then
|
|
494
|
+
chmod +x "$target"
|
|
495
|
+
fi
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
# Get hook files needed for a specific agent
|
|
499
|
+
get_agent_hook_files() {
|
|
500
|
+
local agent_id="$1"
|
|
501
|
+
|
|
502
|
+
case "$agent_id" in
|
|
503
|
+
claude)
|
|
504
|
+
echo "hooks/hooks.json"
|
|
505
|
+
echo "hooks/run-hook.cmd"
|
|
506
|
+
echo "hooks/session-start"
|
|
507
|
+
;;
|
|
508
|
+
codex)
|
|
509
|
+
echo "hooks/hooks-codex.json"
|
|
510
|
+
echo "hooks/run-hook.cmd"
|
|
511
|
+
echo "hooks/session-start-codex"
|
|
512
|
+
;;
|
|
513
|
+
cursor)
|
|
514
|
+
echo "hooks/hooks-cursor.json"
|
|
515
|
+
echo "hooks/run-hook.cmd"
|
|
516
|
+
echo "hooks/session-start"
|
|
517
|
+
;;
|
|
518
|
+
kimi)
|
|
519
|
+
# Kimi uses sessionStart field in plugin.json, no separate hook files
|
|
520
|
+
;;
|
|
521
|
+
opencode)
|
|
522
|
+
# OpenCode uses plugin JS for bootstrap, no separate hook files
|
|
523
|
+
;;
|
|
524
|
+
pi)
|
|
525
|
+
# Pi uses extension TS for bootstrap, no separate hook files
|
|
526
|
+
;;
|
|
527
|
+
esac
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
# Get plugin manifest files for a specific agent
|
|
531
|
+
get_agent_manifest_files() {
|
|
532
|
+
local agent_id="$1"
|
|
533
|
+
|
|
534
|
+
case "$agent_id" in
|
|
535
|
+
claude) echo ".claude-plugin/plugin.json" ;;
|
|
536
|
+
codex) echo ".codex-plugin/plugin.json" ;;
|
|
537
|
+
cursor) echo ".cursor-plugin/plugin.json" ;;
|
|
538
|
+
kimi) echo ".kimi-plugin/plugin.json" ;;
|
|
539
|
+
opencode) echo ".opencode/plugins/spark.js" ;;
|
|
540
|
+
pi) echo ".pi/extensions/spark.ts" ;;
|
|
541
|
+
esac
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
install_for_agent() {
|
|
545
|
+
local agent_id="$1"
|
|
546
|
+
local target_dir
|
|
547
|
+
target_dir="$(get_target_dir "$agent_id")"
|
|
548
|
+
|
|
549
|
+
if $DRY_RUN; then
|
|
550
|
+
info "[DRY-RUN] Would install for $agent_id at $target_dir"
|
|
551
|
+
INSTALL_RESULTS+=("$agent_id:dry-run")
|
|
552
|
+
return 0
|
|
553
|
+
fi
|
|
554
|
+
|
|
555
|
+
# Create target directory
|
|
556
|
+
if ! mkdir -p "$target_dir" 2>/dev/null; then
|
|
557
|
+
error "Cannot create directory: $target_dir"
|
|
558
|
+
INSTALL_ERRORS+=("$agent_id:permission_denied")
|
|
559
|
+
return 1
|
|
560
|
+
fi
|
|
561
|
+
|
|
562
|
+
local method="symlink"
|
|
563
|
+
local hooks_installed=false
|
|
564
|
+
|
|
565
|
+
# 1. Install skills — symlink the entire skills/ directory
|
|
566
|
+
local skills_target="$target_dir/skills"
|
|
567
|
+
local result
|
|
568
|
+
result="$(link_or_copy "$SPARK_ROOT/skills" "$skills_target")" || {
|
|
569
|
+
error "Failed to install skills for $agent_id"
|
|
570
|
+
INSTALL_ERRORS+=("$agent_id:skills_failed")
|
|
571
|
+
return 1
|
|
572
|
+
}
|
|
573
|
+
[ -n "$result" ] && method="$result"
|
|
574
|
+
|
|
575
|
+
# 2. Install hooks (for shell-hook agents)
|
|
576
|
+
local hook_files
|
|
577
|
+
hook_files="$(get_agent_hook_files "$agent_id")"
|
|
578
|
+
|
|
579
|
+
if [ -n "$hook_files" ]; then
|
|
580
|
+
local hooks_dir="$target_dir/hooks"
|
|
581
|
+
mkdir -p "$hooks_dir"
|
|
582
|
+
|
|
583
|
+
while IFS= read -r hook_file; do
|
|
584
|
+
[ -z "$hook_file" ] && continue
|
|
585
|
+
local source_path="$SPARK_ROOT/$hook_file"
|
|
586
|
+
local target_path="$target_dir/$hook_file"
|
|
587
|
+
|
|
588
|
+
if [ ! -f "$source_path" ]; then
|
|
589
|
+
warn "Hook file not found: $source_path"
|
|
590
|
+
continue
|
|
591
|
+
fi
|
|
592
|
+
|
|
593
|
+
copy_file "$source_path" "$target_path"
|
|
594
|
+
done <<< "$hook_files"
|
|
595
|
+
|
|
596
|
+
hooks_installed=true
|
|
597
|
+
fi
|
|
598
|
+
|
|
599
|
+
# 3. Install plugin manifest (for agents that need it locally)
|
|
600
|
+
local manifest_files
|
|
601
|
+
manifest_files="$(get_agent_manifest_files "$agent_id")"
|
|
602
|
+
|
|
603
|
+
if [ -n "$manifest_files" ]; then
|
|
604
|
+
while IFS= read -r manifest_file; do
|
|
605
|
+
[ -z "$manifest_file" ] && continue
|
|
606
|
+
local source_path="$SPARK_ROOT/$manifest_file"
|
|
607
|
+
local manifest_basename
|
|
608
|
+
manifest_basename="$(basename "$manifest_file")"
|
|
609
|
+
|
|
610
|
+
# For extension-style agents, install the plugin/extension file
|
|
611
|
+
case "$agent_id" in
|
|
612
|
+
opencode)
|
|
613
|
+
# OpenCode needs the plugin JS in its plugins directory
|
|
614
|
+
local oc_plugins="$target_dir/plugins"
|
|
615
|
+
mkdir -p "$oc_plugins"
|
|
616
|
+
copy_file "$source_path" "$oc_plugins/spark.js"
|
|
617
|
+
;;
|
|
618
|
+
pi)
|
|
619
|
+
# Pi needs the extension TS in its extensions directory
|
|
620
|
+
local pi_ext="$target_dir/extensions"
|
|
621
|
+
mkdir -p "$pi_ext"
|
|
622
|
+
copy_file "$source_path" "$pi_ext/spark.ts"
|
|
623
|
+
;;
|
|
624
|
+
*)
|
|
625
|
+
# Shell-hook agents: copy manifest to agent-plugin dir
|
|
626
|
+
local plugin_dir="$target_dir/.${agent_id}-plugin"
|
|
627
|
+
# For project scope, put manifest in the target dir itself
|
|
628
|
+
if [ "$SCOPE" = "project" ]; then
|
|
629
|
+
plugin_dir="$(pwd)/.${agent_id}-plugin"
|
|
630
|
+
fi
|
|
631
|
+
if [ -f "$source_path" ]; then
|
|
632
|
+
mkdir -p "$plugin_dir"
|
|
633
|
+
copy_file "$source_path" "$plugin_dir/$manifest_basename"
|
|
634
|
+
fi
|
|
635
|
+
;;
|
|
636
|
+
esac
|
|
637
|
+
done <<< "$manifest_files"
|
|
638
|
+
fi
|
|
639
|
+
|
|
640
|
+
# 4. Print agent-specific post-install notes
|
|
641
|
+
case "$agent_id" in
|
|
642
|
+
opencode)
|
|
643
|
+
info " Note: OpenCode also needs plugin registered in opencode.json."
|
|
644
|
+
info " Add to your opencode.json: \"plugin\": [\"spark@git+https://github.com/adityaaria/SPARK.git\"]"
|
|
645
|
+
;;
|
|
646
|
+
pi)
|
|
647
|
+
info " Note: For full Pi integration, also run: pi install git:github.com/adityaaria/SPARK"
|
|
648
|
+
;;
|
|
649
|
+
esac
|
|
650
|
+
|
|
651
|
+
INSTALL_RESULTS+=("$agent_id:$method:hooks=$hooks_installed")
|
|
652
|
+
success "Installed SPARK for $agent_id ($method) at $target_dir"
|
|
653
|
+
return 0
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
# =============================================================================
|
|
657
|
+
# Lock file
|
|
658
|
+
# =============================================================================
|
|
659
|
+
|
|
660
|
+
write_lock_file() {
|
|
661
|
+
local lock_dir
|
|
662
|
+
if [ "$SCOPE" = "global" ]; then
|
|
663
|
+
lock_dir="${HOME:-$(eval echo ~)}"
|
|
664
|
+
else
|
|
665
|
+
lock_dir="$(pwd)"
|
|
666
|
+
fi
|
|
667
|
+
|
|
668
|
+
local lock_path="$lock_dir/$LOCK_FILE_NAME"
|
|
669
|
+
local timestamp
|
|
670
|
+
timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%S")"
|
|
671
|
+
|
|
672
|
+
if $DRY_RUN; then
|
|
673
|
+
info "[DRY-RUN] Would write lock file to $lock_path"
|
|
674
|
+
return 0
|
|
675
|
+
fi
|
|
676
|
+
|
|
677
|
+
# Build agents JSON manually (no jq dependency)
|
|
678
|
+
local agents_json=""
|
|
679
|
+
for entry in "${INSTALL_RESULTS[@]}"; do
|
|
680
|
+
local agent_id method hooks_installed
|
|
681
|
+
agent_id="$(echo "$entry" | cut -d: -f1)"
|
|
682
|
+
method="$(echo "$entry" | cut -d: -f2)"
|
|
683
|
+
hooks_installed="$(echo "$entry" | cut -d: -f3 | sed 's/hooks=//')"
|
|
684
|
+
|
|
685
|
+
local target_dir
|
|
686
|
+
target_dir="$(get_target_dir "$agent_id")"
|
|
687
|
+
|
|
688
|
+
[ -n "$agents_json" ] && agents_json="$agents_json,"
|
|
689
|
+
agents_json="$agents_json
|
|
690
|
+
\"$agent_id\": {
|
|
691
|
+
\"scope\": \"$SCOPE\",
|
|
692
|
+
\"target\": \"$target_dir\",
|
|
693
|
+
\"method\": \"$method\",
|
|
694
|
+
\"hooks_installed\": $hooks_installed
|
|
695
|
+
}"
|
|
696
|
+
done
|
|
697
|
+
|
|
698
|
+
cat > "$lock_path" <<EOF
|
|
699
|
+
{
|
|
700
|
+
"installer": "spark-install.sh",
|
|
701
|
+
"installer_version": "$VERSION",
|
|
702
|
+
"spark_version": "$SPARK_VERSION",
|
|
703
|
+
"commit": "$SPARK_COMMIT",
|
|
704
|
+
"spark_root": "$SPARK_ROOT",
|
|
705
|
+
"installed_at": "$timestamp",
|
|
706
|
+
"scope": "$SCOPE",
|
|
707
|
+
"agents": {$agents_json
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
EOF
|
|
711
|
+
|
|
712
|
+
success "Lock file written to $lock_path"
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
# =============================================================================
|
|
716
|
+
# Idempotency check
|
|
717
|
+
# =============================================================================
|
|
718
|
+
|
|
719
|
+
check_existing_install() {
|
|
720
|
+
local lock_dir
|
|
721
|
+
if [ "$SCOPE" = "global" ]; then
|
|
722
|
+
lock_dir="${HOME:-$(eval echo ~)}"
|
|
723
|
+
else
|
|
724
|
+
lock_dir="$(pwd)"
|
|
725
|
+
fi
|
|
726
|
+
|
|
727
|
+
local lock_path="$lock_dir/$LOCK_FILE_NAME"
|
|
728
|
+
|
|
729
|
+
if [ -f "$lock_path" ] && ! $FORCE; then
|
|
730
|
+
local existing_version=""
|
|
731
|
+
local existing_commit=""
|
|
732
|
+
|
|
733
|
+
# Parse existing lock file without jq
|
|
734
|
+
existing_version="$(grep -o '"spark_version"[[:space:]]*:[[:space:]]*"[^"]*"' "$lock_path" 2>/dev/null | grep -o '"[^"]*"$' | tr -d '"' || true)"
|
|
735
|
+
existing_commit="$(grep -o '"commit"[[:space:]]*:[[:space:]]*"[^"]*"' "$lock_path" 2>/dev/null | grep -o '"[^"]*"$' | tr -d '"' || true)"
|
|
736
|
+
|
|
737
|
+
echo ""
|
|
738
|
+
warn "SPARK is already installed (version: ${existing_version:-unknown}, commit: ${existing_commit:-unknown})"
|
|
739
|
+
|
|
740
|
+
if [ "$existing_version" = "$SPARK_VERSION" ] && [ "$existing_commit" = "$SPARK_COMMIT" ]; then
|
|
741
|
+
success "Already up to date. Nothing to do."
|
|
742
|
+
info "Use --force to reinstall anyway."
|
|
743
|
+
exit $EXIT_SUCCESS
|
|
744
|
+
fi
|
|
745
|
+
|
|
746
|
+
info "A different version is installed. Updating..."
|
|
747
|
+
info " Installed: $existing_version ($existing_commit)"
|
|
748
|
+
info " Available: $SPARK_VERSION ($SPARK_COMMIT)"
|
|
749
|
+
echo ""
|
|
750
|
+
fi
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
# =============================================================================
|
|
754
|
+
# Summary
|
|
755
|
+
# =============================================================================
|
|
756
|
+
|
|
757
|
+
print_summary() {
|
|
758
|
+
header "Installation Summary"
|
|
759
|
+
|
|
760
|
+
echo " SPARK version: $SPARK_VERSION"
|
|
761
|
+
echo " Commit: ${SPARK_COMMIT:0:12}"
|
|
762
|
+
echo " Scope: $SCOPE"
|
|
763
|
+
echo " Skills: ${#SKILL_NAMES[@]} installed"
|
|
764
|
+
echo ""
|
|
765
|
+
|
|
766
|
+
if [ ${#INSTALL_RESULTS[@]} -gt 0 ]; then
|
|
767
|
+
echo " Agents:"
|
|
768
|
+
for entry in "${INSTALL_RESULTS[@]}"; do
|
|
769
|
+
local agent_id method
|
|
770
|
+
agent_id="$(echo "$entry" | cut -d: -f1)"
|
|
771
|
+
method="$(echo "$entry" | cut -d: -f2)"
|
|
772
|
+
local target_dir
|
|
773
|
+
target_dir="$(get_target_dir "$agent_id")"
|
|
774
|
+
printf " ${C_GREEN}■${C_RESET} %-15s → %s ${C_DIM}(%s)${C_RESET}\n" "$agent_id" "$target_dir" "$method"
|
|
775
|
+
done
|
|
776
|
+
fi
|
|
777
|
+
|
|
778
|
+
if [ ${#INSTALL_ERRORS[@]} -gt 0 ]; then
|
|
779
|
+
echo ""
|
|
780
|
+
echo " Errors:"
|
|
781
|
+
for entry in "${INSTALL_ERRORS[@]}"; do
|
|
782
|
+
printf " ${C_RED}■${C_RESET} %s\n" "$entry"
|
|
783
|
+
done
|
|
784
|
+
fi
|
|
785
|
+
|
|
786
|
+
echo ""
|
|
787
|
+
success "Start a fresh agent session to confirm using-spark loads before coding."
|
|
788
|
+
echo ""
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
# =============================================================================
|
|
792
|
+
# Main
|
|
793
|
+
# =============================================================================
|
|
794
|
+
|
|
795
|
+
main() {
|
|
796
|
+
parse_args "$@"
|
|
797
|
+
|
|
798
|
+
if $HELP; then
|
|
799
|
+
print_help
|
|
800
|
+
exit $EXIT_SUCCESS
|
|
801
|
+
fi
|
|
802
|
+
|
|
803
|
+
header "SPARK Native Installer"
|
|
804
|
+
|
|
805
|
+
# Step 1: Resolve repo
|
|
806
|
+
info "Resolving SPARK repository..."
|
|
807
|
+
resolve_spark_root
|
|
808
|
+
detect_version
|
|
809
|
+
info "SPARK root: $SPARK_ROOT"
|
|
810
|
+
info "Version: $SPARK_VERSION (${SPARK_COMMIT:0:12})"
|
|
811
|
+
|
|
812
|
+
# Step 2: Check existing install
|
|
813
|
+
check_existing_install
|
|
814
|
+
|
|
815
|
+
# Step 3: Discover skills
|
|
816
|
+
info "Discovering skills..."
|
|
817
|
+
discover_skills
|
|
818
|
+
|
|
819
|
+
# Step 4: Detect agents
|
|
820
|
+
echo ""
|
|
821
|
+
info "Detecting installed agents..."
|
|
822
|
+
detect_agents || true # detect_agents returns count via exit code
|
|
823
|
+
|
|
824
|
+
# Step 5: Build selection
|
|
825
|
+
build_selected_agents
|
|
826
|
+
|
|
827
|
+
if [ ${#SELECTED_AGENTS[@]} -eq 0 ]; then
|
|
828
|
+
error "No agents to install for."
|
|
829
|
+
exit $EXIT_NO_AGENTS
|
|
830
|
+
fi
|
|
831
|
+
|
|
832
|
+
# Step 6: Install for each selected agent
|
|
833
|
+
echo ""
|
|
834
|
+
header "Installing"
|
|
835
|
+
|
|
836
|
+
local total=${#SELECTED_AGENTS[@]}
|
|
837
|
+
local current=0
|
|
838
|
+
for agent_id in "${SELECTED_AGENTS[@]}"; do
|
|
839
|
+
current=$((current + 1))
|
|
840
|
+
step "$current" "$total" "Installing for $agent_id..."
|
|
841
|
+
install_for_agent "$agent_id" || true
|
|
842
|
+
done
|
|
843
|
+
|
|
844
|
+
# Step 7: Write lock file
|
|
845
|
+
if [ ${#INSTALL_RESULTS[@]} -gt 0 ]; then
|
|
846
|
+
echo ""
|
|
847
|
+
write_lock_file
|
|
848
|
+
fi
|
|
849
|
+
|
|
850
|
+
# Step 8: Summary
|
|
851
|
+
print_summary
|
|
852
|
+
|
|
853
|
+
# Exit with error if any installs failed
|
|
854
|
+
if [ ${#INSTALL_ERRORS[@]} -gt 0 ]; then
|
|
855
|
+
exit $EXIT_INSTALL_FAILED
|
|
856
|
+
fi
|
|
857
|
+
|
|
858
|
+
exit $EXIT_SUCCESS
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
main "$@"
|