Skills › Compound Engineering › ce-polish › detect-riffrec.sh
detect-riffrec.sh
225 lines · skills/ce-polish/scripts/detect-riffrec.sh at 3449230
1#!/usr/bin/env bash
2#
3# detect-riffrec.sh — report whether a project already carries riffrec live
4# mode: the dependency, its version, and a RiffrecProvider mount.
5#
6# Usage:
7# detect-riffrec.sh [path]
8#
9# Arguments:
10# path (optional) — project root to inspect. Defaults to the repository
11# root via `git rev-parse --show-toplevel`, then to the
12# current directory.
13#
14# Output contract (one JSON object on stdout, exit 0):
15# {
16# "dependency": true|false, riffrec listed in dependencies,
17# devDependencies, or peerDependencies
18# "installed": true|false, node_modules/riffrec/package.json exists
19# "live_build": true|false, the installed dist/index.d.ts names both
20# RiffrecLiveConfig and LOOK_AT_SCREEN_TOOL
21# (a build that answers the endpoint's
22# screen tool, not just any live build)
23# "version": "1.2.3"|null, installed node_modules/riffrec version,
24# else the declared range with a leading
25# ^ ~ = or v stripped; null when absent
26# "mount": true|false, a RiffrecProvider JSX opening element
27# (not a comment or string) exists in a
28# source file outside node_modules and
29# build output
30# "package_manager": "npm"|"pnpm"|"yarn"|"bun"|null
31# from the sibling resolve-package-manager.sh;
32# null when the root has no package.json
33# }
34#
35# Errors (stderr, exit 1):
36# ERROR: <message> — path does not exist or is not a directory
37#
38# Portable sh/bash: no jq, no associative arrays, no GNU-only flags.
40set -u
42SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd -P)
43TARGET_PATH="${1:-}"
45if [ -n "$TARGET_PATH" ]; then
46 if [ ! -d "$TARGET_PATH" ]; then
47 echo "ERROR: path does not exist or is not a directory: $TARGET_PATH" >&2
48 exit 1
49 fi
50else
51 TARGET_PATH=$(git rev-parse --show-toplevel 2>/dev/null)
52 if [ -z "$TARGET_PATH" ]; then
53 TARGET_PATH=$(pwd)
54 fi
55fi
57TARGET_PATH=$(cd "$TARGET_PATH" && pwd -P)
59# Escape a value for a JSON string literal (backslash and double quote only;
60# versions and package-manager names carry nothing else that needs escaping).
61json_string() {
62 printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
63}
65emit() {
66 # $1 dependency $2 version-or-empty $3 mount $4 package-manager-or-empty
67 # $5 installed $6 live_build
68 if [ -n "$2" ]; then
69 version_json="\"$(json_string "$2")\""
70 else
71 version_json="null"
72 fi
73 if [ -n "$4" ]; then
74 pm_json="\"$(json_string "$4")\""
75 else
76 pm_json="null"
77 fi
78 printf '{"dependency":%s,"installed":%s,"live_build":%s,"version":%s,"mount":%s,"package_manager":%s}\n' \
79 "$1" "$5" "$6" "$version_json" "$3" "$pm_json"
80}
82# A real JSX opening element for RiffrecProvider in one file: the tag follows
83# the start of the line or an ordinary JSX expression prefix (whitespace, `(`,
84# `{`, `=`, `>` of an arrow, `?`, `:`, `,`, `&`, `|`, or the `return` keyword),
85# is followed by whitespace, `>`, or end of line, and is not inside a `//`
86# remainder, a `/* */` or `{/* */}` block, or a string (a quote on the same
87# line, or a template literal opened on an earlier line).
88has_jsx_mount() {
89 # $1 file
90 # One scan, one state: at each character the scanner is inside a string
91 # (which quote), inside a block comment, or in code. Comment delimiters
92 # count only in code, so `data-url="http://x"` does not end the line, and
93 # quotes count only in code, so a quote inside a comment does not open a
94 # string. A tag counts only when it starts in code. A template literal
95 # and a block comment keep their state across lines; a single or double
96 # quote does not survive the end of its line.
97 awk '
98 {
99 line = $0
100 n = length(line)
101 for (i = 1; i <= n; i++) {
102 ch = substr(line, i, 1)
103 two = substr(line, i, 2)
104 if (inblock) {
105 if (two == "*/") { inblock = 0; i++ }
106 continue
107 }
108 if (instr != "") {
109 if (ch == "\\") { i++; continue }
110 if (ch == instr) instr = ""
111 continue
112 }
113 if (two == "//") break
114 if (two == "/*") { inblock = 1; i++; continue }
115 if (ch == "\"" || ch == "\047" || ch == "`") { instr = ch; continue }
116 if (ch == "<" && match(substr(line, i), /^<RiffrecProvider([[:space:]>]|$)/)) {
117 before = substr(line, 1, i - 1)
118 if (before ~ /(^|[[:space:](){}=>?:,&|]|return)[[:space:]]*$/) { found = 1; exit }
119 }
120 }
121 if (instr != "`") instr = ""
122 }
123 END { exit found ? 0 : 1 }
124 ' "$1" 2>/dev/null
125}
127# Extract the first `"<key>": "<string>"` pair from a JSON file, wherever it
128# sits on the line. package.json values that matter here are plain strings.
129json_value() {
130 # $1 file $2 key
131 grep -o '"'"$2"'"[[:space:]]*:[[:space:]]*"[^"]*"' "$1" 2>/dev/null \
132 | head -n 1 \
133 | sed -e 's/^"[^"]*"[[:space:]]*:[[:space:]]*"//' -e 's/"$//'
134}
136# The declared range of `riffrec` from dependencies, devDependencies, or
137# peerDependencies only; a `riffrec` key under scripts, overrides,
138# resolutions, or any other section is not a dependency. Prefers a real JSON
139# parse through node; without node, a line-based walk that tracks which
140# top-level section the cursor is in (package.json as written by the package
141# managers is one key per line).
142declared_dependency() {
143 # $1 package.json
144 if command -v node >/dev/null 2>&1; then
145 node -e '
146 const fs = require("fs");
147 let pkg;
148 try { pkg = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); } catch { process.exit(0); }
149 for (const section of ["dependencies", "devDependencies", "peerDependencies"]) {
150 const range = pkg && pkg[section] && pkg[section].riffrec;
151 if (typeof range === "string") { process.stdout.write(range); process.exit(0); }
152 }
153 ' "$1" 2>/dev/null
154 return
155 fi
156 awk '
157 /^[[:space:]]*"(dependencies|devDependencies|peerDependencies)"[[:space:]]*:[[:space:]]*\{/ { inside = 1; next }
158 inside && /^[[:space:]]*\}/ { inside = 0; next }
159 inside && /^[[:space:]]*"riffrec"[[:space:]]*:[[:space:]]*"/ {
160 sub(/^[[:space:]]*"riffrec"[[:space:]]*:[[:space:]]*"/, ""); sub(/".*$/, ""); print; exit
161 }
162 ' "$1" 2>/dev/null
163}
165DEPENDENCY=false
166INSTALLED=false
167LIVE_BUILD=false
168VERSION=""
169MOUNT=false
170PACKAGE_MANAGER=""
172PACKAGE_JSON="$TARGET_PATH/package.json"
174if [ -f "$PACKAGE_JSON" ]; then
175 PACKAGE_MANAGER=$(bash "$SCRIPT_DIR/resolve-package-manager.sh" "$TARGET_PATH" 2>/dev/null | head -n 1)
176 case "$PACKAGE_MANAGER" in
177 npm|pnpm|yarn|bun) ;;
178 *) PACKAGE_MANAGER="" ;;
179 esac
181 DECLARED=$(declared_dependency "$PACKAGE_JSON")
182 if [ -n "$DECLARED" ]; then
183 DEPENDENCY=true
184 INSTALLED_PKG="$TARGET_PATH/node_modules/riffrec/package.json"
185 if [ -f "$INSTALLED_PKG" ]; then
186 INSTALLED=true
187 VERSION=$(json_value "$INSTALLED_PKG" "version")
188 # The built entry types prove the build: RiffrecLiveConfig names the
189 # live provider config, and LOOK_AT_SCREEN_TOOL the page-side handler
190 # for the screen tool the endpoint advertises to the interviewer. An
191 # older live-capable build has the first without the second, and a
192 # declared range or a stale committed dist proves nothing.
193 for dts in "$TARGET_PATH/node_modules/riffrec/dist/index.d.ts" "$TARGET_PATH/node_modules/riffrec/dist/index.d.cts"; do
194 if [ -f "$dts" ] && grep -q "RiffrecLiveConfig" "$dts" 2>/dev/null && grep -q "LOOK_AT_SCREEN_TOOL" "$dts" 2>/dev/null; then
195 LIVE_BUILD=true
196 break
197 fi
198 done
199 fi
200 if [ -z "$VERSION" ]; then
201 VERSION=$(printf '%s' "$DECLARED" | sed -e 's/^[\^~=v]//')
202 fi
203 fi
204fi
206# Mount detection: a JSX opening tag for RiffrecProvider in a source file.
207# Import lines alone do not count; the tag is what mounts the provider.
208if find "$TARGET_PATH" \
209 \( -name node_modules -o -name .git -o -name dist -o -name build -o -name coverage \
210 -o -name .next -o -name .nuxt -o -name .svelte-kit -o -name .turbo -o -name tmp \
211 -o -name vendor -o -name public \) -prune -o \
212 -type f \( -name '*.tsx' -o -name '*.jsx' -o -name '*.ts' -o -name '*.js' -o -name '*.mjs' \) \
213 -print 2>/dev/null \
214 | while IFS= read -r file; do
215 if grep -q '<RiffrecProvider' "$file" 2>/dev/null && has_jsx_mount "$file"; then
216 echo found
217 break
218 fi
219 done \
220 | grep -q found; then
221 MOUNT=true
222fi
224emit "$DEPENDENCY" "$VERSION" "$MOUNT" "$PACKAGE_MANAGER" "$INSTALLED" "$LIVE_BUILD"
225exit 0