-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcrazycode.sh
More file actions
executable file
Β·502 lines (458 loc) Β· 19.7 KB
/
Copy pathcrazycode.sh
File metadata and controls
executable file
Β·502 lines (458 loc) Β· 19.7 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
#!/usr/bin/env bash
_crazycode_main() {
# Colors
local B='\033[1m' D='\033[2m' X='\033[0m'
local BR='\033[1;31m' # bold red
local BG='\033[1;32m' # bold green
local BY='\033[1;33m' # bold yellow
local BB='\033[1;34m' # bold blue
local BM='\033[1;35m' # bold magenta
local BC='\033[1;36m' # bold cyan
local BW='\033[1;37m' # bold white
local MO='\033[38;5;208m' # orange for Muse
local items=("aider" "claude" "codex" "forge" "gemini" "goose" "muse" "opencode")
local cmds=("aider" "claude" "codex" "forge" "gemini" "goose" "muse" "opencode")
local descriptions=("Paul Gauthier" "Anthropic" "OpenAI" "Tailcall" "Google" "AAIF" "Meta" "SST")
local launch_args=(
"--yes-always"
"--dangerously-skip-permissions"
"--sandbox danger-full-access --ask-for-approval never"
""
"--yolo"
""
"--yolo"
""
)
# How each tool opens its own session chooser. An empty entry means "launch
# plain": that tool picks the session from inside its TUI, and every flag we
# could pass here would pin one session instead of letting the user choose.
local resume_args=(
"--restore-chat-history" # aider: one chat log per directory, nothing to choose
"--resume" # claude: no value β interactive session picker
"" # codex: `codex resume` subcommand, handled in _launch_tool
"" # forge: /conversation shows the picker when given no id
"" # gemini: /resume in-app; `--resume` with no value means "latest"
"session --resume" # goose: no picker β resumes the most recent session
"" # muse: `muse resume` subcommand, handled in _launch_tool
"" # opencode: session list in-app; -c would pin the last one
)
# Printed under "Resuming <tool>..." so the user knows where that tool keeps
# its chooser β or why it hasn't got one.
local resume_hints=(
"reloading this folder's chat history β aider has no separate sessions"
""
""
"type /conversation inside forge to pick a conversation"
"type /resume inside gemini to browse saved conversations"
"goose has no session picker β resuming the most recent session"
""
"press ctrl+x then l inside opencode for the session list"
)
local num_items=${#items[@]}
local selected=0
local prev_selected=-1
local _last_tool=-1
# ββ awake mode state ββββββββββββββββββββββββββββββββββββββββββββββ
local sleep_masked=0 idle_inhibited=0 lid_ignored=0 lock_disabled=0
check_sleep() {
systemctl is-enabled sleep.target 2>/dev/null | grep -q masked && sleep_masked=1 || sleep_masked=0
}
check_idle_inhibit() {
idle_inhibited=0
systemd-inhibit --list --no-pager 2>/dev/null | grep -q crazycode-awake && idle_inhibited=1
}
check_lid() {
local val
val=$(grep -i '^HandleLidSwitch=' /etc/systemd/logind.conf 2>/dev/null | tail -1 | cut -d= -f2 | tr -d '[:space:]')
[[ "${val,,}" == "ignore" ]] && lid_ignored=1 || lid_ignored=0
}
check_lock() {
lock_disabled=0
if command -v gsettings &>/dev/null && [ -n "$DISPLAY$WAYLAND_DISPLAY" ]; then
local idle lock
idle=$(gsettings get org.gnome.desktop.session idle-delay 2>/dev/null)
lock=$(gsettings get org.gnome.desktop.screensaver lock-enabled 2>/dev/null)
[[ "$idle" == "uint32 0" ]] && [[ "$lock" == "false" ]] && lock_disabled=1
fi
if command -v kreadconfig5 &>/dev/null; then
local lock
lock=$(kreadconfig5 --group Daemon --key Autolock 2>/dev/null)
[[ "$lock" == "false" ]] && lock_disabled=1
fi
}
check_awake() {
check_sleep
check_idle_inhibit
check_lid
check_lock
}
is_awake() {
[[ $sleep_masked -eq 1 && $idle_inhibited -eq 1 && $lid_ignored -eq 1 && $lock_disabled -eq 1 ]]
}
enable_awake() {
sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target >/dev/null 2>&1
if ! systemd-inhibit --list --no-pager 2>/dev/null | grep -q crazycode-awake; then
setsid systemd-inhibit --what=idle --who=crazycode-awake \
--why="crazycode awake mode" --mode=block sleep infinity \
</dev/null >/dev/null 2>&1 &
disown
fi
sudo sed -i 's/^#\?HandleLidSwitch=.*/HandleLidSwitch=ignore/' /etc/systemd/logind.conf 2>/dev/null
sudo grep -q '^HandleLidSwitch=' /etc/systemd/logind.conf 2>/dev/null \
|| echo 'HandleLidSwitch=ignore' | sudo tee -a /etc/systemd/logind.conf >/dev/null
sudo systemctl kill -s HUP systemd-logind 2>/dev/null
if command -v gsettings &>/dev/null && [ -n "$DISPLAY$WAYLAND_DISPLAY" ]; then
gsettings set org.gnome.desktop.session idle-delay 0 2>/dev/null
gsettings set org.gnome.desktop.screensaver lock-enabled false 2>/dev/null
fi
if command -v kwriteconfig5 &>/dev/null; then
kwriteconfig5 --group Daemon --key Autolock false 2>/dev/null
fi
check_awake
}
disable_awake() {
sudo systemctl unmask sleep.target suspend.target hibernate.target hybrid-sleep.target >/dev/null 2>&1
pkill -f 'systemd-inhibit.*crazycode-awake' 2>/dev/null
pkill -f caffeine-indicator 2>/dev/null
sudo sed -i 's/^#\?HandleLidSwitch=.*/HandleLidSwitch=suspend/' /etc/systemd/logind.conf 2>/dev/null
sudo systemctl kill -s HUP systemd-logind 2>/dev/null
if command -v gsettings &>/dev/null && [ -n "$DISPLAY$WAYLAND_DISPLAY" ]; then
gsettings set org.gnome.desktop.session idle-delay 300 2>/dev/null
gsettings set org.gnome.desktop.screensaver lock-enabled true 2>/dev/null
fi
if command -v kwriteconfig5 &>/dev/null; then
kwriteconfig5 --group Daemon --key Autolock true 2>/dev/null
fi
check_awake
}
# ββ drawing helpers ββββββββββββββββββββββββββββββββββββββββββββββ
get_color() {
case "$1" in
aider) printf "%s" "$BR" ;;
claude) printf "%s" "$BC" ;;
codex) printf "%s" "$BY" ;;
forge) printf "%s" "$BM" ;;
gemini) printf "%s" "$BB" ;;
goose) printf "%s" "$BG" ;;
muse) printf "%s" "$MO" ;;
opencode) printf "%s" "$BW" ;;
*) printf "%s" "$D" ;;
esac
}
awake_count() {
_awake_count=0
[[ $sleep_masked -eq 1 ]] && ((_awake_count++))
[[ $idle_inhibited -eq 1 ]] && ((_awake_count++))
[[ $lid_ignored -eq 1 ]] && ((_awake_count++))
[[ $lock_disabled -eq 1 ]] && ((_awake_count++))
}
get_awake_line() {
awake_count
local count=$_awake_count
if [[ $count -eq 4 ]]; then
printf " ${BW}[c]${X} ${D}camomile${X} ${D}πΏ${X} ${BG}[awake mode on]${X}"
elif [[ $count -gt 0 ]]; then
printf " ${BW}[c]${X} ${BG}coffeeshot${X} ${BG}β${X} ${BY}[partial ${count}/4]${X}"
else
printf " ${BW}[c]${X} ${BG}coffeeshot${X} ${BG}β${X} ${D}[awake mode off]${X}"
fi
}
# _launch_tool <idx> <resume 0|1> [args passed through to the tool...]
# Both leading arguments are mandatory: a caller that omits `resume` makes the
# user's first argument bind to it, where `shift 2` then eats it.
_launch_tool() {
local idx=$1 resume=$2
shift 2
local tool="${items[$idx]}"
local cmd="${cmds[$idx]}"
local color
color=$(get_color "$tool")
if ! command -v "$cmd" &>/dev/null; then
printf "\n ${BR}${B}β${X} ${BW}${tool}${X} ${D}is not installed.${X}\n"
printf " ${D}Run the installer to set it up:${X}\n"
printf " ${D} curl -fsSL https://raw.githubusercontent.com/Ymx1ZQ/crazycode/main/install.sh | bash${X}\n\n"
printf " ${D}press any key to return to menu...${X}"
read -rsn1
return 1
fi
# Per-tool environment overrides applied immediately before invocation.
# forge: telemetry defaults to ON and processes events on US infrastructure
# with no data-processing agreement in place. Opt-out by default; users who
# want telemetry on can `unset FORGE_TRACKER` in their shell after launch.
# goose: GOOSE_MODE defaults to smart_approve (interactive confirmations).
# Override to `auto` to match crazycode's "all tools launch without asking
# permission" stance β same role as --yes-always/--yolo for the others.
local env_prefix=""
if [[ "$tool" == "forge" ]]; then
env_prefix="FORGE_TRACKER=0"
elif [[ "$tool" == "goose" ]]; then
env_prefix="GOOSE_MODE=auto"
fi
# String comparison, not `-eq`: `[[ x -eq 1 ]]` evaluates x as an arithmetic
# expression, and bash performs command substitution while doing so.
if [[ "$resume" == "1" ]]; then
printf "\n ${color}${B}Resuming ${tool}...${X}\n"
[[ -n "${resume_hints[$idx]}" ]] && printf " ${D}${resume_hints[$idx]}${X}\n"
printf "\n"
# codex reaches its picker through a subcommand; `--last` would skip the
# picker, and the launch_args carry the no-approval flags that a resumed
# session needs just as much as a fresh one.
if [[ "$tool" == "codex" ]]; then
# shellcheck disable=SC2086
env $env_prefix ${cmd} resume ${launch_args[$idx]} "$@"
elif [[ "$tool" == "muse" ]]; then
# shellcheck disable=SC2086
env $env_prefix ${cmd} ${launch_args[$idx]} resume "$@"
else
# shellcheck disable=SC2086
env $env_prefix ${cmd} ${launch_args[$idx]} ${resume_args[$idx]} "$@"
fi
else
printf "\n ${color}${B}Launching ${tool}...${X}\n\n"
# shellcheck disable=SC2086
env $env_prefix ${cmd} ${launch_args[$idx]} "$@"
fi
}
_print_status() {
check_awake
local on="${BG}β${X}" off="${BR}β${X}"
printf "\n ${BW}${B}awake mode status${X}\n"
printf " βββββββββββββββββββββββββββ\n"
printf " sleep masked: %b\n" "$( [[ $sleep_masked -eq 1 ]] && echo "$on" || echo "$off" )"
printf " idle inhibitor: %b\n" "$( [[ $idle_inhibited -eq 1 ]] && echo "$on" || echo "$off" )"
printf " lid ignored: %b\n" "$( [[ $lid_ignored -eq 1 ]] && echo "$on" || echo "$off" )"
printf " lock disabled: %b\n" "$( [[ $lock_disabled -eq 1 ]] && echo "$on" || echo "$off" )"
printf " βββββββββββββββββββββββββββ\n"
if is_awake; then
printf " ${BG}${B}awake mode: ON${X}\n\n"
else
printf " ${D}awake mode: OFF${X}\n\n"
fi
}
_print_help() {
printf "\n ${BR}${B}β‘ CRAZYCODE${X} ${D}β AI coding launcher${X}\n\n"
printf " ${BW}Usage:${X} crazycode [command] [args...]\n\n"
printf " ${BW}Commands:${X}\n"
printf " ${BR}aider${X} Launch aider (--yes-always)\n"
printf " ${BC}claude${X} Launch Claude Code (--dangerously-skip-permissions)\n"
printf " ${BY}codex${X} Launch codex (--sandbox danger-full-access)\n"
printf " ${BM}forge${X} Launch ForgeCode (FORGE_TRACKER=0 set by default)\n"
printf " ${BB}gemini${X} Launch Gemini CLI (--yolo)\n"
printf " ${BG}goose${X} Launch Goose (GOOSE_MODE=auto set by default)\n"
printf " ${MO}muse${X} Launch Muse Code (Meta) (--yolo)\n"
printf " ${BW}opencode${X} Launch opencode\n"
printf " ${BG}coffeeshot${X} Toggle awake mode on/off\n"
printf " ${D}status${X} Show awake mode status\n\n"
printf " ${D}Run without arguments to open the interactive TUI.${X}\n\n"
}
_find_tool_index() {
local name="$1" i
for i in "${!items[@]}"; do
[[ "${items[$i]}" == "$name" ]] && echo "$i" && return 0
done
return 1
}
# ββ CLI mode: handle subcommands βββββββββββββββββββββββββββββββββ
if [[ $# -gt 0 ]]; then
local subcmd="$1"
shift
local idx
if idx=$(_find_tool_index "$subcmd"); then
# `0` is the resume flag; everything after it belongs to the tool.
# Resuming from the CLI needs no subcommand of its own β the tool's own
# flag forwards straight through (`crazycode claude --resume`).
_launch_tool "$idx" 0 "$@"
return $?
fi
case "$subcmd" in
coffeeshot)
check_awake
if is_awake; then
disable_awake
printf " ${D}πΏ camomile β awake mode OFF${X}\n"
else
enable_awake
printf " ${BG}β coffeeshot β awake mode ON${X}\n"
fi
return 0
;;
status) _print_status ; return 0 ;;
--help|-h|help) _print_help ; return 0 ;;
*)
printf " ${BR}Unknown command:${X} %s\n" "$subcmd"
_print_help
return 1
;;
esac
fi
# ββ TUI mode βββββββββββββββββββββββββββββββββββββββββββββββββββββ
_cleanup() {
echo -ne "\033[?25h"
stty echo 2>/dev/null
}
trap _cleanup EXIT INT TERM
# header = blank + title + path + [git] + separator
local hdr=4
git rev-parse --is-inside-work-tree &>/dev/null && hdr=5
local _last_session=""
# cache install status (doesn't change during session)
local -a installed=()
local i
for i in "${!cmds[@]}"; do
command -v "${cmds[$i]}" &>/dev/null && installed+=("${BG}β${X}") || installed+=("${BR}β${X}")
done
draw_line() {
local idx=$1 is_selected=$2
local item="${items[$idx]}"
local color
color=$(get_color "$item")
local num=$((idx + 1))
local row=$((hdr + 1 + idx))
echo -ne "\033[${row};1H\033[K"
if [ "$is_selected" -eq 1 ]; then
printf " ${BW}${B}βΆ${X} ${B}${color}%-15s${X} ${D}%s${X} %b" "$item" "${descriptions[$idx]}" "${installed[$idx]}"
else
printf " ${D}${num}${X} ${color}%-15s${X} ${D}%s${X} %b" "$item" "${descriptions[$idx]}" "${installed[$idx]}"
fi
}
draw_awake() {
local row=$((hdr + num_items + 2))
echo -ne "\033[${row};1H\033[K"
get_awake_line
}
draw_all() {
check_awake
clear
printf "\n"
printf " ${BR}${B}β‘ CRAZYCODE${X} ${BW}π ${PWD##*/}${X}\n"
printf " ${D}%s${X}\n" "$PWD"
if [[ $hdr -eq 5 ]]; then
local branch dirty=""
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
[[ -n $(git status --porcelain 2>/dev/null) ]] && dirty=" ${BY}β${X}"
printf " ${D}β${X} ${BW}${branch}${X}%b\n" "$dirty"
fi
printf " ${D}ββββββββββββββββββββββββββββββββββββββ${X}\n"
local i
for i in "${!items[@]}"; do draw_line "$i" 0; done
# Everything below positions its own row, so none of it ends with "\n".
# The newline would be one row more than the layout occupies, and on a
# terminal exactly as tall as the layout that scrolls the menu up one row
# while the draws that follow keep addressing the old rows.
printf "\033[$((hdr + num_items + 1));1H ${D}ββββββββββββββββββββββββββββββββββββββ${X}"
draw_awake
printf "\033[$((hdr + num_items + 3));1H ${D}ββββββββββββββββββββββββββββββββββββββ${X}"
local help_line="${B}ββ/1-8${X}${D} select Β· ${X}${B}enter${X}${D} launch Β· ${X}${B}r${X}${D} resume"
help_line+=" Β· ${X}${B}c${X}${D} toggle awake mode Β· ${X}${B}q${X}${D} quit${X}"
printf "\033[$((hdr + num_items + 4));1H ${D}${help_line}"
local footer_row=$((hdr + num_items + 5))
if [[ -n "$_last_session" ]]; then
printf "\033[${footer_row};1H ${D}β± last session: ${items[$_last_tool]} Β· ${_last_session}${X}"
((footer_row++))
fi
printf "\033[${footer_row};1H ${BY}β ${X} ${D}all tools launch without asking permission${X}"
draw_line "$selected" 1
}
draw_all
trap 'draw_all' WINCH
# ββ main TUI loop ββββββββββββββββββββββββββββββββββββββββββββββββ
while true; do
# ββ input loop ββββββββββββββββββββββββββββββββββββββββββββββββββ
local _resume=0
while true; do
local key=""
read -rsn1 key
case "$key" in
$'\x1b')
read -rsn2 -t 0.1 key2
case "$key2" in
'[A')
prev_selected=$selected
selected=$(( (selected - 1 + num_items) % num_items ))
draw_line "$prev_selected" 0
draw_line "$selected" 1
;;
'[B')
prev_selected=$selected
selected=$(( (selected + 1) % num_items ))
draw_line "$prev_selected" 0
draw_line "$selected" 1
;;
esac
;;
'')
break
;;
c)
# sudo asks for the password below the footer, and what it prints
# there is not one line in general: a first-use lecture, a
# "Sorry, try again." per wrong password, the newline the user's Enter
# echoes on the bottom row. Any of those scrolls the screen, and every
# draw here addresses rows absolutely from the top β an assumption
# only `clear` restores. So repaint the whole screen instead of
# patching the awake line in place; draw_all ends on the selected
# entry, which is also where the cursor belongs.
local _extra=0
[[ -n "$_last_session" ]] && _extra=1
local prompt_row=$((hdr + num_items + 6 + _extra))
echo -ne "\033[${prompt_row};1H\033[K"
sudo -v
if is_awake; then
disable_awake
else
enable_awake
fi
draw_all
;;
[rR])
# Resume mode for whatever is highlighted, available from the first
# keystroke β no tool needs to have run in this crazycode session
# first, and the selection is never overwritten.
_resume=1
break
;;
[1-8])
local num_idx=$((key - 1))
if [[ $num_idx -lt $num_items ]]; then
selected=$num_idx
break
fi
;;
q)
clear
return 0
;;
esac
done
# ββ launch selected tool ββββββββββββββββββββββββββββββββββββββββ
clear
local _t0=$SECONDS
_launch_tool "$selected" "$_resume" "$@"
_last_tool=$selected
stty sane 2>/dev/null
# Drain stray input the child TUI left behind. Terminal query responses
# (cursor-position / device-attributes reports) arrive after the child has
# exited; without this the menu's read loop consumes them as keystrokes β
# a stray digit launches whatever tool maps to it (e.g. opencode β codex).
local _drain
while read -rsn1 -t 0.05 _drain; do :; done
local _elapsed=$(( SECONDS - _t0 ))
local _m=$(( _elapsed / 60 )) _s=$(( _elapsed % 60 ))
if [[ $_m -gt 0 ]]; then
_last_session="${_m}m ${_s}s"
else
_last_session="${_s}s"
fi
draw_all
done
}
# ββ bash completion ββββββββββββββββββββββββββββββββββββββββββββββββββ
_crazycode_completions() {
local cur="${COMP_WORDS[COMP_CWORD]}"
COMPREPLY=( $(compgen -W "aider claude codex forge gemini goose muse opencode coffeeshot status --help" -- "$cur") )
}
# NOTE: completion words match items array + extra commands; update if items change
complete -F _crazycode_completions crazycode
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
_crazycode_main "$@"
fi