@sagmans/dsh-tui 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/dsh-tui-dogfood/SKILL.md +83 -0
- package/.agents/skills/dsh-tui-dogfood/references/home-state.md +78 -0
- package/.agents/skills/dsh-tui-dogfood/scripts/clone-links.mjs +133 -0
- package/.agents/skills/dsh-tui-dogfood/scripts/run-plugin-from-worktree.sh +417 -0
- package/.agents/skills/dsh-tui-update-models/SKILL.md +199 -0
- package/.agents/skills/dsh-tui-update-models/references/model-wiring.md +292 -0
- package/.agents/skills/dsh-tui-update-models/scripts/dump-model-catalog.mjs +314 -0
- package/README.md +154 -27
- package/lib/agent/present.d.ts.map +1 -1
- package/lib/agent/present.js +2 -1
- package/lib/agent/present.js.map +1 -1
- package/lib/cards/composition.d.ts +68 -0
- package/lib/cards/composition.d.ts.map +1 -0
- package/lib/cards/composition.js +117 -0
- package/lib/cards/composition.js.map +1 -0
- package/lib/cards/presenter.d.ts +23 -0
- package/lib/cards/presenter.d.ts.map +1 -0
- package/lib/cards/presenter.js +394 -0
- package/lib/cards/presenter.js.map +1 -0
- package/lib/cards/preview.d.ts +42 -0
- package/lib/cards/preview.d.ts.map +1 -0
- package/lib/cards/preview.js +51 -0
- package/lib/cards/preview.js.map +1 -0
- package/lib/cards.d.ts +12 -120
- package/lib/cards.d.ts.map +1 -1
- package/lib/cards.js +4 -556
- package/lib/cards.js.map +1 -1
- package/lib/compat/section.d.ts +29 -0
- package/lib/compat/section.d.ts.map +1 -0
- package/lib/compat/section.js +82 -0
- package/lib/compat/section.js.map +1 -0
- package/lib/config.d.ts +27 -2
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +61 -1
- package/lib/config.js.map +1 -1
- package/lib/contracts.d.ts +28 -0
- package/lib/contracts.d.ts.map +1 -1
- package/lib/fold-cursor.d.ts +8 -0
- package/lib/fold-cursor.d.ts.map +1 -1
- package/lib/fold-cursor.js +18 -0
- package/lib/fold-cursor.js.map +1 -1
- package/lib/gates/question-card.d.ts +77 -0
- package/lib/gates/question-card.d.ts.map +1 -0
- package/lib/gates/question-card.js +113 -0
- package/lib/gates/question-card.js.map +1 -0
- package/lib/gates/questions.d.ts +127 -0
- package/lib/gates/questions.d.ts.map +1 -0
- package/lib/gates/questions.js +418 -0
- package/lib/gates/questions.js.map +1 -0
- package/lib/gates.d.ts +1 -113
- package/lib/gates.d.ts.map +1 -1
- package/lib/gates.js +1 -484
- package/lib/gates.js.map +1 -1
- package/lib/herdr/client.d.ts +8 -0
- package/lib/herdr/client.d.ts.map +1 -1
- package/lib/herdr/client.js +13 -1
- package/lib/herdr/client.js.map +1 -1
- package/lib/herdr/constants.d.ts +18 -0
- package/lib/herdr/constants.d.ts.map +1 -1
- package/lib/herdr/constants.js +18 -0
- package/lib/herdr/constants.js.map +1 -1
- package/lib/herdr/reporter.d.ts +20 -4
- package/lib/herdr/reporter.d.ts.map +1 -1
- package/lib/herdr/reporter.js +70 -20
- package/lib/herdr/reporter.js.map +1 -1
- package/lib/herdr/state.d.ts +10 -0
- package/lib/herdr/state.d.ts.map +1 -1
- package/lib/herdr/state.js +21 -4
- package/lib/herdr/state.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +348 -2602
- package/lib/index.js.map +1 -1
- package/lib/input/action-catalog.d.ts +74 -0
- package/lib/input/action-catalog.d.ts.map +1 -0
- package/lib/input/action-catalog.js +137 -0
- package/lib/input/action-catalog.js.map +1 -0
- package/lib/input/actions.d.ts +5 -81
- package/lib/input/actions.d.ts.map +1 -1
- package/lib/input/actions.js +4 -625
- package/lib/input/actions.js.map +1 -1
- package/lib/input/completion.d.ts +1 -1
- package/lib/input/completion.d.ts.map +1 -1
- package/lib/input/completion.js +2 -1
- package/lib/input/completion.js.map +1 -1
- package/lib/input/file-index.d.ts +40 -0
- package/lib/input/file-index.d.ts.map +1 -0
- package/lib/input/file-index.js +141 -0
- package/lib/input/file-index.js.map +1 -0
- package/lib/input/file-search.d.ts +1 -38
- package/lib/input/file-search.d.ts.map +1 -1
- package/lib/input/file-search.js +1 -487
- package/lib/input/file-search.js.map +1 -1
- package/lib/input/key-press.d.ts +57 -0
- package/lib/input/key-press.d.ts.map +1 -0
- package/lib/input/key-press.js +218 -0
- package/lib/input/key-press.js.map +1 -0
- package/lib/input/keymap-conflicts.d.ts +59 -0
- package/lib/input/keymap-conflicts.d.ts.map +1 -0
- package/lib/input/keymap-conflicts.js +292 -0
- package/lib/input/keymap-conflicts.js.map +1 -0
- package/lib/input/keymap-settings.js +1 -1
- package/lib/input/keymap-settings.js.map +1 -1
- package/lib/input/keymap.d.ts.map +1 -1
- package/lib/input/keymap.js +3 -1
- package/lib/input/keymap.js.map +1 -1
- package/lib/input/workspace-files.d.ts +10 -0
- package/lib/input/workspace-files.d.ts.map +1 -0
- package/lib/input/workspace-files.js +359 -0
- package/lib/input/workspace-files.js.map +1 -0
- package/lib/install-skills.d.ts +39 -0
- package/lib/install-skills.d.ts.map +1 -0
- package/lib/install-skills.js +273 -0
- package/lib/install-skills.js.map +1 -0
- package/lib/keys-command.d.ts +2 -1
- package/lib/keys-command.d.ts.map +1 -1
- package/lib/keys-command.js +2 -1
- package/lib/keys-command.js.map +1 -1
- package/lib/model-list.d.ts +37 -0
- package/lib/model-list.d.ts.map +1 -0
- package/lib/model-list.js +60 -0
- package/lib/model-list.js.map +1 -0
- package/lib/startup.d.ts +8 -0
- package/lib/startup.d.ts.map +1 -1
- package/lib/startup.js +120 -24
- package/lib/startup.js.map +1 -1
- package/lib/subagents.d.ts +7 -3
- package/lib/subagents.d.ts.map +1 -1
- package/lib/subagents.js +28 -4
- package/lib/subagents.js.map +1 -1
- package/lib/surface/appearance-preferences.d.ts +26 -0
- package/lib/surface/appearance-preferences.d.ts.map +1 -0
- package/lib/surface/appearance-preferences.js +139 -0
- package/lib/surface/appearance-preferences.js.map +1 -0
- package/lib/surface/appearance.d.ts +87 -0
- package/lib/surface/appearance.d.ts.map +1 -0
- package/lib/surface/appearance.js +353 -0
- package/lib/surface/appearance.js.map +1 -0
- package/lib/surface/background-work.d.ts +51 -0
- package/lib/surface/background-work.d.ts.map +1 -0
- package/lib/surface/background-work.js +172 -0
- package/lib/surface/background-work.js.map +1 -0
- package/lib/surface/commands.d.ts +82 -0
- package/lib/surface/commands.d.ts.map +1 -0
- package/lib/surface/commands.js +370 -0
- package/lib/surface/commands.js.map +1 -0
- package/lib/surface/modal-input.d.ts +59 -0
- package/lib/surface/modal-input.d.ts.map +1 -0
- package/lib/surface/modal-input.js +243 -0
- package/lib/surface/modal-input.js.map +1 -0
- package/lib/surface/model-choice.d.ts +54 -0
- package/lib/surface/model-choice.d.ts.map +1 -0
- package/lib/surface/model-choice.js +325 -0
- package/lib/surface/model-choice.js.map +1 -0
- package/lib/surface/preset-choice.d.ts +78 -0
- package/lib/surface/preset-choice.d.ts.map +1 -0
- package/lib/surface/preset-choice.js +163 -0
- package/lib/surface/preset-choice.js.map +1 -0
- package/lib/surface/prompt-input.d.ts +97 -0
- package/lib/surface/prompt-input.d.ts.map +1 -0
- package/lib/surface/prompt-input.js +215 -0
- package/lib/surface/prompt-input.js.map +1 -0
- package/lib/surface/prompt-memory.d.ts +76 -0
- package/lib/surface/prompt-memory.d.ts.map +1 -0
- package/lib/surface/prompt-memory.js +167 -0
- package/lib/surface/prompt-memory.js.map +1 -0
- package/lib/surface/session-lifecycle.d.ts +120 -0
- package/lib/surface/session-lifecycle.d.ts.map +1 -0
- package/lib/surface/session-lifecycle.js +324 -0
- package/lib/surface/session-lifecycle.js.map +1 -0
- package/lib/surface/session-picker.d.ts +39 -0
- package/lib/surface/session-picker.d.ts.map +1 -0
- package/lib/surface/session-picker.js +107 -0
- package/lib/surface/session-picker.js.map +1 -0
- package/lib/surface/session-view.d.ts +86 -0
- package/lib/surface/session-view.d.ts.map +1 -0
- package/lib/surface/session-view.js +215 -0
- package/lib/surface/session-view.js.map +1 -0
- package/lib/surface/staged-turns.d.ts +81 -0
- package/lib/surface/staged-turns.d.ts.map +1 -0
- package/lib/surface/staged-turns.js +206 -0
- package/lib/surface/staged-turns.js.map +1 -0
- package/lib/surface/terminal-lifecycle.d.ts +76 -0
- package/lib/surface/terminal-lifecycle.d.ts.map +1 -0
- package/lib/surface/terminal-lifecycle.js +222 -0
- package/lib/surface/terminal-lifecycle.js.map +1 -0
- package/lib/terminal-text/scan.d.ts +44 -0
- package/lib/terminal-text/scan.d.ts.map +1 -0
- package/lib/terminal-text/scan.js +195 -0
- package/lib/terminal-text/scan.js.map +1 -0
- package/lib/terminal-text/sgr.d.ts +32 -0
- package/lib/terminal-text/sgr.d.ts.map +1 -0
- package/lib/terminal-text/sgr.js +203 -0
- package/lib/terminal-text/sgr.js.map +1 -0
- package/lib/terminal-text.d.ts +1 -1
- package/lib/terminal-text.d.ts.map +1 -1
- package/lib/terminal-text.js +2 -379
- package/lib/terminal-text.js.map +1 -1
- package/lib/theme-command.js +2 -1
- package/lib/theme-command.js.map +1 -1
- package/lib/theme-defaults.d.ts +64 -0
- package/lib/theme-defaults.d.ts.map +1 -0
- package/lib/theme-defaults.js +248 -0
- package/lib/theme-defaults.js.map +1 -0
- package/lib/theme-files.d.ts +1 -1
- package/lib/theme-files.d.ts.map +1 -1
- package/lib/theme-files.js +1 -1
- package/lib/theme-files.js.map +1 -1
- package/lib/theme-resolver.d.ts +36 -0
- package/lib/theme-resolver.d.ts.map +1 -0
- package/lib/theme-resolver.js +112 -0
- package/lib/theme-resolver.js.map +1 -0
- package/lib/theme-schema.d.ts.map +1 -1
- package/lib/theme-schema.js +3 -2
- package/lib/theme-schema.js.map +1 -1
- package/lib/theme-settings.d.ts +5 -7
- package/lib/theme-settings.d.ts.map +1 -1
- package/lib/theme-settings.js +37 -21
- package/lib/theme-settings.js.map +1 -1
- package/lib/theme-tokens.d.ts +0 -89
- package/lib/theme-tokens.d.ts.map +1 -1
- package/lib/theme-tokens.js +0 -356
- package/lib/theme-tokens.js.map +1 -1
- package/lib/theme.d.ts +1 -1
- package/lib/theme.d.ts.map +1 -1
- package/lib/theme.js +2 -1
- package/lib/theme.js.map +1 -1
- package/lib/tool-display.d.ts.map +1 -1
- package/lib/tool-display.js +2 -1
- package/lib/tool-display.js.map +1 -1
- package/lib/transcript/message-content.d.ts +21 -0
- package/lib/transcript/message-content.d.ts.map +1 -0
- package/lib/transcript/message-content.js +61 -0
- package/lib/transcript/message-content.js.map +1 -0
- package/lib/transcript/tool-calls.d.ts +106 -0
- package/lib/transcript/tool-calls.d.ts.map +1 -0
- package/lib/transcript/tool-calls.js +254 -0
- package/lib/transcript/tool-calls.js.map +1 -0
- package/lib/transcript.d.ts +14 -55
- package/lib/transcript.d.ts.map +1 -1
- package/lib/transcript.js +31 -253
- package/lib/transcript.js.map +1 -1
- package/lib/ui/dock.d.ts +14 -2
- package/lib/ui/dock.d.ts.map +1 -1
- package/lib/ui/dock.js +52 -10
- package/lib/ui/dock.js.map +1 -1
- package/lib/ui/editor.d.ts.map +1 -1
- package/lib/ui/editor.js +2 -1
- package/lib/ui/editor.js.map +1 -1
- package/lib/ui/keymap-picker.d.ts +2 -1
- package/lib/ui/keymap-picker.d.ts.map +1 -1
- package/lib/ui/keymap-picker.js +1 -0
- package/lib/ui/keymap-picker.js.map +1 -1
- package/lib/ui/view/gate-card.d.ts +21 -0
- package/lib/ui/view/gate-card.d.ts.map +1 -0
- package/lib/ui/view/gate-card.js +104 -0
- package/lib/ui/view/gate-card.js.map +1 -0
- package/lib/ui/view/tool-card.d.ts +149 -0
- package/lib/ui/view/tool-card.d.ts.map +1 -0
- package/lib/ui/view/tool-card.js +429 -0
- package/lib/ui/view/tool-card.js.map +1 -0
- package/lib/ui/view/transcript-message.d.ts +62 -0
- package/lib/ui/view/transcript-message.d.ts.map +1 -0
- package/lib/ui/view/transcript-message.js +158 -0
- package/lib/ui/view/transcript-message.js.map +1 -0
- package/lib/ui/view.d.ts +105 -223
- package/lib/ui/view.d.ts.map +1 -1
- package/lib/ui/view.js +132 -679
- package/lib/ui/view.js.map +1 -1
- package/package.json +3 -1
- package/themes/violet-orbit.yaml +7 -5
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Clone the selected dsh home so plugin runs never write into the real profile.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
name="$(basename "${BASH_SOURCE[0]}")"
|
|
6
|
+
# The link policy is shared with the validator, so both read one rule set.
|
|
7
|
+
helper_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
8
|
+
default_repo="${DSH_DOGFOOD_DEFAULT_REPO:-$PWD}"
|
|
9
|
+
default_repo="$(cd "$default_repo" && pwd -P)"
|
|
10
|
+
|
|
11
|
+
die() { printf '%s: error: %s\n' "$name" "$*" >&2; exit 1; }
|
|
12
|
+
note() { printf '%s: %s\n' "$name" "$*" >&2; }
|
|
13
|
+
step() { printf '\n==> %s\n' "$*" >&2; }
|
|
14
|
+
|
|
15
|
+
usage() {
|
|
16
|
+
cat <<'EOF'
|
|
17
|
+
Run a dsh plugin from a worktree, in a clone of the real home.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
run-plugin-from-worktree.sh [options] [<worktree-name-or-path>] [-- <dsh args>]
|
|
21
|
+
|
|
22
|
+
The target is the checkout to run. A path is used as given; a name is matched
|
|
23
|
+
against this checkout's worktrees by directory name or branch tail (feat/copy
|
|
24
|
+
matches .../feat/copy). No target means the current checkout.
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
--home DIR scratch home to run in (default: <tmp>/dsh-dogfood/<name>-<id>)
|
|
28
|
+
--source-home DIR home to clone (default: ~/.dsh)
|
|
29
|
+
--profile NAME profile inside the scratch home (default: tui)
|
|
30
|
+
--dsh PATH launcher to run (default: dsh on PATH, or $DSH_BIN)
|
|
31
|
+
--fresh seed the scratch home with credentials and settings only
|
|
32
|
+
--with-sessions copy stored sessions too, so --resume reaches real history
|
|
33
|
+
--reseed discard an existing scratch home and clone again
|
|
34
|
+
--no-build do not run pnpm run build in the target first
|
|
35
|
+
--no-launch set the run up and print the command instead of starting
|
|
36
|
+
--status print what the scratch home is pointed at and exit
|
|
37
|
+
--dry-run print the plan without touching anything
|
|
38
|
+
--clean remove the scratch home and exit
|
|
39
|
+
--list list this repository's worktrees and exit
|
|
40
|
+
-h, --help this text
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
run-plugin-from-worktree.sh feature-x -- --resume
|
|
44
|
+
run-plugin-from-worktree.sh --fresh --profile tui-copy-fix ../dsh-tui-copy
|
|
45
|
+
run-plugin-from-worktree.sh --with-sessions --no-launch
|
|
46
|
+
|
|
47
|
+
The scratch home holds copied credentials and is kept private. --clean requires
|
|
48
|
+
a marker bound to this home, source, and checkout; no unmarked home is deleted.
|
|
49
|
+
EOF
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# --- arguments ---------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
target=""
|
|
55
|
+
home=""
|
|
56
|
+
source_home="$HOME/.dsh"
|
|
57
|
+
profile="tui"
|
|
58
|
+
dsh_bin="${DSH_BIN:-}"
|
|
59
|
+
fresh=0
|
|
60
|
+
with_sessions=0
|
|
61
|
+
reseed=0
|
|
62
|
+
build=1
|
|
63
|
+
launch=1
|
|
64
|
+
dry_run=0
|
|
65
|
+
action=""
|
|
66
|
+
|
|
67
|
+
while [[ $# -gt 0 ]]; do
|
|
68
|
+
case "$1" in
|
|
69
|
+
--home) home="${2:?--home needs a directory}"; shift 2 ;;
|
|
70
|
+
--source-home) source_home="${2:?--source-home needs a directory}"; shift 2 ;;
|
|
71
|
+
--profile) profile="${2:?--profile needs a name}"; shift 2 ;;
|
|
72
|
+
--dsh) dsh_bin="${2:?--dsh needs a path}"; shift 2 ;;
|
|
73
|
+
--fresh) fresh=1; shift ;;
|
|
74
|
+
--with-sessions) with_sessions=1; shift ;;
|
|
75
|
+
--reseed) reseed=1; shift ;;
|
|
76
|
+
--no-build) build=0; shift ;;
|
|
77
|
+
--no-launch) launch=0; shift ;;
|
|
78
|
+
--status) action="status"; shift ;;
|
|
79
|
+
--dry-run) dry_run=1; launch=0; shift ;;
|
|
80
|
+
--clean) action="clean"; shift ;;
|
|
81
|
+
--list) action="list"; shift ;;
|
|
82
|
+
-h|--help) usage; exit 0 ;;
|
|
83
|
+
--) shift; break ;;
|
|
84
|
+
-*) die "unknown option $1 (try --help)" ;;
|
|
85
|
+
*) [[ -z "$target" ]] || die "only one target is accepted (got $target and $1)"
|
|
86
|
+
target="$1"; shift ;;
|
|
87
|
+
esac
|
|
88
|
+
done
|
|
89
|
+
passthrough=("$@")
|
|
90
|
+
|
|
91
|
+
if [[ -n "$source_home" ]]; then source_home="${source_home/#\~/$HOME}"; fi
|
|
92
|
+
|
|
93
|
+
# --- the repository's worktrees ---------------------------------------------
|
|
94
|
+
|
|
95
|
+
worktree_lines() {
|
|
96
|
+
git -C "$default_repo" worktree list --porcelain 2>/dev/null || true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
list_worktrees() {
|
|
100
|
+
local line path="" branch=""
|
|
101
|
+
while IFS= read -r line; do
|
|
102
|
+
case "$line" in
|
|
103
|
+
worktree\ *) path="${line#worktree }" ;;
|
|
104
|
+
branch\ *) branch="${line#branch refs/heads/}" ;;
|
|
105
|
+
"")
|
|
106
|
+
[[ -n "$path" ]] || continue
|
|
107
|
+
if [[ -n "$branch" ]]; then printf '%-44s %s\n' "$path" "($branch)"
|
|
108
|
+
else printf '%-44s %s\n' "$path" "(detached)"; fi
|
|
109
|
+
path=""; branch="" ;;
|
|
110
|
+
esac
|
|
111
|
+
done < <(worktree_lines)
|
|
112
|
+
if [[ -n "$path" ]]; then printf '%-44s %s\n' "$path" "(${branch:-detached})"; fi
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# A name matches a worktree's directory name or the tail of its branch, so both
|
|
116
|
+
# feat/copy-columns and copy-columns reach the same checkout.
|
|
117
|
+
resolve_by_name() {
|
|
118
|
+
local want="$1" line path="" branch="" found=""
|
|
119
|
+
while IFS= read -r line; do
|
|
120
|
+
case "$line" in
|
|
121
|
+
worktree\ *) path="${line#worktree }" ;;
|
|
122
|
+
branch\ *) branch="${line#branch refs/heads/}" ;;
|
|
123
|
+
"")
|
|
124
|
+
[[ -n "$path" ]] || continue
|
|
125
|
+
if [[ "$(basename "$path")" == "$want" || "$branch" == "$want" || "${branch##*/}" == "$want" ]]; then
|
|
126
|
+
[[ -z "$found" ]] || die "'$want' matches more than one worktree; pass a path"
|
|
127
|
+
found="$path"
|
|
128
|
+
fi
|
|
129
|
+
path=""; branch="" ;;
|
|
130
|
+
esac
|
|
131
|
+
done < <(worktree_lines)
|
|
132
|
+
if [[ -n "$path" ]] && [[ "$(basename "$path")" == "$want" || "$branch" == "$want" || "${branch##*/}" == "$want" ]]; then
|
|
133
|
+
[[ -z "$found" ]] || die "'$want' matches more than one worktree; pass a path"
|
|
134
|
+
found="$path"
|
|
135
|
+
fi
|
|
136
|
+
printf '%s' "$found"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
resolve_target() {
|
|
140
|
+
[[ -n "$target" ]] || { printf '%s' "$default_repo"; return; }
|
|
141
|
+
if [[ -d "$target" ]]; then (cd "$target" && pwd -P); return; fi
|
|
142
|
+
local hit sibling
|
|
143
|
+
hit="$(resolve_by_name "$target")"
|
|
144
|
+
if [[ -z "$hit" ]]; then
|
|
145
|
+
sibling="$(cd "$default_repo/.." 2>/dev/null && pwd -P)/$target"
|
|
146
|
+
if [[ -d "$sibling" ]]; then hit="$sibling"; fi
|
|
147
|
+
fi
|
|
148
|
+
[[ -n "$hit" ]] || die "no worktree or directory named '$target' (try --list)"
|
|
149
|
+
(cd "$hit" && pwd -P)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if [[ "$action" == "list" ]]; then list_worktrees; exit 0; fi
|
|
153
|
+
|
|
154
|
+
[[ "$profile" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]] || die "invalid profile name: $profile"
|
|
155
|
+
# A removed checkout cannot supply package.json. Only explicit scratch-home cleanup
|
|
156
|
+
# may use its private marker to recover the original, now absent target identity.
|
|
157
|
+
orphan_clean=0
|
|
158
|
+
if [[ "$action" == clean && -n "$home" && -z "$target" ]]; then
|
|
159
|
+
candidate_home="$(node -e "console.log(require('node:path').resolve(process.argv[1]))" "$home")"
|
|
160
|
+
# Orphan cleanup trusts only a private clone marker owned by this user.
|
|
161
|
+
if [[ -f "$candidate_home/.dsh-dogfood" ]] && node -e '
|
|
162
|
+
const fs = require("node:fs")
|
|
163
|
+
const [dir, marker] = process.argv.slice(1).map((file) => fs.lstatSync(file))
|
|
164
|
+
process.exit(dir.isDirectory() && dir.uid === process.getuid() && (dir.mode & 0o077) === 0 && marker.isFile() && marker.nlink === 1 && marker.uid === process.getuid() && (marker.mode & 0o077) === 0 ? 0 : 1)
|
|
165
|
+
' "$candidate_home" "$candidate_home/.dsh-dogfood"; then
|
|
166
|
+
candidate_target="$(sed -n 's/^target=//p' "$candidate_home/.dsh-dogfood" | head -1)"
|
|
167
|
+
if [[ -n "$candidate_target" && ! -e "$candidate_target/package.json" ]]; then
|
|
168
|
+
node -e "const p=require('node:path'),s=process.argv[1];process.exit(p.isAbsolute(s) && s !== p.parse(s).root && s === p.normalize(s) ? 0 : 1)" "$candidate_target" || die "scratch marker has invalid target path"
|
|
169
|
+
target_path="$candidate_target"
|
|
170
|
+
orphan_clean=1
|
|
171
|
+
fi
|
|
172
|
+
fi
|
|
173
|
+
fi
|
|
174
|
+
if [[ "$orphan_clean" == 0 ]]; then
|
|
175
|
+
target_path="$(resolve_target)"
|
|
176
|
+
[[ -f "$target_path/package.json" ]] || die "$target_path has no package.json"
|
|
177
|
+
package_name="$(node -e "const p = require(process.argv[1]); if (typeof p.name !== 'string') process.exit(1); console.log(p.name)" "$target_path/package.json")" || die "$target_path has an invalid package.json"
|
|
178
|
+
[[ "$package_name" =~ ^(@[a-zA-Z0-9][a-zA-Z0-9._-]*/)?[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]] || die "invalid plugin package name: $package_name"
|
|
179
|
+
if [[ "$build" == 1 ]] && ! node -e "process.exit(typeof require(process.argv[1]).scripts?.build === 'string' && require(process.argv[1]).scripts.build.trim() ? 0 : 1)" "$target_path/package.json"; then
|
|
180
|
+
build=0
|
|
181
|
+
fi
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
# The hash prevents same-named checkouts in different repositories sharing state.
|
|
185
|
+
target_name="$(basename "$target_path")"
|
|
186
|
+
target_id="$(node -e "console.log(require('node:crypto').createHash('sha256').update(process.argv[1]).digest('hex').slice(0, 12))" "$target_path")"
|
|
187
|
+
tmp_root="${TMPDIR:-/tmp}"
|
|
188
|
+
: "${home:=${tmp_root%/}/dsh-dogfood/${target_name}-${target_id}}"
|
|
189
|
+
if [[ "$action" == clean && ! -d "$source_home" ]]; then
|
|
190
|
+
[[ -n "$home" && ! -e "$source_home" && ! -L "$source_home" ]] || die "missing source home requires --clean --home and an absent source path"
|
|
191
|
+
# Cleanup still needs the original physical identity to match its private marker.
|
|
192
|
+
source_home="$(node -e "const fs=require('node:fs'),p=require('node:path');let x=p.resolve(process.argv[1]),b=x;while(!fs.existsSync(b))b=p.dirname(b);console.log(p.join(fs.realpathSync(b),p.relative(b,x)))" "$source_home")"
|
|
193
|
+
else
|
|
194
|
+
[[ -d "$source_home" ]] || die "no home to clone at $source_home; pass --source-home"
|
|
195
|
+
source_home="$(cd "$source_home" && pwd -P)"
|
|
196
|
+
fi
|
|
197
|
+
[[ ! -L "$home" ]] || die "scratch home cannot be a symlink: $home"
|
|
198
|
+
home="$(node -e "const fs=require('node:fs'),p=require('node:path');let x=p.resolve(process.argv[1]),b=x;while(!fs.existsSync(b))b=p.dirname(b);console.log(p.join(fs.realpathSync(b),p.relative(b,x)))" "$home")"
|
|
199
|
+
[[ ! -L "$home" ]] || die "scratch home cannot be a symlink: $home"
|
|
200
|
+
[[ "$home" != "$source_home" && "$home/" != "$source_home/"* && "$source_home/" != "$home/"* ]] || die "scratch home and source home must not overlap"
|
|
201
|
+
user_home="$(cd "$HOME" && pwd -P)"
|
|
202
|
+
[[ "$home/" != "$user_home/"* && "$user_home/" != "$home/"* ]] || die "scratch home cannot overlap the user home"
|
|
203
|
+
[[ "$home/" != "$target_path/"* && "$target_path/" != "$home/"* ]] || die "scratch home cannot overlap the target checkout"
|
|
204
|
+
[[ "$home" != / ]] || die "scratch home cannot be filesystem root"
|
|
205
|
+
for path in "$home" "$source_home" "$target_path"; do [[ "$path" != *$'\n'* ]] || die "paths cannot contain newlines"; done
|
|
206
|
+
marker="$home/.dsh-dogfood"
|
|
207
|
+
profile_json="$home/profiles/$profile/package.json"
|
|
208
|
+
|
|
209
|
+
check_credentials() {
|
|
210
|
+
[[ ! -L "$home/.credentials.yaml" ]] || die "cloned credentials cannot be a symlink"
|
|
211
|
+
[[ -e "$home/.credentials.yaml" ]] || return 0
|
|
212
|
+
node -e "const s=require('node:fs').lstatSync(process.argv[1]);process.exit(s.isFile() && s.nlink === 1 ? 0 : 1)" "$home/.credentials.yaml" || die "cloned credentials cannot be hardlinked or non-regular"
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
check_clone_links() {
|
|
216
|
+
node "$helper_dir/clone-links.mjs" check "$home" "$source_home" || die "cloned home contains unsafe symlink or hardlink"
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
materialize_cloned_links() {
|
|
220
|
+
node "$helper_dir/clone-links.mjs" materialize "$home" "$source_home" || die "cloned home links could not be made self-contained"
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
validate_marker() {
|
|
224
|
+
[[ -f "$marker" && ! -L "$marker" ]] || die "$home has no valid .dsh-dogfood marker; refusing to modify it"
|
|
225
|
+
node -e "const s=require('node:fs').lstatSync(process.argv[1]);process.exit(s.isFile() && s.nlink === 1 ? 0 : 1)" "$marker" || die "$home marker cannot be hardlinked or non-regular"
|
|
226
|
+
[[ "$(marker_value home)" == "$home" && "$(marker_value source)" == "$source_home" && "$(marker_value target)" == "$target_path" && "$(marker_value profile)" == "$profile" ]] || die "$home marker identity does not match home, source, target, and profile"
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
[[ -n "$dsh_bin" ]] || dsh_bin="$(command -v dsh || true)"
|
|
230
|
+
|
|
231
|
+
describe_bundles() {
|
|
232
|
+
[[ -f "$profile_json" ]] || { printf 'none\n'; return; }
|
|
233
|
+
node -e "console.log(require(process.argv[1]).dsh.profile.bundles.join(', '))" "$profile_json" 2>/dev/null || printf 'unreadable\n'
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
marker_value() {
|
|
237
|
+
[[ -f "$marker" ]] || { printf 'unknown'; return; }
|
|
238
|
+
sed -n "s/^$1=//p" "$marker" | head -1
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if [[ "$action" == "status" ]]; then
|
|
242
|
+
[[ -d "$home" ]] || die "no scratch home at $home"
|
|
243
|
+
printf 'home: %s\n' "$home"
|
|
244
|
+
printf 'source: %s\n' "$(marker_value source)"
|
|
245
|
+
printf 'target: %s\n' "$(marker_value target)"
|
|
246
|
+
printf 'profile: %s\n' "$profile"
|
|
247
|
+
printf 'bundles: %s\n' "$(describe_bundles)"
|
|
248
|
+
printf 'link: %s\n' "$(node -e "console.log(require(process.argv[1]).dependencies?.[process.argv[2]] ?? 'absent')" "$profile_json" "$package_name" 2>/dev/null || printf 'absent')"
|
|
249
|
+
exit 0
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
if [[ "$action" == "clean" ]]; then
|
|
253
|
+
[[ -d "$home" ]] || { note "nothing at $home"; exit 0; }
|
|
254
|
+
validate_marker
|
|
255
|
+
rm -rf "$home"
|
|
256
|
+
note "removed $home"
|
|
257
|
+
exit 0
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
if [[ "$dry_run" == 1 ]]; then
|
|
261
|
+
printf 'target: %s\n' "$target_path"
|
|
262
|
+
printf 'home: %s%s\n' "$home" "$([[ -d "$home" ]] && printf ' (exists: reuse)' || printf ' (missing: seed)')"
|
|
263
|
+
printf 'source: %s%s\n' "$source_home" "$([[ "$fresh" == 1 ]] && printf ' (fresh: credentials and settings only)' || printf '')"
|
|
264
|
+
printf 'profile: %s\n' "$profile"
|
|
265
|
+
printf 'build: %s\n' "$([[ "$build" == 1 ]] && printf 'pnpm run build' || printf 'skipped')"
|
|
266
|
+
printf 'launch: DSH_HOME=%s %s --profile %s %s\n' "$home" "${dsh_bin:-dsh}" "$profile" "${passthrough[*]:-}"
|
|
267
|
+
exit 0
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
[[ -n "$dsh_bin" ]] || die "no dsh launcher found; put dsh on PATH or pass --dsh"
|
|
271
|
+
|
|
272
|
+
# --- seed the scratch home ---------------------------------------------------
|
|
273
|
+
|
|
274
|
+
if [[ -d "$home" && "$action" != "clean" && "$dry_run" != 1 ]]; then
|
|
275
|
+
validate_marker
|
|
276
|
+
check_credentials
|
|
277
|
+
chmod 700 "$home"
|
|
278
|
+
if [[ -f "$home/.credentials.yaml" ]]; then chmod 600 "$home/.credentials.yaml"; fi
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
if [[ "$reseed" == 1 && -d "$home" ]]; then
|
|
282
|
+
validate_marker
|
|
283
|
+
rm -rf "$home"
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
if [[ "$fresh" == 1 && -d "$home" ]]; then
|
|
287
|
+
note "$home already exists: --fresh applies when a home is seeded (--reseed to re-seed it)"
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
if [[ ! -d "$home" ]]; then
|
|
291
|
+
[[ -d "$source_home" ]] || die "no home to clone at $source_home; pass --source-home"
|
|
292
|
+
[[ "$with_sessions" != 1 || ! -L "$source_home/sessions" ]] || die "source sessions cannot be a symlink"
|
|
293
|
+
step "seeding $home from $source_home"
|
|
294
|
+
mkdir -p "$home"
|
|
295
|
+
chmod 700 "$home"
|
|
296
|
+
if [[ "$fresh" == 1 ]]; then
|
|
297
|
+
for seed in .credentials.yaml settings.yaml; do
|
|
298
|
+
if [[ -f "$source_home/$seed" ]]; then cp -p "$source_home/$seed" "$home/$seed"; fi
|
|
299
|
+
done
|
|
300
|
+
elif command -v rsync >/dev/null 2>&1; then
|
|
301
|
+
# Preserving the source root's mode can expose copied credentials before final chmod.
|
|
302
|
+
rsync -a --no-perms --exclude '/sessions/' --exclude '/.dsh-dogfood' "$source_home/" "$home/"
|
|
303
|
+
if [[ "$with_sessions" == 1 && -d "$source_home/sessions" ]]; then rsync -a --no-perms "$source_home/sessions/" "$home/sessions/"; fi
|
|
304
|
+
else
|
|
305
|
+
# tar carries the tree without the bulk of the sessions; BSD tar takes the
|
|
306
|
+
# exclude in the same form as GNU tar.
|
|
307
|
+
mkdir -p "$home/sessions"
|
|
308
|
+
(cd "$source_home" && tar -c --exclude './sessions' --exclude './.dsh-dogfood' .) | (cd "$home" && tar -x)
|
|
309
|
+
if [[ "$with_sessions" == 1 && -d "$source_home/sessions" ]]; then (cd "$source_home" && tar -c sessions) | (cd "$home" && tar -x); fi
|
|
310
|
+
fi
|
|
311
|
+
chmod 700 "$home"
|
|
312
|
+
check_credentials
|
|
313
|
+
chmod 600 "$home/.credentials.yaml" 2>/dev/null || true
|
|
314
|
+
if [[ "$with_sessions" != 1 ]]; then
|
|
315
|
+
note "sessions were not copied; --resume starts empty (pass --with-sessions for your history)"
|
|
316
|
+
fi
|
|
317
|
+
fi
|
|
318
|
+
|
|
319
|
+
printf 'home=%s\nsource=%s\ntarget=%s\nprofile=%s\n' "$home" "$source_home" "$target_path" "$profile" > "$marker"
|
|
320
|
+
chmod 600 "$marker"
|
|
321
|
+
materialize_cloned_links
|
|
322
|
+
check_clone_links
|
|
323
|
+
|
|
324
|
+
# --- build, link, run --------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
if [[ "$build" == 1 && ! -d "$target_path/node_modules" ]]; then
|
|
327
|
+
die "$target_path has no node_modules; run pnpm install there first (or pass --no-build)"
|
|
328
|
+
fi
|
|
329
|
+
|
|
330
|
+
if [[ "$build" == 1 ]]; then
|
|
331
|
+
step "building $target_path"
|
|
332
|
+
(cd "$target_path" && pnpm run build) || die "pnpm run build failed in $target_path"
|
|
333
|
+
fi
|
|
334
|
+
|
|
335
|
+
# A profile's local bundles are installed as relative symlinks under its own
|
|
336
|
+
# node_modules, and those paths only resolve at the depth of the home they were
|
|
337
|
+
# installed in. A clone at a different depth (a /tmp scratch home) leaves every
|
|
338
|
+
# one dangling, so dsh refuses to mount the profile. Rebuild each link from the
|
|
339
|
+
# absolute 'link:' spec its package.json already carries. The profile is created
|
|
340
|
+
# from the shipped template when it is missing, then rewritten, because
|
|
341
|
+
# 'dsh plugin add' re-materialises the same relative links and drops the other
|
|
342
|
+
# bundles the profile had.
|
|
343
|
+
step "pointing profile '$profile' at $target_path"
|
|
344
|
+
profile_dir="$home/profiles/$profile"
|
|
345
|
+
[[ ! -L "$home/profiles" && ! -L "$profile_dir" && ! -L "$profile_json" ]] || die "cloned profile cannot contain a symlink in its manifest path"
|
|
346
|
+
if [[ ! -f "$profile_json" ]]; then
|
|
347
|
+
DSH_HOME="$home" "$dsh_bin" --profile "$profile" --from-default-profile "$profile" --help >/dev/null 2>&1 || true
|
|
348
|
+
[[ ! -L "$home/profiles" && ! -L "$profile_dir" && ! -L "$profile_json" ]] || die "cloned profile cannot contain a symlink in its manifest path"
|
|
349
|
+
if [[ ! -f "$profile_json" ]]; then
|
|
350
|
+
DSH_HOME="$home" "$dsh_bin" plugin --profile "$profile" add "$target_path" >&2 || die "could not create profile '$profile' in $home"
|
|
351
|
+
fi
|
|
352
|
+
fi
|
|
353
|
+
[[ -f "$profile_json" ]] || die "profile '$profile' has no package.json at $profile_json"
|
|
354
|
+
node -e "const s=require('node:fs').lstatSync(process.argv[1]);process.exit(s.isFile() && s.nlink === 1 ? 0 : 1)" "$profile_json" || die "cloned profile manifest cannot be hardlinked or non-regular"
|
|
355
|
+
|
|
356
|
+
# Legacy callers require a prelisted bundle; generic callers can add one in the clone.
|
|
357
|
+
if [[ "${DSH_DOGFOOD_REQUIRE_LISTED:-0}" == 1 ]] && ! node -e "process.exit(require(process.argv[1]).dsh?.profile?.bundles?.includes(process.argv[2]) ? 0 : 1)" "$profile_json" "$package_name"; then
|
|
358
|
+
die "profile '$profile' does not list $package_name (bundles: $(describe_bundles)); add it to the profile first"
|
|
359
|
+
fi
|
|
360
|
+
node -e '
|
|
361
|
+
const fs = require("node:fs")
|
|
362
|
+
const path = require("node:path")
|
|
363
|
+
const [manifestPath, directory, originalDirectory, packageName, targetPath] = process.argv.slice(1)
|
|
364
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"))
|
|
365
|
+
if (!Array.isArray(manifest.dsh?.profile?.bundles)) throw Error("invalid profile bundle list")
|
|
366
|
+
if (!manifest.dsh.profile.bundles.includes(packageName)) manifest.dsh.profile.bundles.push(packageName)
|
|
367
|
+
manifest.dependencies ||= {}
|
|
368
|
+
manifest.dependencies[packageName] = "link:" + targetPath
|
|
369
|
+
const moduleDir = path.join(directory, "node_modules")
|
|
370
|
+
const entries = Object.entries(manifest.dependencies).filter(([, spec]) => typeof spec === "string" && spec.startsWith("link:"))
|
|
371
|
+
const packagePattern = /^(@[a-zA-Z0-9][a-zA-Z0-9._-]*\/)?[a-zA-Z0-9][a-zA-Z0-9._-]*$/
|
|
372
|
+
const state = (file) => fs.lstatSync(file, { throwIfNoEntry: false })
|
|
373
|
+
for (const part of [directory, path.dirname(directory), moduleDir]) {
|
|
374
|
+
if (state(part)?.isSymbolicLink()) throw Error("unsafe symlink in profile path: " + part)
|
|
375
|
+
}
|
|
376
|
+
for (const [name, spec] of entries) {
|
|
377
|
+
if (!packagePattern.test(name)) throw Error("invalid link dependency name: " + name)
|
|
378
|
+
if (name.includes("\n") || spec.includes("\n") || spec.includes("\0")) throw Error("unsafe link dependency: " + name)
|
|
379
|
+
const destination = path.join(moduleDir, name)
|
|
380
|
+
if (state(path.dirname(destination))?.isSymbolicLink()) throw Error("unsafe symlink in node_modules: " + name)
|
|
381
|
+
const existing = state(destination)
|
|
382
|
+
if (existing && !existing.isSymbolicLink()) throw Error("unsafe existing module at " + destination)
|
|
383
|
+
}
|
|
384
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n")
|
|
385
|
+
for (const [name, spec] of entries) {
|
|
386
|
+
const destination = path.join(moduleDir, name)
|
|
387
|
+
const target = path.resolve(originalDirectory, spec.slice("link:".length))
|
|
388
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true })
|
|
389
|
+
if (state(destination)) fs.unlinkSync(destination)
|
|
390
|
+
fs.symlinkSync(target, destination)
|
|
391
|
+
}
|
|
392
|
+
' "$profile_json" "$profile_dir" "$source_home/profiles/$profile" "$package_name" "$target_path" || die "could not safely relink profile '$profile'"
|
|
393
|
+
# Newly materialized dependency links must obey the same source-home boundary.
|
|
394
|
+
check_clone_links
|
|
395
|
+
|
|
396
|
+
bundles="$(describe_bundles)"
|
|
397
|
+
if ! node -e "process.exit(require(process.argv[1]).dsh.profile.bundles.includes(process.argv[2]) ? 0 : 1)" "$profile_json" "$package_name"; then
|
|
398
|
+
die "profile '$profile' does not list $package_name after relink (bundles: $bundles)"
|
|
399
|
+
fi
|
|
400
|
+
|
|
401
|
+
printf '\n' >&2
|
|
402
|
+
printf 'home: %s\n' "$home" >&2
|
|
403
|
+
printf 'target: %s\n' "$target_path" >&2
|
|
404
|
+
printf 'profile: %s (%s)\n' "$profile" "$bundles" >&2
|
|
405
|
+
|
|
406
|
+
run_command=(env "DSH_HOME=$home" "$dsh_bin" --profile "$profile")
|
|
407
|
+
if [[ "${#passthrough[@]}" -gt 0 ]]; then
|
|
408
|
+
run_command+=("${passthrough[@]}")
|
|
409
|
+
fi
|
|
410
|
+
|
|
411
|
+
if [[ "$launch" != 1 ]]; then
|
|
412
|
+
printf '\nrun it with:\n %s\n' "${run_command[*]}" >&2
|
|
413
|
+
exit 0
|
|
414
|
+
fi
|
|
415
|
+
|
|
416
|
+
step "starting the surface"
|
|
417
|
+
exec "${run_command[@]}"
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dsh-tui-update-models
|
|
3
|
+
description: "Use when providers, models, prices, context windows, or reasoning effort levels must be added, changed, subset, or removed in a dsh profile: the llm-pi-ai provider config, the dsh-provider-extra owned catalog, settings.yaml, and a profile's cordis.patch.yml."
|
|
4
|
+
---
|
|
5
|
+
# Update dsh providers and models
|
|
6
|
+
|
|
7
|
+
Every model the surface can pick comes from one of two owners, resolved at
|
|
8
|
+
composition time and read back by `/model`, `dsh --profile tui list-models`, and
|
|
9
|
+
the ctx gauge. Find the owner first; then edit exactly one place.
|
|
10
|
+
|
|
11
|
+
## 1. Find the owner
|
|
12
|
+
|
|
13
|
+
| Where the wiring is | What it owns |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `~/.dsh/profiles/<profile>/cordis.patch.yml` | The patch layer: applied after every bundle layer, last entry wins. Holds the catalog or `llm-pi-ai` config a developer actually edits. |
|
|
16
|
+
| `~/.dsh/settings.yaml` | Host settings, hot-reloaded. A `llm-pi-ai:` or `llm-deepseek:` section overrides that adapter's patch entry without a restart - it is what the web Models page writes, and what an additive sign-in declares a route in. The legacy `dsh-provider-extra:` section carries `extraModels` and `codexExtraModels` only. **A catalog is never settings-owned**: managed mode installs no section, reads no overlay, and turns off settings projection for its own Config, so the profile patch is the only place a catalog exists. |
|
|
17
|
+
| `~/.dsh/profiles/<profile>/package.json` | Bundles and their dependency specs (`link:` for a checkout, a range for a release). |
|
|
18
|
+
| `node_modules/<bundle>/cordis.patch.yml` | The bundle's own defaults: disabled base rows, inserted rows, default config. |
|
|
19
|
+
| `node_modules/@earendil-works/pi-ai/dist/providers/data/<source>.json` | Installed per-provider catalog: ids, and the cost/context/effort data a template clone or a `filter` match inherits. |
|
|
20
|
+
| `node_modules/@sagmans/dsh-provider-extra/docs/catalog.md` | The catalog's own field list and rejection rules, matching the plugin build the profile resolves. Read it before inventing a field. |
|
|
21
|
+
|
|
22
|
+
Entry shapes in the patch layer:
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
- id: llm-pi-ai # id-targeted config override
|
|
26
|
+
config: { ... } # replaces that row's config wholesale - restate every field
|
|
27
|
+
- { "id": "llm-deepseek", "disabled": true } # retire a row a bundle ships
|
|
28
|
+
- insert: # add a row the profile does not have
|
|
29
|
+
- id: web-search-exa
|
|
30
|
+
name: '@deepseek-ai/dsh-web-search-exa'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
A config in a patch **replaces** the base config; it never merges field by field.
|
|
34
|
+
|
|
35
|
+
## 2. Pick the approach
|
|
36
|
+
|
|
37
|
+
| Intent | Approach |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| Add or re-declare providers additively | `llm-pi-ai` config: `providers.<route>.models[]` |
|
|
40
|
+
| One new id that behaves like a catalog sibling | `extraModels: [{ id, template, name? }]` (clone the sibling's wire behavior) |
|
|
41
|
+
| Serve only some of a route's catalog | a `models:` whitelist of ids, in the order the picker should show |
|
|
42
|
+
| Own the whole model directory (gateways, subscriptions, logins) | `dsh-provider-extra` `catalog` v1, and disable the base rows |
|
|
43
|
+
| Serve most of an installed source, minus a few ids | catalog route `filter: { include?, exclude? }` - keeps every installed fact |
|
|
44
|
+
| Point a route at your own endpoint | catalog route with `api` + `baseURL` and no `source` |
|
|
45
|
+
| Retire a model or a provider | drop it from the list, or disable the row that offers it |
|
|
46
|
+
|
|
47
|
+
A catalog route declares **exactly one** of `models` and `filter`; `models: []`
|
|
48
|
+
is the explicit empty selection, and `filter` needs an installed `source`.
|
|
49
|
+
|
|
50
|
+
`dsh-provider-extra` catalog skeleton, with the two override layers a correct
|
|
51
|
+
entry needs as soon as a vendor number differs from the inherited one:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
- id: dsh-provider-extra
|
|
55
|
+
config:
|
|
56
|
+
catalog:
|
|
57
|
+
version: 1
|
|
58
|
+
default: { provider: opencode-go-session, model: deepseek-flash, reasoningEffort: max }
|
|
59
|
+
providers:
|
|
60
|
+
- id: openai
|
|
61
|
+
name: OpenAI API
|
|
62
|
+
source: openai # pi-ai provider id; its data is the base
|
|
63
|
+
auth: { apiKeyRef: OPENAI_API_KEY } # omit auth for a keyless local route
|
|
64
|
+
models:
|
|
65
|
+
- id: gpt-6-sol
|
|
66
|
+
name: GPT-6 Sol
|
|
67
|
+
aliases: [gpt-6-sol-preview] # extra selector inputs, never extra rows
|
|
68
|
+
template: gpt-5.6-sol # inherits wire behavior AND stale metadata
|
|
69
|
+
defaultMaxTokens: 128000 # request cap, only when the request gives none
|
|
70
|
+
metadata: # override what the vendor documented
|
|
71
|
+
contextWindow: 1050000
|
|
72
|
+
cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 }
|
|
73
|
+
- id: local-llm
|
|
74
|
+
name: Local llama.cpp
|
|
75
|
+
api: openai-completions # own endpoint: api is required, source is absent
|
|
76
|
+
baseURL: http://127.0.0.1:8080/v1
|
|
77
|
+
models:
|
|
78
|
+
- id: qwen3-coder
|
|
79
|
+
name: Qwen3 Coder
|
|
80
|
+
metadata: # no installed sibling: every fact is declared
|
|
81
|
+
reasoning: true
|
|
82
|
+
input: [text]
|
|
83
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
|
84
|
+
contextWindow: 262144
|
|
85
|
+
maxTokens: 65536
|
|
86
|
+
- id: zai-all
|
|
87
|
+
name: Z.AI Coding Plan
|
|
88
|
+
source: zai
|
|
89
|
+
auth: { apiKeyRef: ZAI_API_KEY }
|
|
90
|
+
filter: { include: ['glm-*'], exclude: ['glm-4*'] } # '*' is the only wildcard
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Rules the resolver enforces, each rejecting the **whole** candidate: `version` is
|
|
94
|
+
exactly `1`; an unknown field anywhere; duplicate provider ids, model ids, or
|
|
95
|
+
aliases; a `source` no installed pi-ai provider ships; `api` beside `source`, or
|
|
96
|
+
absent when `source` is absent; `baseURL` absent, non-HTTP(S), or carrying
|
|
97
|
+
credentials; a `transport` on a route whose source is not `openai-codex`; a
|
|
98
|
+
`fallbackSessionId` on a route whose source is not `opencode-go`; `auth` naming
|
|
99
|
+
both modes, or `apiKeyRef` on `openai-codex`, or a `credentialProvider` that is
|
|
100
|
+
not the `source`; both or neither of `models`/`filter`; an unknown `template`; a
|
|
101
|
+
model with no resolvable template and incomplete `metadata`; a
|
|
102
|
+
`defaultMaxTokens` above the model's `maxTokens`; a `metadata.api` that changes a
|
|
103
|
+
template-backed protocol; an empty catalog whose `default` is not `null`; a
|
|
104
|
+
`default` outside the served selection; a `reasoningEffort` the model does not
|
|
105
|
+
support. Omitting `catalog` is not `providers: []`: the first keeps the plugin's
|
|
106
|
+
legacy additive routes, the second serves nothing.
|
|
107
|
+
|
|
108
|
+
Ownership is exclusive and checked at boot. Disable **every** competing row in
|
|
109
|
+
the whole profile - on `dsh-base` those are `llm-pi-ai`, `llm-deepseek`, and
|
|
110
|
+
`agent-default-model` - and mount provider-extra after them. A surviving adapter,
|
|
111
|
+
provider directory, or default owner logs `CATALOG_OWNER_COLLISION` and fails
|
|
112
|
+
listing, selection, dispatch, and sign-in closed until it is gone.
|
|
113
|
+
|
|
114
|
+
The pinned host reads only `provider` and `model` from `catalog.default`: it
|
|
115
|
+
ignores `reasoningEffort`, so effort is chosen in the session surface. Saving a
|
|
116
|
+
default needs the host's `configEditor`; a host without one rejects with
|
|
117
|
+
`CONFIG_PERSISTENCE_UNAVAILABLE`, and the profile patch stays the place to edit.
|
|
118
|
+
|
|
119
|
+
## 3. Reasoning efforts
|
|
120
|
+
|
|
121
|
+
pi-ai levels are `off, minimal, low, medium, high, xhigh, max`. In
|
|
122
|
+
`metadata.thinkingLevelMap`:
|
|
123
|
+
|
|
124
|
+
| Value | Meaning |
|
|
125
|
+
| --- | --- |
|
|
126
|
+
| `null` | level is not offered |
|
|
127
|
+
| absent | offered, sent to the vendor under this same name |
|
|
128
|
+
| a string | offered, sent as that vendor value (OpenAI's `none`, codex's `low` for `minimal`) |
|
|
129
|
+
|
|
130
|
+
`xhigh` and `max` are offered **only** when explicitly mapped. `off` means "send no
|
|
131
|
+
reasoning parameter" on the OpenAI and codex adapters. Vendors name the same
|
|
132
|
+
concept differently - map it, never rename the vendor value.
|
|
133
|
+
|
|
134
|
+
## 4. Costs
|
|
135
|
+
|
|
136
|
+
Per 1M tokens: `{ input, output, cacheRead, cacheWrite }`, plus optional
|
|
137
|
+
`tiers: [{ inputTokensAbove, input, output, cacheRead, cacheWrite }]`. `0` means
|
|
138
|
+
unknown pricing on a credit plan, not free inference. A vendor that charges more
|
|
139
|
+
above an input threshold needs its tier row, otherwise long prompts are
|
|
140
|
+
understated. `metadata.maxTokens` is model capacity and never a request cap;
|
|
141
|
+
`defaultMaxTokens` is the request default and must not exceed that capacity.
|
|
142
|
+
|
|
143
|
+
## 5. Verify before and after the edit
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
dsh --profile tui list-models # every reachable route, id<TAB>name
|
|
147
|
+
# resolved cost/ctx/efforts, naming the layer each number came from:
|
|
148
|
+
~/.agents/skills/dsh-tui-update-models/scripts/dump-model-catalog.mjs \
|
|
149
|
+
[--home "$DSH_HOME"] [--profile tui] [--data <pi-ai data dir>] [--json]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The dump marks each value `installed`, `template:<id>`, `route`, or `metadata`, so
|
|
153
|
+
an inherited number is visible instead of trusted, expands a `filter` route
|
|
154
|
+
against the installed data the same way the resolver does, and exits 1 on the
|
|
155
|
+
declarations the resolver refuses - incomplete metadata, a request default above
|
|
156
|
+
capacity, both membership styles, a default the served selection does not
|
|
157
|
+
contain.
|
|
158
|
+
|
|
159
|
+
Then check the vendor's own page for price, context window, max output, and
|
|
160
|
+
accepted effort values - the installed catalog lags vendors, and a template
|
|
161
|
+
clone carries a sibling's numbers.
|
|
162
|
+
|
|
163
|
+
To prove a catalog on a disposable home instead of the live one, provider-extra
|
|
164
|
+
ships a clone-only setup that composes the candidate and disables the competing
|
|
165
|
+
rows for you:
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
cd <dsh-provider-extra checkout> && pnpm catalog:setup \
|
|
169
|
+
--helper ~/.agents/skills/dsh-tui-dogfood/scripts/run-plugin-from-worktree.sh \
|
|
170
|
+
--source-home ~/.dsh --home /tmp/dsh-catalog-check --profile tui \
|
|
171
|
+
--catalog /tmp/catalog-config.json [--allow-row ROW_ID] [--dsh /abs/path/to/dsh/lib/bin.js]
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`--catalog` is a private JSON file holding only `{"catalog": {...}}`. Success
|
|
175
|
+
means `composition-verified`, not authenticated runtime; launch only the clone
|
|
176
|
+
command it prints.
|
|
177
|
+
|
|
178
|
+
## 6. Pitfalls
|
|
179
|
+
|
|
180
|
+
| Symptom | Cause |
|
|
181
|
+
| --- | --- |
|
|
182
|
+
| A new model shows the sibling's price or ctx | it was cloned from a `template`; declare `metadata` |
|
|
183
|
+
| A window that is really a pricing threshold | vendors publish e.g. "above 272K input priced 2x" beside the real window |
|
|
184
|
+
| `off` missing on a reasoning model | `thinkingLevelMap.off` is `null` or absent where the vendor takes `none` |
|
|
185
|
+
| A stealth model with no vendor page | its numbers exist only in the routing catalog (pi.dev model page, models.dev) |
|
|
186
|
+
| Config ignored after an edit | a linked bundle loads `lib/` or `dist/`; run the build in that checkout |
|
|
187
|
+
| ctx gauge and auto-compact trigger early/late | the declared `contextWindow` is wrong, not the gauge |
|
|
188
|
+
| Same model id, two context windows | each route inherits its own source's numbers; declare `metadata` on both |
|
|
189
|
+
| Everything fails closed, logs `CATALOG_OWNER_COLLISION` | a competing adapter, provider directory, or default owner row survived |
|
|
190
|
+
| A new field looks accepted but does nothing | the resolver rejects unknown fields; a stale plugin build silently predates it |
|
|
191
|
+
| `/model` saves a default that reverts | the host has no `configEditor`: `CONFIG_PERSISTENCE_UNAVAILABLE`; edit the patch |
|
|
192
|
+
| Route serves nothing after a `filter` edit | patterns match installed ids only, and `exclude` wins over `include` |
|
|
193
|
+
|
|
194
|
+
Never put a key in a profile or a patch: name it (`apiKeyEnv`, `apiKeyRef`,
|
|
195
|
+
`credentialProvider`) and keep the value in the environment or `$DSH_HOME/.env`.
|
|
196
|
+
Test catalog edits in a cloned home - see the `dsh-tui-dogfood` skill.
|
|
197
|
+
|
|
198
|
+
Full field lists, credentials, and the vendor-verification checklist:
|
|
199
|
+
[references/model-wiring.md](references/model-wiring.md).
|