its-magic 0.1.2-39 → 0.1.2-42

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/installer.sh CHANGED
@@ -1,643 +1,649 @@
1
- #!/usr/bin/env sh
2
- set -e
3
-
4
- # BUG-0004: keep startup shell options POSIX-safe for /bin/sh execution.
5
- # Do not use bash-only "set" flags in this unconditional startup path.
6
-
7
- SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
8
- SOURCE_ROOT="$SCRIPT_DIR/template"
9
- MANIFEST_NAME="docs/engineering/context/installer-owned-paths.manifest"
10
- REPO_URL="https://github.com/fl0wm0ti0n/its-magic"
11
- APP_VERSION=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$SCRIPT_DIR/package.json" 2>/dev/null | head -n 1)
12
- [ -z "$APP_VERSION" ] && APP_VERSION="unknown"
13
-
14
- show_banner() {
15
- printf "\n"
16
- printf "\033[1;35m ██╗████████╗███████╗ ███╗ ███╗ █████╗ ██████╗ ██╗ ██████╗\033[0m\n"
17
- printf "\033[1;35m ██║╚══██╔══╝██╔════╝ ████╗ ████║██╔══██╗██╔════╝ ██║██╔════╝\033[0m\n"
18
- printf "\033[1;35m ██║ ██║ ███████╗█████╗██╔████╔██║███████║██║ ███╗██║██║ \033[0m\n"
19
- printf "\033[1;36m ██║ ██║ ╚════██║╚════╝██║╚██╔╝██║██╔══██║██║ ██║██║██║ \033[0m\n"
20
- printf "\033[1;36m ██║ ██║ ███████║ ██║ ╚═╝ ██║██║ ██║╚██████╔╝██║╚██████╗\033[0m\n"
21
- printf "\033[1;36m ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝\033[0m\n"
22
- printf "\n"
23
- printf "\033[1;33m AI dev team\033[0m\n"
24
- printf "\n"
25
- }
26
-
27
- show_help() {
28
- show_banner
29
- printf "its-magic v%s\n" "$APP_VERSION"
30
- printf "Repository: %s\n\n" "$REPO_URL"
31
- printf "Install AI dev team workflow files into any Cursor repository.\n\n"
32
- printf "Usage:\n"
33
- printf " its-magic --target <path> [--mode <mode>] [--backup] [--create]\n"
34
- printf " its-magic --clean-repo [--target <path>] [--yes]\n"
35
- printf " its-magic --help | --version\n\n"
36
- printf "Install options:\n"
37
- printf " --target <path> Path to the repository where workflow files are installed.\n"
38
- printf " If omitted you will be prompted interactively.\n"
39
- printf " --mode <mode> How to handle files that already exist in the target:\n"
40
- printf " missing Only copy files that do not exist yet (default).\n"
41
- printf " Safe for repos that already have some workflow files.\n"
42
- printf " overwrite Replace every file, even if it already exists.\n"
43
- printf " Combine with --backup to keep a snapshot first.\n"
44
- printf " interactive Ask per file whether to overwrite or skip.\n"
45
- printf " upgrade Update framework files while preserving user data.\n"
46
- printf " Use after updating its-magic to a newer version.\n"
47
- printf " --backup Before overwriting, save existing files to backups/<timestamp>/.\n"
48
- printf " Ignored when mode is 'missing' (nothing gets replaced).\n"
49
- printf " --create Create the target directory if it does not exist.\n\n"
50
- printf " Note: installer bootstraps runbook TEST/LINT/TYPECHECK commands from\n"
51
- printf " OS+stack detection; unresolved TEST_COMMAND fails fast with\n"
52
- printf " [RUNBOOK_BOOTSTRAP_ERROR] diagnostics.\n"
53
- printf " Note: scratchpad Model B: .cursor/scratchpad.md is\n"
54
- printf " materialized when missing; Python 3 on PATH is required for validation.\n"
55
- printf " Recovery: python installer.py --scratchpad-postinstall --target <repo> --mode missing\n\n"
56
- printf "Clean options:\n"
57
- printf " --clean-repo Remove all its-magic workflow artifacts from the target repo\n"
58
- printf " (owned paths from installer manifest, including .cursor,\n"
59
- printf " docs/product, docs/engineering, docs/user-guides, sprints,\n"
60
- printf " handoffs, decisions, workflow scripts, CI files, and\n"
61
- printf " installer metadata under its_magic/ (legacy .its-magic-version\n"
62
- printf " is also removed when present). Your own source code is never touched.\n"
63
- printf " --target <path> Repo to clean (default: current directory).\n"
64
- printf " --yes Skip the confirmation prompt.\n\n"
65
- printf "Info:\n"
66
- printf " --help, -h Show this help and exit.\n"
67
- printf " --version, -v Print the installed version and exit.\n\n"
68
- printf "Examples:\n"
69
- printf " its-magic --target . --mode missing Safe first-time setup\n"
70
- printf " its-magic --target . --mode upgrade Update framework, keep user data\n"
71
- printf " its-magic --target . --mode overwrite --backup Replace all files, keep backup\n"
72
- printf " its-magic --clean-repo --target . --yes Remove workflow artifacts silently\n\n"
73
- }
74
-
75
- ensure_parent() {
76
- dir=$(dirname "$1")
77
- [ -d "$dir" ] || mkdir -p "$dir"
78
- }
79
-
80
- list_source_files() {
81
- source_root="$1"
82
- shift
83
- for rel in "$@"; do
84
- src="$source_root/$rel"
85
- if [ -f "$src" ]; then
86
- echo "$rel"
87
- elif [ -d "$src" ]; then
88
- find "$src" -type f | sed "s|^$source_root/||"
89
- fi
90
- done | sort -u
91
- }
92
-
93
- get_manifest_paths() {
94
- section="$1"
95
- awk -v s="$section" '
96
- BEGIN { in_section=0 }
97
- /^[[:space:]]*#/ { next }
98
- /^[[:space:]]*$/ { next }
99
- /^\[/ {
100
- in_section = ($0 == "[" s "]")
101
- next
102
- }
103
- { if (in_section) print $0 }
104
- ' "$OWNERSHIP_MANIFEST"
105
- }
106
-
107
- backup_files() {
108
- target_root="$1"
109
- shift
110
- timestamp=$(date -u +"%Y%m%d-%H%M%SZ")
111
- backup_root="$target_root/backups/$timestamp"
112
- for rel in "$@"; do
113
- src="$target_root/$rel"
114
- if [ -f "$src" ]; then
115
- dst="$backup_root/$rel"
116
- ensure_parent "$dst"
117
- cp -p "$src" "$dst"
118
- fi
119
- done
120
- echo "$backup_root"
121
- }
122
-
123
- choose_mode() {
124
- printf "%s\n" "Select install mode:"
125
- printf "%s\n" "1) missing-only (copy only files that do not exist)"
126
- printf "%s\n" "2) overwrite-all (replace existing files)"
127
- printf "%s\n" "3) interactive (prompt per file)"
128
- printf "%s\n" "4) upgrade (update framework files, preserve user data)"
129
- printf "%s" "Enter 1, 2, 3, or 4: "
130
- read -r choice
131
- case "$choice" in
132
- 1) echo "missing" ;;
133
- 2) echo "overwrite" ;;
134
- 4) echo "upgrade" ;;
135
- *) echo "interactive" ;;
136
- esac
137
- }
138
-
139
- scratchpad_postinstall() {
140
- target_root="$1"
141
- mode="$2"
142
- installer_py="$SCRIPT_DIR/installer.py"
143
- if [ ! -f "$installer_py" ]; then
144
- printf "%s\n" "[SCRATCHPAD_POSTINSTALL_ERROR] installer.py missing next to installer.sh."
145
- exit 1
146
- fi
147
- if command -v python3 >/dev/null 2>&1; then
148
- python3 "$installer_py" --scratchpad-postinstall --target "$target_root" --mode "$mode" || exit $?
149
- elif command -v python >/dev/null 2>&1; then
150
- python "$installer_py" --scratchpad-postinstall --target "$target_root" --mode "$mode" || exit $?
151
- else
152
- printf "%s\n" "[SCRATCHPAD_POSTINSTALL_ERROR] PYTHON_NOT_FOUND: Python 3 is required for scratchpad materialization/validation (Model B)."
153
- exit 1
154
- fi
155
- }
156
-
157
- validate_install_completeness() {
158
- target_root="$1"
159
- installer_py="$SCRIPT_DIR/installer.py"
160
- if [ ! -f "$installer_py" ]; then
161
- printf "%s\n" "[INSTALL_COMPLETENESS_FAILED] installer.py missing next to installer.sh."
162
- exit 1
163
- fi
164
- if command -v python3 >/dev/null 2>&1; then
165
- python3 "$installer_py" --validate-install-completeness --target "$target_root" || exit $?
166
- elif command -v python >/dev/null 2>&1; then
167
- python "$installer_py" --validate-install-completeness --target "$target_root" || exit $?
168
- else
169
- printf "%s\n" "[INSTALL_COMPLETENESS_FAILED] PYTHON_NOT_FOUND: Python is required for deterministic installer completeness validation."
170
- exit 1
171
- fi
172
- }
173
-
174
- classify_file() {
175
- rel="$1"
176
- case "$rel" in
177
- README.md) echo "mixed" ;;
178
- .cursor/commands/*|.cursor/rules/*|.cursor/agents/*|.cursor/skills/*) echo "framework" ;;
179
- .cursor/hooks/*|.cursor/hooks.json|.cursor/scratchpad.local.example.md) echo "framework" ;;
180
- .github/workflows/*|scripts/validate-and-push*|scripts/sync_push_gates.py|docs/engineering/context/*|its_magic/*) echo "framework" ;;
181
- .its-magic-version|its_magic/.its-magic-version|its_magic/README.md) echo "framework" ;;
182
- docs/product/*|docs/engineering/*|docs/user-guides/*) echo "user-data" ;;
183
- sprints/*|handoffs/*|decisions/*) echo "user-data" ;;
184
- *) echo "framework" ;;
185
- esac
186
- }
187
-
188
- read_installed_version() {
189
- primary="$1/its_magic/.its-magic-version"
190
- legacy="$1/.its-magic-version"
191
- if [ -f "$primary" ]; then
192
- cat "$primary" | tr -d '\n'
193
- return 0
194
- fi
195
- if [ -f "$legacy" ]; then
196
- cat "$legacy" | tr -d '\n'
197
- return 0
198
- fi
199
- printf "unknown"
200
- }
201
-
202
- write_installed_version() {
203
- vf="$1/its_magic/.its-magic-version"
204
- ensure_parent "$vf"
205
- printf "%s" "$2" > "$vf"
206
- legacy="$1/.its-magic-version"
207
- [ -f "$legacy" ] && rm -f "$legacy"
208
- }
209
-
210
- sync_root_readme_to_its_magic() {
211
- target_root="$1"
212
- [ -f "$target_root/README.md" ] || return 1
213
- dst="$target_root/its_magic/README.md"
214
- ensure_parent "$dst"
215
- cp -p "$target_root/README.md" "$dst"
216
- return 0
217
- }
218
-
219
- read_runbook_key() {
220
- runbook_path="$1"
221
- key="$2"
222
- [ -f "$runbook_path" ] || { printf ""; return; }
223
- awk -F: -v k="$key" '$1==k { sub(/^[[:space:]]*/, "", $2); print $2; exit }' "$runbook_path"
224
- }
225
-
226
- write_runbook_key() {
227
- runbook_path="$1"
228
- key="$2"
229
- value="$3"
230
- [ -f "$runbook_path" ] || return 1
231
- tmp="$runbook_path.tmp.$$"
232
- awk -v k="$key" -v v="$value" '
233
- BEGIN { changed=0 }
234
- index($0, k":") == 1 && changed==0 { print k": "v; changed=1; next }
235
- { print $0 }
236
- END { if (changed==0) exit 2 }
237
- ' "$runbook_path" > "$tmp" || { rm -f "$tmp"; return 1; }
238
- mv "$tmp" "$runbook_path"
239
- return 0
240
- }
241
-
242
- package_has_script() {
243
- target_root="$1"
244
- script_name="$2"
245
- pkg="$target_root/package.json"
246
- [ -f "$pkg" ] || return 1
247
- command -v node >/dev/null 2>&1 || return 1
248
- node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));const s=(p.scripts||{})[process.argv[2]];process.exit((typeof s==='string'&&s.trim())?0:1);" "$pkg" "$script_name" >/dev/null 2>&1
249
- }
250
-
251
- detect_runbook_defaults() {
252
- target_root="$1"
253
- TEST_CANDIDATE=""
254
- LINT_CANDIDATE=""
255
- TYPECHECK_CANDIDATE=""
256
- if [ -f "$target_root/package.json" ] && package_has_script "$target_root" "test"; then
257
- TEST_CANDIDATE="npm run test"
258
- package_has_script "$target_root" "lint" && LINT_CANDIDATE="npm run lint"
259
- package_has_script "$target_root" "typecheck" && TYPECHECK_CANDIDATE="npm run typecheck"
260
- return 0
261
- fi
262
- if [ -f "$target_root/go.mod" ]; then
263
- TEST_CANDIDATE="go test ./..."
264
- return 0
265
- fi
266
- if [ -f "$target_root/pyproject.toml" ] || [ -f "$target_root/requirements.txt" ] || [ -f "$target_root/setup.py" ]; then
267
- TEST_CANDIDATE="python -m pytest"
268
- return 0
269
- fi
270
- if [ -f "$target_root/tests/run-tests.sh" ]; then
271
- TEST_CANDIDATE="sh tests/run-tests.sh"
272
- return 0
273
- fi
274
- }
275
-
276
- validate_bootstrap_command() {
277
- target_root="$1"
278
- key="$2"
279
- cmd="$3"
280
- [ -n "$cmd" ] || { BOOTSTRAP_VALID="false"; BOOTSTRAP_REASON="${key}_UNDETECTED"; return 0; }
281
- case "$cmd" in
282
- "npm run "*)
283
- command -v npm >/dev/null 2>&1 || { BOOTSTRAP_VALID="false"; BOOTSTRAP_REASON="NPM_NOT_FOUND"; return 0; }
284
- script_name=$(printf "%s" "$cmd" | sed 's/^npm run[[:space:]]\+//')
285
- package_has_script "$target_root" "$script_name" || { BOOTSTRAP_VALID="false"; BOOTSTRAP_REASON="NPM_SCRIPT_MISSING:$script_name"; return 0; }
286
- BOOTSTRAP_VALID="true"; BOOTSTRAP_REASON="OK"; return 0
287
- ;;
288
- "python -m pytest")
289
- command -v python >/dev/null 2>&1 || { BOOTSTRAP_VALID="false"; BOOTSTRAP_REASON="PYTHON_NOT_FOUND"; return 0; }
290
- BOOTSTRAP_VALID="true"; BOOTSTRAP_REASON="OK"; return 0
291
- ;;
292
- "go test "*)
293
- command -v go >/dev/null 2>&1 || { BOOTSTRAP_VALID="false"; BOOTSTRAP_REASON="GO_NOT_FOUND"; return 0; }
294
- [ -f "$target_root/go.mod" ] || { BOOTSTRAP_VALID="false"; BOOTSTRAP_REASON="GO_MOD_MISSING"; return 0; }
295
- BOOTSTRAP_VALID="true"; BOOTSTRAP_REASON="OK"; return 0
296
- ;;
297
- "sh "*)
298
- command -v sh >/dev/null 2>&1 || { BOOTSTRAP_VALID="false"; BOOTSTRAP_REASON="SH_NOT_FOUND"; return 0; }
299
- [ -f "$target_root/tests/run-tests.sh" ] || { BOOTSTRAP_VALID="false"; BOOTSTRAP_REASON="RUN_TESTS_SH_MISSING"; return 0; }
300
- BOOTSTRAP_VALID="true"; BOOTSTRAP_REASON="OK"; return 0
301
- ;;
302
- esac
303
- exe=$(printf "%s" "$cmd" | awk '{print $1}')
304
- command -v "$exe" >/dev/null 2>&1 || { BOOTSTRAP_VALID="false"; BOOTSTRAP_REASON="EXECUTABLE_NOT_FOUND:$exe"; return 0; }
305
- BOOTSTRAP_VALID="true"; BOOTSTRAP_REASON="OK"
306
- }
307
-
308
- bootstrap_runbook_commands() {
309
- target_root="$1"
310
- runbook="$target_root/docs/engineering/runbook.md"
311
- [ -f "$runbook" ] || { BOOTSTRAP_OK="true"; BOOTSTRAP_NOTES=""; return 0; }
312
- BOOTSTRAP_NOTES=""
313
- APPLIED=""
314
- detect_runbook_defaults "$target_root"
315
- for key in TEST_COMMAND LINT_COMMAND TYPECHECK_COMMAND; do
316
- current=$(read_runbook_key "$runbook" "$key")
317
- [ -n "$current" ] && continue
318
- candidate=""
319
- [ "$key" = "TEST_COMMAND" ] && candidate="$TEST_CANDIDATE"
320
- [ "$key" = "LINT_COMMAND" ] && candidate="$LINT_CANDIDATE"
321
- [ "$key" = "TYPECHECK_COMMAND" ] && candidate="$TYPECHECK_CANDIDATE"
322
- if [ -z "$candidate" ]; then
323
- if [ "$key" = "TEST_COMMAND" ]; then
324
- BOOTSTRAP_NOTES="${BOOTSTRAP_NOTES}[RUNBOOK_BOOTSTRAP_ERROR] TEST_COMMAND_UNRESOLVED: could not detect a valid baseline test command. Fix: define TEST_COMMAND in docs/engineering/runbook.md or add detectable stack markers (package.json scripts.test, pyproject.toml, go.mod)."$'\n'
325
- fi
326
- continue
327
- fi
328
- validate_bootstrap_command "$target_root" "$key" "$candidate"
329
- if [ "$BOOTSTRAP_VALID" = "true" ]; then
330
- if write_runbook_key "$runbook" "$key" "$candidate"; then
331
- if [ -z "$APPLIED" ]; then APPLIED="$key=$candidate"; else APPLIED="$APPLIED, $key=$candidate"; fi
332
- fi
333
- elif [ "$key" = "TEST_COMMAND" ]; then
334
- BOOTSTRAP_NOTES="${BOOTSTRAP_NOTES}[RUNBOOK_BOOTSTRAP_ERROR] TEST_COMMAND_INVALID:$BOOTSTRAP_REASON. Fix: set a valid TEST_COMMAND in docs/engineering/runbook.md."$'\n'
335
- fi
336
- done
337
- [ -n "$APPLIED" ] && BOOTSTRAP_NOTES="${BOOTSTRAP_NOTES}[RUNBOOK_BOOTSTRAP] Applied defaults: $APPLIED"$'\n'
338
- final_test=$(read_runbook_key "$runbook" "TEST_COMMAND")
339
- if [ -n "$final_test" ]; then BOOTSTRAP_OK="true"; else BOOTSTRAP_OK="false"; fi
340
- }
341
-
342
- prompt_yes_no() {
343
- label="$1"
344
- default="$2"
345
- suffix="y/N"
346
- [ "$default" = "true" ] && suffix="Y/n"
347
- printf "%s [%s]: " "$label" "$suffix"
348
- read -r value
349
- value=$(printf "%s" "$value" | tr 'A-Z' 'a-z')
350
- if [ -z "$value" ]; then
351
- [ "$default" = "true" ] && return 0 || return 1
352
- fi
353
- [ "$value" = "y" ] || [ "$value" = "yes" ]
354
- }
355
-
356
- TARGET=""
357
- MODE=""
358
- BACKUP="false"
359
- CREATE="false"
360
- CLEAN_REPO="false"
361
- ASSUME_YES="false"
362
- SHOW_HELP="false"
363
- SHOW_VERSION="false"
364
-
365
- if [ $# -eq 0 ]; then
366
- SHOW_HELP="true"
367
- fi
368
-
369
- while [ $# -gt 0 ]; do
370
- case "$1" in
371
- --target) TARGET="$2"; shift 2 ;;
372
- --mode) MODE="$2"; shift 2 ;;
373
- --backup) BACKUP="true"; shift 1 ;;
374
- --create) CREATE="true"; shift 1 ;;
375
- --clean-repo) CLEAN_REPO="true"; shift 1 ;;
376
- --yes) ASSUME_YES="true"; shift 1 ;;
377
- --help|-h) SHOW_HELP="true"; shift 1 ;;
378
- --version|-v) SHOW_VERSION="true"; shift 1 ;;
379
- *) shift 1 ;;
380
- esac
381
- done
382
-
383
- if [ "$SHOW_VERSION" = "true" ]; then
384
- printf "its-magic v%s\n" "$APP_VERSION"
385
- exit 0
386
- fi
387
-
388
- if [ "$SHOW_HELP" = "true" ]; then
389
- show_help
390
- exit 0
391
- fi
392
-
393
- if [ ! -d "$SOURCE_ROOT" ]; then
394
- printf "%s\n" "[INSTALL_SOURCE_ERROR] template directory is missing. Reinstall its-magic package."
395
- exit 1
396
- fi
397
-
398
- OWNERSHIP_MANIFEST="$SOURCE_ROOT/$MANIFEST_NAME"
399
- if [ ! -f "$OWNERSHIP_MANIFEST" ]; then
400
- FALLBACK_MANIFEST="$SCRIPT_DIR/$MANIFEST_NAME"
401
- if [ -f "$FALLBACK_MANIFEST" ]; then
402
- OWNERSHIP_MANIFEST="$FALLBACK_MANIFEST"
403
- else
404
- printf "%s\n" "[INSTALL_SOURCE_ERROR] installer-owned-paths.manifest not found. Reinstall its-magic package."
405
- exit 1
406
- fi
407
- fi
408
-
409
- if [ "$CLEAN_REPO" = "true" ]; then
410
- if [ -z "$TARGET" ]; then
411
- TARGET="."
412
- fi
413
- if [ ! -d "$TARGET" ]; then
414
- printf "%s\n" "Target directory does not exist."
415
- exit 1
416
- fi
417
- TARGET_ROOT=$(cd "$TARGET" && pwd)
418
- if [ "$ASSUME_YES" != "true" ]; then
419
- if ! prompt_yes_no "Clean its-magic workflow artifacts in $TARGET_ROOT?" "false"; then
420
- printf "%s\n" "Aborted."
421
- exit 1
422
- fi
423
- fi
424
- CLEAN_PATHS=$(get_manifest_paths "clean_paths")
425
- if [ -z "$CLEAN_PATHS" ]; then
426
- printf "%s\n" "[INSTALL_MANIFEST_ERROR] clean_paths section is empty in $OWNERSHIP_MANIFEST"
427
- exit 1
428
- fi
429
- for rel in $CLEAN_PATHS; do
430
- path="$TARGET_ROOT/$rel"
431
- if [ -e "$path" ]; then
432
- rm -rf "$path"
433
- printf "%s\n" "Removed: $rel"
434
- fi
435
- done
436
- printf "%s\n" "Clean completed."
437
- exit 0
438
- fi
439
-
440
- if [ -z "$TARGET" ]; then
441
- printf "%s" "Target repository path: "
442
- read -r TARGET
443
- fi
444
-
445
- if [ ! -d "$TARGET" ]; then
446
- if [ "$CREATE" = "true" ] || prompt_yes_no "Target missing. Create?" "false"; then
447
- mkdir -p "$TARGET"
448
- else
449
- printf "%s\n" "Target directory does not exist."
450
- exit 1
451
- fi
452
- fi
453
- TARGET_ROOT=$(cd "$TARGET" && pwd)
454
-
455
- if [ -z "$MODE" ]; then
456
- MODE=$(choose_mode)
457
- fi
458
-
459
- if [ "$MODE" = "overwrite" ] || [ "$MODE" = "interactive" ]; then
460
- if [ "$BACKUP" = "false" ]; then
461
- if prompt_yes_no "Backup existing files before overwrite?" "false"; then
462
- BACKUP="true"
463
- fi
464
- fi
465
- fi
466
-
467
- INCLUDE_PATHS=$(get_manifest_paths "install_include_paths")
468
- if [ -z "$INCLUDE_PATHS" ]; then
469
- printf "%s\n" "[INSTALL_MANIFEST_ERROR] install_include_paths section is empty in $OWNERSHIP_MANIFEST"
470
- exit 1
471
- fi
472
-
473
- FILES=$(list_source_files "$SOURCE_ROOT" $INCLUDE_PATHS)
474
- if [ -z "$FILES" ]; then
475
- printf "%s\n" "No source files found to install."
476
- exit 1
477
- fi
478
-
479
- if [ "$BACKUP" = "true" ] && [ "$MODE" = "overwrite" ]; then
480
- overwrite_candidates=""
481
- for rel in $FILES; do
482
- [ -f "$TARGET_ROOT/$rel" ] && overwrite_candidates="$overwrite_candidates $rel"
483
- done
484
- if [ -n "$overwrite_candidates" ]; then
485
- backup_root=$(backup_files "$TARGET_ROOT" $overwrite_candidates)
486
- printf "%s\n" "Backup created at: $backup_root"
487
- fi
488
- fi
489
-
490
- if [ "$MODE" = "upgrade" ]; then
491
- OLD_VER=$(read_installed_version "$TARGET_ROOT")
492
- printf "\n\033[1;36mUpgrading from v%s to v%s\033[0m\n\n" "$OLD_VER" "$APP_VERSION"
493
-
494
- if [ "$BACKUP" = "true" ]; then
495
- backup_candidates=""
496
- for rel in $FILES; do
497
- cat=$(classify_file "$rel")
498
- [ "$cat" = "framework" ] && [ -f "$TARGET_ROOT/$rel" ] && backup_candidates="$backup_candidates $rel"
499
- done
500
- if [ -n "$backup_candidates" ]; then
501
- backup_root=$(backup_files "$TARGET_ROOT" $backup_candidates)
502
- printf "%s\n" "Backup created at: $backup_root"
503
- fi
504
- fi
505
-
506
- count_added=0; list_added=""
507
- count_updated=0; list_updated=""
508
- count_unchanged=0
509
- count_preserved=0
510
- count_review=0; list_review=""
511
- scratchpad_example_rel=".cursor/scratchpad.local.example.md"
512
- scratchpad_example_status="not-seen"
513
-
514
- for rel in $FILES; do
515
- src="$SOURCE_ROOT/$rel"
516
- dst="$TARGET_ROOT/$rel"
517
- cat=$(classify_file "$rel")
518
-
519
- if [ ! -f "$dst" ]; then
520
- ensure_parent "$dst"
521
- cp -p "$src" "$dst"
522
- count_added=$((count_added + 1))
523
- list_added="$list_added $rel"
524
- [ "$rel" = "$scratchpad_example_rel" ] && scratchpad_example_status="added"
525
- continue
526
- fi
527
-
528
- if [ "$cat" = "framework" ]; then
529
- if cmp -s "$src" "$dst"; then
530
- count_unchanged=$((count_unchanged + 1))
531
- [ "$rel" = "$scratchpad_example_rel" ] && scratchpad_example_status="unchanged"
532
- else
533
- ensure_parent "$dst"
534
- cp -p "$src" "$dst"
535
- count_updated=$((count_updated + 1))
536
- list_updated="$list_updated $rel"
537
- [ "$rel" = "$scratchpad_example_rel" ] && scratchpad_example_status="updated"
538
- fi
539
- continue
540
- fi
541
-
542
- if [ "$cat" = "user-data" ]; then
543
- count_preserved=$((count_preserved + 1))
544
- continue
545
- fi
546
-
547
- if [ "$cat" = "mixed" ]; then
548
- count_preserved=$((count_preserved + 1))
549
- if ! cmp -s "$src" "$dst"; then
550
- count_review=$((count_review + 1))
551
- list_review="$list_review $rel"
552
- fi
553
- continue
554
- fi
555
- done
556
-
557
- scratchpad_postinstall "$TARGET_ROOT" "upgrade"
558
- validate_install_completeness "$TARGET_ROOT"
559
-
560
- write_installed_version "$TARGET_ROOT" "$APP_VERSION"
561
- sync_root_readme_to_its_magic "$TARGET_ROOT" || true
562
- bootstrap_runbook_commands "$TARGET_ROOT"
563
- [ -n "$BOOTSTRAP_NOTES" ] && printf "%s" "$BOOTSTRAP_NOTES"
564
- [ "$BOOTSTRAP_OK" = "true" ] || exit 1
565
-
566
- show_banner
567
- printf "\033[1;32mUpgrade complete: v%s -> v%s\033[0m\n\n" "$OLD_VER" "$APP_VERSION"
568
- if [ "$count_added" -gt 0 ]; then
569
- printf " \033[1;32mAdded (new): %s files\033[0m\n" "$count_added"
570
- for f in $list_added; do printf " %s\n" "$f"; done
571
- fi
572
- if [ "$count_updated" -gt 0 ]; then
573
- printf " \033[1;33mUpdated (framework): %s files\033[0m\n" "$count_updated"
574
- for f in $list_updated; do printf " %s\n" "$f"; done
575
- fi
576
- printf " Unchanged: %s files\n" "$count_unchanged"
577
- printf " Preserved (user): %s files\n" "$count_preserved"
578
- [ "$scratchpad_example_status" = "not-seen" ] && scratchpad_example_status="not-in-manifest"
579
- printf " Scratchpad example: %s (.cursor/scratchpad.local.example.md)\n" "$scratchpad_example_status"
580
- printf " Scratchpad layers: post-install refreshed example-first, then baseline (see [SCRATCHPAD_LAYER] lines).\n"
581
- [ -f "$TARGET_ROOT/.cursor/scratchpad.local.md" ] && printf " User local file: preserved (.cursor/scratchpad.local.md)\n"
582
- if [ "$count_review" -gt 0 ]; then
583
- printf "\n \033[1;35mReview recommended: %s files\033[0m\n" "$count_review"
584
- for f in $list_review; do printf " %s\n" "$f"; done
585
- printf " Check .cursor/scratchpad.local.example.md for new flags.\n"
586
- fi
587
- printf "\nRepository: %s\n\n" "$REPO_URL"
588
- exit 0
589
- fi
590
-
591
- for rel in $FILES; do
592
- src="$SOURCE_ROOT/$rel"
593
- dst="$TARGET_ROOT/$rel"
594
- if [ "$MODE" = "missing" ]; then
595
- [ -f "$dst" ] && continue
596
- ensure_parent "$dst"
597
- cp -p "$src" "$dst"
598
- continue
599
- fi
600
- if [ "$MODE" = "overwrite" ]; then
601
- ensure_parent "$dst"
602
- cp -p "$src" "$dst"
603
- continue
604
- fi
605
- if [ "$MODE" = "interactive" ]; then
606
- if [ ! -f "$dst" ]; then
607
- ensure_parent "$dst"
608
- cp -p "$src" "$dst"
609
- continue
610
- fi
611
- printf "%s" "File exists: $rel | [o]verwrite [s]kip [q]uit: "
612
- read -r answer
613
- answer=$(printf "%s" "$answer" | tr 'A-Z' 'a-z')
614
- if [ "$answer" = "q" ]; then
615
- printf "%s\n" "Aborted."
616
- exit 1
617
- fi
618
- if [ "$answer" = "o" ]; then
619
- if [ "$BACKUP" = "true" ]; then
620
- backup_root=$(backup_files "$TARGET_ROOT" "$rel")
621
- printf "%s\n" "Backed up: $rel -> $backup_root"
622
- fi
623
- ensure_parent "$dst"
624
- cp -p "$src" "$dst"
625
- fi
626
- fi
627
- done
628
-
629
- scratchpad_postinstall "$TARGET_ROOT" "$MODE"
630
- validate_install_completeness "$TARGET_ROOT"
631
-
632
- write_installed_version "$TARGET_ROOT" "$APP_VERSION"
633
- sync_root_readme_to_its_magic "$TARGET_ROOT" || true
634
- bootstrap_runbook_commands "$TARGET_ROOT"
635
- [ -n "$BOOTSTRAP_NOTES" ] && printf "%s" "$BOOTSTRAP_NOTES"
636
- [ "$BOOTSTRAP_OK" = "true" ] || exit 1
637
-
638
- show_banner
639
- printf "its-magic v%s\n" "$APP_VERSION"
640
- printf "Repository: %s\n\n" "$REPO_URL"
641
- printf "\033[1;32m Installation complete!\033[0m\n\n"
642
- exit 0
643
-
1
+ #!/usr/bin/env sh
2
+ set -e
3
+
4
+ # BUG-0004: keep startup shell options POSIX-safe for /bin/sh execution.
5
+ # Do not use bash-only "set" flags in this unconditional startup path.
6
+
7
+ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
8
+ SOURCE_ROOT="$SCRIPT_DIR/template"
9
+ MANIFEST_NAME="docs/engineering/context/installer-owned-paths.manifest"
10
+ REPO_URL="https://github.com/fl0wm0ti0n/its-magic"
11
+ APP_VERSION=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$SCRIPT_DIR/package.json" 2>/dev/null | head -n 1)
12
+ [ -z "$APP_VERSION" ] && APP_VERSION="unknown"
13
+
14
+ show_banner() {
15
+ printf "\n"
16
+ printf "\033[1;35m ██╗████████╗███████╗ ███╗ ███╗ █████╗ ██████╗ ██╗ ██████╗\033[0m\n"
17
+ printf "\033[1;35m ██║╚══██╔══╝██╔════╝ ████╗ ████║██╔══██╗██╔════╝ ██║██╔════╝\033[0m\n"
18
+ printf "\033[1;35m ██║ ██║ ███████╗█████╗██╔████╔██║███████║██║ ███╗██║██║ \033[0m\n"
19
+ printf "\033[1;36m ██║ ██║ ╚════██║╚════╝██║╚██╔╝██║██╔══██║██║ ██║██║██║ \033[0m\n"
20
+ printf "\033[1;36m ██║ ██║ ███████║ ██║ ╚═╝ ██║██║ ██║╚██████╔╝██║╚██████╗\033[0m\n"
21
+ printf "\033[1;36m ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝\033[0m\n"
22
+ printf "\n"
23
+ printf "\033[1;33m AI dev team\033[0m\n"
24
+ printf "\n"
25
+ }
26
+
27
+ show_help() {
28
+ show_banner
29
+ printf "its-magic v%s\n" "$APP_VERSION"
30
+ printf "Repository: %s\n\n" "$REPO_URL"
31
+ printf "Install AI dev team workflow files into any Cursor repository.\n\n"
32
+ printf "Usage:\n"
33
+ printf " its-magic --target <path> [--mode <mode>] [--backup] [--create]\n"
34
+ printf " its-magic --clean-repo [--target <path>] [--yes]\n"
35
+ printf " its-magic --help | --version\n\n"
36
+ printf "Install options:\n"
37
+ printf " --target <path> Path to the repository where workflow files are installed.\n"
38
+ printf " If omitted you will be prompted interactively.\n"
39
+ printf " --mode <mode> How to handle files that already exist in the target:\n"
40
+ printf " missing Only copy files that do not exist yet (default).\n"
41
+ printf " Safe for repos that already have some workflow files.\n"
42
+ printf " overwrite Replace every file, even if it already exists.\n"
43
+ printf " Combine with --backup to keep a snapshot first.\n"
44
+ printf " interactive Ask per file whether to overwrite or skip.\n"
45
+ printf " upgrade Update framework files while preserving user data.\n"
46
+ printf " Use after updating its-magic to a newer version.\n"
47
+ printf " --backup Before overwriting, save existing files to backups/<timestamp>/.\n"
48
+ printf " Ignored when mode is 'missing' (nothing gets replaced).\n"
49
+ printf " --create Create the target directory if it does not exist.\n\n"
50
+ printf " Note: installer bootstraps runbook TEST/LINT/TYPECHECK commands from\n"
51
+ printf " OS+stack detection; unresolved TEST_COMMAND fails fast with\n"
52
+ printf " [RUNBOOK_BOOTSTRAP_ERROR] diagnostics.\n"
53
+ printf " Note: scratchpad Model B: .cursor/scratchpad.md is\n"
54
+ printf " materialized when missing; Python 3 on PATH is required for validation.\n"
55
+ printf " Recovery: python installer.py --scratchpad-postinstall --target <repo> --mode missing\n\n"
56
+ printf "Clean options:\n"
57
+ printf " --clean-repo Remove all its-magic workflow artifacts from the target repo\n"
58
+ printf " (owned paths from installer manifest, including .cursor,\n"
59
+ printf " docs/product, docs/engineering, docs/user-guides, sprints,\n"
60
+ printf " handoffs, decisions, workflow scripts, CI files, and\n"
61
+ printf " installer metadata under its_magic/ (legacy .its-magic-version\n"
62
+ printf " is also removed when present). Your own source code is never touched.\n"
63
+ printf " --target <path> Repo to clean (default: current directory).\n"
64
+ printf " --yes Skip the confirmation prompt.\n\n"
65
+ printf "Info:\n"
66
+ printf " --help, -h Show this help and exit.\n"
67
+ printf " --version, -v Print the installed version and exit.\n\n"
68
+ printf "Examples:\n"
69
+ printf " its-magic --target . --mode missing Safe first-time setup\n"
70
+ printf " its-magic --target . --mode upgrade Update framework, keep user data\n"
71
+ printf " its-magic --target . --mode overwrite --backup Replace all files, keep backup\n"
72
+ printf " its-magic --clean-repo --target . --yes Remove workflow artifacts silently\n\n"
73
+ }
74
+
75
+ ensure_parent() {
76
+ dir=$(dirname "$1")
77
+ [ -d "$dir" ] || mkdir -p "$dir"
78
+ }
79
+
80
+ list_source_files() {
81
+ source_root="$1"
82
+ shift
83
+ for rel in "$@"; do
84
+ src="$source_root/$rel"
85
+ if [ -f "$src" ]; then
86
+ echo "$rel"
87
+ elif [ -d "$src" ]; then
88
+ find "$src" -type f | sed "s|^$source_root/||"
89
+ fi
90
+ done | sort -u
91
+ }
92
+
93
+ get_manifest_paths() {
94
+ section="$1"
95
+ # BUG-0008: strip trailing CR so CRLF manifests (Windows-published npm tarballs)
96
+ # still match [section] headers under POSIX awk on Linux.
97
+ awk -v s="$section" '
98
+ BEGIN { in_section=0 }
99
+ {
100
+ sub(/\r$/, "")
101
+ }
102
+ /^[[:space:]]*#/ { next }
103
+ /^[[:space:]]*$/ { next }
104
+ /^\[/ {
105
+ in_section = ($0 == "[" s "]")
106
+ next
107
+ }
108
+ { if (in_section) print $0 }
109
+ ' "$OWNERSHIP_MANIFEST"
110
+ }
111
+
112
+ backup_files() {
113
+ target_root="$1"
114
+ shift
115
+ timestamp=$(date -u +"%Y%m%d-%H%M%SZ")
116
+ backup_root="$target_root/backups/$timestamp"
117
+ for rel in "$@"; do
118
+ src="$target_root/$rel"
119
+ if [ -f "$src" ]; then
120
+ dst="$backup_root/$rel"
121
+ ensure_parent "$dst"
122
+ cp -p "$src" "$dst"
123
+ fi
124
+ done
125
+ echo "$backup_root"
126
+ }
127
+
128
+ choose_mode() {
129
+ printf "%s\n" "Select install mode:"
130
+ printf "%s\n" "1) missing-only (copy only files that do not exist)"
131
+ printf "%s\n" "2) overwrite-all (replace existing files)"
132
+ printf "%s\n" "3) interactive (prompt per file)"
133
+ printf "%s\n" "4) upgrade (update framework files, preserve user data)"
134
+ printf "%s" "Enter 1, 2, 3, or 4: "
135
+ read -r choice
136
+ case "$choice" in
137
+ 1) echo "missing" ;;
138
+ 2) echo "overwrite" ;;
139
+ 4) echo "upgrade" ;;
140
+ *) echo "interactive" ;;
141
+ esac
142
+ }
143
+
144
+ scratchpad_postinstall() {
145
+ target_root="$1"
146
+ mode="$2"
147
+ installer_py="$SCRIPT_DIR/installer.py"
148
+ if [ ! -f "$installer_py" ]; then
149
+ printf "%s\n" "[SCRATCHPAD_POSTINSTALL_ERROR] installer.py missing next to installer.sh."
150
+ exit 1
151
+ fi
152
+ if command -v python3 >/dev/null 2>&1; then
153
+ python3 "$installer_py" --scratchpad-postinstall --target "$target_root" --mode "$mode" || exit $?
154
+ elif command -v python >/dev/null 2>&1; then
155
+ python "$installer_py" --scratchpad-postinstall --target "$target_root" --mode "$mode" || exit $?
156
+ else
157
+ printf "%s\n" "[SCRATCHPAD_POSTINSTALL_ERROR] PYTHON_NOT_FOUND: Python 3 is required for scratchpad materialization/validation (Model B)."
158
+ exit 1
159
+ fi
160
+ }
161
+
162
+ validate_install_completeness() {
163
+ target_root="$1"
164
+ installer_py="$SCRIPT_DIR/installer.py"
165
+ if [ ! -f "$installer_py" ]; then
166
+ printf "%s\n" "[INSTALL_COMPLETENESS_FAILED] installer.py missing next to installer.sh."
167
+ exit 1
168
+ fi
169
+ if command -v python3 >/dev/null 2>&1; then
170
+ python3 "$installer_py" --validate-install-completeness --target "$target_root" || exit $?
171
+ elif command -v python >/dev/null 2>&1; then
172
+ python "$installer_py" --validate-install-completeness --target "$target_root" || exit $?
173
+ else
174
+ printf "%s\n" "[INSTALL_COMPLETENESS_FAILED] PYTHON_NOT_FOUND: Python is required for deterministic installer completeness validation."
175
+ exit 1
176
+ fi
177
+ }
178
+
179
+ classify_file() {
180
+ rel="$1"
181
+ case "$rel" in
182
+ README.md) echo "mixed" ;;
183
+ .cursor/commands/*|.cursor/rules/*|.cursor/agents/*|.cursor/skills/*) echo "framework" ;;
184
+ .cursor/hooks/*|.cursor/hooks.json|.cursor/scratchpad.local.example.md) echo "framework" ;;
185
+ .github/workflows/*|scripts/validate-and-push*|scripts/sync_push_gates.py|docs/engineering/context/*|its_magic/*) echo "framework" ;;
186
+ .its-magic-version|its_magic/.its-magic-version|its_magic/README.md) echo "framework" ;;
187
+ docs/product/*|docs/engineering/*|docs/user-guides/*) echo "user-data" ;;
188
+ sprints/*|handoffs/*|decisions/*) echo "user-data" ;;
189
+ *) echo "framework" ;;
190
+ esac
191
+ }
192
+
193
+ read_installed_version() {
194
+ primary="$1/its_magic/.its-magic-version"
195
+ legacy="$1/.its-magic-version"
196
+ if [ -f "$primary" ]; then
197
+ cat "$primary" | tr -d '\n'
198
+ return 0
199
+ fi
200
+ if [ -f "$legacy" ]; then
201
+ cat "$legacy" | tr -d '\n'
202
+ return 0
203
+ fi
204
+ printf "unknown"
205
+ }
206
+
207
+ write_installed_version() {
208
+ vf="$1/its_magic/.its-magic-version"
209
+ ensure_parent "$vf"
210
+ printf "%s" "$2" > "$vf"
211
+ legacy="$1/.its-magic-version"
212
+ [ -f "$legacy" ] && rm -f "$legacy"
213
+ return 0
214
+ }
215
+
216
+ sync_root_readme_to_its_magic() {
217
+ target_root="$1"
218
+ [ -f "$target_root/README.md" ] || return 1
219
+ dst="$target_root/its_magic/README.md"
220
+ ensure_parent "$dst"
221
+ cp -p "$target_root/README.md" "$dst"
222
+ return 0
223
+ }
224
+
225
+ read_runbook_key() {
226
+ runbook_path="$1"
227
+ key="$2"
228
+ [ -f "$runbook_path" ] || { printf ""; return; }
229
+ awk -F: -v k="$key" '$1==k { sub(/^[[:space:]]*/, "", $2); print $2; exit }' "$runbook_path"
230
+ }
231
+
232
+ write_runbook_key() {
233
+ runbook_path="$1"
234
+ key="$2"
235
+ value="$3"
236
+ [ -f "$runbook_path" ] || return 1
237
+ tmp="$runbook_path.tmp.$$"
238
+ awk -v k="$key" -v v="$value" '
239
+ BEGIN { changed=0 }
240
+ index($0, k":") == 1 && changed==0 { print k": "v; changed=1; next }
241
+ { print $0 }
242
+ END { if (changed==0) exit 2 }
243
+ ' "$runbook_path" > "$tmp" || { rm -f "$tmp"; return 1; }
244
+ mv "$tmp" "$runbook_path"
245
+ return 0
246
+ }
247
+
248
+ package_has_script() {
249
+ target_root="$1"
250
+ script_name="$2"
251
+ pkg="$target_root/package.json"
252
+ [ -f "$pkg" ] || return 1
253
+ command -v node >/dev/null 2>&1 || return 1
254
+ node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));const s=(p.scripts||{})[process.argv[2]];process.exit((typeof s==='string'&&s.trim())?0:1);" "$pkg" "$script_name" >/dev/null 2>&1
255
+ }
256
+
257
+ detect_runbook_defaults() {
258
+ target_root="$1"
259
+ TEST_CANDIDATE=""
260
+ LINT_CANDIDATE=""
261
+ TYPECHECK_CANDIDATE=""
262
+ if [ -f "$target_root/package.json" ] && package_has_script "$target_root" "test"; then
263
+ TEST_CANDIDATE="npm run test"
264
+ package_has_script "$target_root" "lint" && LINT_CANDIDATE="npm run lint"
265
+ package_has_script "$target_root" "typecheck" && TYPECHECK_CANDIDATE="npm run typecheck"
266
+ return 0
267
+ fi
268
+ if [ -f "$target_root/go.mod" ]; then
269
+ TEST_CANDIDATE="go test ./..."
270
+ return 0
271
+ fi
272
+ if [ -f "$target_root/pyproject.toml" ] || [ -f "$target_root/requirements.txt" ] || [ -f "$target_root/setup.py" ]; then
273
+ TEST_CANDIDATE="python -m pytest"
274
+ return 0
275
+ fi
276
+ if [ -f "$target_root/tests/run-tests.sh" ]; then
277
+ TEST_CANDIDATE="sh tests/run-tests.sh"
278
+ return 0
279
+ fi
280
+ }
281
+
282
+ validate_bootstrap_command() {
283
+ target_root="$1"
284
+ key="$2"
285
+ cmd="$3"
286
+ [ -n "$cmd" ] || { BOOTSTRAP_VALID="false"; BOOTSTRAP_REASON="${key}_UNDETECTED"; return 0; }
287
+ case "$cmd" in
288
+ "npm run "*)
289
+ command -v npm >/dev/null 2>&1 || { BOOTSTRAP_VALID="false"; BOOTSTRAP_REASON="NPM_NOT_FOUND"; return 0; }
290
+ script_name=$(printf "%s" "$cmd" | sed 's/^npm run[[:space:]]\+//')
291
+ package_has_script "$target_root" "$script_name" || { BOOTSTRAP_VALID="false"; BOOTSTRAP_REASON="NPM_SCRIPT_MISSING:$script_name"; return 0; }
292
+ BOOTSTRAP_VALID="true"; BOOTSTRAP_REASON="OK"; return 0
293
+ ;;
294
+ "python -m pytest")
295
+ command -v python >/dev/null 2>&1 || { BOOTSTRAP_VALID="false"; BOOTSTRAP_REASON="PYTHON_NOT_FOUND"; return 0; }
296
+ BOOTSTRAP_VALID="true"; BOOTSTRAP_REASON="OK"; return 0
297
+ ;;
298
+ "go test "*)
299
+ command -v go >/dev/null 2>&1 || { BOOTSTRAP_VALID="false"; BOOTSTRAP_REASON="GO_NOT_FOUND"; return 0; }
300
+ [ -f "$target_root/go.mod" ] || { BOOTSTRAP_VALID="false"; BOOTSTRAP_REASON="GO_MOD_MISSING"; return 0; }
301
+ BOOTSTRAP_VALID="true"; BOOTSTRAP_REASON="OK"; return 0
302
+ ;;
303
+ "sh "*)
304
+ command -v sh >/dev/null 2>&1 || { BOOTSTRAP_VALID="false"; BOOTSTRAP_REASON="SH_NOT_FOUND"; return 0; }
305
+ [ -f "$target_root/tests/run-tests.sh" ] || { BOOTSTRAP_VALID="false"; BOOTSTRAP_REASON="RUN_TESTS_SH_MISSING"; return 0; }
306
+ BOOTSTRAP_VALID="true"; BOOTSTRAP_REASON="OK"; return 0
307
+ ;;
308
+ esac
309
+ exe=$(printf "%s" "$cmd" | awk '{print $1}')
310
+ command -v "$exe" >/dev/null 2>&1 || { BOOTSTRAP_VALID="false"; BOOTSTRAP_REASON="EXECUTABLE_NOT_FOUND:$exe"; return 0; }
311
+ BOOTSTRAP_VALID="true"; BOOTSTRAP_REASON="OK"
312
+ }
313
+
314
+ bootstrap_runbook_commands() {
315
+ target_root="$1"
316
+ runbook="$target_root/docs/engineering/runbook.md"
317
+ [ -f "$runbook" ] || { BOOTSTRAP_OK="true"; BOOTSTRAP_NOTES=""; return 0; }
318
+ BOOTSTRAP_NOTES=""
319
+ APPLIED=""
320
+ detect_runbook_defaults "$target_root"
321
+ for key in TEST_COMMAND LINT_COMMAND TYPECHECK_COMMAND; do
322
+ current=$(read_runbook_key "$runbook" "$key")
323
+ [ -n "$current" ] && continue
324
+ candidate=""
325
+ [ "$key" = "TEST_COMMAND" ] && candidate="$TEST_CANDIDATE"
326
+ [ "$key" = "LINT_COMMAND" ] && candidate="$LINT_CANDIDATE"
327
+ [ "$key" = "TYPECHECK_COMMAND" ] && candidate="$TYPECHECK_CANDIDATE"
328
+ if [ -z "$candidate" ]; then
329
+ if [ "$key" = "TEST_COMMAND" ]; then
330
+ BOOTSTRAP_NOTES="${BOOTSTRAP_NOTES}[RUNBOOK_BOOTSTRAP_ERROR] TEST_COMMAND_UNRESOLVED: could not detect a valid baseline test command. Fix: define TEST_COMMAND in docs/engineering/runbook.md or add detectable stack markers (package.json scripts.test, pyproject.toml, go.mod)."$'\n'
331
+ fi
332
+ continue
333
+ fi
334
+ validate_bootstrap_command "$target_root" "$key" "$candidate"
335
+ if [ "$BOOTSTRAP_VALID" = "true" ]; then
336
+ if write_runbook_key "$runbook" "$key" "$candidate"; then
337
+ if [ -z "$APPLIED" ]; then APPLIED="$key=$candidate"; else APPLIED="$APPLIED, $key=$candidate"; fi
338
+ fi
339
+ elif [ "$key" = "TEST_COMMAND" ]; then
340
+ BOOTSTRAP_NOTES="${BOOTSTRAP_NOTES}[RUNBOOK_BOOTSTRAP_ERROR] TEST_COMMAND_INVALID:$BOOTSTRAP_REASON. Fix: set a valid TEST_COMMAND in docs/engineering/runbook.md."$'\n'
341
+ fi
342
+ done
343
+ [ -n "$APPLIED" ] && BOOTSTRAP_NOTES="${BOOTSTRAP_NOTES}[RUNBOOK_BOOTSTRAP] Applied defaults: $APPLIED"$'\n'
344
+ final_test=$(read_runbook_key "$runbook" "TEST_COMMAND")
345
+ if [ -n "$final_test" ]; then BOOTSTRAP_OK="true"; else BOOTSTRAP_OK="false"; fi
346
+ }
347
+
348
+ prompt_yes_no() {
349
+ label="$1"
350
+ default="$2"
351
+ suffix="y/N"
352
+ [ "$default" = "true" ] && suffix="Y/n"
353
+ printf "%s [%s]: " "$label" "$suffix"
354
+ read -r value
355
+ value=$(printf "%s" "$value" | tr 'A-Z' 'a-z')
356
+ if [ -z "$value" ]; then
357
+ [ "$default" = "true" ] && return 0 || return 1
358
+ fi
359
+ [ "$value" = "y" ] || [ "$value" = "yes" ]
360
+ }
361
+
362
+ TARGET=""
363
+ MODE=""
364
+ BACKUP="false"
365
+ CREATE="false"
366
+ CLEAN_REPO="false"
367
+ ASSUME_YES="false"
368
+ SHOW_HELP="false"
369
+ SHOW_VERSION="false"
370
+
371
+ if [ $# -eq 0 ]; then
372
+ SHOW_HELP="true"
373
+ fi
374
+
375
+ while [ $# -gt 0 ]; do
376
+ case "$1" in
377
+ --target) TARGET="$2"; shift 2 ;;
378
+ --mode) MODE="$2"; shift 2 ;;
379
+ --backup) BACKUP="true"; shift 1 ;;
380
+ --create) CREATE="true"; shift 1 ;;
381
+ --clean-repo) CLEAN_REPO="true"; shift 1 ;;
382
+ --yes) ASSUME_YES="true"; shift 1 ;;
383
+ --help|-h) SHOW_HELP="true"; shift 1 ;;
384
+ --version|-v) SHOW_VERSION="true"; shift 1 ;;
385
+ *) shift 1 ;;
386
+ esac
387
+ done
388
+
389
+ if [ "$SHOW_VERSION" = "true" ]; then
390
+ printf "its-magic v%s\n" "$APP_VERSION"
391
+ exit 0
392
+ fi
393
+
394
+ if [ "$SHOW_HELP" = "true" ]; then
395
+ show_help
396
+ exit 0
397
+ fi
398
+
399
+ if [ ! -d "$SOURCE_ROOT" ]; then
400
+ printf "%s\n" "[INSTALL_SOURCE_ERROR] template directory is missing. Reinstall its-magic package."
401
+ exit 1
402
+ fi
403
+
404
+ OWNERSHIP_MANIFEST="$SOURCE_ROOT/$MANIFEST_NAME"
405
+ if [ ! -f "$OWNERSHIP_MANIFEST" ]; then
406
+ FALLBACK_MANIFEST="$SCRIPT_DIR/$MANIFEST_NAME"
407
+ if [ -f "$FALLBACK_MANIFEST" ]; then
408
+ OWNERSHIP_MANIFEST="$FALLBACK_MANIFEST"
409
+ else
410
+ printf "%s\n" "[INSTALL_SOURCE_ERROR] installer-owned-paths.manifest not found. Reinstall its-magic package."
411
+ exit 1
412
+ fi
413
+ fi
414
+
415
+ if [ "$CLEAN_REPO" = "true" ]; then
416
+ if [ -z "$TARGET" ]; then
417
+ TARGET="."
418
+ fi
419
+ if [ ! -d "$TARGET" ]; then
420
+ printf "%s\n" "Target directory does not exist."
421
+ exit 1
422
+ fi
423
+ TARGET_ROOT=$(cd "$TARGET" && pwd)
424
+ if [ "$ASSUME_YES" != "true" ]; then
425
+ if ! prompt_yes_no "Clean its-magic workflow artifacts in $TARGET_ROOT?" "false"; then
426
+ printf "%s\n" "Aborted."
427
+ exit 1
428
+ fi
429
+ fi
430
+ CLEAN_PATHS=$(get_manifest_paths "clean_paths")
431
+ if [ -z "$CLEAN_PATHS" ]; then
432
+ printf "%s\n" "[INSTALL_MANIFEST_ERROR] clean_paths section is empty in $OWNERSHIP_MANIFEST"
433
+ exit 1
434
+ fi
435
+ for rel in $CLEAN_PATHS; do
436
+ path="$TARGET_ROOT/$rel"
437
+ if [ -e "$path" ]; then
438
+ rm -rf "$path"
439
+ printf "%s\n" "Removed: $rel"
440
+ fi
441
+ done
442
+ printf "%s\n" "Clean completed."
443
+ exit 0
444
+ fi
445
+
446
+ if [ -z "$TARGET" ]; then
447
+ printf "%s" "Target repository path: "
448
+ read -r TARGET
449
+ fi
450
+
451
+ if [ ! -d "$TARGET" ]; then
452
+ if [ "$CREATE" = "true" ] || prompt_yes_no "Target missing. Create?" "false"; then
453
+ mkdir -p "$TARGET"
454
+ else
455
+ printf "%s\n" "Target directory does not exist."
456
+ exit 1
457
+ fi
458
+ fi
459
+ TARGET_ROOT=$(cd "$TARGET" && pwd)
460
+
461
+ if [ -z "$MODE" ]; then
462
+ MODE=$(choose_mode)
463
+ fi
464
+
465
+ if [ "$MODE" = "overwrite" ] || [ "$MODE" = "interactive" ]; then
466
+ if [ "$BACKUP" = "false" ]; then
467
+ if prompt_yes_no "Backup existing files before overwrite?" "false"; then
468
+ BACKUP="true"
469
+ fi
470
+ fi
471
+ fi
472
+
473
+ INCLUDE_PATHS=$(get_manifest_paths "install_include_paths")
474
+ if [ -z "$INCLUDE_PATHS" ]; then
475
+ printf "%s\n" "[INSTALL_MANIFEST_ERROR] install_include_paths section is empty in $OWNERSHIP_MANIFEST"
476
+ exit 1
477
+ fi
478
+
479
+ FILES=$(list_source_files "$SOURCE_ROOT" $INCLUDE_PATHS)
480
+ if [ -z "$FILES" ]; then
481
+ printf "%s\n" "No source files found to install."
482
+ exit 1
483
+ fi
484
+
485
+ if [ "$BACKUP" = "true" ] && [ "$MODE" = "overwrite" ]; then
486
+ overwrite_candidates=""
487
+ for rel in $FILES; do
488
+ [ -f "$TARGET_ROOT/$rel" ] && overwrite_candidates="$overwrite_candidates $rel"
489
+ done
490
+ if [ -n "$overwrite_candidates" ]; then
491
+ backup_root=$(backup_files "$TARGET_ROOT" $overwrite_candidates)
492
+ printf "%s\n" "Backup created at: $backup_root"
493
+ fi
494
+ fi
495
+
496
+ if [ "$MODE" = "upgrade" ]; then
497
+ OLD_VER=$(read_installed_version "$TARGET_ROOT")
498
+ printf "\n\033[1;36mUpgrading from v%s to v%s\033[0m\n\n" "$OLD_VER" "$APP_VERSION"
499
+
500
+ if [ "$BACKUP" = "true" ]; then
501
+ backup_candidates=""
502
+ for rel in $FILES; do
503
+ cat=$(classify_file "$rel")
504
+ [ "$cat" = "framework" ] && [ -f "$TARGET_ROOT/$rel" ] && backup_candidates="$backup_candidates $rel"
505
+ done
506
+ if [ -n "$backup_candidates" ]; then
507
+ backup_root=$(backup_files "$TARGET_ROOT" $backup_candidates)
508
+ printf "%s\n" "Backup created at: $backup_root"
509
+ fi
510
+ fi
511
+
512
+ count_added=0; list_added=""
513
+ count_updated=0; list_updated=""
514
+ count_unchanged=0
515
+ count_preserved=0
516
+ count_review=0; list_review=""
517
+ scratchpad_example_rel=".cursor/scratchpad.local.example.md"
518
+ scratchpad_example_status="not-seen"
519
+
520
+ for rel in $FILES; do
521
+ src="$SOURCE_ROOT/$rel"
522
+ dst="$TARGET_ROOT/$rel"
523
+ cat=$(classify_file "$rel")
524
+
525
+ if [ ! -f "$dst" ]; then
526
+ ensure_parent "$dst"
527
+ cp -p "$src" "$dst"
528
+ count_added=$((count_added + 1))
529
+ list_added="$list_added $rel"
530
+ [ "$rel" = "$scratchpad_example_rel" ] && scratchpad_example_status="added"
531
+ continue
532
+ fi
533
+
534
+ if [ "$cat" = "framework" ]; then
535
+ if cmp -s "$src" "$dst"; then
536
+ count_unchanged=$((count_unchanged + 1))
537
+ [ "$rel" = "$scratchpad_example_rel" ] && scratchpad_example_status="unchanged"
538
+ else
539
+ ensure_parent "$dst"
540
+ cp -p "$src" "$dst"
541
+ count_updated=$((count_updated + 1))
542
+ list_updated="$list_updated $rel"
543
+ [ "$rel" = "$scratchpad_example_rel" ] && scratchpad_example_status="updated"
544
+ fi
545
+ continue
546
+ fi
547
+
548
+ if [ "$cat" = "user-data" ]; then
549
+ count_preserved=$((count_preserved + 1))
550
+ continue
551
+ fi
552
+
553
+ if [ "$cat" = "mixed" ]; then
554
+ count_preserved=$((count_preserved + 1))
555
+ if ! cmp -s "$src" "$dst"; then
556
+ count_review=$((count_review + 1))
557
+ list_review="$list_review $rel"
558
+ fi
559
+ continue
560
+ fi
561
+ done
562
+
563
+ scratchpad_postinstall "$TARGET_ROOT" "upgrade"
564
+ validate_install_completeness "$TARGET_ROOT"
565
+
566
+ write_installed_version "$TARGET_ROOT" "$APP_VERSION"
567
+ sync_root_readme_to_its_magic "$TARGET_ROOT" || true
568
+ bootstrap_runbook_commands "$TARGET_ROOT"
569
+ [ -n "$BOOTSTRAP_NOTES" ] && printf "%s" "$BOOTSTRAP_NOTES"
570
+ [ "$BOOTSTRAP_OK" = "true" ] || exit 1
571
+
572
+ show_banner
573
+ printf "\033[1;32mUpgrade complete: v%s -> v%s\033[0m\n\n" "$OLD_VER" "$APP_VERSION"
574
+ if [ "$count_added" -gt 0 ]; then
575
+ printf " \033[1;32mAdded (new): %s files\033[0m\n" "$count_added"
576
+ for f in $list_added; do printf " %s\n" "$f"; done
577
+ fi
578
+ if [ "$count_updated" -gt 0 ]; then
579
+ printf " \033[1;33mUpdated (framework): %s files\033[0m\n" "$count_updated"
580
+ for f in $list_updated; do printf " %s\n" "$f"; done
581
+ fi
582
+ printf " Unchanged: %s files\n" "$count_unchanged"
583
+ printf " Preserved (user): %s files\n" "$count_preserved"
584
+ [ "$scratchpad_example_status" = "not-seen" ] && scratchpad_example_status="not-in-manifest"
585
+ printf " Scratchpad example: %s (.cursor/scratchpad.local.example.md)\n" "$scratchpad_example_status"
586
+ printf " Scratchpad layers: post-install refreshed example-first, then baseline (see [SCRATCHPAD_LAYER] lines).\n"
587
+ [ -f "$TARGET_ROOT/.cursor/scratchpad.local.md" ] && printf " User local file: preserved (.cursor/scratchpad.local.md)\n"
588
+ if [ "$count_review" -gt 0 ]; then
589
+ printf "\n \033[1;35mReview recommended: %s files\033[0m\n" "$count_review"
590
+ for f in $list_review; do printf " %s\n" "$f"; done
591
+ printf " Check .cursor/scratchpad.local.example.md for new flags.\n"
592
+ fi
593
+ printf "\nRepository: %s\n\n" "$REPO_URL"
594
+ exit 0
595
+ fi
596
+
597
+ for rel in $FILES; do
598
+ src="$SOURCE_ROOT/$rel"
599
+ dst="$TARGET_ROOT/$rel"
600
+ if [ "$MODE" = "missing" ]; then
601
+ [ -f "$dst" ] && continue
602
+ ensure_parent "$dst"
603
+ cp -p "$src" "$dst"
604
+ continue
605
+ fi
606
+ if [ "$MODE" = "overwrite" ]; then
607
+ ensure_parent "$dst"
608
+ cp -p "$src" "$dst"
609
+ continue
610
+ fi
611
+ if [ "$MODE" = "interactive" ]; then
612
+ if [ ! -f "$dst" ]; then
613
+ ensure_parent "$dst"
614
+ cp -p "$src" "$dst"
615
+ continue
616
+ fi
617
+ printf "%s" "File exists: $rel | [o]verwrite [s]kip [q]uit: "
618
+ read -r answer
619
+ answer=$(printf "%s" "$answer" | tr 'A-Z' 'a-z')
620
+ if [ "$answer" = "q" ]; then
621
+ printf "%s\n" "Aborted."
622
+ exit 1
623
+ fi
624
+ if [ "$answer" = "o" ]; then
625
+ if [ "$BACKUP" = "true" ]; then
626
+ backup_root=$(backup_files "$TARGET_ROOT" "$rel")
627
+ printf "%s\n" "Backed up: $rel -> $backup_root"
628
+ fi
629
+ ensure_parent "$dst"
630
+ cp -p "$src" "$dst"
631
+ fi
632
+ fi
633
+ done
634
+
635
+ scratchpad_postinstall "$TARGET_ROOT" "$MODE"
636
+ validate_install_completeness "$TARGET_ROOT"
637
+
638
+ write_installed_version "$TARGET_ROOT" "$APP_VERSION"
639
+ sync_root_readme_to_its_magic "$TARGET_ROOT" || true
640
+ bootstrap_runbook_commands "$TARGET_ROOT"
641
+ [ -n "$BOOTSTRAP_NOTES" ] && printf "%s" "$BOOTSTRAP_NOTES"
642
+ [ "$BOOTSTRAP_OK" = "true" ] || exit 1
643
+
644
+ show_banner
645
+ printf "its-magic v%s\n" "$APP_VERSION"
646
+ printf "Repository: %s\n\n" "$REPO_URL"
647
+ printf "\033[1;32m Installation complete!\033[0m\n\n"
648
+ exit 0
649
+