its-magic 0.1.2-39 → 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,643 +1,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
- 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
+ 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
+