-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·1793 lines (1764 loc) · 76.3 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·1793 lines (1764 loc) · 76.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/bash
# agentsync installer for a managed Mac: no admin rights, no interactive prompts, safe to re-run.
#
# Usage: scripts/install.sh [--source-local FOLDER ...] [--confirm-install-agent [--launcher PATH]]
# [--report-only] [--version] [--help]
# scripts/install.sh --list-folders
# scripts/install.sh --log-start AGENT | --log STEP KIND WHAT FIX
#
# --source-local FOLDER sync this folder (e.g. one inside ~/Library/CloudStorage/OneDrive-<Org>) as a live
# local source; repeatable, and already-configured folders are left as they are
# --confirm-install-agent optional background sync, the operator's choice (a setup agent never passes it):
# also build the signed launcher, sync once, install and start the two LaunchAgents
# (agentsync install-agent) and wait for the first background run (steps 3, 6-8)
# --launcher PATH with --confirm-install-agent: use this prebuilt, signed AgentSyncLauncher.app
# instead of building one
# --report-only only write the setup report (step 9), then exit; installs and logs nothing, except
# that it closes the friction log's current attempt ("<time> | end | finished") when
# that attempt has no end line yet
# --version print the commit of this checkout ("source commit: <sha> dirty <fingerprint>" when it
# has local changes; see the setup log), then "setup-prompt-compat N" as the last line
# (the line step 1 of the setup prompt checks)
# --list-folders only list the folders this Mac syncs, as candidates for --source-local: every
# folder 1 or 2 levels inside each ~/Library/CloudStorage/<provider> (names only: no
# file is opened; no dot folders), sorted, one full path per line, at most 200 then
# "(N more)"; installs nothing and writes no report, only a list-folders step in the
# setup log. Exit 0 listed; 3 none (OneDrive not signed in, or nothing synced yet);
# 4 this terminal app was denied access (macOS "Operation not permitted"), or macOS
# is still asking; its NEXT: line says which and names the click
# --log-start AGENT the setup prompt's friction log (see "Friction log" below): start an attempt
# --log STEP KIND WHAT FIX
# append one event to it; KIND is question, click, approval, deviation, error or prompt
#
# The config is $AGENTSYNC_CONFIG, else ~/agent-context/sources.toml.
#
# Friction log: --log-start and --log only append to $AGENTSYNC_FRICTION_LOG (default
# ~/agent-context/setup/friction.md; the directory is made 0700 and the file 0600, under umask 077), which
# `agentsync setup-report` reads. Each must be the first argument and takes no other option; they need no uv
# and no agentsync, write no install.log line, no install.out and no report, and print one confirmation line.
# --log-start AGENT appends "Attempt: <UTC>", "Prompt: v<SETUP_PROMPT_COMPAT>" and "Agent: AGENT" lines
# --log STEP KIND WHAT FIX
# appends "<UTC> | step STEP | KIND | WHAT | FIX" ("step 2" as STEP is read as 2; a STEP
# that is not a number, or "-", leaves the step column out and moves a non-number into
# WHAT). A KIND outside the six, or a count other than four, exits 2 and still appends
# an "error" line that names it and keeps what was given
# --report-only closes the attempt: it appends "<UTC> | end | finished" before writing the report, only when the
# last "Attempt:" has no such line (so running it twice adds one), and prints one confirmation line.
# Arguments are written as given (printf '%s'): nothing in them is expanded or run, so a backtick, $( ) or a
# typographic ’ is logged as text; a line break inside one becomes a space (one event per line).
#
# Steps, each skipped when already done:
# 1. uv in ~/.local/bin (the official installer, without touching shell profiles) unless one is on PATH
# 2. uv tool install agentsync from the checkout holding this script (a uv-managed Python 3.11; the system
# trust store for TLS; UV_TOOL_BIN_DIR pinned to ~/.local/bin, so the binary sits where the guides say);
# skipped when the last install came from this same clean checkout commit
# 3. with --confirm-install-agent only: the signed launcher at ~/Applications/AgentSyncLauncher.app, built
# with launcher/build.sh when developer tools exist (SIGN_IDENTITY passes through for a Developer ID
# build), or copied from --launcher PATH; else a valid installed one is kept. An up-to-date one is never
# rebuilt, because an ad-hoc rebuild is a new TCC identity and macOS would ask again (the developer
# variable AGENTSYNC_REBUILD_LAUNCHER=1 rebuilds it anyway)
# 4. agentsync add-source for each --source-local folder, else the flagless agentsync init: each creates
# whatever is missing (sources.toml, the docs repo and its scaffold, the inbox, the state dir) and is
# idempotent; opening the manifest migrates it (an upgrade may bring a newer schema)
# 5. status: agentsync status (its TCC probe may raise the one-time "wants to access files managed by"
# prompt). Any [FAIL] line stops steps 6-8 and the run exits 1, except the launcher's own TCC_PENDING (a
# "tcc.<source>" line, only with --confirm-install-agent), which the wait (step 8) asks the Allow for. A
# listing macOS holds for an Allow click in this terminal is a source.<id>.listable [FAIL]: it stops them
# 6. first-sync, whenever the config has a folder to sync (a [[source]] other than the inbox) and step 5 has
# no [FAIL] that stops it: agentsync sync --once --materialise-budget 0 (a non-zero exit fails the run;
# 75, a cycle already running, skips): no downloads, so the files already on this Mac are converted now,
# no online-only file is downloaded here and this step's time does not grow with the folders' size; each
# later sync downloads and converts the online-only files it deferred, within its per-run budget. Its
# output is shown as its "converted N, deferred M online-only" line(s), each prefixed
# "first sync: " (and logged as the step's note converted-N-deferred-M), else as all it printed. The
# flag is passed unconditionally: sync --help hides it, and the agentsync installed here always has it
# With --confirm-install-agent and a first sync that ran:
# 7. agent: agentsync install-agent
# 8. wait: launchctl kickstart gui/<uid>/com.agentsync.poll, then launchctl print every 3 s for up to
# $AGENTSYNC_WAIT_SECONDS (default 180 s, 3 minutes) until the first background run is past the macOS
# access check or has exited 0 (the runs count before and after, never a fixed sleep). Past the check:
# the job is running and its launcher (the job's pid) has logged, since step 7 began, a CANARY_OK line
# for every --canary path in the job's arguments and no TCC_PENDING or TCC_DENIED line; it then prints
# "background sync: running (...)" and leaves that run converting in the background. A job without
# canaries waits for an exit 0. While macOS waits for Allow (TCC_PENDING in the launcher log, or exit
# 79) it prints one "ACTION:" line and starts the job again after each attempt.
# Steps 6 and 8 and the closing status (see NEXT below) print a progress line at least every 15 s, so a
# coding tool that stops a command which has
# printed nothing for a while does not stop this one. A whole run with --confirm-install-agent takes the
# first sync's time plus at most the 3-minute wait: give it a 10-minute command timeout. A stopped run
# (SIGTERM, SIGINT, SIGHUP) still logs its end (rc 143, 130, 129), writes the report and prints NEXT:.
# 9. report, at every exit after the arguments are read (failures and usage errors too) except in a dry run
# or a --list-folders run: agentsync setup-report --out $AGENTSYNC_SETUP_REPORT (default
# ~/agent-context/setup-report.md); when agentsync is missing or that fails, a shell report with the
# same headings (machine facts, install.log, this run's doctor output, friction.md; the home path,
# login name, full name, OneDrive-<org> and the --source-local folder names redacted); when the report
# ends with its issue link (https://github.com/renchris/agent-context-sync/issues/new?template=...), one
# line "issue link (review the report first): <link>" follows, the last line before NEXT:
# and finally one line starting "NEXT:" with the single next step and, in brackets, the report path. A run
# that ends with a folder to sync and nothing failed ends on the loop's NEXT (KISS K02): install.sh runs
# `agentsync status` once more with its NEXT line on, prints none of its output and lifts its first "NEXT:"
# line ("run ~/.local/bin/agentsync sync and follow its NEXT line" when it prints none); with
# --confirm-install-agent the line starts "background sync: running; " (or "ok; "). A [FAIL] line in that
# status is printed and the NEXT says to fix it; a listing macOS held for an Allow click in this terminal is
# the NEXT and the run exits 1. A run without a folder over an existing config ends on that status's NEXT
# too (its "no folder is synced yet" step). Nothing
# else it prints is an instruction: agentsync is always called by its full path (~/.local/bin/agentsync), so
# nothing needs adding to PATH or to a shell profile; uv's "not on your PATH ... update-shell" hint is
# filtered out of its output; every other agentsync call gets AGENTSYNC_NO_NEXT_HINT=1 (no "next:" hints of
# its own), and with --confirm-install-agent step 5 gets AGENTSYNC_AGENT_STEP_PENDING=1 (its launchd.* lines
# then say the agent step below installs the LaunchAgents instead of naming a command).
#
# Setup prompt: SETUP_PROMPT_COMPAT (below) is the N of "setup prompt vN" in README.md ("Set up on a new Mac:
# one prompt"), whose step 1 requires `install.sh --version` to print at least that number. Bump both
# whenever the prompt starts to depend on new behaviour of this installer or of agentsync, so an older
# published checkout stops at step 1 instead of failing later (tests/test_install_oneshot.py checks they
# match).
#
# Setup log: every real run (never a dry run or --report-only) appends to $AGENTSYNC_SETUP_LOG (default
# ~/agent-context/setup/install.log, directory 0700) one "start" line (compat, install.sh commit when the source
# is a checkout: "commit=<sha>" or, with local changes, "commit=<sha>-dirty tree=<fingerprint>", the
# fingerprint being the first 12 hex digits of the SHA-256 of `git diff HEAD` in the source, which reproduces
# it; the source, launchd=simulated under the test seam, the arguments), one line per step (UTC start, step, seconds,
# exit status, done / skipped / failed: uv, agentsync, launcher, config, status, first-sync, agent, wait; or
# list-folders alone), then the report step's line, then one "end" line (exit status, total seconds, the report
# included). The report is written while a provisional end line is the log's last line, so it reads a
# finished run; that line is then replaced by the report step's line and the final end line (when another
# line followed it meanwhile, the report step's line is appended instead). `agentsync setup-report` reads it
# and redacts it. git runs read-only (GIT_OPTIONAL_LOCKS=0: not even the index's stat
# cache is rewritten). Every log write also sets the setup directory to 0700 and install.log, friction.md
# and install.out in it to 0600 (files made earlier with a looser mode included).
#
# Output copy: the same runs also copy everything they print (stdout and stderr, as the agent saw it) to
# install.out next to install.log (0600): one "# run=<id> <UTC> install.sh <arguments>" line, then the output.
# Only its last 2000 lines are kept. `agentsync setup-report` may embed its (redacted) tail.
#
# Exit status: 0 when every step ran, 2 on a usage error and when a run created the config and has no folder to
# sync (its NEXT names --list-folders; a run over an existing config exits 0), 1 when a step failed (a status
# [FAIL] that stops step 6, a listing held for an Allow click, a network/proxy/TLS failure
# of uv, a failed first sync, a background run that exited 80, and --confirm-install-agent that did not
# install the LaunchAgents), 3 when the wait ran out before a background run exited 0 (exit 79 then: macOS
# still waits for Allow), 143 / 130 / 129 when stopped by a signal; --list-folders: see above. Every exit
# after argument parsing ends with one "NEXT:" line; for a background exit 80 (an earlier "Don't Allow") or
# a 79 at the timeout it names the click: turn on agentsync-launcher in System Settings > Privacy & Security
# > Files and Folders. agentsync exit codes named in messages: 0 ok, 75 lock busy, 77 sign-in required, 78
# configuration invalid, 79 TCC pending (macOS waits for Allow), 80 TCC denied. --log-start and --log:
# 0 logged, 2 a usage error (a bad KIND or argument count: see "Friction log"), 1 the friction log
# could not be written; neither prints a NEXT: line.
#
# Dry run: AGENTSYNC_INSTALL_DRY_RUN=1 prints every step that would change something and changes nothing (no
# log, no install.out, no report, no friction-log line); its NEXT: line says to re-run without it. With
# --log-start or --log it prints one "dry run:" line instead, and exits 0.
#
# Test-only seams, never for a real Mac: AGENTSYNC_SIMULATE_LAUNCHD=1 replaces install-agent, the kickstart
# and the wait with "SIMULATED" lines (no plist, no launchctl write; the wait succeeds at once; the log and
# the report record "launchd: simulated"), for sandboxed runs of the setup prompt. AGENTSYNC_LAUNCHCTL (the
# launchctl to run, default /bin/launchctl), AGENTSYNC_WAIT_POLL_SECONDS (default 3),
# AGENTSYNC_PROGRESS_SECONDS (default 15), AGENTSYNC_LIST_TIMEOUT (default 90: seconds --list-folders waits
# on one provider folder while macOS asks) and AGENTSYNC_LIST_TOTAL_SECONDS (default 100: its cap across all
# providers, under a coding tool's 2-minute default command timeout) are for the stubbed tests.
SETUP_PROMPT_COMPAT=7 # the README prompt's "setup prompt vN": bump both together (see the header)
set -euo pipefail
# ------------------------------------------------------------------------------------------------ friction log
# --log-start / --log (see "Friction log" in the header), handled before anything else: no uv, no
# git, no install.log, no install.out, no report. Every argument is written with printf '%s', never evaluated.
# The setup prompt's friction log (the path `agentsync setup-report` reads too).
friction_file() {
local f="${AGENTSYNC_FRICTION_LOG:-$HOME/agent-context/setup/friction.md}"
case "$f" in
\~/*) f="$HOME/${f#\~/}" ;;
esac
printf '%s' "$f"
}
FRICTION_KINDS="question, click, approval, deviation, error or prompt"
# Append TEXT (whole lines) to the friction log: its directory 0700 when this creates it (or it is the default
# ~/agent-context/setup), the file 0600; a file that does not end in a newline gets one first.
friction_append() {
local f d
f="$(friction_file)"
d="${f%/*}"
[ "$d" != "$f" ] || d="."
umask 077
if [ ! -d "$d" ]; then
mkdir -p "$d" || return 1
elif [ "$d" = "$HOME/agent-context/setup" ] && [ ! -L "$d" ] && [ -O "$d" ]; then
chmod 700 "$d" 2>/dev/null || true
fi
if [ -s "$f" ] && [ -n "$(tail -c 1 "$f" 2>/dev/null)" ]; then
printf '\n' >>"$f" || return 1
fi
printf '%s' "$1" >>"$f" || return 1
if [ -f "$f" ] && [ ! -L "$f" ] && [ -O "$f" ]; then chmod 600 "$f" 2>/dev/null || true; fi
}
friction_cmd() { # OPTION ARGS...: the friction-log options; their exit status
local op="$1" now line step kind what fix note="" v a rc=0
shift
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
case "$op" in
--log-start)
if [ $# -ne 1 ]; then
printf 'usage error: --log-start takes one argument, your tool and model id, and no other option (see --help)\n' >&2
return 2
fi
v="${1//$'\r'/ }"
v="${v//$'\n'/ }"
friction_append "Attempt: $now"$'\n'"Prompt: v$SETUP_PROMPT_COMPAT"$'\n'"Agent: ${v:-unknown}"$'\n' || rc=1
[ "$rc" -ne 0 ] || printf 'friction log: attempt started in %s\n' "$(friction_file)"
;;
--log)
if [ $# -ne 4 ]; then
v=""
for a in "$@"; do v="${v:+$v / }$a"; done
v="${v//$'\r'/ }"
v="${v//$'\n'/ }"
friction_append "$now | error | install.sh --log got $# argument(s), not 4 (STEP KIND WHAT FIX, each in single quotes): ${v:--} | -"$'\n' || true
printf 'usage error: --log takes four arguments, STEP KIND WHAT FIX, each in single quotes (see --help); logged as an error line\n' >&2
return 2
fi
step="${1//$'\r'/ }"
step="${step//$'\n'/ }"
kind="${2//$'\r'/ }"
kind="${kind//$'\n'/ }"
what="${3//$'\r'/ }"
what="${what//$'\n'/ }"
fix="${4//$'\r'/ }"
fix="${fix//$'\n'/ }"
case "$step" in
[Ss]tep\ *) step="${step#[Ss]tep }" ;;
esac
case "$step" in
'' | *[!0-9]*)
[ "$step" = "-" ] || [ -z "$step" ] || what="(step $step) $what"
step=""
;;
esac
line="$now |${step:+ step $step |}"
case "$kind" in
question | click | approval | deviation | error | prompt)
line="$line $kind | $what | $fix"
;;
*)
line="$line error | install.sh --log: unknown kind '$kind' (not $FRICTION_KINDS); the event was: $what | $fix"
note="usage error: --log kind '$kind' is not $FRICTION_KINDS; logged as an error line"
rc=2
;;
esac
if ! friction_append "$line"$'\n'; then
rc=1
elif [ "$rc" -eq 0 ]; then
printf 'friction log: %s logged in %s\n' "$kind" "$(friction_file)"
fi
[ -z "$note" ] || printf '%s (see --help)\n' "$note" >&2
;;
--log-end) # hidden, left out of --help: step 3 of a saved v6 prompt runs "--log-end && --report-only"
if [ $# -ne 0 ]; then
printf 'usage error: --log-end takes no argument and no other option (see --help)\n' >&2
return 2
fi
friction_close_attempt # the same idempotent close as --report-only, so the pair adds one end line
;;
esac
[ "$rc" -ne 1 ] || printf 'error: could not write the friction log %s\n' "$(friction_file)" >&2
return "$rc"
}
# --report-only: close the friction log's current attempt with "<UTC> | end | finished", only when its last
# "Attempt:" has no end line yet (so a second --report-only adds none). No log or no attempt: nothing to close.
friction_close_attempt() {
local f
f="$(friction_file)"
[ -f "$f" ] || return 0
awk '/^Attempt:/ { open = 1; ended = 0; next } /\| end \| finished[[:space:]]*$/ { ended = 1 }
END { exit !(open && !ended) }' "$f" || return 0
if friction_append "$(date -u +%Y-%m-%dT%H:%M:%SZ) | end | finished"$'\n'; then
printf 'friction log: attempt finished in %s\n' "$f"
else
printf 'warning: could not write the friction log %s\n' "$f" >&2
fi
}
case "${1:-}" in
--log-start | --log | --log-end)
if [ "${AGENTSYNC_INSTALL_DRY_RUN:-}" = "1" ]; then # the dry run changes nothing, the friction log included
printf 'dry run: %s would write to the friction log %s; nothing is written\n' "$1" "$(friction_file)"
exit 0
fi
rc=0
friction_cmd "$@" || rc=$?
exit "$rc"
;;
esac
ORIG_ARGS="" # every flag given, for a re-run after a failure
BASE_ARGS="" # the same without --source-local FOLDER, for a re-run once the folders are in the config
ALL_ARGS="" # every argument as given, for install.out
skip_next=0
for a in "$@"; do
ALL_ARGS="$ALL_ARGS $(printf '%q' "$a")"
if [ "$skip_next" -eq 1 ]; then
skip_next=0
ORIG_ARGS="$ORIG_ARGS $(printf '%q' "$a")"
continue
fi
case "$a" in
--report-only | --no-report | --list-folders | --version | -h | --help) ;; # NEXT is for the real run
--source-local)
skip_next=1
ORIG_ARGS="$ORIG_ARGS $(printf '%q' "$a")"
;;
*)
ORIG_ARGS="$ORIG_ARGS $(printf '%q' "$a")"
BASE_ARGS="$BASE_ARGS $(printf '%q' "$a")"
;;
esac
done
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo="$(cd "$here/.." && pwd)"
SELF="$(printf '%q' "$here/$(basename "${BASH_SOURCE[0]}")")" # this script, absolute, for re-run commands
DRY_RUN=0
[ "${AGENTSYNC_INSTALL_DRY_RUN:-}" != "1" ] || DRY_RUN=1 # the dry run (see the header)
INSTALL_AGENT=0
REBUILD=0
[ "${AGENTSYNC_REBUILD_LAUNCHER:-}" != "1" ] || REBUILD=1 # developer only: rebuild an up-to-date launcher (step 3)
REPORT=1
REPORT_ONLY=0
LIST_FOLDERS=0
LAUNCHER_SRC=""
SOURCE=""
SOURCE_KIND="-"
SOURCE_LOCALS=()
FOLDERS=()
CONFIG="${AGENTSYNC_CONFIG:-$HOME/agent-context/sources.toml}"
APP_NAME="AgentSyncLauncher.app"
APP_DEST="$HOME/Applications/$APP_NAME"
UV_INSTALLER_URL="https://astral.sh/uv/install.sh"
PROMPT_TEXT='“agentsync-launcher” wants to access files managed by “<your sync app, e.g. OneDrive>”'
REPORT_PATH="${AGENTSYNC_SETUP_REPORT:-$HOME/agent-context/setup-report.md}"
SIMULATE=0
[ "${AGENTSYNC_SIMULATE_LAUNCHD:-}" != "1" ] || SIMULATE=1
LAUNCHCTL="${AGENTSYNC_LAUNCHCTL:-/bin/launchctl}"
WAIT_SECONDS="${AGENTSYNC_WAIT_SECONDS:-180}"
WAIT_POLL="${AGENTSYNC_WAIT_POLL_SECONDS:-3}"
PROGRESS_SECONDS="${AGENTSYNC_PROGRESS_SECONDS:-15}" # a progress line at least this often in steps 6 and 8
LIST_TIMEOUT="${AGENTSYNC_LIST_TIMEOUT:-90}"
LIST_TOTAL="${AGENTSYNC_LIST_TOTAL_SECONDS:-100}"
POLL_LABEL="com.agentsync.poll"
ISSUE_URL="https://github.com/renchris/agent-context-sync/issues/new?template=setup-report.yml" # setup_report.ISSUE_URL
ISSUE_LINK_PREFIX="issue link (review the report first): "
OUT_MAX_LINES=2000 # install.out keeps this many lines
TEE_PIDS="" # the two tee processes copying this run's output to install.out
# agentsync prints no "next:" hints of its own under this installer: its NEXT line is the only instruction.
export AGENTSYNC_NO_NEXT_HINT=1
# uv installs the agentsync binary into ~/.local/bin, the path every guide and NEXT line names (KISS K02).
export UV_TOOL_BIN_DIR="$HOME/.local/bin"
AGENTSYNC=""
COMMIT="-"
DIRTY="" # the SOURCE checkout's local-change fingerprint (tree_fingerprint), empty when clean or unknown
PARSED=0 # 1 once the arguments are read: from then on every exit writes the report and a NEXT line
NEXT_MSG="" # the NEXT line the EXIT trap prints
RERUN="" # the command a stopped run names (default: this script with the same arguments)
LAST_ERROR="" # the last error message, for the shell report
DOCTOR_LOG="" # this run's doctor output, for the shell report
SYNC_OUT="" # the first sync's stdout
LOOP_OUT="" # the closing status's output (the loop's NEXT)
END_LINE="" # the provisional end line in install.log (set at the exit, before the report)
REPORT_LINE="" # the report step's line, written before the end line (finish_setup_log)
ACTION_SHOWN=0 # the ACTION line is printed once
say() { printf '%s\n' "$*"; }
warn() { printf 'warning: %s\n' "$*" >&2; }
fail() {
local rc=$? # the failed command's status (every caller is `cmd || fail ...`)
[ "$rc" -ne 0 ] || rc=1
step_end failed "$rc"
printf 'error: %s\n' "$*" >&2
LAST_ERROR="$*"
NEXT_MSG="fix the error above, then re-run: $SELF$ORIG_ARGS"
exit 1
}
usage_error() {
printf 'usage error: %s (see --help)\n' "$*" >&2
LAST_ERROR="usage error: $*"
if [ "$PARSED" -eq 1 ]; then
log_start
NEXT_MSG="fix the usage error above (see $SELF --help), then run the corrected command"
fi
exit 2
}
show_help() { awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "$0"; }
have_devtools() {
local d
d="$(/usr/bin/xcode-select -p 2>/dev/null || true)"
[ -n "$d" ] && [ -d "$d" ]
}
# git, read-only: GIT_OPTIONAL_LOCKS=0 keeps even `git diff` from rewriting the index's stat cache.
git_ro() { GIT_OPTIONAL_LOCKS=0 /usr/bin/git "$@"; }
# The first 12 hex digits of the SHA-256 of `git diff HEAD` in the checkout $1: which local changes ran.
tree_fingerprint() {
git_ro -C "$1" diff --no-ext-diff --no-color HEAD -- 2>/dev/null | /usr/bin/shasum -a 256 | cut -c1-12
}
# "<sha12>" of the checkout $1, "<sha12> dirty <fingerprint>" with local changes; empty when git cannot say.
checkout_commit() {
local c
have_devtools || return 0
c="$(git_ro -C "$1" rev-parse --short=12 HEAD 2>/dev/null || true)"
[ -n "$c" ] || return 0
if git_ro -C "$1" diff --quiet HEAD -- 2>/dev/null; then
printf '%s' "$c"
else
printf '%s dirty %s' "$c" "$(tree_fingerprint "$1")"
fi
}
# The commit of the checkout at $1 without running git (no developer tools: /usr/bin/git would offer to
# install them); empty when it cannot be read.
git_head_file() {
local head ref
head="$(cat "$1/.git/HEAD" 2>/dev/null || true)"
case "$head" in
"ref: "*)
ref="${head#ref: }"
if [ -f "$1/.git/$ref" ]; then
head="$(cat "$1/.git/$ref")"
else
head="$(awk -v r="$ref" '$2 == r { print $1; exit }' "$1/.git/packed-refs" 2>/dev/null || true)"
fi
;;
esac
case "$head" in
[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]*) printf '%s' "$head" | cut -c1-12 ;;
esac
}
show_version() { # the compat line last: step 1 of the setup prompt reads the last line of its output
local c
if [ -e "$repo/.git" ]; then
c="$(checkout_commit "$repo")"
[ -n "$c" ] || c="$(git_head_file "$repo")"
case "$c" in
*" dirty "*) say "source commit: $c (local changes in this checkout; git pull --ff-only keeps them)" ;;
*) say "source commit: ${c:-unknown}" ;;
esac
fi
say "setup-prompt-compat $SETUP_PROMPT_COMPAT"
}
# What an agentsync (or launchd) exit code means, for the messages.
rc_meaning() {
case "$1" in
0) printf 'ok' ;;
1) printf 'failed' ;;
2) printf 'usage error' ;;
3) printf 'the wait for the first background run ran out' ;;
75) printf 'lock busy: another agentsync cycle is running' ;;
77) printf 'sign-in required' ;;
78) printf 'configuration invalid' ;;
79) printf 'TCC pending: macOS is waiting for Allow' ;;
80) printf "TCC denied: an earlier \"Don't Allow\"" ;;
129) printf 'stopped by SIGHUP' ;;
130) printf 'stopped by SIGINT' ;;
143) printf 'stopped by SIGTERM (a tool timeout?)' ;;
*) printf 'exit %s' "$1" ;;
esac
}
# Run a command for at most $1 seconds (bash 3.2 and macOS have no timeout(1)); its status, 143 if killed.
with_timeout() {
local t="$1" pid wd rc=0
shift
"$@" &
pid=$!
(
sleep "$t"
kill "$pid"
) >/dev/null 2>&1 &
wd=$!
wait "$pid" || rc=$?
kill "$wd" 2>/dev/null || true
wait "$wd" 2>/dev/null || true
return "$rc"
}
# Run a command, or only print it in a dry run.
run() {
if [ "$DRY_RUN" -eq 1 ]; then
printf '[dry-run]'
printf ' %q' "$@"
printf '\n'
return 0
fi
"$@"
}
# Setup log (see the header): appended to, never printed; a log that cannot be written is ignored.
SETUP_LOG="${AGENTSYNC_SETUP_LOG:-$HOME/agent-context/setup/install.log}"
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)-$$"
STEP=""
STEP_T0=0
STEP_AT=""
STARTED=0
utc_now() { date -u +%Y-%m-%dT%H:%M:%SZ; }
SETUP_DIR="$(dirname "$SETUP_LOG")"
INSTALL_OUT="$SETUP_DIR/install.out"
# The setup directory 0700 and its install.log, friction.md and install.out 0600, also when an earlier run or
# the agent made them looser; only what this user owns (never $HOME itself, never through a symlink).
tighten_setup_modes() {
local f
if [ -d "$SETUP_DIR" ] && [ ! -L "$SETUP_DIR" ] && [ -O "$SETUP_DIR" ] && [ "$SETUP_DIR" != "$HOME" ]; then
chmod 700 "$SETUP_DIR"
fi
for f in "$SETUP_LOG" "$(friction_file)" "$INSTALL_OUT"; do
if [ -f "$f" ] && [ ! -L "$f" ] && [ -O "$f" ]; then chmod 600 "$f"; fi
done
}
log_at() { # STAMP TEXT...: one line "<stamp> run=<id> <text>"; nothing in a dry run or under --report-only
local stamp="$1"
shift
[ "$DRY_RUN" -eq 0 ] && [ "$REPORT_ONLY" -eq 0 ] || return 0
(
umask 077 # a created log directory (and ~/agent-context) is 0700, the log 0600
mkdir -p "$SETUP_DIR" && printf '%s run=%s %s\n' "$stamp" "$RUN_ID" "$*" >>"$SETUP_LOG"
tighten_setup_modes
) 2>/dev/null || true
}
log_start() { # the run's "start" line, once
local sim=""
[ "$STARTED" -eq 0 ] || return 0
STARTED=1
[ "$SIMULATE" -eq 0 ] || sim=" launchd=simulated"
log_at "$(utc_now)" "start install.sh compat=$SETUP_PROMPT_COMPAT commit=$COMMIT${DIRTY:+ tree=$DIRTY}" \
"kind=$SOURCE_KIND" \
"source=$(printf '%q' "${SOURCE:--}")$sim args=${ORIG_ARGS# }"
}
# install.out (see the header): keep its last $OUT_MAX_LINES lines. Only while no tee writes to it (a tee holds
# the old file open, so its later lines would go to a replaced file).
trim_install_out() {
[ -f "$INSTALL_OUT" ] && [ ! -L "$INSTALL_OUT" ] || return 0
[ "$(wc -l <"$INSTALL_OUT" | tr -d ' ')" -gt "$OUT_MAX_LINES" ] || return 0
(
umask 077
tail -n "$OUT_MAX_LINES" "$INSTALL_OUT" >"$INSTALL_OUT.$$.tmp" && mv -f "$INSTALL_OUT.$$.tmp" "$INSTALL_OUT"
) 2>/dev/null || rm -f "$INSTALL_OUT.$$.tmp"
}
# Copy stdout and stderr (each still to where it went) to install.out, from here to the exit. The tees
# ignore INT, TERM and HUP: a tool that stops the whole process group still gets the end of the run (its
# report and NEXT line), and they end when the run closes its output (stop_capture).
start_capture() {
[ "$DRY_RUN" -eq 0 ] && [ "$REPORT_ONLY" -eq 0 ] || return 0
(
umask 077
mkdir -p "$SETUP_DIR" && touch "$INSTALL_OUT" && tighten_setup_modes
) 2>/dev/null || return 0
[ -f "$INSTALL_OUT" ] && [ ! -L "$INSTALL_OUT" ] && [ -w "$INSTALL_OUT" ] || return 0
trim_install_out
printf '# run=%s %s install.sh%s\n' "$RUN_ID" "$(utc_now)" "$ALL_ARGS" >>"$INSTALL_OUT" 2>/dev/null || return 0
exec > >(
trap '' INT TERM HUP
exec tee -a "$INSTALL_OUT"
)
TEE_PIDS="$!"
exec 2> >(
trap '' INT TERM HUP
exec tee -a "$INSTALL_OUT" >&2
)
TEE_PIDS="$TEE_PIDS $!"
}
# At the exit: close this run's output, give the tees up to 3 s to copy the rest (a process this run started
# and left running, such as a stopped first sync, may hold the output open longer), then trim install.out.
stop_capture() {
local p n=0 alive
[ -n "$TEE_PIDS" ] || return 0
exec >/dev/null 2>&1
while [ "$n" -lt 30 ]; do
alive=0
for p in $TEE_PIDS; do
! kill -0 "$p" 2>/dev/null || alive=1
done
[ "$alive" -eq 1 ] || break
sleep 0.1
n=$((n + 1))
done
[ "$alive" -eq 1 ] || trim_install_out
}
step_start() {
STEP="$1"
STEP_T0=$SECONDS
STEP_AT="$(utc_now)"
}
step_end() { # RESULT [RC] [NOTE]: close the open step (done | skipped | failed)
local text
[ -n "$STEP" ] || return 0
text="step=$STEP seconds=$((SECONDS - STEP_T0)) rc=${2:-0} result=$1${3:+ note=$3}"
if [ "$STEP" = "report" ] && [ -n "$END_LINE" ]; then
REPORT_LINE="$STEP_AT run=$RUN_ID $text" # finish_setup_log puts it before the end line
else
log_at "$STEP_AT" "$text"
fi
STEP=""
}
# After the report (see "Setup log" in the header): replace the provisional end line, when it is still the
# log's last line, with the report step's line and the final end line; else append the report step's line.
finish_setup_log() { # RC
[ -n "$REPORT_LINE" ] && [ "$DRY_RUN" -eq 0 ] && [ "$REPORT_ONLY" -eq 0 ] || return 0
(
umask 077
if [ -f "$SETUP_LOG" ] && [ ! -L "$SETUP_LOG" ] && [ "$(tail -n 1 "$SETUP_LOG")" = "$END_LINE" ]; then
{
sed '$d' "$SETUP_LOG"
printf '%s\n' "$REPORT_LINE" "$(utc_now) run=$RUN_ID end rc=$1 seconds=$SECONDS"
} >"$SETUP_LOG.$$.tmp" && mv -f "$SETUP_LOG.$$.tmp" "$SETUP_LOG"
else
printf '%s\n' "$REPORT_LINE" >>"$SETUP_LOG"
fi
tighten_setup_modes
) 2>/dev/null || rm -f "$SETUP_LOG.$$.tmp"
REPORT_LINE=""
}
# ------------------------------------------------------------------------------------------------ report
# The agentsync that can write the report: this run's, else one a previous install left.
report_agentsync() {
local d
if [ -n "$AGENTSYNC" ] && [ -x "$AGENTSYNC" ]; then
printf '%s' "$AGENTSYNC"
return 0
fi
for d in "${UV_TOOL_BIN_DIR:-}" "${XDG_BIN_HOME:-}" "$HOME/.local/bin"; do
if [ -n "$d" ] && [ -x "$d/agentsync" ]; then
printf '%s' "$d/agentsync"
return 0
fi
done
}
# "value<TAB>placeholder<TAB>w" lines (w: whole words only) for redact_stream; the %q form too, since
# install.log quotes paths that way.
redaction_pairs() {
local cs="$HOME/Library/CloudStorage" n=0 e org f rel part name tok user
add_pair() {
[ "${#1}" -ge 3 ] || return 0
printf '%s\t%s\t%s\n' "$1" "$2" "${3:-}"
local q
q="$(printf '%q' "$1")"
[ "$q" = "$1" ] || printf '%s\t%s\t%s\n' "$q" "$2" "${3:-}"
}
if [ -n "${TMPDIR:-}" ] && [ -d "$TMPDIR" ]; then # this account's per-user temp folder links reports
add_pair "$(cd "$TMPDIR" && pwd -P)" "<tmp>"
add_pair "${TMPDIR%/}" "<tmp>"
fi
add_pair "$HOME" "~"
if [ -d "$cs" ]; then
for e in "$cs"/*; do
[ -e "$e" ] || continue
e="$(basename "$e")"
case "$e" in
OneDrive-Personal | OneDrive-Personal\ *) continue ;;
OneDrive-*) org="${e#OneDrive-}" ;;
SharedLibraries-*) org="${e#SharedLibraries-}" ;;
*) continue ;;
esac
n=$((n + 1))
add_pair "$org" "<org-$n>"
done
fi
n=0
for f in ${FOLDERS[@]+"${FOLDERS[@]}"}; do
case "$f" in
"$cs"/*/*) rel="${f#"$cs"/*/}" ;;
*) continue ;;
esac
while [ -n "$rel" ]; do
part="${rel%%/*}"
[ "$part" = "$rel" ] && rel="" || rel="${rel#*/}"
n=$((n + 1))
add_pair "$part" "<folder-$n>"
done
done
name="$(id -F 2>/dev/null || true)"
if [ -n "$name" ]; then
add_pair "$name" "<name>"
for tok in $name; do
add_pair "$tok" "<name>" w
done
fi
user="$(id -un 2>/dev/null || true)"
[ -z "$user" ] || add_pair "$user" "<user>" w
}
redact_stream() {
REDACT_PAIRS="$(redaction_pairs)" awk '
function lit(s, v, p, word, out, i, n, pre, post) {
out = ""
n = length(v)
while ((i = index(s, v)) > 0) {
pre = (i > 1) ? substr(s, i - 1, 1) : substr(out, length(out), 1)
post = substr(s, i + n, 1)
if (word && (pre ~ /[A-Za-z0-9_]/ || post ~ /[A-Za-z0-9_]/)) {
out = out substr(s, 1, i + n - 1)
} else {
out = out substr(s, 1, i - 1) p
}
s = substr(s, i + n)
}
return out s
}
BEGIN { np = split(ENVIRON["REDACT_PAIRS"], rows, "\n") }
{
line = $0
for (k = 1; k <= np; k++) {
if (split(rows[k], f, "\t") < 2) continue
line = lit(line, f[1], f[2], f[3] == "w")
}
print line
}'
}
launchd_field() { # TEXT KEY: the value of a top-level "KEY = value" line of `launchctl print`
printf '%s\n' "$1" | sed -n "s/^ $2 = //p" | head -1
}
launchd_print() { with_timeout 10 "$LAUNCHCTL" print "gui/$(id -u)/$POLL_LABEL" 2>/dev/null; }
leading_int() { # "79: ..." -> 79; "(never exited)" -> empty
case "$1" in
[0-9]*) printf '%s' "${1%%[!0-9]*}" ;;
esac
}
# The shell report's body (redact_stream redacts it): the same headings as `agentsync setup-report`.
fallback_body() {
local rc="$1" why="$2" friction="$3" cs="$HOME/Library/CloudStorage" p v k n
say "# agentsync setup report"
say ""
say "## Summary"
say ""
say "- written by: scripts/install.sh shell fallback ($why), $(utc_now)"
say "- install.sh: setup-prompt-compat $SETUP_PROMPT_COMPAT, source commit $COMMIT${DIRTY:+ tree=$DIRTY}, exit $rc ($(rc_meaning "$rc"))"
if [ "$SIMULATE" -eq 1 ]; then say "- launchd: simulated"; else say "- launchd: real"; fi
[ -z "$LAST_ERROR" ] || say "- last error: $LAST_ERROR"
say ""
say "## Agent friction log"
say ""
if [ -n "$friction" ]; then
printf '%s\n' "$friction"
else
say "No friction log at $(friction_file)."
fi
say ""
say "## Environment"
say ""
say "- macOS: $(sw_vers -productVersion 2>/dev/null || echo '?') ($(sw_vers -buildVersion 2>/dev/null || echo '?'))"
say "- arch: $(uname -m)"
v="$(with_timeout 5 /usr/bin/profiles status -type enrollment 2>&1 </dev/null | tr '\n' ';' | sed 's/;$//; s/;/; /g' || true)"
say "- MDM enrollment (profiles status -type enrollment): ${v:-unknown}"
v="$(/usr/bin/xcode-select -p 2>/dev/null || true)"
say "- developer tools (xcode-select -p): ${v:-none}"
v="$(command -v uv 2>/dev/null || true)"
[ -n "$v" ] || { [ ! -x "$HOME/.local/bin/uv" ] || v="$HOME/.local/bin/uv"; }
say "- uv: ${v:-not found}"
if have_devtools; then
v="$(command -v python3 2>/dev/null || true)"
say "- python3: ${v:-not found}"
else
say "- python3: only the /usr/bin stub (no developer tools; not run)"
fi
[ ! -d "$HOME/.local/share/uv/python" ] || say "- uv-managed Pythons: $(find "$HOME/.local/share/uv/python" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')"
v="$(report_agentsync)"
say "- agentsync: ${v:-not installed}"
v=""
for k in HTTPS_PROXY https_proxy HTTP_PROXY http_proxy ALL_PROXY all_proxy NO_PROXY no_proxy; do
eval "p=\${$k:-}"
[ -z "$p" ] || v="$v $k"
done
if [ -n "$v" ]; then say "- proxy environment: yes (set:$v)"; else say "- proxy environment: no"; fi
n=0
k=0
if [ -d "$cs" ]; then
n="$(find "$cs" -mindepth 1 -maxdepth 1 ! -name '.*' 2>/dev/null | wc -l | tr -d ' ')"
k="$(find "$cs" -mindepth 1 -maxdepth 1 -name 'OneDrive-*' 2>/dev/null | wc -l | tr -d ' ')"
fi
say "- ~/Library/CloudStorage providers: $n (OneDrive: $k)"
say ""
say "## Installer"
say ""
if [ -f "$SETUP_LOG" ]; then
say "The last 60 lines of $SETUP_LOG:"
say ""
say "~~~"
tail -60 "$SETUP_LOG"
say "~~~"
else
say "No install log at $SETUP_LOG."
fi
say ""
say "## Configuration"
say ""
if [ -f "$CONFIG" ]; then
say "- config: $CONFIG (exists)"
say "- [[source]] tables: $(grep -c '^[[:space:]]*\[\[source\]\]' "$CONFIG" 2>/dev/null || true)"
else
say "- config: $CONFIG (missing)"
fi
say ""
say "## Doctor"
say ""
if [ -n "$DOCTOR_LOG" ] && [ -s "$DOCTOR_LOG" ]; then
say "This run's agentsync doctor output:"
say ""
say "~~~"
cat "$DOCTOR_LOG"
say "~~~"
else
say "Not run ($why)."
fi
say ""
say "## Status"
say ""
say "Not run ($why)."
say ""
say "## Background runs"
say ""
if [ "$SIMULATE" -eq 1 ]; then
say "- launchd: simulated (AGENTSYNC_SIMULATE_LAUNCHD=1, a test seam: nothing was installed or started)"
else
if [ -f "$HOME/Library/LaunchAgents/$POLL_LABEL.plist" ]; then v="present"; else v="absent"; fi
say "- $POLL_LABEL plist: $v"
p="$(launchd_print || true)"
if [ -n "$p" ]; then
v="$(launchd_field "$p" "last exit code")"
k="$(leading_int "$v")"
say "- loaded: state $(launchd_field "$p" state), runs $(launchd_field "$p" runs), last exit code ${v:-?}${k:+ ($(rc_meaning "$k"))}"
else
say "- loaded: no"
fi
fi
say ""
say "## Recent errors"
say ""
[ -z "$LAST_ERROR" ] || say "- $LAST_ERROR"
if [ -f "$SETUP_LOG" ] && grep -q 'result=failed' "$SETUP_LOG" 2>/dev/null; then
say "- failed steps in the install log:"
say ""
say "~~~"
grep 'result=failed' "$SETUP_LOG" | tail -10
say "~~~"
elif [ -z "$LAST_ERROR" ]; then
say "None recorded."
fi
say ""
say "## Redaction"
say ""
say "Shell fallback redaction: the home path (~), the login name (<user>), the full name (<name>), the"
say "organisation after OneDrive- (<org-N>) and the --source-local folder names (<folder-N>). Read the report"
say "before sending it: anything else confidential is yours to remove."
}
# The friction log for the shell report: friction.md, else what an earlier report holds under its heading.
friction_text() {
local f
f="$(friction_file)"
if [ -f "$f" ]; then
head -c 262144 "$f"
elif [ -f "$REPORT_PATH" ]; then
awk '/^## / { found = ($0 == "## Agent friction log"); next } found { print }' "$REPORT_PATH" |
grep -v '^<!-- agent:' | grep -v '^No friction log at ' | sed '/./,$!d'
fi
}
write_fallback_report() { # RC WHY
local dir tmp friction
dir="$(dirname "$REPORT_PATH")"
(
umask 077
mkdir -p "$dir"
) 2>/dev/null || return 1
tmp="$dir/.setup-report.$$.tmp"
friction="$(friction_text 2>/dev/null || true)"
(
umask 077
{
fallback_body "$1" "$2" "$friction" | redact_stream
printf '\n%s\n' "$ISSUE_URL" # the issue form, after redaction (a login may match the URL's owner)
} >"$tmp"
) 2>/dev/null || {
rm -f "$tmp"
return 1
}
mv -f "$tmp" "$REPORT_PATH"
}
write_report() { # RC: write the setup report at $REPORT_PATH; 0 when written, 2 in a dry run
local rc="$1" bin out src=0 why="agentsync is not installed"
if [ "$DRY_RUN" -eq 1 ]; then
run "${AGENTSYNC:-agentsync}" setup-report --out "$REPORT_PATH" --config "$CONFIG"
return 2
fi
step_start report
bin="$(report_agentsync || true)"
if [ -n "$bin" ]; then
out="$(with_timeout 120 "$bin" setup-report --out "$REPORT_PATH" --config "$CONFIG" </dev/null 2>&1)" || src=$?
if [ "$src" -eq 0 ] && [ -s "$REPORT_PATH" ]; then
say "report: $REPORT_PATH (agentsync setup-report)"
step_end "done" 0 agentsync
return 0
fi
if [ "$src" -eq 0 ]; then
warn "agentsync setup-report exited 0 but wrote no $REPORT_PATH"
why="agentsync setup-report wrote no report"
else
warn "agentsync setup-report failed ($(rc_meaning "$src")): $(printf '%s' "$out" | tail -1)"
why="agentsync setup-report failed with exit $src"
fi
fi
if write_fallback_report "$rc" "$why"; then
say "report: $REPORT_PATH (shell fallback: $why)"
step_end "done" 0 fallback
return 0
fi
warn "could not write the setup report $REPORT_PATH"
step_end failed 1
return 1
}
on_exit() {
local rc=$? suffix="" link secs
trap - EXIT
set +e
[ "$PARSED" -eq 1 ] || exit "$rc"
step_end failed "$rc"
if [ "$STARTED" -eq 1 ]; then
END_AT="$(utc_now)"
secs=$SECONDS
log_at "$END_AT" "end rc=$rc seconds=$secs" # provisional: the report reads a finished run
END_LINE="$END_AT run=$RUN_ID end rc=$rc seconds=$secs"
fi
if [ "$REPORT" -eq 1 ]; then
write_report "$rc"
case $? in
0)
suffix=" [setup report: $REPORT_PATH]"
link="$(report_issue_link)"
if [ -n "$link" ]; then
[ "$REPORT_ONLY" -eq 0 ] ||
NEXT_MSG="review the setup report, then paste it into the issue the link above opens, or send it privately (nothing is sent for you)"
say "$ISSUE_LINK_PREFIX$link" # the last line before NEXT (step 3 of the setup prompt names it)
fi
;;
2) ;; # a dry run: printed, not written
*)
if [ "$REPORT_ONLY" -eq 1 ]; then
rc=1
NEXT_MSG="the setup report could not be written (see the warning above); send ${SETUP_LOG%/*}/friction.md instead"
fi
;;
esac
fi
finish_setup_log "$rc"
[ -z "$DOCTOR_LOG" ] || rm -f "$DOCTOR_LOG"
[ -z "$SYNC_OUT" ] || rm -f "$SYNC_OUT"
[ -z "$LOOP_OUT" ] || rm -f "$LOOP_OUT"
[ -z "$NEXT_MSG" ] || say "NEXT: $NEXT_MSG$suffix"
stop_capture
exit "$rc"
}
# The issue link the setup report ends with (its last line, when it starts with $ISSUE_URL), else empty.
report_issue_link() {
local l
l="$(sed '/^[[:space:]]*$/d' "$REPORT_PATH" 2>/dev/null | tail -1)"
case "$l" in
"$ISSUE_URL"*) printf '%s' "$l" ;;
esac
}
# A signal (a coding tool's timeout sends SIGTERM) still ends through on_exit: the log's end line, the report
# and a NEXT line that says to run the same command again.
on_signal() {
LAST_ERROR="install.sh was stopped by a signal ($(rc_meaning "$1")) before it finished"
printf 'error: %s\n' "$LAST_ERROR" >&2
NEXT_MSG="this run was stopped before it finished ($(rc_meaning "$1")); it is safe to re-run: ${RERUN:-$SELF$ORIG_ARGS}"
exit "$1"
}
# What macOS calls the app behind a ~/Library/CloudStorage entry ("OneDrive-Contoso" -> OneDrive).
provider_label() {
case "$1" in
OneDrive* | SharedLibraries*) printf 'OneDrive' ;;
GoogleDrive*) printf 'Google Drive' ;;
*) printf '%s' "${1%%-*}" ;;
esac
}
# This terminal app's name, as System Settings lists it, when the terminal says (else empty).
terminal_app() {
case "${TERM_PROGRAM:-}" in
Apple_Terminal) printf 'Terminal' ;;
iTerm.app) printf 'iTerm' ;;
vscode) printf 'Visual Studio Code' ;;
WarpTerminal) printf 'Warp' ;;
ghostty) printf 'Ghostty' ;;
*) printf '%s' "${TERM_PROGRAM:-}" ;;
esac
}
# --list-folders: the folders 1-2 levels inside each ~/Library/CloudStorage/<provider>, names only (find reads
# directory entries and their metadata; no file is opened, so nothing is downloaded). Sets NEXT_MSG; returns
# 0 listed, 3 none, 4 denied or still asking.
list_folders() {
local cs="$HOME/Library/CloudStorage" tmp e label rc total providers=0 denied="" asking="" term max=200
local deadline=$((SECONDS + LIST_TOTAL)) t
step_start list-folders
tmp="$(mktemp -d)"
: >"$tmp/list"
if [ -d "$cs" ] && ! ls "$cs" >/dev/null 2>"$tmp/err"; then
grep -q 'Operation not permitted' "$tmp/err" && denied="OneDrive"
fi
for e in "$cs"/*; do
[ -d "$e" ] || continue