@erclx/canon 5.4.0 → 5.5.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.
Files changed (36) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/bash-cli-script/REQUIREMENT.md +10 -9
  3. package/claude/skills/bash-cli-script/SKILL.md +10 -7
  4. package/claude/skills/bash-cli-script/references/template.md +4 -28
  5. package/claude/skills/read-frames/REQUIREMENT.md +1 -0
  6. package/claude/skills/read-frames/SKILL.md +1 -0
  7. package/claude/skills/teach-workspace/SKILL.md +4 -4
  8. package/claude/skills/ux-measure/REQUIREMENT.md +2 -1
  9. package/claude/skills/ux-measure/SKILL.md +3 -1
  10. package/claude/skills/ux-walkthrough/REQUIREMENT.md +3 -0
  11. package/claude/skills/ux-walkthrough/SKILL.md +1 -0
  12. package/docs/agents/architecture-record.md +5 -3
  13. package/docs/agents/context-audit-checks.md +1 -1
  14. package/docs/agents/context-audit.md +3 -3
  15. package/docs/agents/index.md +2 -2
  16. package/docs/agents/teach-authoring.md +3 -3
  17. package/docs/agents/teach.md +11 -5
  18. package/docs/workflow/skill-map.md +1 -2
  19. package/governance/rules/lang/120-bash.md +4 -5
  20. package/package.json +1 -1
  21. package/src/audits/catalog.ts +7 -1
  22. package/src/claude/cases/authoring.ts +0 -5
  23. package/src/commands/context.ts +25 -2
  24. package/src/commands/serve.ts +2 -92
  25. package/src/commands/teach.ts +142 -17
  26. package/src/context/architecture.ts +82 -5
  27. package/src/context/gate.ts +10 -3
  28. package/src/gate/measures.ts +9 -4
  29. package/src/gate/stages.ts +1 -1
  30. package/src/serve/report.ts +110 -0
  31. package/src/teach/nav.ts +12 -1
  32. package/standards/architecture.md +14 -3
  33. package/claude/skills/bash-script/REQUIREMENT.md +0 -36
  34. package/claude/skills/bash-script/SKILL.md +0 -100
  35. package/claude/skills/bash-script/references/patterns.md +0 -250
  36. package/claude/skills/bash-script/references/prompts.md +0 -100
package/src/teach/nav.ts CHANGED
@@ -1613,8 +1613,19 @@ export async function generateNav(
1613
1613
  const detail = found.workspace
1614
1614
  const metas = await readLessonMetas(root, detail)
1615
1615
 
1616
+ // The generated base sheet is rewritten on every run so a CLI upgrade
1617
+ // reaches it, while a legacy workspace holding only a hand-authored
1618
+ // course.css is never handed a base sheet its lessons do not link.
1616
1619
  const cssPath = join(root, detail.path, TEACH_ASSETS, TEACH_STYLESHEET)
1617
- if (!existsSync(cssPath)) await writeStylesheet(root, detail.slug)
1620
+ const basePath = join(
1621
+ root,
1622
+ detail.path,
1623
+ TEACH_ASSETS,
1624
+ TEACH_STYLESHEET_BASE,
1625
+ )
1626
+ if (existsSync(basePath) || !existsSync(cssPath)) {
1627
+ await writeStylesheet(root, detail.slug)
1628
+ }
1618
1629
 
1619
1630
  const contentsPath = join(root, detail.path, 'index.html')
1620
1631
  await writeFile(
@@ -65,6 +65,17 @@ A decision's reasoning stays correct while the numbers it cites move. The anchor
65
65
  - Do not edit a claim in the pass that first anchors it. The anchor states what the claim was measured against, so changing both at once leaves nothing to check the anchor against.
66
66
  - Refresh the anchor whenever the number is re-read, whether or not it moved. A confirmed number and an unread one are the same text without the date.
67
67
 
68
+ ## Revisit condition
69
+
70
+ A decision says why it won and rarely says when that reason stops holding, so an entry can go stale while every word in it stays true. The revisit sentence names the finding that would show the harm the decision prevents has gone or moved.
71
+
72
+ - Close each decision with one sentence of the form `Revisit when <finding>.` Name something a reader could notice, such as a platform release, a measured count crossing a line, or a dependency dropping a behavior.
73
+ - Draft it from the harm the decision prevents, never from the mechanism it uses. A mechanism can keep working long after the reason for choosing it is gone, so re-checking it confirms the wrong fact.
74
+ - Place the sentence after the reasoning and before any verification anchor, so the anchor stays the entry's last sentence.
75
+ - Give a heading holding several principles one sentence, for the premise they share, rather than one per principle.
76
+ - Adopt the requirement by stating the clause `Every decision closes with a revisit sentence.` in the record itself. The clause is the record's own, like the cap, so a checker reads it from the file and gates only a record that states it. A record stating none is reported and never gated.
77
+ - A checker reads presence alone. A vacuous sentence passes it, so the sentence is only as useful as the finding it names.
78
+
68
79
  ## Entry cap
69
80
 
70
81
  Every session pays for this file before any work starts, so a heavy read is a real cost. A file that reads heavy is carrying too many decisions, not decisions written too long, so the bound is a count of decisions rather than of words.
@@ -78,20 +89,20 @@ Every session pays for this file before any work starts, so a heavy read is a re
78
89
 
79
90
  ## Template
80
91
 
81
- The anchor sentence closes a decision whose reasoning cites a measured number and is absent from one that cites none. Each heading below names a slot, so a record starts with one decision per slot and renames each heading to the choice it records.
92
+ The revisit sentence closes the reasoning of every decision. The anchor sentence follows it on a decision whose reasoning cites a measured number and is absent from one that cites none. Each heading below names a slot, so a record starts with one decision per slot and renames each heading to the choice it records.
82
93
 
83
94
  ```markdown
84
95
  # Architecture
85
96
 
86
97
  ## Overview
87
98
 
88
- This record holds at most 12 decisions.
99
+ This record holds at most 12 decisions. Every decision closes with a revisit sentence.
89
100
 
90
101
  ## Key technical decisions
91
102
 
92
103
  ### Stack and runtime
93
104
 
94
- What was chosen, the alternative that lost, and why. Measured at <short-sha> on <YYYY-MM-DD>.
105
+ What was chosen, the alternative that lost, and why. Revisit when <finding>. Measured at <short-sha> on <YYYY-MM-DD>.
95
106
 
96
107
  ### Delivery
97
108
 
@@ -1,36 +0,0 @@
1
- ---
2
- name: bash-script
3
- description: Scope boundary for the interactive script house style and the stream discipline underneath it
4
- ---
5
-
6
- # Bash script requirement
7
-
8
- ## Gap
9
-
10
- Without this skill, a session asked for a human-facing shell tool writes one whose frame decoration, log lines, and prompts all land on stdout, so the script cannot be piped and any data it emits arrives mixed with its own presentation.
11
-
12
- Three more failures share a cause, which is that an interactive script is written as though a person is always watching. A prompt blocks forever when stdin is not a terminal, which is the shape every CI job and every agent run has. A cancellation path prints both a cancel line and an error, or an exit trap prints a closing frame the success path already printed, so the same run reports twice. And each generated script picks its own icons, casing, and frame characters, so a folder of them reads as several unrelated tools.
13
-
14
- The visual conventions this skill fixes are otherwise arbitrary. Their value is that they are the same across every script, which is a property no individual script can establish for itself.
15
-
16
- ## Must
17
-
18
- - Keep every frame, log line, and prompt on stderr, and reserve stdout for data. `--help` is the one exception, since its consumer is a person rather than a pipe.
19
- - Guard every prompt on an attached terminal and refuse inside the frame rather than blocking on a read that will never return
20
- - Give the timeline one owner per exit path, so success, cancellation, and error each close it exactly once
21
- - Copy the shared templates rather than restating them, keeping only the colors and functions the script uses
22
-
23
- ## Must not
24
-
25
- - Assert a convention the generated script cannot be checked against by reading it. A rule that cannot be verified from the output is style with no gate behind it.
26
- - Carry a second copy of the timeline, logging, or prompt implementations. The bundled reference is the single copy, and a body that restates it drifts from it.
27
-
28
- ## Guards
29
-
30
- - A request for a script with no human at the terminal stops and routes to `bash-cli-script` rather than generating a timeline nothing will render
31
-
32
- ## Out of scope
33
-
34
- - Non-interactive automation, CI, and agent-run scripts: `bash-cli-script`, which keeps the error handling and the stdout contract and drops the timeline, the icons, and the prompts
35
- - GitHub Actions workflow files: `ci-workflow`
36
- - What the generated script does. This skill fixes the shape of the output and the stream it goes to, and the commands belong to the request.
@@ -1,100 +0,0 @@
1
- ---
2
- name: bash-script
3
- description: Generates production Bash scripts with a visual timeline UI, state-based interactive prompts, and strict error handling. Use when asked for "a human-facing shell tool", an interactive bash script, or a DevOps tool with framed terminal output. Do NOT use for a non-interactive automation, CI, or pipeline script, that is `bash-cli-script`.
4
- ---
5
-
6
- # Bash script
7
-
8
- Generate production-ready Bash scripts for DevOps and CLI workflows. Enforce strict formatting with a visual timeline UI and state-based interactivity.
9
-
10
- Load `${CLAUDE_SKILL_DIR}/references/patterns.md` for the timeline lifecycle, logging, help-screen, and full-script code templates, and `${CLAUDE_SKILL_DIR}/references/prompts.md` for the `ask()` and `select_option()` templates when the script prompts. `${CLAUDE_SKILL_DIR}` expands to this skill's own directory, so the path resolves from any project. Copy those definitions verbatim, keeping only the colors and functions the script actually uses.
11
-
12
- ## Guards
13
-
14
- - A request for a script with no human at the terminal stops and routes to `bash-cli-script`. CI jobs, cron entries, agent-run tasks, and any script whose output is consumed by a pipe render no timeline, so generating one costs the frame and returns nothing.
15
-
16
- ## Script setup
17
-
18
- - Start with `#!/usr/bin/env bash`, `set -e`, and `set -o pipefail`.
19
- - Bash 4+ features (namerefs, associative arrays) are allowed.
20
- - Implement a visual help screen via `show_help` if the script accepts arguments.
21
- - Do not rely on unset variables. Use `${VAR:-default}`.
22
-
23
- ## Visual timeline
24
-
25
- - Maintain a vertical timeline (`│`) from `┌` to `└` throughout all output.
26
- - Write all frame output (`┌`, `│`, `├`, `└`, log lines, prompts) to stderr via `>&2`. Write data (JSON, lists, piped values) to stdout. `--help` is the exception and prints to stdout.
27
- - Open the timeline once at the start of `main()` via `open_timeline "Title"`, before any logic, prompts, or checks.
28
- - Close the timeline via `trap close_timeline EXIT`, registered immediately after `open_timeline`.
29
- - On success, disable with `trap - EXIT`, then print `└\n` and the success message manually.
30
- - On cancellation and error, never print `└` manually. The trap owns those exits.
31
- - Use state transitions for interactive prompts: `◆` active, `◇` inactive.
32
- - Do not add diamonds (`◆` or `◇`) to non-interactive log functions.
33
- - On cancellation, show `◇ ... Cancelled`, exit 1, and make no `log_error` call. Both `ask()` and `select_option()` handle escape cancellation identically.
34
- - Guard interactive prompts against non-TTY stdin with `[ -t 0 ]` and render a framed `log_error` when a TTY is required but absent. Never let `read` block silently on piped input.
35
-
36
- ## Code style
37
-
38
- - Decompose by responsibility: each function does one thing, `main()` orchestrates only.
39
- - Name functions verb-first: `validate_input`, `deploy_service`, `install_dependencies`.
40
- - Do not use global variables except exports from `ask()`.
41
- - Do not define unused color variables.
42
- - Comment only a fact the reader cannot recover from the code, and follow the code-comment rule in `.claude/rules/` when the project installs it.
43
- - Use sentence case for section headers and log messages. Proper nouns and product names retain their casing.
44
- - Quote variables inside parameter expansions: `"${file#"$dir"/}"` not `"${file#$dir/}"`.
45
- - Quote variables in test brackets: `[ "$i" -eq "$cur" ]` not `[ $i -eq $cur ]`.
46
- - Guard commands that return non-zero on valid empty results: `grep ... || true`, `diff ... || true`.
47
-
48
- ## Output hygiene
49
-
50
- - Show external tool output by default (git, npm, gh).
51
- - Include context in error messages: `log_error "npm install failed: check package.json"`.
52
- - Do not echo command names before running them. Output speaks for itself.
53
- - Do not log "Starting..." and "Finished..." around every action.
54
- - Do not log intermediate variable assignments.
55
- - Use `log_add` for item writes (files created, entries added, keys written).
56
- - Use `log_info` for status confirmations only ("up to date", "check passed").
57
- - Use `log_warn` for drift, skipped states, or recoverable issues.
58
- - Never use `log_info` for file or entry writes.
59
-
60
- ## Icon usage
61
-
62
- Interactive prompts only:
63
-
64
- - `◆` (green): active user input required
65
- - `◇` (grey): completed input, transition via `\r\033[K` to rewrite the `◆` line in place
66
- - `❯` (green): selected option in a menu
67
- - plain text (grey): unselected option in a menu
68
-
69
- Non-interactive logs:
70
-
71
- - `├` section branch (`log_step`)
72
- - `✓` (green): success (`log_info`)
73
- - `!` (yellow): warning (`log_warn`)
74
- - `✗` (red): error (`log_error`)
75
- - `+` (green): add item (`log_add`)
76
- - `-` (red): remove item (`log_rem`)
77
-
78
- ## Validation
79
-
80
- Before responding, verify:
81
-
82
- - File starts with `#!/usr/bin/env bash`, `set -e`, `set -o pipefail` and uses exactly 2 spaces for indentation.
83
- - Timeline opens via `open_timeline "Title"`, which writes `┌` and `│ Title` to stderr.
84
- - Timeline closes via `trap close_timeline EXIT` registered immediately after `open_timeline`. Success paths use `trap - EXIT` then manual `└\n` (to stderr) then the success message. Cancellation and error paths never print `└` manually.
85
- - `open_timeline` and `close_timeline` are defined and write to stderr via `>&2`.
86
- - All frame output (`│`, `├`, `└`, log lines, interactive prompts) writes to stderr via `>&2`. Stdout carries data only. `--help` is the exception.
87
- - Interactive prompts (`ask`, `select_option`) guard with `[ -t 0 ]` and call `log_error` if stdin is not a TTY.
88
- - The timeline (`│`) appears in all log functions, and interactive prompts use `◆` → `◇` transitions.
89
- - `ask()` uses `\r\033[K` to rewrite the `◆` line in place, with no `\033[1A` cursor-up sequences.
90
- - `ask()` drains trailing escape bytes with `read -rsn2 -t 0.001 _ || true` before cancelling.
91
- - All log messages use sentence case (proper nouns and product names exempt).
92
- - Only defined color variables are used in the script.
93
- - Cancellation shows a single `◇ ... Cancelled` line without a subsequent `log_error`.
94
- - Escape key in `ask()` triggers `◇ ... Cancelled` and exits, consistent with `select_option()`.
95
- - Functions follow single responsibility: each does one thing, `main()` delegates to helpers.
96
- - Logging is concise: no "Starting.../Finished..." bloat, no intermediate variable logging.
97
- - `log_add` is used for all file, entry, and key writes.
98
- - `log_info` is used for status confirmations only, not writes.
99
- - Every section header uses `log_step`, including the first one after the title block.
100
- - File ends with exactly one empty line.
@@ -1,250 +0,0 @@
1
- # Bash script patterns
2
-
3
- Code templates for the `bash-script` skill. Copy verbatim, keeping only the colors and functions the script uses.
4
-
5
- ## Timeline structure
6
-
7
- ```plaintext
8
- ┌ # Start boundary (alone on its own line)
9
- │ Title # Script or context title (immediately after ┌)
10
- │ # Persistent vertical line (grey)
11
- ├ Section Branch # Section headers (no diamond)
12
- │ ✓ Log message # Info/success logs
13
- │ ! Warning # Warning logs
14
- │ ✗ Error # Error logs
15
- └ # End boundary
16
- ```
17
-
18
- ## Color palette
19
-
20
- Define only the colors the script uses from this set:
21
-
22
- ```bash
23
- GREEN='\033[0;32m' # Success/Active
24
- RED='\033[0;31m' # Error/Delete
25
- YELLOW='\033[0;33m' # Warning
26
- WHITE='\033[1;37m' # Active text
27
- GREY='\033[0;90m' # Timeline/Inactive
28
- CYAN='\033[0;36m' # Optional accent
29
- MAGENTA='\033[0;35m' # Optional highlight
30
- NC='\033[0m' # Reset
31
- ```
32
-
33
- ## Timeline lifecycle
34
-
35
- Define `open_timeline` and `close_timeline`. Call `open_timeline "Title"` at the start of `main()`, then register `close_timeline` as the EXIT trap. The trap guarantees `└` prints on every exit path: normal completion, `exit 1` from cancellation, or unexpected errors. Do not print `└` manually anywhere else.
36
-
37
- ```bash
38
- open_timeline() {
39
- echo -e "${GREY}┌${NC}" >&2
40
- [ -n "${1:-}" ] && echo -e "${GREY}│${NC} ${WHITE}$1${NC}" >&2
41
- }
42
-
43
- close_timeline() {
44
- echo -e "${GREY}└${NC}" >&2
45
- }
46
- ```
47
-
48
- Register the trap inside `main()` right after opening:
49
-
50
- ```bash
51
- open_timeline "Script title"
52
- trap close_timeline EXIT
53
- ```
54
-
55
- ## Logging
56
-
57
- All log functions include the `│` prefix and write to stderr.
58
-
59
- ```bash
60
- log_info() { echo -e "${GREY}│${NC} ${GREEN}✓${NC} $1" >&2; }
61
- log_warn() { echo -e "${GREY}│${NC} ${YELLOW}!${NC} $1" >&2; }
62
- log_error() { echo -e "${GREY}│${NC} ${RED}✗${NC} $1" >&2; exit 1; }
63
- log_step() { echo -e "${GREY}│${NC}\n${GREY}├${NC} ${WHITE}$1${NC}" >&2; }
64
- log_add() { echo -e "${GREY}│${NC} ${GREEN}+${NC} $1" >&2; }
65
- log_rem() { echo -e "${GREY}│${NC} ${RED}-${NC} $1" >&2; }
66
- ```
67
-
68
- ## Section headers
69
-
70
- Use `log_step` for every section header, including the first. It emits a leading blank `│` line to separate the banner from the first section and to give breathing room between subsequent sections:
71
-
72
- ```bash
73
- open_timeline "Script title"
74
- trap close_timeline EXIT
75
-
76
- log_step "Deploy"
77
- log_step "Verify"
78
- ```
79
-
80
- Renders as:
81
-
82
- ```plaintext
83
- ┌
84
- │ Script title
85
- │
86
- ├ Deploy
87
- ...
88
- │
89
- ├ Verify
90
- ...
91
- └
92
- ```
93
-
94
- ## Help system
95
-
96
- ```bash
97
- show_help() {
98
- echo -e "${GREY}┌${NC}"
99
- echo -e "${GREY}├${NC} ${WHITE}Usage:${NC} ./script.sh [options]"
100
- echo -e "${GREY}│${NC}"
101
- echo -e "${GREY}│${NC} ${WHITE}Options:${NC}"
102
- echo -e "${GREY}│${NC} -h, --help ${GREY}# Show this help message${NC}"
103
- echo -e "${GREY}│${NC} [flag] ${GREY}# [Description]${NC}"
104
- echo -e "${GREY}└${NC}"
105
- exit 0
106
- }
107
- ```
108
-
109
- ## Error handling helper
110
-
111
- ```bash
112
- run_check() {
113
- local cmd=$1
114
- local err_msg=$2
115
- if ! eval "$cmd"; then
116
- log_error "$err_msg"
117
- fi
118
- }
119
- ```
120
-
121
- ## Full script structure
122
-
123
- ```bash
124
- #!/usr/bin/env bash
125
- set -e
126
- set -o pipefail
127
-
128
- [Color definitions - only used colors]
129
-
130
- [Function definitions - only needed functions]
131
-
132
- open_timeline() {
133
- echo -e "${GREY}┌${NC}" >&2
134
- [ -n "${1:-}" ] && echo -e "${GREY}│${NC} ${WHITE}$1${NC}" >&2
135
- }
136
-
137
- close_timeline() {
138
- echo -e "${GREY}└${NC}" >&2
139
- }
140
-
141
- check_dependencies() {
142
- [Verify required tools installed]
143
- }
144
-
145
- main() {
146
- check_dependencies
147
-
148
- open_timeline "Script title"
149
- trap close_timeline EXIT
150
-
151
- log_step "First section"
152
- [Script logic with timeline maintained]
153
-
154
- trap - EXIT
155
- echo -e "${GREY}└${NC}\n" >&2
156
- echo -e "${GREEN}✓ Final success message${NC}"
157
- }
158
-
159
- main "$@"
160
- ```
161
-
162
- ## Worked example
163
-
164
- ```bash
165
- #!/usr/bin/env bash
166
- set -e
167
- set -o pipefail
168
-
169
- GREEN='\033[0;32m'
170
- RED='\033[0;31m'
171
- YELLOW='\033[0;33m'
172
- WHITE='\033[1;37m'
173
- GREY='\033[0;90m'
174
- NC='\033[0m'
175
-
176
- log_info() { echo -e "${GREY}│${NC} ${GREEN}✓${NC} $1" >&2; }
177
- log_error() { echo -e "${GREY}│${NC} ${RED}✗${NC} $1" >&2; exit 1; }
178
- log_step() { echo -e "${GREY}│${NC}\n${GREY}├${NC} ${WHITE}$1${NC}" >&2; }
179
- log_add() { echo -e "${GREY}│${NC} ${GREEN}+${NC} $1" >&2; }
180
-
181
- open_timeline() {
182
- echo -e "${GREY}┌${NC}" >&2
183
- [ -n "${1:-}" ] && echo -e "${GREY}│${NC} ${WHITE}$1${NC}" >&2
184
- }
185
-
186
- close_timeline() {
187
- echo -e "${GREY}└${NC}" >&2
188
- }
189
-
190
- ask() {
191
- local prompt_text=$1
192
- local var_name=$2
193
- local default_val=$3
194
- local input=""
195
- local char
196
- local display_default=""
197
- if [ -n "$default_val" ]; then
198
- display_default=" (${default_val})"
199
- fi
200
- if [ ! -t 0 ]; then
201
- log_error "${prompt_text} requires a TTY"
202
- fi
203
- echo -e "${GREY}│${NC}" >&2
204
- echo -ne "${GREEN}◆${NC} ${prompt_text}${display_default} " >&2
205
- while IFS= read -r -s -n1 char; do
206
- if [[ $char == $'\x1b' ]]; then
207
- read -rsn2 -t 0.001 _ || true
208
- echo -ne "\r\033[K" >&2
209
- echo -e "${GREY}◇${NC} ${prompt_text} ${RED}Cancelled${NC}" >&2
210
- exit 1
211
- elif [[ $char == $'\x7f' || $char == $'\x08' ]]; then
212
- if [ -n "$input" ]; then
213
- input="${input%?}"
214
- echo -ne "\b \b" >&2
215
- fi
216
- elif [[ -z "$char" ]]; then
217
- break
218
- else
219
- input+="$char"
220
- echo -n "$char" >&2
221
- fi
222
- done
223
- [ -z "$input" ] && input="$default_val"
224
- echo -ne "\r\033[K" >&2
225
- echo -e "${GREY}◇${NC} ${prompt_text} ${WHITE}${input}${NC}" >&2
226
- export "$var_name"="$input"
227
- }
228
-
229
- check_dependencies() {
230
- command -v npm >/dev/null 2>&1 || log_error "npm not installed"
231
- }
232
-
233
- main() {
234
- check_dependencies
235
-
236
- open_timeline "Project Setup"
237
- trap close_timeline EXIT
238
-
239
- ask "Project name?" "PROJECT_NAME" "my-app"
240
-
241
- log_step "Installing dependencies"
242
- npm install vite
243
- log_add "vite@latest"
244
-
245
- log_info "Setup complete"
246
- echo -e "\n${GREEN}✓ Project created successfully${NC}"
247
- }
248
-
249
- main "$@"
250
- ```
@@ -1,100 +0,0 @@
1
- # Bash script prompts
2
-
3
- Interactive prompt templates for the `bash-script` skill, read only when the script prompts. Copy verbatim, alongside the colors and logging functions from `patterns.md`.
4
-
5
- Both prompts transition `◆` to `◇`, guard against non-TTY stdin with `[ -t 0 ]`, and write UI to stderr.
6
-
7
- ```bash
8
- ask() {
9
- local prompt_text=$1
10
- local var_name=$2
11
- local default_val=$3
12
- local input=""
13
- local char
14
- local display_default=""
15
- if [ -n "$default_val" ]; then
16
- display_default=" (${default_val})"
17
- fi
18
- if [ ! -t 0 ]; then
19
- log_error "${prompt_text} requires a TTY"
20
- fi
21
- echo -e "${GREY}│${NC}" >&2
22
- echo -ne "${GREEN}◆${NC} ${prompt_text}${display_default} " >&2
23
- while IFS= read -r -s -n1 char; do
24
- if [[ $char == $'\x1b' ]]; then
25
- read -rsn2 -t 0.001 _ || true
26
- echo -ne "\r\033[K" >&2
27
- echo -e "${GREY}◇${NC} ${prompt_text} ${RED}Cancelled${NC}" >&2
28
- exit 1
29
- elif [[ $char == $'\x7f' || $char == $'\x08' ]]; then
30
- if [ -n "$input" ]; then
31
- input="${input%?}"
32
- echo -ne "\b \b" >&2
33
- fi
34
- elif [[ -z "$char" ]]; then
35
- break
36
- else
37
- input+="$char"
38
- echo -n "$char" >&2
39
- fi
40
- done
41
- [ -z "$input" ] && input="$default_val"
42
- echo -ne "\r\033[K" >&2
43
- echo -e "${GREY}◇${NC} ${prompt_text} ${WHITE}${input}${NC}" >&2
44
- export "$var_name"="$input"
45
- }
46
- ```
47
-
48
- ```bash
49
- select_option() {
50
- local prompt_text=$1
51
- shift
52
- local options=("$@")
53
- local cur=0
54
- local count=${#options[@]}
55
-
56
- if [ ! -t 0 ]; then
57
- log_error "${prompt_text} requires a TTY"
58
- fi
59
-
60
- echo -ne "${GREY}│${NC}\n${GREEN}◆${NC} ${prompt_text}\n" >&2
61
-
62
- while true; do
63
- for i in "${!options[@]}"; do
64
- if [ "$i" -eq "$cur" ]; then
65
- echo -e "${GREY}│${NC} ${GREEN}❯ ${options[$i]}${NC}" >&2
66
- else
67
- echo -e "${GREY}│${NC} ${GREY}${options[$i]}${NC}" >&2
68
- fi
69
- done
70
-
71
- read -rsn1 key
72
- case "$key" in
73
- $'\x1b')
74
- if read -rsn2 -t 0.001 key_seq; then
75
- if [[ "$key_seq" == "[A" ]]; then cur=$(( (cur - 1 + count) % count )); fi
76
- if [[ "$key_seq" == "[B" ]]; then cur=$(( (cur + 1) % count )); fi
77
- else
78
- echo -en "\033[$((count + 1))A\033[J" >&2
79
- echo -e "\033[1A${GREY}│${NC}\n${GREY}◇${NC} ${prompt_text} ${RED}Cancelled${NC}" >&2
80
- exit 1
81
- fi
82
- ;;
83
- "k") cur=$(( (cur - 1 + count) % count ));;
84
- "j") cur=$(( (cur + 1) % count ));;
85
- "q")
86
- echo -en "\033[$((count + 1))A\033[J" >&2
87
- echo -e "\033[1A${GREY}│${NC}\n${GREY}◇${NC} ${prompt_text} ${RED}Cancelled${NC}" >&2
88
- exit 1
89
- ;;
90
- "") break ;;
91
- esac
92
-
93
- echo -en "\033[${count}A" >&2
94
- done
95
-
96
- echo -en "\033[$((count + 1))A\033[J" >&2
97
- echo -e "\033[1A${GREY}│${NC}\n${GREY}◇${NC} ${prompt_text} ${WHITE}${options[$cur]}${NC}" >&2
98
- SELECTED_OPTION="${options[$cur]}"
99
- }
100
- ```