gaia-framework 1.0.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,786 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # ─────────────────────────────────────────────────────────────────────────────
5
+ # GAIA Framework Installer
6
+ # Installs, updates, validates, and reports on GAIA installations.
7
+ # ─────────────────────────────────────────────────────────────────────────────
8
+
9
+ readonly VERSION="1.0.0"
10
+ readonly GITHUB_REPO="https://github.com/J-louage/Gaia-framework.git"
11
+ readonly MANIFEST_REL="_gaia/_config/manifest.yaml"
12
+
13
+ # Temp dir tracking for cleanup
14
+ TEMP_CLONE_DIR=""
15
+
16
+ # ─── Colors & Formatting ────────────────────────────────────────────────────
17
+
18
+ if [[ -t 1 ]]; then
19
+ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'
20
+ BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'
21
+ RESET='\033[0m'
22
+ else
23
+ RED=''; GREEN=''; YELLOW=''; BLUE=''; CYAN=''; BOLD=''; DIM=''; RESET=''
24
+ fi
25
+
26
+ info() { printf "${BLUE}ℹ${RESET} %s\n" "$*"; }
27
+ success() { printf "${GREEN}✔${RESET} %s\n" "$*"; }
28
+ warn() { printf "${YELLOW}⚠${RESET} %s\n" "$*"; }
29
+ error() { printf "${RED}✖${RESET} %s\n" "$*" >&2; }
30
+ step() { printf "${CYAN}→${RESET} %s\n" "$*"; }
31
+ detail() { printf "${DIM} %s${RESET}\n" "$*"; }
32
+
33
+ # ─── Globals (set by argument parsing) ──────────────────────────────────────
34
+
35
+ CMD=""
36
+ SOURCE_FLAG=""
37
+ TARGET=""
38
+ OPT_YES=false
39
+ OPT_DRY_RUN=false
40
+ OPT_VERBOSE=false
41
+
42
+ # ─── Utility Functions ──────────────────────────────────────────────────────
43
+
44
+ cleanup() {
45
+ if [[ -n "$TEMP_CLONE_DIR" && -d "$TEMP_CLONE_DIR" ]]; then
46
+ rm -rf "$TEMP_CLONE_DIR"
47
+ [[ "$OPT_VERBOSE" == true ]] && detail "Cleaned up temp dir: $TEMP_CLONE_DIR"
48
+ fi
49
+ }
50
+ trap cleanup EXIT
51
+
52
+ clone_from_github() {
53
+ if ! command -v git &>/dev/null; then
54
+ error "git is required to clone from GitHub but was not found."
55
+ error "Install git or provide a local source with --source."
56
+ exit 1
57
+ fi
58
+ TEMP_CLONE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/gaia-framework-XXXXXX")"
59
+ info "Cloning GAIA from GitHub..." >&2
60
+ if git clone --depth 1 "$GITHUB_REPO" "$TEMP_CLONE_DIR" 2>/dev/null; then
61
+ success "Cloned to temporary directory" >&2
62
+ else
63
+ error "Failed to clone from $GITHUB_REPO"
64
+ exit 1
65
+ fi
66
+ echo "$TEMP_CLONE_DIR"
67
+ }
68
+
69
+ resolve_source() {
70
+ local resolved=""
71
+
72
+ # 1. --source flag
73
+ if [[ -n "$SOURCE_FLAG" ]]; then
74
+ resolved="$SOURCE_FLAG"
75
+ [[ "$OPT_VERBOSE" == true ]] && detail "Source from --source flag: $resolved" >&2
76
+ # 2. $GAIA_SOURCE env var
77
+ elif [[ -n "${GAIA_SOURCE:-}" ]]; then
78
+ resolved="$GAIA_SOURCE"
79
+ [[ "$OPT_VERBOSE" == true ]] && detail "Source from \$GAIA_SOURCE: $resolved" >&2
80
+ # 3. Self-detect: script's own directory
81
+ elif [[ -d "$(dirname "$(realpath "$0")")/_gaia" ]]; then
82
+ resolved="$(dirname "$(realpath "$0")")"
83
+ [[ "$OPT_VERBOSE" == true ]] && detail "Source from script location: $resolved" >&2
84
+ # 4. GitHub clone
85
+ else
86
+ resolved="$(clone_from_github)"
87
+ fi
88
+
89
+ echo "$resolved"
90
+ }
91
+
92
+ validate_source() {
93
+ local src="$1"
94
+ if [[ ! -f "$src/$MANIFEST_REL" ]]; then
95
+ error "Invalid GAIA source: $src"
96
+ error "Expected $MANIFEST_REL but it was not found."
97
+ exit 1
98
+ fi
99
+ }
100
+
101
+ copy_if_missing() {
102
+ local src="$1" dst="$2"
103
+ if [[ -e "$dst" ]]; then
104
+ [[ "$OPT_VERBOSE" == true ]] && detail "Skipped (exists): $dst"
105
+ return 0
106
+ fi
107
+ if [[ "$OPT_DRY_RUN" == true ]]; then
108
+ detail "[dry-run] Would copy: $dst"
109
+ return 0
110
+ fi
111
+ mkdir -p "$(dirname "$dst")"
112
+ cp "$src" "$dst"
113
+ [[ "$OPT_VERBOSE" == true ]] && detail "Copied: $dst"
114
+ }
115
+
116
+ copy_with_backup() {
117
+ local src="$1" dst="$2" backup_dir="$3"
118
+ if [[ ! -e "$dst" ]]; then
119
+ if [[ "$OPT_DRY_RUN" == true ]]; then
120
+ detail "[dry-run] Would copy: $dst"
121
+ return 0
122
+ fi
123
+ mkdir -p "$(dirname "$dst")"
124
+ cp "$src" "$dst"
125
+ [[ "$OPT_VERBOSE" == true ]] && detail "Copied (new): $dst"
126
+ return 0
127
+ fi
128
+ # Compare files — skip if identical
129
+ if cmp -s "$src" "$dst"; then
130
+ [[ "$OPT_VERBOSE" == true ]] && detail "Unchanged: $dst"
131
+ return 0
132
+ fi
133
+ if [[ "$OPT_DRY_RUN" == true ]]; then
134
+ detail "[dry-run] Would update (with backup): $dst"
135
+ return 0
136
+ fi
137
+ # Back up the existing file
138
+ local rel_path="${dst#$TARGET/}"
139
+ local backup_path="$backup_dir/$rel_path"
140
+ mkdir -p "$(dirname "$backup_path")"
141
+ cp "$dst" "$backup_path"
142
+ cp "$src" "$dst"
143
+ [[ "$OPT_VERBOSE" == true ]] && detail "Updated (backed up): $dst"
144
+ }
145
+
146
+ append_if_missing() {
147
+ local file="$1" marker="$2" content="$3"
148
+ if [[ -f "$file" ]] && grep -qF "$marker" "$file"; then
149
+ [[ "$OPT_VERBOSE" == true ]] && detail "Already in $(basename "$file"): $marker"
150
+ return 0
151
+ fi
152
+ if [[ "$OPT_DRY_RUN" == true ]]; then
153
+ detail "[dry-run] Would append to $(basename "$file"): $marker"
154
+ return 0
155
+ fi
156
+ # Add a newline before appending if file exists and doesn't end with newline
157
+ if [[ -f "$file" ]] && [[ -s "$file" ]] && [[ "$(tail -c 1 "$file")" != "" ]]; then
158
+ printf '\n' >> "$file"
159
+ fi
160
+ printf '%s\n' "$content" >> "$file"
161
+ }
162
+
163
+ prompt_value() {
164
+ local prompt_text="$1" default="$2"
165
+ if [[ "$OPT_YES" == true ]]; then
166
+ echo "$default"
167
+ return 0
168
+ fi
169
+ local value
170
+ printf "${BOLD}%s${RESET} [%s]: " "$prompt_text" "$default" >&2
171
+ read -r value
172
+ echo "${value:-$default}"
173
+ }
174
+
175
+ extract_yaml_value() {
176
+ local file="$1" key="$2"
177
+ grep "^${key}:" "$file" 2>/dev/null | sed "s/^${key}:[[:space:]]*//" | sed 's/^"//;s/"$//' || echo ""
178
+ }
179
+
180
+ count_files() {
181
+ local dir="$1" pattern="${2:-*}"
182
+ find "$dir" -name "$pattern" -type f 2>/dev/null | wc -l | tr -d ' '
183
+ }
184
+
185
+ # ─── cmd_init ───────────────────────────────────────────────────────────────
186
+
187
+ cmd_init() {
188
+ local source
189
+ source="$(resolve_source)"
190
+ validate_source "$source"
191
+
192
+ local src_version
193
+ src_version="$(extract_yaml_value "$source/_gaia/_config/global.yaml" "framework_version")"
194
+
195
+ printf "\n${BOLD}GAIA Framework Installer v%s${RESET}\n" "$VERSION"
196
+ printf " Source: %s\n" "$source"
197
+ printf " Target: %s\n" "$TARGET"
198
+ printf " Version: %s\n\n" "${src_version:-unknown}"
199
+
200
+ if [[ "$OPT_DRY_RUN" == true ]]; then
201
+ warn "Dry-run mode — no files will be written"
202
+ echo ""
203
+ fi
204
+
205
+ if [[ -d "$TARGET/_gaia" ]]; then
206
+ warn "Target already contains _gaia/ — use 'update' to refresh framework files."
207
+ if [[ "$OPT_YES" != true ]]; then
208
+ printf "Continue with init anyway? [y/N]: "
209
+ local confirm; read -r confirm
210
+ [[ "$confirm" =~ ^[Yy] ]] || { info "Aborted."; exit 0; }
211
+ fi
212
+ fi
213
+
214
+ # Step 1: Create target docs directories
215
+ step "Creating documentation directories..."
216
+ for dir in planning-artifacts implementation-artifacts test-artifacts creative-artifacts; do
217
+ if [[ "$OPT_DRY_RUN" == true ]]; then
218
+ detail "[dry-run] Would create: docs/$dir/"
219
+ else
220
+ mkdir -p "$TARGET/docs/$dir"
221
+ fi
222
+ done
223
+
224
+ # Step 2: Copy _gaia/ recursively (excluding checkpoints and .resolved/*.yaml)
225
+ step "Copying framework files..."
226
+ if [[ "$OPT_DRY_RUN" == true ]]; then
227
+ detail "[dry-run] Would copy _gaia/ (excluding checkpoints and .resolved/*.yaml)"
228
+ else
229
+ mkdir -p "$TARGET/_gaia"
230
+ rsync -a \
231
+ --exclude='_memory/checkpoints/*.yaml' \
232
+ --exclude='_memory/checkpoints/completed/*.yaml' \
233
+ --exclude='.resolved/*.yaml' \
234
+ --exclude='_memory/*-sidecar/*.md' \
235
+ --exclude='_memory/*-sidecar/*.yaml' \
236
+ "$source/_gaia/" "$TARGET/_gaia/"
237
+ fi
238
+
239
+ # Step 3: Create memory sidecar directories with .gitkeep
240
+ step "Creating memory sidecar directories..."
241
+ local sidecar_dirs=(
242
+ "checkpoints"
243
+ "checkpoints/completed"
244
+ "architect-sidecar"
245
+ "devops-sidecar"
246
+ "orchestrator-sidecar"
247
+ "pm-sidecar"
248
+ "security-sidecar"
249
+ "sm-sidecar"
250
+ "storyteller-sidecar"
251
+ "tech-writer-sidecar"
252
+ "test-architect-sidecar"
253
+ )
254
+ for dir in "${sidecar_dirs[@]}"; do
255
+ if [[ "$OPT_DRY_RUN" == true ]]; then
256
+ detail "[dry-run] Would create: _gaia/_memory/$dir/"
257
+ else
258
+ mkdir -p "$TARGET/_gaia/_memory/$dir"
259
+ touch "$TARGET/_gaia/_memory/$dir/.gitkeep"
260
+ fi
261
+ done
262
+
263
+ # Step 4: Create .resolved directories with .gitkeep
264
+ step "Creating .resolved directories..."
265
+ local resolved_dirs=("core" "lifecycle" "creative" "testing")
266
+ for mod in "${resolved_dirs[@]}"; do
267
+ if [[ "$OPT_DRY_RUN" == true ]]; then
268
+ detail "[dry-run] Would create: _gaia/$mod/.resolved/"
269
+ else
270
+ mkdir -p "$TARGET/_gaia/$mod/.resolved"
271
+ touch "$TARGET/_gaia/$mod/.resolved/.gitkeep"
272
+ fi
273
+ done
274
+
275
+ # Step 5: Customize global.yaml
276
+ step "Configuring global.yaml..."
277
+ local project_name user_name
278
+ local dir_name
279
+ dir_name="$(basename "$TARGET")"
280
+ project_name="$(prompt_value "Project name" "$dir_name")"
281
+ user_name="$(prompt_value "User name" "$(whoami)")"
282
+
283
+ if [[ "$OPT_DRY_RUN" == true ]]; then
284
+ detail "[dry-run] Would set project_name=$project_name, user_name=$user_name"
285
+ else
286
+ local global_file="$TARGET/_gaia/_config/global.yaml"
287
+ if [[ -f "$global_file" ]]; then
288
+ # Use portable sed for both macOS and Linux
289
+ if [[ "$(uname)" == "Darwin" ]]; then
290
+ sed -i '' "s/^project_name:.*/project_name: \"$project_name\"/" "$global_file"
291
+ sed -i '' "s/^user_name:.*/user_name: \"$user_name\"/" "$global_file"
292
+ else
293
+ sed -i "s/^project_name:.*/project_name: \"$project_name\"/" "$global_file"
294
+ sed -i "s/^user_name:.*/user_name: \"$user_name\"/" "$global_file"
295
+ fi
296
+ fi
297
+ fi
298
+
299
+ # Step 6: Copy CLAUDE.md (skip if exists)
300
+ step "Setting up CLAUDE.md..."
301
+ if [[ -f "$source/CLAUDE.md" ]]; then
302
+ copy_if_missing "$source/CLAUDE.md" "$TARGET/CLAUDE.md"
303
+ fi
304
+
305
+ # Step 7: Copy slash commands to .claude/commands/
306
+ step "Installing slash commands..."
307
+ if [[ -d "$source/.claude/commands" ]]; then
308
+ if [[ "$OPT_DRY_RUN" == true ]]; then
309
+ local cmd_count
310
+ cmd_count="$(find "$source/.claude/commands" -name 'gaia*.md' -type f | wc -l | tr -d ' ')"
311
+ detail "[dry-run] Would copy $cmd_count slash commands to .claude/commands/"
312
+ else
313
+ mkdir -p "$TARGET/.claude/commands"
314
+ for cmd_file in "$source/.claude/commands"/gaia*.md; do
315
+ [[ -f "$cmd_file" ]] || continue
316
+ local basename_cmd
317
+ basename_cmd="$(basename "$cmd_file")"
318
+ copy_if_missing "$cmd_file" "$TARGET/.claude/commands/$basename_cmd"
319
+ done
320
+ fi
321
+ fi
322
+
323
+ # Step 8: Append GAIA entries to .gitignore
324
+ step "Updating .gitignore..."
325
+ local gitignore_block
326
+ gitignore_block="$(cat <<'GITIGNORE'
327
+
328
+ # GAIA Framework — runtime artifacts
329
+ _gaia/_memory/checkpoints/*.yaml
330
+ _gaia/_memory/*-sidecar/*.md
331
+ _gaia/_memory/*-sidecar/*.yaml
332
+ _gaia/**/.resolved/*.yaml
333
+ !_gaia/**/.resolved/.gitkeep
334
+ GITIGNORE
335
+ )"
336
+ append_if_missing "$TARGET/.gitignore" "# GAIA Framework — runtime artifacts" "$gitignore_block"
337
+
338
+ # Summary
339
+ echo ""
340
+ success "GAIA Framework installed successfully!"
341
+ echo ""
342
+ printf " ${BOLD}Project:${RESET} %s\n" "$project_name"
343
+ printf " ${BOLD}User:${RESET} %s\n" "$user_name"
344
+ printf " ${BOLD}Version:${RESET} %s\n" "${src_version:-1.0.0}"
345
+ printf " ${BOLD}Target:${RESET} %s\n" "$TARGET"
346
+ echo ""
347
+ info "Next steps:"
348
+ detail "1. cd $TARGET"
349
+ detail "2. Run /gaia-build-configs to generate resolved configs"
350
+ detail "3. Run /gaia to start the orchestrator"
351
+ echo ""
352
+ }
353
+
354
+ # ─── cmd_update ─────────────────────────────────────────────────────────────
355
+
356
+ cmd_update() {
357
+ local source
358
+ source="$(resolve_source)"
359
+ validate_source "$source"
360
+
361
+ if [[ ! -d "$TARGET/_gaia" ]]; then
362
+ error "No GAIA installation found at $TARGET"
363
+ error "Run 'init' first to install GAIA."
364
+ exit 1
365
+ fi
366
+
367
+ local src_version
368
+ src_version="$(extract_yaml_value "$source/_gaia/_config/global.yaml" "framework_version")"
369
+ local cur_version
370
+ cur_version="$(extract_yaml_value "$TARGET/_gaia/_config/global.yaml" "framework_version")"
371
+
372
+ printf "\n${BOLD}GAIA Framework Updater v%s${RESET}\n" "$VERSION"
373
+ printf " Source: %s (v%s)\n" "$source" "${src_version:-unknown}"
374
+ printf " Target: %s (v%s)\n\n" "$TARGET" "${cur_version:-unknown}"
375
+
376
+ if [[ "$OPT_DRY_RUN" == true ]]; then
377
+ warn "Dry-run mode — no files will be written"
378
+ echo ""
379
+ fi
380
+
381
+ if [[ "$OPT_YES" != true ]]; then
382
+ printf "Proceed with update? [y/N]: "
383
+ local confirm; read -r confirm
384
+ [[ "$confirm" =~ ^[Yy] ]] || { info "Aborted."; exit 0; }
385
+ fi
386
+
387
+ local timestamp
388
+ timestamp="$(date +%Y%m%d-%H%M%S)"
389
+ local backup_dir="$TARGET/_gaia/_backups/$timestamp"
390
+
391
+ # Update framework structure — these directories get fully refreshed
392
+ # NEVER touch: global.yaml, _memory/, .resolved/, CLAUDE.md
393
+ local update_targets=(
394
+ "core/engine"
395
+ "core/agents"
396
+ "core/tasks"
397
+ "core/workflows"
398
+ "lifecycle/agents"
399
+ "lifecycle/workflows"
400
+ "lifecycle/templates"
401
+ "lifecycle/checklists"
402
+ "lifecycle/teams"
403
+ "dev/agents"
404
+ "dev/skills"
405
+ "dev/knowledge"
406
+ "creative/agents"
407
+ "creative/workflows"
408
+ "creative/teams"
409
+ "creative/data"
410
+ "testing/agents"
411
+ "testing/workflows"
412
+ "testing/knowledge"
413
+ "_config/manifest.yaml"
414
+ )
415
+
416
+ step "Updating framework files..."
417
+ local updated=0 skipped=0
418
+
419
+ for entry in "${update_targets[@]}"; do
420
+ local src_path="$source/_gaia/$entry"
421
+ local dst_path="$TARGET/_gaia/$entry"
422
+
423
+ if [[ ! -e "$src_path" ]]; then
424
+ [[ "$OPT_VERBOSE" == true ]] && detail "Source missing, skipped: $entry"
425
+ continue
426
+ fi
427
+
428
+ if [[ -f "$src_path" ]]; then
429
+ # Single file
430
+ copy_with_backup "$src_path" "$dst_path" "$backup_dir"
431
+ updated=$((updated + 1))
432
+ elif [[ -d "$src_path" ]]; then
433
+ # Directory — update each file
434
+ while IFS= read -r -d '' file; do
435
+ local rel="${file#$src_path/}"
436
+ copy_with_backup "$file" "$dst_path/$rel" "$backup_dir"
437
+ updated=$((updated + 1))
438
+ done < <(find "$src_path" -type f -print0)
439
+ fi
440
+ done
441
+
442
+ # Update slash commands (add new ones, update existing with backup)
443
+ step "Updating slash commands..."
444
+ if [[ -d "$source/.claude/commands" ]]; then
445
+ mkdir -p "$TARGET/.claude/commands"
446
+ for cmd_file in "$source/.claude/commands"/gaia*.md; do
447
+ [[ -f "$cmd_file" ]] || continue
448
+ local basename_cmd
449
+ basename_cmd="$(basename "$cmd_file")"
450
+ copy_with_backup "$cmd_file" "$TARGET/.claude/commands/$basename_cmd" "$backup_dir"
451
+ done
452
+ fi
453
+
454
+ # Update version in global.yaml (only the framework_version field)
455
+ if [[ -n "$src_version" && "$src_version" != "$cur_version" ]]; then
456
+ step "Updating framework version: $cur_version → $src_version"
457
+ if [[ "$OPT_DRY_RUN" != true ]]; then
458
+ local global_file="$TARGET/_gaia/_config/global.yaml"
459
+ if [[ "$(uname)" == "Darwin" ]]; then
460
+ sed -i '' "s/^framework_version:.*/framework_version: \"$src_version\"/" "$global_file"
461
+ else
462
+ sed -i "s/^framework_version:.*/framework_version: \"$src_version\"/" "$global_file"
463
+ fi
464
+ fi
465
+ fi
466
+
467
+ # Summary
468
+ echo ""
469
+ if [[ -d "$backup_dir" ]]; then
470
+ local backup_count
471
+ backup_count="$(find "$backup_dir" -type f | wc -l | tr -d ' ')"
472
+ success "Update complete! $backup_count file(s) backed up to:"
473
+ detail "$backup_dir"
474
+ else
475
+ success "Update complete! All files were already up to date."
476
+ fi
477
+ echo ""
478
+ info "Run /gaia-build-configs to regenerate resolved configs."
479
+ echo ""
480
+ }
481
+
482
+ # ─── cmd_validate ───────────────────────────────────────────────────────────
483
+
484
+ cmd_validate() {
485
+ if [[ ! -d "$TARGET/_gaia" ]]; then
486
+ error "No GAIA installation found at $TARGET"
487
+ exit 1
488
+ fi
489
+
490
+ printf "\n${BOLD}GAIA Framework Validation${RESET}\n"
491
+ printf " Target: %s\n\n" "$TARGET"
492
+
493
+ local pass=0 fail=0
494
+
495
+ check() {
496
+ local label="$1" condition="$2"
497
+ if eval "$condition"; then
498
+ printf " ${GREEN}✔${RESET} %s\n" "$label"
499
+ pass=$((pass + 1))
500
+ else
501
+ printf " ${RED}✖${RESET} %s\n" "$label"
502
+ fail=$((fail + 1))
503
+ fi
504
+ }
505
+
506
+ # Manifest
507
+ check "manifest.yaml exists" "[[ -f '$TARGET/$MANIFEST_REL' ]]"
508
+
509
+ # Global config fields
510
+ local global="$TARGET/_gaia/_config/global.yaml"
511
+ check "global.yaml exists" "[[ -f '$global' ]]"
512
+ if [[ -f "$global" ]]; then
513
+ check "global.yaml has project_name" "grep -q '^project_name:' '$global'"
514
+ check "global.yaml has user_name" "grep -q '^user_name:' '$global'"
515
+ check "global.yaml has framework_version" "grep -q '^framework_version:' '$global'"
516
+ fi
517
+
518
+ # Module directories
519
+ for mod in core lifecycle dev creative testing; do
520
+ check "Module: $mod" "[[ -d '$TARGET/_gaia/$mod' ]]"
521
+ done
522
+
523
+ # .resolved directories
524
+ for mod in core lifecycle creative testing; do
525
+ check ".resolved: $mod" "[[ -d '$TARGET/_gaia/$mod/.resolved' ]]"
526
+ done
527
+
528
+ # Sidecar directories
529
+ local sidecar_dirs=(
530
+ "checkpoints" "checkpoints/completed"
531
+ "architect-sidecar" "devops-sidecar" "orchestrator-sidecar"
532
+ "pm-sidecar" "security-sidecar" "sm-sidecar"
533
+ "storyteller-sidecar" "tech-writer-sidecar" "test-architect-sidecar"
534
+ )
535
+ for dir in "${sidecar_dirs[@]}"; do
536
+ check "Sidecar: $dir" "[[ -d '$TARGET/_gaia/_memory/$dir' ]]"
537
+ done
538
+
539
+ # CLAUDE.md
540
+ check "CLAUDE.md exists" "[[ -f '$TARGET/CLAUDE.md' ]]"
541
+
542
+ # Slash commands
543
+ check "Slash commands directory" "[[ -d '$TARGET/.claude/commands' ]]"
544
+ if [[ -d "$TARGET/.claude/commands" ]]; then
545
+ local cmd_count
546
+ cmd_count="$(find "$TARGET/.claude/commands" -name 'gaia*.md' -type f 2>/dev/null | wc -l | tr -d ' ')"
547
+ check "Slash commands present (found: $cmd_count)" "[[ $cmd_count -gt 0 ]]"
548
+ fi
549
+
550
+ # Docs directories
551
+ for dir in planning-artifacts implementation-artifacts test-artifacts creative-artifacts; do
552
+ check "Docs: $dir" "[[ -d '$TARGET/docs/$dir' ]]"
553
+ done
554
+
555
+ # Version
556
+ local version
557
+ version="$(extract_yaml_value "$TARGET/_gaia/_config/global.yaml" "framework_version")"
558
+
559
+ echo ""
560
+ if [[ $fail -eq 0 ]]; then
561
+ success "All $pass checks passed (v${version:-unknown})"
562
+ echo ""
563
+ return 0
564
+ else
565
+ error "$fail of $((pass + fail)) checks failed"
566
+ echo ""
567
+ exit 1
568
+ fi
569
+ }
570
+
571
+ # ─── cmd_status ─────────────────────────────────────────────────────────────
572
+
573
+ cmd_status() {
574
+ if [[ ! -d "$TARGET/_gaia" ]]; then
575
+ error "No GAIA installation found at $TARGET"
576
+ exit 1
577
+ fi
578
+
579
+ local global="$TARGET/_gaia/_config/global.yaml"
580
+ local version project_name user_name
581
+ version="$(extract_yaml_value "$global" "framework_version")"
582
+ project_name="$(extract_yaml_value "$global" "project_name")"
583
+ user_name="$(extract_yaml_value "$global" "user_name")"
584
+
585
+ printf "\n${BOLD}GAIA Framework Status${RESET}\n\n"
586
+ printf " ${BOLD}Version:${RESET} %s\n" "${version:-unknown}"
587
+ printf " ${BOLD}Project:${RESET} %s\n" "${project_name:-unknown}"
588
+ printf " ${BOLD}User:${RESET} %s\n" "${user_name:-unknown}"
589
+
590
+ # Modules
591
+ local modules=()
592
+ for mod in core lifecycle dev creative testing; do
593
+ [[ -d "$TARGET/_gaia/$mod" ]] && modules+=("$mod")
594
+ done
595
+ printf " ${BOLD}Modules:${RESET} %s (%s)\n" "${#modules[@]}" "${modules[*]}"
596
+
597
+ # Slash commands
598
+ local cmd_count=0
599
+ if [[ -d "$TARGET/.claude/commands" ]]; then
600
+ cmd_count="$(find "$TARGET/.claude/commands" -name 'gaia*.md' -type f 2>/dev/null | wc -l | tr -d ' ')"
601
+ fi
602
+ printf " ${BOLD}Commands:${RESET} %s slash commands\n" "$cmd_count"
603
+
604
+ # Sidecar status
605
+ local sidecar_count=0
606
+ local sidecar_populated=0
607
+ for dir in "$TARGET/_gaia/_memory"/*-sidecar; do
608
+ [[ -d "$dir" ]] || continue
609
+ ((sidecar_count++))
610
+ local file_count
611
+ file_count="$(find "$dir" -type f ! -name '.gitkeep' 2>/dev/null | wc -l | tr -d ' ')"
612
+ [[ $file_count -gt 0 ]] && ((sidecar_populated++))
613
+ done
614
+ printf " ${BOLD}Sidecars:${RESET} %s directories (%s with data)\n" "$sidecar_count" "$sidecar_populated"
615
+
616
+ # .resolved population
617
+ local resolved_count=0
618
+ local resolved_populated=0
619
+ for mod in core lifecycle creative testing; do
620
+ local rdir="$TARGET/_gaia/$mod/.resolved"
621
+ [[ -d "$rdir" ]] || continue
622
+ ((resolved_count++))
623
+ local yaml_count
624
+ yaml_count="$(find "$rdir" -name '*.yaml' -type f 2>/dev/null | wc -l | tr -d ' ')"
625
+ [[ $yaml_count -gt 0 ]] && ((resolved_populated++))
626
+ done
627
+ printf " ${BOLD}.resolved:${RESET} %s directories (%s populated)\n" "$resolved_count" "$resolved_populated"
628
+
629
+ # Checkpoints
630
+ local checkpoint_count=0
631
+ if [[ -d "$TARGET/_gaia/_memory/checkpoints" ]]; then
632
+ checkpoint_count="$(find "$TARGET/_gaia/_memory/checkpoints" -name '*.yaml' -type f 2>/dev/null | wc -l | tr -d ' ')"
633
+ fi
634
+ printf " ${BOLD}Checkpoints:${RESET} %s\n" "$checkpoint_count"
635
+
636
+ echo ""
637
+ }
638
+
639
+ # ─── Usage & Argument Parsing ───────────────────────────────────────────────
640
+
641
+ usage() {
642
+ cat <<EOF
643
+ ${BOLD}GAIA Framework Installer v${VERSION}${RESET}
644
+
645
+ Usage: gaia-install.sh <command> [options] [target]
646
+
647
+ ${BOLD}Commands:${RESET}
648
+ init Install GAIA into a project
649
+ update Update framework files (preserves config and memory)
650
+ validate Check installation integrity
651
+ status Show installation info
652
+
653
+ ${BOLD}Options:${RESET}
654
+ --source <path> Local GAIA source (or clones from GitHub if omitted)
655
+ --yes Skip confirmation prompts
656
+ --dry-run Show what would be done without making changes
657
+ --verbose Show detailed progress
658
+ --help Show this help message
659
+
660
+ ${BOLD}Examples:${RESET}
661
+ gaia-install.sh init ~/my-new-project
662
+ gaia-install.sh init --source ~/local-gaia ~/my-project
663
+ gaia-install.sh update ~/my-existing-project
664
+ gaia-install.sh validate .
665
+ gaia-install.sh status .
666
+
667
+ ${BOLD}Source resolution order:${RESET}
668
+ 1. --source flag
669
+ 2. \$GAIA_SOURCE environment variable
670
+ 3. Script's own directory (if it contains _gaia/)
671
+ 4. GitHub clone ($GITHUB_REPO)
672
+ EOF
673
+ }
674
+
675
+ parse_args() {
676
+ if [[ $# -eq 0 ]]; then
677
+ usage
678
+ exit 0
679
+ fi
680
+
681
+ # First argument is the command
682
+ case "$1" in
683
+ init|update|validate|status)
684
+ CMD="$1"
685
+ shift
686
+ ;;
687
+ --help|-h|help)
688
+ usage
689
+ exit 0
690
+ ;;
691
+ --version|-v)
692
+ echo "gaia-install.sh v${VERSION}"
693
+ exit 0
694
+ ;;
695
+ *)
696
+ error "Unknown command: $1"
697
+ echo ""
698
+ usage
699
+ exit 1
700
+ ;;
701
+ esac
702
+
703
+ # Parse remaining options
704
+ while [[ $# -gt 0 ]]; do
705
+ case "$1" in
706
+ --source)
707
+ if [[ -z "${2:-}" ]]; then
708
+ error "--source requires a path argument"
709
+ exit 1
710
+ fi
711
+ SOURCE_FLAG="$2"
712
+ shift 2
713
+ ;;
714
+ --yes|-y)
715
+ OPT_YES=true
716
+ shift
717
+ ;;
718
+ --dry-run)
719
+ OPT_DRY_RUN=true
720
+ shift
721
+ ;;
722
+ --verbose)
723
+ OPT_VERBOSE=true
724
+ shift
725
+ ;;
726
+ --help|-h)
727
+ usage
728
+ exit 0
729
+ ;;
730
+ -*)
731
+ error "Unknown option: $1"
732
+ exit 1
733
+ ;;
734
+ *)
735
+ if [[ -n "$TARGET" ]]; then
736
+ error "Unexpected argument: $1"
737
+ exit 1
738
+ fi
739
+ TARGET="$1"
740
+ shift
741
+ ;;
742
+ esac
743
+ done
744
+
745
+ # Default target to current directory
746
+ if [[ -z "$TARGET" ]]; then
747
+ TARGET="."
748
+ fi
749
+
750
+ # Resolve target to absolute path
751
+ if [[ "$TARGET" == "." ]]; then
752
+ TARGET="$(pwd)"
753
+ else
754
+ # Create target dir for init if it doesn't exist
755
+ if [[ "$CMD" == "init" && ! -d "$TARGET" ]]; then
756
+ if [[ "$OPT_DRY_RUN" == true ]]; then
757
+ detail "[dry-run] Would create target directory: $TARGET"
758
+ # Resolve to absolute path without cd
759
+ case "$TARGET" in
760
+ /*) ;; # already absolute
761
+ *) TARGET="$(pwd)/$TARGET" ;;
762
+ esac
763
+ else
764
+ mkdir -p "$TARGET"
765
+ TARGET="$(cd "$TARGET" && pwd)"
766
+ fi
767
+ else
768
+ TARGET="$(cd "$TARGET" 2>/dev/null && pwd || echo "$TARGET")"
769
+ fi
770
+ fi
771
+ }
772
+
773
+ # ─── Main ───────────────────────────────────────────────────────────────────
774
+
775
+ main() {
776
+ parse_args "$@"
777
+
778
+ case "$CMD" in
779
+ init) cmd_init ;;
780
+ update) cmd_update ;;
781
+ validate) cmd_validate ;;
782
+ status) cmd_status ;;
783
+ esac
784
+ }
785
+
786
+ main "$@"