@ramarivera/coding-buddy 0.8.0 → 0.9.1

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 (56) hide show
  1. package/README.md +23 -4
  2. package/adapters/omp/context.ts +1 -1
  3. package/adapters/omp/events.ts +286 -81
  4. package/adapters/omp/index.ts +3 -1
  5. package/adapters/omp/renderers.ts +4 -54
  6. package/adapters/omp/storage.ts +6 -1
  7. package/adapters/omp/ui.ts +17 -6
  8. package/adapters/pi/events.ts +280 -83
  9. package/adapters/pi/index.ts +11 -2
  10. package/adapters/pi/renderers.ts +3 -47
  11. package/adapters/pi/storage.ts +6 -1
  12. package/adapters/pi/ui.ts +17 -6
  13. package/adapters/shared/file-storage.ts +22 -0
  14. package/adapters/shared/widget-layout.ts +248 -2
  15. package/cli/settings.ts +1 -1
  16. package/cli/tui.tsx +1 -1
  17. package/core/command-service.ts +13 -0
  18. package/core/engine.ts +11 -2
  19. package/core/identity.ts +15 -0
  20. package/core/model.ts +3 -0
  21. package/core/ports.ts +4 -0
  22. package/core/reactions.ts +29 -2
  23. package/core/render-model.ts +4 -3
  24. package/package.json +6 -3
  25. package/scripts/ci/Dockerfile.vhs +125 -0
  26. package/scripts/ci/check-statusline.sh +441 -14
  27. package/scripts/ci/check-vhs-goldens.sh +97 -0
  28. package/scripts/ci/fonts.conf +37 -0
  29. package/scripts/ci/goldens/baseline-40.png +0 -0
  30. package/scripts/ci/goldens/baseline-80.png +0 -0
  31. package/scripts/ci/goldens/cjk-80.png +0 -0
  32. package/scripts/ci/goldens/emoji-80.png +0 -0
  33. package/scripts/ci/render-statusline-pty.py +107 -0
  34. package/scripts/ci/render-vhs-goldens.sh +226 -0
  35. package/scripts/ci/statusline.tape +28 -2
  36. package/scripts/ci/vhs-density-fixtures.sh +27 -0
  37. package/scripts/ci/vhs-run-statusline.sh +28 -0
  38. package/scripts/ci/write-density-status.ts +86 -0
  39. package/scripts/gen-emoji-widths.ts +41 -17
  40. package/scripts/paths.sh +3 -1
  41. package/server/art.ts +70 -17
  42. package/server/hooks/buddy-comment.ts +166 -17
  43. package/server/hooks/file-type-react.ts +1 -1
  44. package/server/hooks/mood-react.ts +1 -1
  45. package/server/hooks/name-react.ts +1 -1
  46. package/server/hooks/react.ts +1 -1
  47. package/server/hooks/reaction-data.ts +10 -3
  48. package/server/index.ts +67 -27
  49. package/server/path.ts +3 -0
  50. package/server/state.ts +147 -41
  51. package/skills/buddy/SKILL.md +1 -1
  52. package/statusline/buddy-status.sh +488 -76
  53. package/statusline/emoji-text.data +3 -0
  54. package/statusline/emoji-widths.data +2 -3
  55. package/statusline/substatus.sh +52 -10
  56. package/scripts/postinstall.cjs +0 -42
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env python3
2
+ """Render buddy-status.sh under a fixed PTY winsize.
3
+
4
+ buddy-status.sh probes the parent TTY via stty, so COLUMNS alone is not
5
+ enough when CI (or a wide agent terminal) already has a PTY. This helper
6
+ forks a PTY, sets TIOCSWINSZ to the requested cols, and prints the raw
7
+ statusline bytes to stdout.
8
+
9
+ Elapsed wall time for the child (statusline) is printed on stderr as:
10
+ STATUSLINE_ELAPSED_MS=<int>
11
+ so the golden harness can budget Claude's ~1s kill without counting
12
+ Python/import overhead outside the child.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import fcntl
18
+ import os
19
+ import pty
20
+ import select
21
+ import struct
22
+ import sys
23
+ import termios
24
+ import time
25
+
26
+
27
+ def _positive_int(env_name: str, default: int) -> int:
28
+ value = os.environ.get(env_name, "")
29
+ if value and value.isdigit():
30
+ parsed = int(value)
31
+ if parsed > 0:
32
+ return parsed
33
+ return default
34
+
35
+ def main() -> int:
36
+ if len(sys.argv) != 5:
37
+ print(
38
+ "usage: render-statusline-pty.py <cols> <root> <config_dir> <script>",
39
+ file=sys.stderr,
40
+ )
41
+ return 2
42
+
43
+ cols = int(sys.argv[1])
44
+ root = sys.argv[2]
45
+ config_dir = sys.argv[3]
46
+ script = sys.argv[4]
47
+ rows = _positive_int("BUDDY_STATUSLINE_ROWS", 30)
48
+ payload = os.environ.get(
49
+ "STATUSLINE_PAYLOAD",
50
+ '{"session_id":"ci","model":{"display_name":"CI"},"workspace":{"current_dir":"."}}',
51
+ )
52
+
53
+ # Build a tiny pipeline so buddy-status.sh can `cat` its JSON stdin.
54
+ cmd = (
55
+ "printf %s "
56
+ + repr(payload)
57
+ + " | "
58
+ + f"COLUMNS={cols} BUDDY_FAKE_NOW=0 BUDDY_STATUSLINE_ROWS={rows} "
59
+ + f"CLAUDE_CONFIG_DIR={config_dir!r} "
60
+ + "CLAUDE_CODE_SESSION_ID= TMUX_PANE= BUDDY_SHELL= "
61
+ + f"bash {script!r}"
62
+ )
63
+
64
+ t0 = time.perf_counter()
65
+ pid, fd = pty.fork()
66
+ if pid == 0:
67
+ os.chdir(root)
68
+ os.execvp("bash", ["bash", "-c", cmd])
69
+
70
+ fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, cols, 0, 0))
71
+ out = bytearray()
72
+ deadline = time.time() + 15.0
73
+ while time.time() < deadline:
74
+ readable, _, _ = select.select([fd], [], [], 0.1)
75
+ if fd in readable:
76
+ try:
77
+ chunk = os.read(fd, 8192)
78
+ except OSError:
79
+ break
80
+ if not chunk:
81
+ break
82
+ out.extend(chunk)
83
+ continue
84
+
85
+ waited = os.waitpid(pid, os.WNOHANG)
86
+ if waited[0] == pid:
87
+ while True:
88
+ readable, _, _ = select.select([fd], [], [], 0.05)
89
+ if fd not in readable:
90
+ break
91
+ try:
92
+ chunk = os.read(fd, 8192)
93
+ except OSError:
94
+ chunk = b""
95
+ if not chunk:
96
+ break
97
+ out.extend(chunk)
98
+ break
99
+
100
+ elapsed_ms = int((time.perf_counter() - t0) * 1000)
101
+ print(f"STATUSLINE_ELAPSED_MS={elapsed_ms}", file=sys.stderr)
102
+ sys.stdout.buffer.write(bytes(out))
103
+ return 0
104
+
105
+
106
+ if __name__ == "__main__":
107
+ raise SystemExit(main())
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env bash
2
+ # Render statusline VHS goldens (PNG frames) for the committed fixtures.
3
+ #
4
+ # Goldens are platform-sensitive (font rasterization). This script ALWAYS
5
+ # renders inside a pinned linux/amd64 container unless you pass --native
6
+ # (which the container entrypoint uses). That way a macOS developer and
7
+ # GitHub Actions ubuntu-latest produce comparable pixels.
8
+ #
9
+ # Usage (from repo root):
10
+ # bash scripts/ci/render-vhs-goldens.sh # -> scripts/ci/vhs-out/
11
+ # bash scripts/ci/render-vhs-goldens.sh --update # also refresh scripts/ci/goldens/
12
+ #
13
+ # Re-baseline (MUST go through this script / the container — never host macOS vhs):
14
+ # bun run ci:vhs:update
15
+ # # inspect scripts/ci/goldens/*.png, then:
16
+ # git add scripts/ci/goldens/*.png
17
+ #
18
+ # Requires: docker (host path). Inside the container: vhs, ffmpeg, ttyd, bun, jq.
19
+ set -euo pipefail
20
+
21
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
22
+ cd "$ROOT"
23
+
24
+ UPDATE=0
25
+ NATIVE=0
26
+ OUT_DIR="scripts/ci/vhs-out"
27
+ GOLDEN_DIR="scripts/ci/goldens"
28
+ IMAGE_NAME="${VHS_GOLDEN_IMAGE:-coding-buddy-vhs:local}"
29
+ DOCKERFILE="scripts/ci/Dockerfile.vhs"
30
+
31
+ for arg in "$@"; do
32
+ case "$arg" in
33
+ --update) UPDATE=1 ;;
34
+ --native) NATIVE=1 ;;
35
+ -h|--help)
36
+ sed -n '2,20p' "$0"
37
+ exit 0
38
+ ;;
39
+ *)
40
+ echo "unknown arg: $arg" >&2
41
+ exit 2
42
+ ;;
43
+ esac
44
+ done
45
+
46
+ # ── Host path: build pinned linux/amd64 image and re-exec inside it ──────────
47
+ if [ "$NATIVE" -eq 0 ]; then
48
+ command -v docker >/dev/null 2>&1 || {
49
+ echo "FAIL: docker is required to render platform-matched VHS goldens" >&2
50
+ echo " Install Docker, or if you are already inside scripts/ci/Dockerfile.vhs," >&2
51
+ echo " re-run with --native." >&2
52
+ exit 1
53
+ }
54
+
55
+ echo "build: $IMAGE_NAME (linux/amd64) from $DOCKERFILE"
56
+ docker build \
57
+ --platform linux/amd64 \
58
+ -f "$DOCKERFILE" \
59
+ -t "$IMAGE_NAME" \
60
+ scripts/ci
61
+
62
+ args=("--native")
63
+ [ "$UPDATE" -eq 1 ] && args+=("--update")
64
+
65
+ # Host creates output dirs world-writable so the container's uid 1000 can
66
+ # write through the bind mount (GHA checkout owner is not 1000).
67
+ mkdir -p "$OUT_DIR" "$GOLDEN_DIR"
68
+ chmod a+rwx "$OUT_DIR" "$GOLDEN_DIR" 2>/dev/null || true
69
+
70
+ echo "run: docker render (${args[*]})"
71
+ set +e
72
+ # Match the image USER (vhs:1000). Chromium will not start as root.
73
+ # HOME is the image's /home/vhs so rod can cache its browser there.
74
+ # seccomp=unconfined is required for Chromium under Docker Desktop/OrbStack.
75
+ docker run --rm \
76
+ --platform linux/amd64 \
77
+ --security-opt seccomp=unconfined \
78
+ --user 1000:1000 \
79
+ -e CI= \
80
+ -e COLORTERM=truecolor \
81
+ -e HOME=/home/vhs \
82
+ -e XDG_CACHE_HOME=/home/vhs/.cache \
83
+ -e VHS_GOLDEN_IMAGE="$IMAGE_NAME" \
84
+ -v "$ROOT":/work \
85
+ -w /work \
86
+ "$IMAGE_NAME" \
87
+ bash scripts/ci/render-vhs-goldens.sh "${args[@]}"
88
+ status=$?
89
+ set -e
90
+
91
+ # Fix ownership when the host user is non-root (best-effort).
92
+ if [ "$(id -u)" -ne 0 ]; then
93
+ docker run --rm --platform linux/amd64 \
94
+ --user 0:0 \
95
+ -v "$ROOT":/work -w /work \
96
+ "$IMAGE_NAME" \
97
+ bash -lc "chown -R $(id -u):$(id -g) scripts/ci/vhs-out scripts/ci/goldens 2>/dev/null || true" \
98
+ >/dev/null 2>&1 || true
99
+ fi
100
+ exit "$status"
101
+ fi
102
+
103
+ # ── Native path (inside the pinned container, or VHS_NATIVE override) ────────
104
+ command -v vhs >/dev/null 2>&1 || { echo "vhs not on PATH (native mode)" >&2; exit 1; }
105
+ command -v ffmpeg >/dev/null 2>&1 || { echo "ffmpeg not on PATH (native mode)" >&2; exit 1; }
106
+ command -v ttyd >/dev/null 2>&1 || { echo "ttyd not on PATH (native mode)" >&2; exit 1; }
107
+ command -v bun >/dev/null 2>&1 || { echo "bun not on PATH (native mode)" >&2; exit 1; }
108
+
109
+ echo "native render host=$(uname -s)/$(uname -m) vhs=$(vhs --version 2>/dev/null | head -n1)"
110
+ echo "font: $(fc-match 'DejaVu Sans Mono' 2>/dev/null || echo 'fc-match unavailable')"
111
+
112
+ # Prefer cleaning contents over rm -rf of the directory itself: on CI the
113
+ # host owns scripts/ci/vhs-out (created world-writable above) and uid 1000
114
+ # cannot unlink the directory from scripts/.
115
+ rm -rf "${OUT_DIR:?}/"* 2>/dev/null || true
116
+ mkdir -p "$OUT_DIR"
117
+
118
+ env_ci_was="${CI-}"
119
+ unset CI || true
120
+ export COLORTERM=truecolor
121
+ # Pin rows so VHS golden renders stay at full density even when the terminal
122
+ # emulator reports a smaller size. vhs-run-statusline.sh falls back to 50 too.
123
+ export BUDDY_STATUSLINE_ROWS=50
124
+
125
+ # Fixture generation uses only in-repo modules (server/art.ts). Never touch
126
+ # the developer's live ~/.config/claude/buddy-state — CLAUDE_CONFIG_DIR is
127
+ # pointed at temp fixture dirs below.
128
+ gen_dir="$OUT_DIR/fixtures"
129
+ mkdir -p "$gen_dir/cjk/buddy-state" "$gen_dir/emoji/buddy-state"
130
+ cp scripts/ci/statusline-fixture/buddy-state/config.json "$gen_dir/cjk/buddy-state/config.json"
131
+ cp scripts/ci/statusline-fixture/buddy-state/config.json "$gen_dir/emoji/buddy-state/config.json"
132
+
133
+ bun "$ROOT/scripts/ci/write-density-status.ts" "$gen_dir/cjk/buddy-state/status.json" <<'JSON'
134
+ {"name":"Daffodil","species":"duck","rarity":"common","stars":"★","face":"(°>","eye":"°","shiny":false,"hat":"none","reaction":"","muted":false,"achievement":"","level":1,"xp":0,"mood":"focused"}
135
+ JSON
136
+ cp "$gen_dir/cjk/buddy-state/status.json" "$gen_dir/emoji/buddy-state/status.json"
137
+
138
+ bun -e "
139
+ import { writeFileSync } from 'fs';
140
+ writeFileSync(process.argv[1], JSON.stringify({
141
+ reaction: '编译通过了 你好世界', timestamp: Date.now(), reason: 'turn',
142
+ }));
143
+ writeFileSync(process.argv[2], JSON.stringify({
144
+ reaction: 'ship it 🎉 ❤️ ✨', timestamp: Date.now(), reason: 'success',
145
+ }));
146
+ " \
147
+ "$gen_dir/cjk/buddy-state/reaction.default.json" \
148
+ "$gen_dir/emoji/buddy-state/reaction.default.json"
149
+
150
+ render_one() {
151
+ local name="$1"
152
+ local cols="$2"
153
+ local lines="$3"
154
+ local fixture_rel="$4"
155
+ local tape out_png out_gif
156
+ tape="$OUT_DIR/${name}.tape"
157
+ out_png="$OUT_DIR/${name}.png"
158
+ out_gif="$OUT_DIR/${name}.gif"
159
+
160
+ # Relative paths only — vhs's tape parser treats absolute "/..." paths as
161
+ # separate commands. JSON stays out of the tape via vhs-run-statusline.sh.
162
+ #
163
+ # Hide/Show: the Type+Enter setup must not appear in the golden. Previous
164
+ # macOS baselines accidentally baked the shell command into the frame, which
165
+ # made pixel goldens useless as a layout check.
166
+ cat > "$tape" <<EOF
167
+ Output ${out_gif}
168
+ Set Width $(( cols * 9 ))
169
+ Set Height $(( lines * 18 ))
170
+ Set FontSize 14
171
+ Set FontFamily "DejaVu Sans Mono"
172
+ Set Padding 12
173
+ Set Margin 0
174
+ Set Theme "Catppuccin Mocha"
175
+ Set Shell "bash"
176
+ Set TypingSpeed 0ms
177
+ Set Framerate 10
178
+ Hide
179
+ Type "PS1=; tput civis 2>/dev/null; clear; bash scripts/ci/vhs-run-statusline.sh ${cols} ${fixture_rel}"
180
+ Enter
181
+ Sleep 800ms
182
+ Show
183
+ Sleep 2s
184
+ EOF
185
+
186
+ echo "render: $name (cols=$cols fixture=$fixture_rel)"
187
+ vhs "$tape"
188
+ [ -f "$out_gif" ] || { echo "missing gif: $out_gif" >&2; exit 1; }
189
+
190
+ # Extract the final settled frame as a still PNG.
191
+ local nframes last
192
+ nframes=$(ffprobe -v error -select_streams v:0 -count_frames \
193
+ -show_entries stream=nb_read_frames -of csv=p=0 "$out_gif" || echo 1)
194
+ last=0
195
+ if [ "${nframes:-0}" -gt 1 ] 2>/dev/null; then
196
+ last=$(( nframes - 1 ))
197
+ fi
198
+ ffmpeg -hide_banner -loglevel error -y \
199
+ -i "$out_gif" \
200
+ -vf "select=eq(n\,${last})" \
201
+ -update 1 -frames:v 1 \
202
+ "$out_png"
203
+ [ -f "$out_png" ] || { echo "missing png: $out_png" >&2; exit 1; }
204
+ }
205
+
206
+ render_one "baseline-80" 80 18 "scripts/ci/statusline-fixture"
207
+ render_one "baseline-40" 40 18 "scripts/ci/statusline-fixture"
208
+ render_one "cjk-80" 80 18 "scripts/ci/vhs-out/fixtures/cjk"
209
+ # Emoji reaction needs room for the bubble (INNER_W default 44). Narrow
210
+ # coverage lives in baseline-40; this fixture is about glyph rasterization.
211
+ render_one "emoji-80" 80 18 "scripts/ci/vhs-out/fixtures/emoji"
212
+
213
+ if [ "$UPDATE" -eq 1 ]; then
214
+ mkdir -p "$GOLDEN_DIR"
215
+ for png in "$OUT_DIR"/*.png; do
216
+ base=$(basename "$png")
217
+ cp "$png" "$GOLDEN_DIR/$base"
218
+ echo "updated golden: $GOLDEN_DIR/$base"
219
+ done
220
+ fi
221
+
222
+ if [ -n "${env_ci_was}" ]; then
223
+ export CI="$env_ci_was"
224
+ fi
225
+
226
+ echo "OK: vhs renders written to $OUT_DIR (native=$(uname -s)/$(uname -m))"
@@ -3,7 +3,33 @@ Set Width 1400
3
3
  Set Height 500
4
4
  Set FontSize 14
5
5
  Set Shell "bash"
6
- Type "echo '{\"session_id\":\"ci\",\"model\":{\"display_name\":\"CI\"},\"workspace\":{\"current_dir\":\".\"}}' | CLAUDE_CONFIG_DIR=scripts/ci/statusline-fixture bash statusline/buddy-status.sh"
6
+
7
+ # Generate temporary full/compact/minimal fixtures from the committed baseline
8
+ # without touching production fixtures. The helper augments status.json with
9
+ # compactFrames/minimalFrames when getStatusFrames() emits them.
10
+ Hide
11
+ Type "bash scripts/ci/vhs-density-fixtures.sh"
7
12
  Enter
8
- Sleep 12s
13
+ Sleep 2s
14
+ Show
15
+
16
+ Type "clear"
17
+ Enter
18
+ Sleep 500ms
19
+
20
+ # Full density: 50 rows gives the six-ish-line art + optional bubble.
21
+ Type "COLUMNS=80 BUDDY_STATUSLINE_ROWS=50 bash scripts/ci/vhs-run-statusline.sh 80 scripts/ci/vhs-out/density-fixtures/full"
22
+ Enter
23
+ Sleep 2s
24
+
25
+ # Compact density: three art rows + name, no bubble.
26
+ Type "COLUMNS=80 BUDDY_STATUSLINE_ROWS=50 bash scripts/ci/vhs-run-statusline.sh 80 scripts/ci/vhs-out/density-fixtures/compact"
27
+ Enter
28
+ Sleep 2s
29
+
30
+ # Minimal density: a single sprite/face + name line.
31
+ Type "COLUMNS=80 BUDDY_STATUSLINE_ROWS=50 bash scripts/ci/vhs-run-statusline.sh 80 scripts/ci/vhs-out/density-fixtures/minimal"
32
+ Enter
33
+ Sleep 2s
34
+
9
35
  Screenshot scripts/ci/statusline.png
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ # Generate temporary full/compact/minimal statusline fixtures for the VHS tape.
3
+ # Leaves the committed production fixture untouched and writes into the ignored
4
+ # scripts/ci/vhs-out/ workspace.
5
+ set -euo pipefail
6
+
7
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
8
+ cd "$ROOT"
9
+
10
+ src_status="$ROOT/scripts/ci/statusline-fixture/buddy-state/status.json"
11
+ out_dir="$ROOT/scripts/ci/vhs-out/density-fixtures"
12
+
13
+ rm -rf "$out_dir"
14
+ mkdir -p "$out_dir"/{full,compact,minimal}/buddy-state
15
+
16
+ for d in full compact minimal; do
17
+ cp "$src_status" "$out_dir/$d/buddy-state/status.json"
18
+ printf '{"useCombinedStatus":false,"statuslineDensity":"%s"}\n' "$d" \
19
+ > "$out_dir/$d/buddy-state/config.json"
20
+ done
21
+
22
+ # Augment the committed fixture with full/compact/minimal frame arrays.
23
+ cat "$src_status" | bun "$ROOT/scripts/ci/write-density-status.ts" "$out_dir/full/buddy-state/status.json"
24
+ cp "$out_dir/full/buddy-state/status.json" "$out_dir/compact/buddy-state/status.json"
25
+ cp "$out_dir/full/buddy-state/status.json" "$out_dir/minimal/buddy-state/status.json"
26
+
27
+ echo "$out_dir"
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bash
2
+ # Invoked from VHS tapes. Args: <cols> <fixture-relpath-from-repo-root>
3
+ # BUDDY_STATUSLINE_ROWS defaults to 50 (full density) so golden renders are not
4
+ # sensitive to the VHS terminal's actual row count.
5
+ set -euo pipefail
6
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
7
+ cols="${1:?cols}"
8
+ fixture_rel="${2:?fixture}"
9
+ payload='{"session_id":"ci","model":{"display_name":"CI"},"workspace":{"current_dir":"."}}'
10
+
11
+ if [[ "$fixture_rel" = /* ]]; then
12
+ cfg_dir="$fixture_rel"
13
+ else
14
+ cfg_dir="$ROOT/$fixture_rel"
15
+ fi
16
+
17
+ rows="${BUDDY_STATUSLINE_ROWS:-50}"
18
+
19
+ cd "$ROOT"
20
+ printf '%s' "$payload" \
21
+ | COLUMNS="$cols" \
22
+ BUDDY_FAKE_NOW=0 \
23
+ BUDDY_STATUSLINE_ROWS="$rows" \
24
+ CLAUDE_CONFIG_DIR="$cfg_dir" \
25
+ CLAUDE_CODE_SESSION_ID= \
26
+ TMUX_PANE= \
27
+ BUDDY_SHELL= \
28
+ bash statusline/buddy-status.sh
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Derives a status.json fixture with full/compact/minimal frame arrays.
4
+ *
5
+ * Reads a base status object from stdin, computes frames from species/eye/hat,
6
+ * and writes the augmented object to the file named by the first argument.
7
+ *
8
+ * Usage:
9
+ * cat base-status.json | bun scripts/ci/write-density-status.ts dest.json
10
+ * bun scripts/ci/write-density-status.ts dest.json < base-status.json
11
+ */
12
+ import { writeFileSync } from "fs";
13
+ import { getStatusFrames } from "../../server/art.ts";
14
+ import { renderFace, resolveEyeGlyph, type Species } from "../../core/engine.ts";
15
+
16
+ function deriveCompactFrames(fullFrames: string[]): string[] {
17
+ return fullFrames.map((body) => {
18
+ const lines = body.split("\n");
19
+ const selected: string[] = [];
20
+ for (const line of lines) {
21
+ if (selected.length >= 3) break;
22
+ if (line.trim() !== "") selected.push(line);
23
+ }
24
+ while (selected.length < 3) selected.push("");
25
+ return selected.join("\n");
26
+ });
27
+ }
28
+
29
+ function deriveMinimalFrames(species: Species, eye: string): string[] {
30
+ const face = renderFace(species, resolveEyeGlyph(eye));
31
+ return Array.from({ length: 4 }, () => face);
32
+ }
33
+
34
+ async function main() {
35
+ const dest = process.argv[2];
36
+ if (!dest) {
37
+ console.error("usage: write-density-status.ts <dest.json>");
38
+ process.exit(2);
39
+ }
40
+
41
+ let input = "";
42
+ for await (const chunk of process.stdin) {
43
+ input += chunk;
44
+ }
45
+ const base = input ? JSON.parse(input) : {};
46
+
47
+ const bones = {
48
+ species: base.species ?? "pikachu",
49
+ rarity: base.rarity ?? "common",
50
+ eye: base.eye ?? "o",
51
+ hat: base.hat ?? "none",
52
+ shiny: base.shiny ?? false,
53
+ stats: base.stats ?? { DEBUGGING: 50, PATIENCE: 50, CHAOS: 50, WISDOM: 50, SNARK: 50 },
54
+ peak: base.peak ?? "DEBUGGING",
55
+ dump: base.dump ?? "PATIENCE",
56
+ };
57
+
58
+ const result = getStatusFrames(bones) as {
59
+ frames: string[];
60
+ frameSequence: number[];
61
+ compactFrames?: string[];
62
+ minimalFrames?: string[];
63
+ };
64
+
65
+ const compactFrames =
66
+ result.compactFrames && result.compactFrames.length > 0
67
+ ? result.compactFrames
68
+ : deriveCompactFrames(result.frames);
69
+
70
+ const minimalFrames =
71
+ result.minimalFrames && result.minimalFrames.length > 0
72
+ ? result.minimalFrames
73
+ : deriveMinimalFrames(bones.species as Species, bones.eye);
74
+
75
+ const out = {
76
+ ...base,
77
+ frames: result.frames,
78
+ compactFrames,
79
+ minimalFrames,
80
+ frameSequence: result.frameSequence,
81
+ };
82
+
83
+ writeFileSync(dest, JSON.stringify(out));
84
+ }
85
+
86
+ main();
@@ -5,30 +5,54 @@
5
5
  *
6
6
  * Regenerate: bun run gen:emoji-widths
7
7
  *
8
- * The emitted list holds decimal codepoints in U+2600-U+27BF that default to
9
- * emoji rendering (2 cols). Everything outside that range is handled by
10
- * simple range checks in the bash/TS code, so we only need to export the
11
- * ambiguous Miscellaneous Symbols + Dingbats block.
12
- */
8
+ * The emitted list holds every codepoint with Emoji_Presentation=Yes, so the
9
+ * bash width walker stays in parity with server/art.ts for both BMP symbols
10
+ * and supplementary-plane emoji.
13
11
 
12
+ */
14
13
  import { writeFileSync } from "fs";
15
14
  import { join } from "path";
16
15
 
17
16
  const OUT = join(import.meta.dir, "..", "statusline", "emoji-widths.data");
17
+ const TEXT_OUT = join(import.meta.dir, "..", "statusline", "emoji-text.data");
18
18
 
19
19
  const EMOJI_PRES_RE = /\p{Emoji_Presentation}/u;
20
- const codepoints: number[] = [];
21
- for (let cp = 0x2600; cp <= 0x27BF; cp++) {
22
- if (EMOJI_PRES_RE.test(String.fromCodePoint(cp))) codepoints.push(cp);
20
+ const EMOJI_RE = /\p{Emoji}/u;
21
+ const presentation: number[] = [];
22
+ const textDefault: number[] = [];
23
+ for (let cp = 0; cp <= 0x10FFFF; cp++) {
24
+ const character = String.fromCodePoint(cp);
25
+ if (EMOJI_PRES_RE.test(character)) presentation.push(cp);
26
+ else if (EMOJI_RE.test(character)) textDefault.push(cp);
23
27
  }
24
28
 
25
- const content = [
26
- "# Auto-generated by scripts/gen-emoji-widths.ts — do not edit by hand.",
27
- "# Decimal codepoints in U+2600-U+27BF with Unicode Emoji_Presentation=Yes.",
28
- "# Regenerate with: bun run gen:emoji-widths",
29
- codepoints.join(" "),
30
- "",
31
- ].join("\n");
29
+ function encodeRanges(codepoints: number[]): string {
30
+ const ranges: string[] = [];
31
+ let start = codepoints[0];
32
+ let end = start;
33
+ for (const codepoint of codepoints.slice(1)) {
34
+ if (codepoint === end + 1) {
35
+ end = codepoint;
36
+ continue;
37
+ }
38
+ ranges.push(start === end ? String(start) : `${start}-${end}`);
39
+ start = end = codepoint;
40
+ }
41
+ ranges.push(start === end ? String(start) : `${start}-${end}`);
42
+ return ranges.join(",");
43
+ }
32
44
 
33
- writeFileSync(OUT, content);
34
- console.log(`Wrote ${codepoints.length} codepoints to ${OUT}`);
45
+ const header = "# Auto-generated by scripts/gen-emoji-widths.ts — do not edit by hand.";
46
+ writeFileSync(OUT, [
47
+ header,
48
+ "# Decimal codepoint ranges with Unicode Emoji_Presentation=Yes.",
49
+ encodeRanges(presentation),
50
+ "",
51
+ ].join("\n"));
52
+ writeFileSync(TEXT_OUT, [
53
+ header,
54
+ "# Decimal codepoint ranges with Unicode Emoji=Yes and Emoji_Presentation=No.",
55
+ encodeRanges(textDefault),
56
+ "",
57
+ ].join("\n"));
58
+ console.log(`Wrote ${presentation.length} presentation and ${textDefault.length} text-default codepoints`);
package/scripts/paths.sh CHANGED
@@ -31,7 +31,9 @@ else
31
31
  CLAUDE_USER_CONFIG="$HOME/.claude.json"
32
32
  fi
33
33
 
34
- if [[ -n "${CLAUDE_CONFIG_DIR:-}" ]]; then
34
+ if [[ -n "${CODING_BUDDY_USER_ID:-}" ]]; then
35
+ BUDDY_STATE_DIR="${CODING_BUDDY_STATE_DIR:-$HOME/.coding-buddy/shared}"
36
+ elif [[ -n "${CLAUDE_CONFIG_DIR:-}" ]]; then
35
37
  BUDDY_STATE_DIR="$CLAUDE_CONFIG_DIR/buddy-state"
36
38
  else
37
39
  BUDDY_STATE_DIR="$HOME/.claude-buddy"