saving-token 0.1.0 → 1.1.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/README.md CHANGED
@@ -1,46 +1,81 @@
1
1
  # saving-token
2
2
 
3
- A native writer makes file changes; the parent agent decides and reviews. You choose the writer model during setup. The parent model and global defaults stay unchanged.
3
+ A native writer makes file changes; the host directs and reviews. The writer
4
+ model is selected during setup, while the host model and global defaults stay
5
+ unchanged.
4
6
 
5
- Inspired by [karpathy-guidelines](https://github.com/multica-ai/andrej-karpathy-skills). Delegation aims to reduce parent-context output; extra context and retries can increase total tokens.
7
+ Inspired by [karpathy-guidelines](https://github.com/multica-ai/andrej-karpathy-skills).
8
+
9
+ The writer is used only when the host model is stronger; otherwise the host
10
+ works directly.
6
11
 
7
12
  ## Install
8
13
 
9
- ### Codex via npm (after publication)
14
+ ### Interactive setup
15
+
16
+ ```bash
17
+ bash skills/saving-token/scripts/setup.sh
18
+ ```
19
+
20
+ Use **Up/Down** and **Enter** to choose the platform, writer model, optional
21
+ reasoning effort, and final action. Press **Esc** to cancel. The final menu
22
+ starts on **Cancel**. Model entries are suggestions only; availability is not
23
+ checked. The effort menu includes the host default and values accepted by the
24
+ target; account and model support still depend on the host.
25
+
26
+ The model menu has no free-text entry. Use `--model` for a custom model ID or
27
+ for non-interactive setup:
28
+
29
+ ```bash
30
+ bash skills/saving-token/scripts/setup.sh \
31
+ --target codex --model '<model-id>' --yes
32
+ ```
33
+
34
+ `--target` and `--model` are required without a terminal. Add
35
+ `--reasoning-effort <effort>` when supported by the selected model. `--yes`
36
+ approves the displayed installation and replacements; it never chooses a
37
+ model. Rerun setup to change the writer. Extra files in an existing Skill
38
+ directory are preserved.
10
39
 
11
- After the package is published to npm, run:
40
+ ### npm wrapper
41
+
42
+ After publication, run:
12
43
 
13
44
  ```bash
14
45
  npx saving-token@latest install --agent codex
15
46
  ```
16
47
 
17
- This interactively prompts you to choose a writer model and optional reasoning effort, then asks you to confirm installation.
48
+ The wrapper supports `install --agent codex` and keeps the interactive menus.
49
+ Use the Bash script directly for `--target`, `--model`, `--reasoning-effort`,
50
+ or `--yes`.
18
51
 
19
- ### Manual / multi-platform
52
+ To refresh only the suggested models in an existing Codex installation, run:
20
53
 
21
54
  ```bash
22
- bash skills/saving-token/scripts/setup.sh
55
+ npx saving-token@latest update-models --agent codex
23
56
  ```
24
57
 
25
- Choose **platform model optional reasoning effort → confirm**. The model menu offers a few suggestions and a custom ID option, with no default selection. Suggestions do not verify account availability. Leave effort blank to use the host's default behavior.
58
+ It changes no writer configuration and shows no setup menus. Use the Bash
59
+ script with `--update-models --target claude` or `cursor` for those platforms.
60
+
61
+ ### Installed paths
26
62
 
27
- | Platform | Skill location | Writer configuration |
63
+ | Platform | Skill | Writer configuration |
28
64
  |---|---|---|
29
65
  | Codex | `${CODEX_HOME:-~/.codex}/skills/saving-token/` | `${CODEX_HOME:-~/.codex}/agents/saving-token-writer.toml` |
30
66
  | Claude Code | `~/.claude/skills/saving-token/` | `~/.claude/agents/saving-token-writer.md` |
31
67
  | Cursor | `~/.cursor/skills/saving-token/` | `~/.cursor/agents/saving-token-writer.md` |
32
68
 
33
- Restart the host, then invoke `$saving-token` in Codex or `/saving-token` in standalone Claude Code and Cursor. Automatic Skill discovery remains available. Verify the writer model in runtime task metadata when exposed; generated configuration alone does not prove routing. Claude Code and Cursor may substitute a model under account or administrator policy.
69
+ Restart the host, then invoke `$saving-token` in Codex or `/saving-token` in
70
+ standalone Claude Code and Cursor. A generated configuration does not prove
71
+ runtime routing; verify the model in exposed task metadata when available.
72
+ Claude Code and Cursor may substitute a model under account or administrator
73
+ policy.
34
74
 
35
- To automate an already approved installation:
36
-
37
- ```bash
38
- bash skills/saving-token/scripts/setup.sh --target codex --model '<model-id>' --yes
39
- ```
40
-
41
- `--target` and `--model` are required without a terminal. Add `--reasoning-effort <effort>` only if supported by the selected model. Explicit `--model` skips the menu; `--yes` approves installation and displayed replacements, without choosing a model.
42
-
43
- Rerun setup to change the writer model. Existing distributed Skill files and the writer configuration are replaced after confirmation; extra files in the Skill directory are preserved. Setup also works from the installed Skill's `scripts/setup.sh`.
75
+ The writer implements and verifies delegated changes. The host personally
76
+ reviews the actual changes and verification evidence against the requirements,
77
+ sends defects back to the same writer for fixes and re-review, and announces
78
+ completion only after review passes.
44
79
 
45
80
  ## Claude Code plugin
46
81
 
@@ -49,15 +84,12 @@ Rerun setup to change the writer model. Existing distributed Skill files and the
49
84
  /plugin install saving-token@saving-token
50
85
  ```
51
86
 
52
- Claude's plugin configuration collects the required `writer_model` as text; it does not use the script's menu. The plugin leaves reasoning effort to the host. Invoke `/saving-token:saving-token`; its writer is `saving-token:saving-token-writer`. Change the model through plugin configuration, then reload the session. Use either the plugin or standalone installation.
53
-
54
- ## Structure
55
-
56
- - [`skills/saving-token/SKILL.md`](skills/saving-token/SKILL.md): canonical delegation and review rules.
57
- - `skills/saving-token/scripts/setup.sh` and `assets/`: standalone installer and native agent templates.
58
- - `agents/` and `.claude-plugin/`: Claude plugin writer and marketplace metadata.
59
- - `AGENTS.md`, `CLAUDE.md`, [`CURSOR.md`](CURSOR.md), and `.cursor/rules/`: repository adapters that point to the Skill.
60
-
61
- Other hosts can read the Skill or `AGENTS.md`, but delegation requires writable native subagents and independent model configuration. Without those capabilities, support is instruction-only.
87
+ The plugin collects `writer_model` through its configuration and leaves effort
88
+ to the host. Invoke `/saving-token:saving-token`; its writer is
89
+ `saving-token:saving-token-writer`. Use either the plugin or standalone setup.
62
90
 
63
- Official references: [Codex subagents](https://developers.openai.com/codex/subagents), [Claude subagents](https://code.claude.com/docs/en/sub-agents), [Claude plugins](https://code.claude.com/docs/en/plugins-reference), [Cursor subagents](https://cursor.com/docs/subagents), [Agent Skills](https://agentskills.io/specification).
91
+ Official references: [Codex subagents](https://developers.openai.com/codex/subagents),
92
+ [Claude subagents](https://code.claude.com/docs/en/sub-agents),
93
+ [Claude plugins](https://code.claude.com/docs/en/plugins-reference),
94
+ [Cursor subagents](https://cursor.com/docs/subagents), and
95
+ [Agent Skills](https://agentskills.io/specification).
@@ -8,11 +8,11 @@ const args = process.argv.slice(2);
8
8
 
9
9
  if (
10
10
  args.length !== 3 ||
11
- args[0] !== 'install' ||
11
+ !['install', 'update-models'].includes(args[0]) ||
12
12
  args[1] !== '--agent' ||
13
13
  args[2] !== 'codex'
14
14
  ) {
15
- console.error('Usage: saving-token install --agent codex');
15
+ console.error('Usage: saving-token <install|update-models> --agent codex');
16
16
  process.exit(1);
17
17
  }
18
18
 
@@ -24,7 +24,12 @@ const setupPath = path.join(
24
24
  'scripts',
25
25
  'setup.sh'
26
26
  );
27
- const result = spawnSync('bash', [setupPath, '--target', 'codex'], {
27
+ const setupArgs = [setupPath, '--target', 'codex'];
28
+ if (args[0] === 'update-models') {
29
+ setupArgs.push('--update-models');
30
+ }
31
+
32
+ const result = spawnSync('bash', setupArgs, {
28
33
  stdio: 'inherit'
29
34
  });
30
35
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saving-token",
3
- "version": "0.1.0",
3
+ "version": "1.1.0",
4
4
  "bin": {
5
5
  "saving-token": "bin/saving-token.cjs"
6
6
  },
@@ -18,24 +18,31 @@ If your identity is `saving-token-writer` or `saving-token:saving-token-writer`,
18
18
  - If a writable native writer or independent model configuration is unavailable, report instruction-only compatibility and ask before implementing directly.
19
19
  - Configuration is not runtime proof. Check exposed model metadata and disclose mismatches; never silently substitute a model yourself. Claude Code and Cursor may apply host-level fallback.
20
20
 
21
+ ## Model gate
22
+
23
+ - Delegate only when the current host model is stronger than the configured writer model.
24
+ - If the comparison is unknown, do not delegate; the parent completes the task directly under normal authorization.
25
+
21
26
  ## Delegate
22
27
 
23
28
  - Delegate every file write, including edits, deletions, documents, temporary scripts, and formatting.
24
29
  - Give goals and acceptance criteria, not implementation text to paste. Prefer file paths and issue locations over copied source or conversation history.
25
30
  - Batch related changes into one scoped task and reuse the writer for revisions.
26
31
  - The writer follows project rules and authorization boundaries, preserves user edits, implements directly, and runs relevant checks.
32
+ - Put exact focused commands in `Verify`; the writer runs them with the change.
33
+ - For slow E2E or independent confirmation, use a verification-only native subagent. It must not modify files and returns commands, exit codes, result counts when available, and code failures or environmental blockers.
27
34
 
28
35
  ```text
29
36
  Goal: <expected behavior>
30
- Scope: <paths; prohibited changes>
31
- Verify: <acceptance criteria and checks>
32
- Return: changed paths; check results; blockers.
37
+ Scope: <paths; prohibited changes; or verification-only, no file changes>
38
+ Verify: <exact commands>
39
+ Return: changed paths; commands, exit codes, result counts when available; code failures or environmental blockers.
33
40
  ```
34
41
 
35
42
  ## Accept
36
43
 
37
- - Inspect the actual diff, new files, and check evidence; never accept from the writer summary alone.
38
- - Send remaining defects and acceptance conditions to the same writer, then review again. Stop and report blockers or repeated failures without progress.
44
+ - After the writer finishes, the host must personally review the actual diff, new files, and verification evidence against the requirements, never relying on the writer's summary alone; send any defects to the same writer for fixes and re-review, and announce completion only after the review passes.
45
+ - Stop and report blockers or repeated failures without progress.
39
46
  - Report the outcome, verification, and material routing uncertainty concisely. Keep full logs in files when needed.
40
47
 
41
48
  Delegation aims to reduce parent-context output; a second context and retries can increase total token usage.
@@ -12,4 +12,5 @@ disallowedTools:
12
12
  Implement directly. Never delegate or invoke another subagent.
13
13
  Follow project rules and authorization boundaries, and preserve existing user edits.
14
14
  Make only the requested changes and run the requested checks.
15
- Return changed paths, check commands and results, and blockers only.
15
+ For verification-only work, do not modify files.
16
+ Return changed paths; commands, exit codes, result counts when available; and code failures or environmental blockers only.
@@ -7,5 +7,6 @@ developer_instructions = """
7
7
  Implement directly. Do not delegate further.
8
8
  Follow project rules and preserve existing user edits.
9
9
  Run the requested checks.
10
- Return changed paths, check results, and blockers only.
10
+ For verification-only work, do not modify files.
11
+ Return changed paths; commands, exit codes, result counts when available; and code failures or environmental blockers only.
11
12
  """
@@ -11,4 +11,5 @@ is_background: false
11
11
  Implement directly. Never delegate or invoke another subagent.
12
12
  Follow project rules and authorization boundaries, and preserve existing user edits.
13
13
  Make only the requested changes and run the requested checks.
14
- Return changed paths, check commands and results, and blockers only.
14
+ For verification-only work, do not modify files.
15
+ Return changed paths; commands, exit codes, result counts when available; and code failures or environmental blockers only.
@@ -3,16 +3,102 @@ set -euo pipefail
3
3
 
4
4
  fail() { printf 'Error: %s\n' "$*" >&2; exit 1; }
5
5
 
6
+ cursor_hidden=false
7
+ terminal_state=''
8
+ restore_cursor() {
9
+ if [[ "$cursor_hidden" == true ]]; then
10
+ printf '\033[?25h'
11
+ cursor_hidden=false
12
+ fi
13
+ if [[ -n "$terminal_state" ]]; then
14
+ stty "$terminal_state" 2>/dev/null || stty echo
15
+ terminal_state=''
16
+ fi
17
+ }
18
+ trap restore_cursor EXIT
19
+ trap 'exit 130' INT TERM
20
+
21
+ choose_menu() {
22
+ local prompt=$1
23
+ shift
24
+ local -a labels values
25
+ local entry label value key sequence
26
+ local selected=0 rendered=false line_count
27
+ MENU_RESULT=''
28
+
29
+ for entry in "$@"; do
30
+ label=${entry%%|*}
31
+ value=${entry#*|}
32
+ labels[${#labels[@]}]=$label
33
+ values[${#values[@]}]=$value
34
+ done
35
+ line_count=$(( ${#labels[@]} + 1 ))
36
+ terminal_state=$(stty -g)
37
+ stty -echo
38
+ printf '\033[?25l'
39
+ cursor_hidden=true
40
+
41
+ while :; do
42
+ if [[ "$rendered" == true ]]; then
43
+ printf '\033[%sA' "$line_count"
44
+ fi
45
+ printf '\033[2K\r%s (Up/Down, Enter to select, Esc to cancel)\n' "$prompt"
46
+ for entry in "${!labels[@]}"; do
47
+ printf '\033[2K\r'
48
+ if [[ "$entry" -eq "$selected" ]]; then
49
+ printf '> %s\n' "${labels[$entry]}"
50
+ else
51
+ printf ' %s\n' "${labels[$entry]}"
52
+ fi
53
+ done
54
+ rendered=true
55
+
56
+ if ! IFS= read -r -s -n 1 key; then
57
+ restore_cursor
58
+ return 2
59
+ fi
60
+ case "$key" in
61
+ ''|$'\n'|$'\r')
62
+ MENU_RESULT=${values[$selected]}
63
+ restore_cursor
64
+ printf '\n'
65
+ return 0
66
+ ;;
67
+ $'\033')
68
+ if IFS= read -r -s -n 2 -t 1 sequence; then
69
+ case "$sequence" in
70
+ '[A') selected=$(( (selected + ${#labels[@]} - 1) % ${#labels[@]} )) ;;
71
+ '[B') selected=$(( (selected + 1) % ${#labels[@]} )) ;;
72
+ *) restore_cursor; printf '\n'; return 1 ;;
73
+ esac
74
+ else
75
+ restore_cursor
76
+ printf '\n'
77
+ return 1
78
+ fi
79
+ ;;
80
+ esac
81
+ done
82
+ }
83
+
84
+ cancel_install() {
85
+ printf 'Installation cancelled; no files were changed.\n'
86
+ exit 0
87
+ }
88
+
6
89
  usage() {
7
90
  cat <<'HELP'
8
91
  Usage: setup.sh [--target codex|claude|cursor] [--model ID]
9
92
  [--reasoning-effort EFFORT] [--yes]
93
+ setup.sh --update-models --target codex|claude|cursor
10
94
 
11
- Choose a platform and writer model, preview, then confirm installation.
95
+ Use Up/Down and Enter to choose a platform, model, effort, and confirmation.
96
+ Esc cancels before any files are changed.
12
97
  --target PLATFORM Prompted when omitted; required without a terminal.
13
98
  --model ID Skip the model menu; required without a terminal.
14
99
  --reasoning-effort EFFORT Optional. Omit to use the host's default behavior.
15
100
  --yes Approve installation and displayed replacements.
101
+ --update-models Update only model suggestions in an installed Skill.
16
102
  -h, --help Show this help.
17
103
 
18
104
  Model suggestions are not an account availability check. Use only models and
@@ -28,6 +114,7 @@ reasoning_effort=''
28
114
  model_was_set=false
29
115
  effort_was_set=false
30
116
  assume_yes=false
117
+ update_models=false
31
118
 
32
119
  while [[ $# -gt 0 ]]; do
33
120
  case "$1" in
@@ -40,16 +127,24 @@ while [[ $# -gt 0 ]]; do
40
127
  esac
41
128
  shift 2 ;;
42
129
  --yes) assume_yes=true; shift ;;
130
+ --update-models) update_models=true; shift ;;
43
131
  -h|--help) usage; exit 0 ;;
44
132
  *) fail "Unknown option: $1" ;;
45
133
  esac
46
134
  done
47
135
 
48
136
  [[ -n "${HOME:-}" ]] || fail 'HOME is not set.'
137
+ if [[ "$update_models" == true ]]; then
138
+ [[ -n "$target" ]] || fail 'Pass --target with --update-models.'
139
+ [[ "$model_was_set" == false && "$effort_was_set" == false && "$assume_yes" == false ]] || fail '--update-models cannot be combined with --model, --reasoning-effort, or --yes.'
140
+ fi
49
141
  if [[ -z "$target" ]]; then
50
142
  [[ -t 0 ]] || fail 'Pass --target in non-interactive use.'
51
- IFS= read -r -p 'Platform: 1) codex 2) claude 3) cursor: ' target || fail 'No platform selected.'
52
- case "$target" in 1) target=codex ;; 2) target=claude ;; 3) target=cursor ;; esac
143
+ if choose_menu 'Choose a platform:' 'Codex|codex' 'Claude Code|claude' 'Cursor|cursor'; then
144
+ target=$MENU_RESULT
145
+ else
146
+ cancel_install
147
+ fi
53
148
  fi
54
149
 
55
150
  case "$target" in
@@ -58,37 +153,75 @@ case "$target" in
58
153
  skill_target="$codex_home/skills/saving-token"
59
154
  agent_dir="$codex_home/agents"
60
155
  agent_file='saving-token-writer.toml'
61
- models=(gpt-5.6-luna gpt-5.6-terra gpt-5.3-codex-spark)
156
+ models=(gpt-6-luna gpt-6-sol gpt-6-astra)
62
157
  ;;
63
158
  claude)
64
159
  skill_target="$HOME/.claude/skills/saving-token"
65
160
  agent_dir="$HOME/.claude/agents"
66
161
  agent_file='saving-token-writer.md'
67
- models=(haiku sonnet opus)
162
+ models=(claude-haiku-4-5-20251001 claude-sonnet-5 claude-opus-5-5 claude-fable-5-1)
68
163
  ;;
69
164
  cursor)
70
165
  skill_target="$HOME/.cursor/skills/saving-token"
71
166
  agent_dir="$HOME/.cursor/agents"
72
167
  agent_file='saving-token-writer.md'
73
- models=(composer-2.5 gpt-5.6-sol)
168
+ models=(composer-2.5 gpt-5.6-luna gpt-5.6-terra gpt-5.6-sol claude-sonnet-5 claude-opus-5-5 claude-fable-5-1)
74
169
  ;;
75
170
  *) fail "Unsupported target '$target'. Expected codex, claude, or cursor." ;;
76
171
  esac
77
172
 
173
+ update_installed_model_suggestions() {
174
+ local installed_setup temporary_setup model_line
175
+
176
+ installed_setup="$skill_target/scripts/setup.sh"
177
+ [[ -f "$installed_setup" ]] || fail "No installed Skill script found at $installed_setup. Run install first."
178
+
179
+ model_line=" models=(${models[*]})"
180
+
181
+ temporary_setup="$(mktemp "$(dirname -- "$installed_setup")/.saving-token-setup.XXXXXX")"
182
+ cp -p "$installed_setup" "$temporary_setup"
183
+ if ! awk -v target="$target" -v model_line="$model_line" '
184
+ $0 == " " target ")" { in_target = 1; print; next }
185
+ in_target && $0 == " ;;" { in_target = 0; print; next }
186
+ in_target && $0 ~ /^ models=\([^)]*\)$/ {
187
+ replacements++
188
+ print model_line
189
+ next
190
+ }
191
+ { print }
192
+ END { exit replacements == 1 ? 0 : 1 }
193
+ ' "$installed_setup" > "$temporary_setup"; then
194
+ rm -f "$temporary_setup"
195
+ fail "Could not find one model list for $target in $installed_setup. Run install to refresh the Skill."
196
+ fi
197
+ if ! bash -n "$temporary_setup"; then
198
+ rm -f "$temporary_setup"
199
+ fail 'Updated Skill script failed syntax validation.'
200
+ fi
201
+ if cmp -s "$installed_setup" "$temporary_setup"; then
202
+ rm -f "$temporary_setup"
203
+ printf 'Model suggestions for %s are already current.\n' "$target"
204
+ return
205
+ fi
206
+
207
+ mv -f "$temporary_setup" "$installed_setup"
208
+ printf 'Updated model suggestions for %s in %s. Writer configuration was not changed.\n' "$target" "$installed_setup"
209
+ }
210
+
211
+ if [[ "$update_models" == true ]]; then
212
+ update_installed_model_suggestions
213
+ exit 0
214
+ fi
215
+
78
216
  if [[ "$model_was_set" != true ]]; then
79
217
  [[ -t 0 ]] || fail 'Pass --model in non-interactive use.'
80
- printf '\nWriter model suggestions (availability depends on your account):\n'
81
- for i in "${!models[@]}"; do printf ' %s) %s\n' "$((i + 1))" "${models[$i]}"; done
82
- printf ' c) Custom model ID\n q) Cancel\n'
83
- IFS= read -r -p 'Choose a model: ' choice || fail 'No model selected.'
84
- case "$choice" in
85
- q|Q) printf 'Installation cancelled; no files were changed.\n'; exit 0 ;;
86
- c|C) IFS= read -r -p 'Writer model ID: ' writer_model || fail 'No model selected.' ;;
87
- [1-3])
88
- [[ "$choice" -le "${#models[@]}" ]] || fail 'Invalid model selection.'
89
- writer_model=${models[$((choice - 1))]} ;;
90
- *) fail 'Choose a listed number, c, or q; no model is selected by default.' ;;
91
- esac
218
+ model_options=()
219
+ for model in "${models[@]}"; do model_options[${#model_options[@]}]="$model|$model"; done
220
+ if choose_menu 'Choose a writer model:' "${model_options[@]}"; then
221
+ writer_model=$MENU_RESULT
222
+ else
223
+ cancel_install
224
+ fi
92
225
  fi
93
226
  [[ "$writer_model" =~ ^[a-zA-Z0-9][a-zA-Z0-9._:/-]*$ ]] || fail 'Model ID must be nonempty, without whitespace or parameters; use letters, digits, ., _, :, /, or -.'
94
227
 
@@ -97,7 +230,24 @@ case "$writer_model" in
97
230
  esac
98
231
 
99
232
  if [[ "$effort_was_set" != true && "$assume_yes" != true && -t 0 ]]; then
100
- IFS= read -r -p 'Reasoning effort (Enter to omit): ' reasoning_effort || fail 'Input cancelled.'
233
+ effort_options=('Use host default (omit)|')
234
+ case "$target" in
235
+ codex) effort_options+=(
236
+ 'none|none' 'minimal|minimal' 'low|low' 'medium|medium' 'high|high'
237
+ 'xhigh|xhigh' 'max|max' 'ultra|ultra'
238
+ ) ;;
239
+ claude) effort_options+=(
240
+ 'low|low' 'medium|medium' 'high|high' 'xhigh|xhigh' 'max|max'
241
+ ) ;;
242
+ cursor) effort_options+=(
243
+ 'low|low' 'medium|medium' 'high|high' 'xhigh|xhigh' 'max|max' 'ultra|ultra'
244
+ ) ;;
245
+ esac
246
+ if choose_menu 'Choose reasoning effort (model support may vary):' "${effort_options[@]}"; then
247
+ reasoning_effort=$MENU_RESULT
248
+ else
249
+ cancel_install
250
+ fi
101
251
  fi
102
252
  case "$reasoning_effort" in
103
253
  ''|none|minimal|low|medium|high|xhigh|max|ultra) ;;
@@ -135,11 +285,12 @@ fi
135
285
  [[ ! -e "$agent_target" ]] || printf 'Existing writer configuration will be replaced.\n'
136
286
 
137
287
  if [[ "$assume_yes" != true ]]; then
138
- IFS= read -r -p 'Install this configuration? [y/N] ' answer || fail 'Confirmation required; use --yes after approval.'
139
- case "$answer" in
140
- y|Y|yes|YES|Yes) ;;
141
- *) printf 'Installation cancelled; no files were changed.\n'; exit 0 ;;
142
- esac
288
+ [[ -t 0 ]] || fail 'Pass --yes in non-interactive use.'
289
+ if choose_menu 'Install this configuration?' 'Cancel|no' 'Install|yes'; then
290
+ [[ "$MENU_RESULT" == yes ]] || cancel_install
291
+ else
292
+ cancel_install
293
+ fi
143
294
  else
144
295
  printf 'Install this configuration? yes (--yes)\n'
145
296
  fi