its-magic 0.1.2-38 → 0.1.2-40

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