Repository navigation
Expand file tree
/
Copy pathsp
More file actions
executable file
·731 lines (665 loc) · 30.8 KB
/
Copy pathsp
File metadata and controls
executable file
·731 lines (665 loc) · 30.8 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
#!/usr/bin/env bash
# sp: safe shpool session manager.
# Expected nonzero statuses are part of interactive control flow, so this
# command deliberately avoids `set -e`. Every state-changing path checks its
# result explicitly and is covered by proof-bound failure tests.
set -u
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
# shellcheck source=session_kit_common
source "$SCRIPT_DIR/session_kit_common"
INVENTORY_CORE=${SESSION_KIT_INVENTORY_CORE:-"$SCRIPT_DIR/../lib/session_inventory.py"}
# Background creates and restores run with no client terminal. The daemon
# copies TERM from this environment into the new shell; with no TERM — or the
# TERM=dumb that systemd/cron inject — every provider inside starts colorless
# and stays that way for the life of the session (environment is fixed at
# exec). Scoped to non-tty callers only: a human on a genuinely limited
# terminal keeps the TERM they declared.
if [[ ! -t 1 ]]; then
case "${TERM:-}" in
'' | dumb) export TERM=xterm-256color ;;
esac
fi
PICKER_REFUSED_STATUS=74
PICKER_ATTACH_FAILED_STATUS=75
# The short synopsis every argument mistake prints. The full reference is one
# command away rather than forty lines of stderr on a typo.
usage() {
cat <<'EOF'
usage: sp <command> [arguments]
sp help [topic]
Run 'sp help' for every command with a one-line description, or
'sp help <topic>' for one area in detail.
EOF
}
# Every refusal carries the one prefix and says what was wrong before it shows
# the synopsis: "usage:" on its own names the shape of the command but never
# the mistake, and a person reading only the first line learned nothing.
sp_refuse() {
printf 'session-kit: %s\n' "$*" >&2
usage >&2
exit 2
}
# `sp account` and `sp worktree` hand their remaining words to python. Left
# unchecked, a typo reached argparse, which answered in its own voice: a sixth
# error prefix, the file name of an internal module, and the full list of
# machine subcommands `sp help` withholds on purpose. The verb is checked here
# instead, and the refusal names only the commands a person is meant to type.
#
# The machine subcommands stay callable -- the picker runs `sp account choices`
# -- they are simply never printed.
sp_require_subcommand() {
local group=$1 candidate=$2 known= human=
case "$group" in
account)
known="list adopt-default enroll verify configure-feeds choices binding
source bind launch-profile resume-profile sync-ui switch-prepare
switch-apply switch-commit switch-rollback switch-status"
human="list adopt-default enroll verify configure-feeds"
;;
worktree)
known="list materialize teardown sweep release bind lookup"
human="list materialize teardown sweep"
;;
esac
local word
for word in $known; do
[[ $word == "$candidate" ]] && return 0
done
printf 'session-kit: no %s command named %s\n' "$group" "$candidate" >&2
printf 'commands: %s\n' "$human" >&2
exit 2
}
# The complete human command reference, grouped by the job being done. The
# machine verbs — picker-*, restore-exact — are deliberately absent: the
# picker and the managed shell call them with proof arguments a person does
# not hold, so listing them here would only invite someone to run one by hand.
# `sp help machine` names them for anyone reading the source.
sp_help_overview() {
cat <<'EOF'
sp manages Claude Code, Codex, and shell sessions inside shpool.
usage: sp <command> [arguments]
sp help [topic]
Sessions
sp list every managed session with its live state
sp new [claude|codex|shell] [project-alias] [--account ALIAS] [--model ID] [--launch-key KEY]
[--origin human|machine] a machine session stays out of the default picker view
[--worktree BRANCH] runs the session in that branch's own git worktree
[--no-worktree] a delegated session shares the checkout instead of getting its own
a machine session in a git repository gets its own working copy by
default, and gives it back when it closes
Codex also accepts [--prompt-file OWNER-PRIVATE-FILE]
sp go <session> open a ready session in this terminal
sp takeover <session> move an attached session here, after a check
sp close <session> close one session
sp change-model <session> <model> [--model-anyway]
move an idle conversation to another model;
a model this machine would answer with a
different one is refused, never substituted
sp account-auto-switch <session> [--apply]
say whether an idle conversation would be
carried off a subscription account that
has run dry; --apply actually moves it
sp detail <session> everything known about one session
sp health manager, daemon, and resource state
sp prune close sessions that are idle and empty
sp repair <session> rebuild after a proven shpool handoff failure
sp verify-start <session> re-prove a provider start left pending
Names and colors
sp name <session> <title> set a local title
sp name reset <session> remove the local title
sp self-name <2-5-word-title> title the session you are running in
sp color <session> <color> set one session's color
Claude: red|blue|green|yellow|purple|orange|pink|cyan
Codex: lime|magenta|silver|sand|sky|sea
sp color reset <session> return to the derived color
sp color reconcile give every live session its own color
Delegated work
sp teardown <session> [--merged-into REF] [--force]
close a worker and prune its worktree
sp worktree list worktrees materialized for delegated work
sp worktree materialize --repo DIR --branch BRANCH
sp worktree teardown --branch BRANCH [--merged-into REF] [--force]
Accounts
sp account list [claude|codex] enrolled accounts and last verification
sp account adopt-default <claude|codex> <alias> <email>
sp account enroll <claude|codex> <alias> <email>
sp account verify <claude|codex> <alias>
sp account configure-feeds <roster-json> <advice-json>
History and recovery
sp find <text> search history for text
sp history <session> show one session's history
sp recover closed conversations you can restore
sp recover --allow-large-ledger deliberately stream a pathological ledger
sp restore number|selector bring one back, by what sp recover shows beside it
sp restore --allow-large-ledger number|selector
restore from the deliberate large stream
Topics
sp help sessions start, open, move, and close sessions
sp help names titles and colors
sp help accounts provider profiles and account switching
sp help history history, search, and restore
sp help selectors what <session> accepts
sp help unavailable session records with no shell, and how they clear
sp help exit-codes what every status a Session Kit command returns means
sp help completion turn on tab completion
sp help machine the verbs the picker and the managed shell call
<session> is a session number or an exact shpool ID. Both -h and --help print
this reference, with or without shpool installed.
EOF
}
sp_help_topic() {
case "$1" in
sessions)
cat <<'EOF'
Sessions
sp list every managed session with its live state
sp new [claude|codex|shell] [project-alias] [--account ALIAS] [--model ID]
[--origin human|machine]
[--worktree BRANCH] [--no-worktree]
[--model-anyway]
sp go <session> open a ready session in this terminal
sp takeover <session> move an attached session here, after a check
sp close <session> close one session
sp change-model <session> <model> [--model-anyway]
move an idle conversation to another model;
a model this machine would answer with a
different one is refused, never substituted
sp account-auto-switch <session> [--apply]
say whether an idle conversation would be
carried off a subscription account that
has run dry; --apply actually moves it
sp detail <session> everything known about one session
sp health manager, daemon, and resource state
sp prune close sessions that are idle and empty
sp repair <session> rebuild after a proven shpool handoff failure
sp verify-start <session> re-prove a provider start left pending
`sp change-model` restarts the provider on the model named and resumes the same
conversation. It runs only on an idle session: a restart in the middle of a
turn would lose that turn.
`sp account-auto-switch` says whether one idle conversation would be moved to
another enrolled account because its own is spent, and changes nothing until
`--apply` is given. It refuses unless the usage feed is fresh, refuses any
account already past the reserve, refuses a conversation whose live account
disagrees with its record or that is running a model asked for by name, and
moves a conversation at most once -- a session that spends a second account
stops and says so. It never switches an account back on.
`$XDG_STATE_HOME/session-kit/account-switching-off` turns it off, and the same
file turns the manual switch and enrolment off with it.
`sp new <project-alias>` alone uses the provider stored for that alias. A
session number stays with its session for the current host boot.
`sp go` opens a detached session. `sp takeover` moves an attached one after a
fresh identity check, and the window that had it returns to its picker; the
conversation is never duplicated. Closing ends the session only, and keeps the
conversation and its history.
`sp repair` is for direct evidence of an unrecoverable shpool handoff failure.
Silence alone does not qualify, and if every session is unreachable at once,
repair is the wrong tool.
Automation closes only with an explicit opt-in and the exact shpool ID:
SESSION_KIT_NONINTERACTIVE=1 SESSION_KIT_CONFIRM_ID=<exact-shpool-id> \
sp close <exact-shpool-id>
EOF
;;
names | colors | color)
cat <<'EOF'
Names and colors
sp name <session> <title> set a local title
sp name reset <session> remove the local title
sp self-name <2-5-word-title> title the session you are running in
sp color <session> <color> set one session's color
sp color reset <session> return to the derived color
sp color reconcile give every live session its own color
Claude sessions take red, blue, green, yellow, purple, orange, pink, and cyan —
the eight names Claude Code itself accepts. Codex sessions take lime, magenta,
silver, sand, sky, and sea. The two palettes never overlap, so a Claude session
and a Codex session cannot look alike.
Title priority: a local manual name, then an explicit provider rename, then a
retained automatic title, then a provider-and-project description, then a
shortened identity, then the shell command. `sp name reset` removes only the
local name; it does not hand the session back to automatic naming.
A color is derived from the conversation identity, so it is the same every time
you return. `sp color reconcile` settles every live session at once, moves only
the ones that have to move, and changes nothing on a second run.
EOF
;;
accounts | account)
cat <<'EOF'
Accounts
sp account list [claude|codex] enrolled accounts and last verification
sp account adopt-default <claude|codex> <alias> <email>
record the login this machine already has
sp account enroll <claude|codex> <alias> <email>
create a separate provider profile
sp account verify <claude|codex> <alias>
confirm a profile is signed in
sp account configure-feeds <roster-json> <advice-json>
point account advice at local feeds
sp new <claude|codex> [project-alias] --account <alias>
Claude Code profiles use separate CLAUDE_CONFIG_DIR roots and Codex profiles
separate CODEX_HOME roots. Enroll, complete the provider's own sign-in, then
verify. Session Kit never asks for, copies, or prints a provider token.
Changing the account of an existing conversation is a picker action, not a
command: select the session and choose `Change account`. A switch proceeds only
when fresh evidence proves the exact conversation has no active turn, tool,
hook, or subagent work. Nothing is asked first; if the evidence is not there,
the switch is refused.
Which accounts can be selected
An account is selectable when its profile is enabled and the account roster
reports its health as `ok`. The roster's `serving` flag is a routing toggle
for scheduled proxy work: it is displayed, and it never decides an interactive
session, which signs in with that profile's own login and no proxy.
Health comes from the two feeds `sp account configure-feeds` points at — a
roster of accounts and a rotation advice file — and each carries its own
timestamp. Past SESSION_KIT_ACCOUNT_ADVICE_MAX_AGE_SECONDS (600 by default),
or with no feed configured at all, nothing is known about any account's
health. Every enabled account then stays selectable, its row reads `ready
(feed stale)` or `ready (feed absent)`, and the picker names the feed's state
above its footer. Stale advice recommends nothing; it never blocks a choice.
A machine with no feeds configured behaves this way from the first session.
Refusals name the account, not a guess about the login:
Account work is not selectable: its health check expired.
There is no account 4 on this screen. Numbers shown here work.
The same rule answers `sp new --account <alias>`, which refuses with `account
work is not selectable: its health check expired`. The other states an account
can carry are `its profile is disabled` and `it is not in the account roster`.
`sp account verify` refreshes a profile's own record; the roster health itself
comes from whatever writes the feed.
EOF
;;
history | recovery | recover)
cat <<'EOF'
History and recovery
sp find <text> search history for text
sp history <session> show one session's history
sp recover closed conversations you can restore
sp recover --allow-large-ledger deliberately stream a pathological ledger
sp restore number|selector bring one back, by what sp recover shows beside it
sp restore --allow-large-ledger number|selector
restore from the deliberate large stream
A recording captures everything on the screen, so it is off unless you asked
for it. A session with no recording falls back to its conversation transcript,
for Claude and for Codex. Both histories include readable tool calls and tool
output. `sp find` groups matching lines under session names and reports counts.
`sp recover` lists what you closed on purpose next to what a crash took,
newest first. A conversation stays on that list until it is restored. One
whose transcript this machine can no longer read stays as well, as history
only, and the row says so; a plain shell has no conversation to reopen, so its
row offers history only too. A row keeps the session number it has on every
other screen; a conversation whose number was retired goes by the name shown
beside it, or -- when two rows answer to one name, or another row already
answers to that name -- by the time of its own event, printed where its number
would be. A name and an event time belong to the list rather than to the
conversation, so each screen writes down what it printed beside each row: if
the word you type has come to mean a different conversation since you read it,
`sp restore` says so and restores nothing.
Normal refreshes refuse a ledger above the pathological-file ceiling. The
error names its exact size and tells you to use `sp recover
--allow-large-ledger`; that deliberate form validates and streams the complete
file with bounded memory instead of hiding its rows. Its footer gives the
matching `sp restore --allow-large-ledger number` command.
Restoring uses an exact Claude Code or Codex UUID and never falls back to
recency, directory, or "latest". An open conversation cannot be restored into a
second writable session: open the session that has it, or fork it
deliberately.
EOF
;;
selectors | selector | session | targets)
cat <<'EOF'
Selectors
<session> is either the session number shown in the picker and in `sp list`, or
the exact shpool ID. Every command that takes one accepts both.
Session numbers are display conveniences. They are stable for the current host
boot and never change meaning under a repaint, but they describe what is on
screen, not which conversation you mean. Never automate a move, close, repair,
or cleanup by number — pass the exact shpool ID.
No Session Kit screen prints a shpool ID or a provider UUID, and no flag reveals
one. Identifiers live in the 0600 state files, in the JSON modes
(`shpool_status --json`, `--lookup`), and in proofs. Commands still accept an ID
wherever they accept a number, so a script that already holds one keeps working.
EOF
;;
unavailable | quarantined | phantom)
cat <<'EOF'
Unavailable session records
The session manager can keep listing a session whose shell process no longer
exists: a kill that hit an already-dead process aborts its own bookkeeping, and
the entry stays in the list with nothing behind it.
Session Kit quarantines those entries. They have no session number, they are
not drawn in the picker, and every `sp` verb refuses them — there is no shell
to attach to, take over, close, or repair, and a number that resolved to one
would be a number that cannot act. The picker's More screen counts them so the
state is never silent.
They clear on their own:
* the next cleanup pass removes a record whose shell and every process from
its session have been proved absent, and
* restarting the session manager clears every stale entry at once, which also
ends every live session, so it is not a routine step.
Cleanup re-proves the exact manager generation and session start time, then
uses the manager's supported attach-and-exit path with a hard timeout and
checks that the entry is gone. A changed manager, a reused name, an unreadable
process census, or any surviving session process leaves the record quarantined
here instead of guessing.
EOF
;;
exit-codes | exit | status | codes)
cat <<'EOF'
Exit codes
Every Session Kit command uses the same statuses.
0 the command did what it was asked to do
1 refused or failed; the reason is on stderr
2 wrong arguments, or a selector that matched no single session
3 live evidence was refused as stale, partial, or ambiguous
(shpool_status --strict-json, --guard-json), or shpool_reaper found its
disable sentinel
64 the launcher was invoked under a name that is not a Session Kit command
69 something required is unavailable: the current-release link, a helper
missing from the pinned release, or Bash 4 or newer on macOS
74 the picker refused the action it was asked to take
75 the picker's attach failed after the action was allowed
76 a provider-exit reopen ran and the reopened provider exited non-zero;
the managed shell stops at the recovery menu again rather than closing
78 the launcher found a release or manager link outside the install root
127 codex_resume_here could not find the codex command
143 sp was terminated (SIGTERM or SIGHUP)
A refusal is a status, not a suggestion: nothing partial has happened. Session
Kit disables a whole action rather than performing part of it.
EOF
;;
completion | complete | tab)
cat <<'EOF'
Tab completion
Session Kit ships one Bash completion file and installs a copy under each
command name that takes arguments:
~/.local/share/bash-completion/completions/sp
~/.local/share/bash-completion/completions/session-kit
~/.local/share/bash-completion/completions/shpool_status
~/.local/share/bash-completion/completions/shpool_reaper
~/.local/share/bash-completion/completions/codex_resume_here
With the bash-completion package present, a new shell loads the right file on
demand. Without it, source one copy from your own shell startup file:
source ~/.local/share/bash-completion/completions/sp
It completes verbs, help topics, flags, provider names, color names, and the
project aliases in your projects file. It never runs a session command and
never asks the manager for state, so a Tab press cannot change anything or
block on a snapshot — which is also why it does not complete session numbers.
Set SESSION_KIT_COMPLETION_DIR to install the copies somewhere else, or to
"none" to skip them. Uninstall removes only files that still carry the kit's
own marker line, so an edited copy is left alone.
EOF
;;
machine | internal | picker)
cat <<'EOF'
Machine verbs
These exist for the picker and the managed shell, which call them with proof
arguments a person does not hold. They are not part of the human command set,
they are not listed in `sp help`, and running one by hand is a good way to be
refused.
sp picker-open <session> the picker's own attach path
sp picker-takeover <session>
sp picker-close <session>
sp picker-recover <session>
sp picker-history <session>
sp picker-name <session> <title>
sp picker-name-reset <session>
sp picker-title-refresh <session>
sp picker-fork <session>
sp picker-account-switch <session> <alias>
sp picker-change-model <session> <model>
sp account-auto-switch <session> [--dry-run]
sp restore-exact <provider> <uuid> <cwd> [account-alias]
sp worktree bind --path DIR --shpool-id ID
sp worktree lookup --path DIR
A picker verb answers with 74 when it refuses the action and 75 when the attach
itself failed after the action was allowed, so the picker can tell the two
apart. Use the human verbs — `sp go`, `sp close`, `sp name`, `sp recover` —
for anything you type yourself.
EOF
;;
*)
return 1
;;
esac
}
# Help must answer before anything else can refuse. It is the one command that
# has to work on a machine where shpool was never installed, or was installed
# and later broke, because it is where the instructions for fixing that live.
sp_help() {
local topic=${1:-}
if (($# > 1)); then
printf 'session-kit: sp help takes one topic\n' >&2
usage >&2
return 2
fi
if [[ -z $topic ]]; then
sp_help_overview
return 0
fi
sp_help_topic "$topic" && return 0
printf 'session-kit: there is no help topic named %s\n' "$topic" >&2
printf 'topics: sessions names accounts history selectors unavailable exit-codes completion machine\n' >&2
return 2
}
# sp's action implementations live beside this program under ../lib/sh, in a
# Git checkout and in an installed release alike: the launcher always execs
# this file from inside its own release, so SCRIPT_DIR resolves to that same
# tree. Every module defines functions only. They are sourced here, ahead of
# the first trap, so no handler is ever installed before the function it names
# exists, and after SCRIPT_DIR, session_kit_common, INVENTORY_CORE, and the
# picker status codes are in place.
MODULE_DIR=$SCRIPT_DIR/../lib/sh
# shellcheck source=lib/sh/sp_core.sh
source "$MODULE_DIR/sp_core.sh"
# shellcheck source=lib/sh/sp_picker.sh
source "$MODULE_DIR/sp_picker.sh"
# shellcheck source=lib/sh/sp_provider_bounce.sh
source "$MODULE_DIR/sp_provider_bounce.sh"
# shellcheck source=lib/sh/sp_sessions.sh
source "$MODULE_DIR/sp_sessions.sh"
# shellcheck source=lib/sh/sp_commands.sh
source "$MODULE_DIR/sp_commands.sh"
# Two cleanups, both required, neither an alternative to the other. This is a
# merge of two branches that each added one to the same three traps.
# `sk_handoff_guard_abandon` puts stderr back where the caller had it, so an
# interrupted `sp` cannot leave the launcher's capture bypassed. It
# deliberately does NOT touch the terminal: a trap can fire while an attach
# client is still running, and nothing may fight a live client for the line
# discipline. The terminal repair happens only on the normal return from
# picker_attach_id, where the client has exited and been waited on.
# `sk_prebake_abort` ends what `sp` itself started: the first-frame colour step
# runs a real hidden provider, and the reap at the end of that function never
# runs when the shell is being torn down -- exactly the path that left a
# four-day-old provider pair alive on this machine.
trap 'sk_handoff_guard_abandon; sk_prebake_abort; cleanup_snapshot' EXIT
# Ctrl-C must actually stop sp. A trap that only cleaned up let bash resume
# the script mid-flow with the snapshot deleted from under it, so no sp
# operation could ever be interrupted. Held locks release with the process.
trap 'sk_handoff_guard_abandon; sk_prebake_abort; cleanup_snapshot; trap - INT; kill -INT "$$"' INT
trap 'sk_handoff_guard_abandon; sk_prebake_abort; cleanup_snapshot; exit 143' TERM HUP
command=${1:-list}
# Help is answered before shpool is required. `sp --help` on a machine where
# shpool is missing, half-installed, or broken used to print "shpool executable
# unavailable" and nothing else -- the one moment the reference is most needed
# was the one moment it could not be reached.
case "$command" in
help | -h | --help)
shift
sp_help "$@"
exit $?
;;
list)
[[ $# -le 1 ]] || sp_refuse "sp list takes nothing else"
;;
esac
sk_require_shpool || exit 1
case "$command" in
list)
exec "$SCRIPT_DIR/shpool_status"
;;
new)
shift
start_new "$@"
;;
name)
if [[ ${2:-} == reset ]]; then
[[ $# -eq 3 ]] || sp_refuse "sp name reset takes one session"
name_target "$3" delete
else
[[ $# -eq 3 ]] || sp_refuse "sp name takes one session and one title"
name_target "$2" set "$3"
fi
;;
self-name)
[[ $# -eq 2 ]] || sp_refuse "sp self-name takes one title"
exec python3 "$INVENTORY_CORE" automatic-title self-name "$2"
;;
color)
if [[ ${2:-} == reconcile ]]; then
[[ $# -eq 2 ]] || sp_refuse "sp color reconcile takes nothing else"
color_reconcile
elif [[ ${2:-} == reset ]]; then
[[ $# -eq 3 ]] || sp_refuse "sp color reset takes one session"
color_target "$3" delete
else
[[ $# -eq 3 ]] || sp_refuse "sp color takes one session and one color"
color_target "$2" set "$3"
fi
;;
go|resume|attach)
[[ $# -eq 2 ]] || sp_refuse "sp go takes one session"
go_target "$2"
;;
takeover)
[[ $# -eq 2 ]] || sp_refuse "sp takeover takes one session"
takeover_target "$2"
;;
close|kill)
[[ $# -eq 2 ]] || sp_refuse "sp close takes one session"
close_target "$2"
;;
change-model)
[[ $# -eq 3 || ($# -eq 4 && $4 == --model-anyway) ]] ||
sp_refuse "sp change-model takes one session and one model"
change_model_target "$2" "$3" "${4:-}"
;;
teardown)
shift
[[ $# -gt 0 ]] || sp_refuse "sp teardown takes one session"
teardown_target "$@"
;;
worktree)
shift
[[ $# -gt 0 ]] || sp_refuse "sp worktree takes a command"
sp_require_subcommand worktree "$1"
exec python3 "$INVENTORY_CORE" worktree "$@"
;;
prune)
[[ $# -eq 1 ]] || sp_refuse "sp prune takes nothing else"
show_prune
;;
detail)
[[ $# -eq 2 ]] || sp_refuse "sp detail takes one session"
exec "$SCRIPT_DIR/shpool_status" --detail "$2"
;;
account)
shift
[[ $# -gt 0 ]] || sp_refuse "sp account takes a command"
sp_require_subcommand account "$1"
exec python3 "$INVENTORY_CORE" account "$@"
;;
history|log)
[[ $# -eq 2 ]] || sp_refuse "sp history takes one session"
show_history "$2"
;;
find|grep)
shift
[[ $# -gt 0 ]] || sp_refuse "sp find takes text to search for"
find_history "$*"
;;
health|resources)
show_health
;;
recover)
if [[ $# -eq 1 ]]; then
show_recovery no
elif [[ $# -eq 2 && $2 == --allow-large-ledger ]]; then
show_recovery yes
else
sp_refuse "sp recover takes only --allow-large-ledger"
fi
;;
restore)
# A name can be several words, and quoting one is not something a person
# should have to know, so everything after the verb is the selector.
shift
allow_large=no
if [[ ${1:-} == --allow-large-ledger ]]; then
allow_large=yes
shift
fi
[[ $# -ge 1 ]] || sp_refuse "sp restore takes one number from sp recover, or the selector beside a session that has none"
restore_listed "$*" "$allow_large"
;;
verify-start)
[[ $# -eq 2 ]] || sp_refuse "sp verify-start takes one session"
verify_start_target "$2"
;;
picker-open)
[[ $# -eq 2 ]] || sp_refuse "$command takes different arguments"
picker_open "$2"
;;
picker-takeover)
[[ $# -eq 2 ]] || sp_refuse "$command takes different arguments"
picker_takeover "$2"
;;
picker-close)
[[ $# -eq 2 ]] || sp_refuse "$command takes different arguments"
picker_close "$2"
;;
picker-recover)
[[ $# -eq 2 ]] || sp_refuse "$command takes different arguments"
picker_recover "$2"
;;
repair)
[[ $# -eq 2 ]] || sp_refuse "sp repair takes one session"
repair_target "$2"
;;
picker-history)
[[ $# -eq 2 ]] || sp_refuse "$command takes different arguments"
picker_history "$2"
;;
picker-name)
[[ $# -eq 3 ]] || sp_refuse "$command takes different arguments"
picker_alias "$2" set "$3"
;;
picker-name-reset)
[[ $# -eq 2 ]] || sp_refuse "$command takes different arguments"
picker_alias "$2" delete
;;
picker-title-refresh)
[[ $# -eq 2 ]] || sp_refuse "$command takes different arguments"
picker_refresh_title "$2"
;;
picker-fork)
[[ $# -eq 2 ]] || sp_refuse "$command takes different arguments"
picker_fork "$2"
;;
picker-account-switch)
[[ $# -eq 3 ]] || sp_refuse "$command takes different arguments"
picker_account_switch "$2" "$3"
;;
picker-change-model)
[[ $# -eq 3 ]] || sp_refuse "$command takes different arguments"
picker_change_model "$2" "$3"
;;
account-auto-switch)
[[ $# -eq 2 || ($# -eq 3 && ($3 == --apply || $3 == --dry-run)) ]] ||
sp_refuse "sp account-auto-switch takes one session and an optional --apply"
account_auto_switch_target "$2" "${3:-}"
;;
restore-exact)
[[ $# -ge 4 && $# -le 6 ]] || sp_refuse "$command takes different arguments"
restore_exact "$2" "$3" "$4" "${5:-}" "${6:-}"
;;
*)
sp_refuse "there is no command named $command"
;;
esac