@biffo/cli 0.212.1 → 0.214.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.
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # Give the Postgres-dependent test lane a database with a CURRENT schema, and
4
+ # print its DSN.
5
+ #
6
+ # ## Why this exists
7
+ #
8
+ # `scripts/verify.sh` grew a `pg-test` check (#1089) because on 2026-08-02 nine
9
+ # of thirteen locally-catchable failing CI steps across the estate were one
10
+ # repo's real-Postgres lane -- a required check with no local counterpart at all.
11
+ # But a gate can only run that lane against a database, and no repo documented
12
+ # how to get one: no compose file, no script, no DSN written down. The container
13
+ # that existed on the workstation had been created ad hoc in some earlier session
14
+ # and held a scatter of scratch databases. That undocumented setup WAS the
15
+ # fail-open, because a gate nobody can run is not a gate.
16
+ #
17
+ # ## Why freshness, not rebuild-every-time
18
+ #
19
+ # The expensive failure is not a slow rebuild, it is a STALE one. Measured on
20
+ # tabsii-platform while writing this: a database built about an hour earlier,
21
+ # before two PRs merged, produced **23 failures** in a module that had nothing to
22
+ # do with the change in hand. Rebuilt from the same tree it passed 336/336, and
23
+ # passed 336 again on an immediate re-run -- so the lane was genuinely
24
+ # re-runnable and every one of those failures was the old schema.
25
+ #
26
+ # That is the worst shape a local gate can have. Twenty-three red tests that are
27
+ # not your fault teach people the gate is unreliable, and an unreliable gate gets
28
+ # bypassed -- which H4 pre-registered as the condition refuting the whole
29
+ # local-gate programme. So the schema inputs are fingerprinted and a rebuild
30
+ # happens only when they actually changed: reuse ~0.3s, rebuild ~4s.
31
+ #
32
+ # ## Why it is generic
33
+ #
34
+ # It adapts to the repo rather than being told about it, for the same reason
35
+ # `verify.sh` does: forks drift, and a per-instance copy of this would drift from
36
+ # the DDL layout it is meant to build. Everything instance-specific is DERIVED --
37
+ # the schema directories from `db/imports/*/`, the engine image from whether the
38
+ # DDL asks for PostGIS, and the did-it-build threshold from the number of
39
+ # policies the DDL itself declares. Nothing here names a product.
40
+ #
41
+ # ## Usage
42
+ #
43
+ # eval "$(sh scripts/pg-test-db.sh --export)" # export BIFFO_TEST_PG_DSN
44
+ # sh scripts/pg-test-db.sh # print the DSN on stdout
45
+ # sh scripts/pg-test-db.sh --recreate # force a rebuild
46
+ #
47
+ # Only the DSN reaches stdout, so it is safe to capture; progress goes to stderr.
48
+ #
49
+ # Overridable: BIFFO_PG_HOST, BIFFO_PG_PORT, BIFFO_PG_USER, BIFFO_PG_PASSWORD,
50
+ # BIFFO_PG_DB, BIFFO_PG_CONTAINER, BIFFO_PG_IMAGE.
51
+
52
+ set -eu
53
+
54
+ HOST="${BIFFO_PG_HOST:-localhost}"
55
+ PORT="${BIFFO_PG_PORT:-55432}"
56
+ USER_="${BIFFO_PG_USER:-postgres}"
57
+ PASS="${BIFFO_PG_PASSWORD:-postgres}"
58
+ DB="${BIFFO_PG_DB:-biffo_test}"
59
+ CONTAINER="${BIFFO_PG_CONTAINER:-biffo-pg-test}"
60
+
61
+ RECREATE=0
62
+ EXPORT=0
63
+ for arg in "$@"; do
64
+ case "$arg" in
65
+ --recreate) RECREATE=1 ;;
66
+ --export) EXPORT=1 ;;
67
+ -h | --help)
68
+ sed -n '2,48p' "$0" | sed 's/^#\{1,2\} \{0,1\}//'
69
+ exit 0
70
+ ;;
71
+ *)
72
+ echo "unknown argument: $arg" >&2
73
+ exit 2
74
+ ;;
75
+ esac
76
+ done
77
+
78
+ say() { echo "pg-test-db: $*" >&2; }
79
+
80
+ REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd)
81
+ cd "$REPO_ROOT"
82
+
83
+ # --- what this repo's schema is made of --------------------------------------
84
+ #
85
+ # `db/imports/<name>/*.sql` is the Biffo DDL-import convention that the API's own
86
+ # `ddl_import.list_sql_files` reads at startup, so deriving from it means this
87
+ # script and the running app agree by construction rather than by someone
88
+ # remembering to update both.
89
+ DDL_FILES=$(find db/imports -mindepth 2 -maxdepth 2 -name '*.sql' 2>/dev/null | LC_ALL=C sort || true)
90
+ ALEMBIC_DIR=""
91
+ for _d in services/api .; do
92
+ [ -f "$_d/alembic.ini" ] && ALEMBIC_DIR="$_d" && break
93
+ done
94
+
95
+ if [ -z "$DDL_FILES" ] && [ -z "$ALEMBIC_DIR" ]; then
96
+ say "no db/imports/*/ DDL and no alembic.ini - this repo has no schema to build"
97
+ exit 1
98
+ fi
99
+
100
+ # PostGIS or plain, decided by what the DDL asks for. A plain `postgres` image
101
+ # fails on the first `CREATE EXTENSION postgis`, and picking the heavier image
102
+ # unconditionally would slow every repo that does not need it.
103
+ if [ -n "$DDL_FILES" ] && echo "$DDL_FILES" | xargs grep -liE 'EXTENSION[[:space:]]+(IF[[:space:]]+NOT[[:space:]]+EXISTS[[:space:]]+)?postgis' >/dev/null 2>&1; then
104
+ IMAGE="${BIFFO_PG_IMAGE:-postgis/postgis:16-3.4}"
105
+ else
106
+ IMAGE="${BIFFO_PG_IMAGE:-postgres:16}"
107
+ fi
108
+
109
+ export PGPASSWORD="$PASS"
110
+ psql_admin() { psql -q -h "$HOST" -p "$PORT" -U "$USER_" -d postgres "$@"; }
111
+ psql_db() { psql -q -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" "$@"; }
112
+
113
+ # --- 1. a reachable server ---------------------------------------------------
114
+ #
115
+ # Started here rather than assumed, because "docker run one yourself" is exactly
116
+ # the tribal knowledge this script replaces. An already-running server is reused.
117
+ if ! psql_admin -c 'SELECT 1' >/dev/null 2>&1; then
118
+ if ! command -v docker >/dev/null 2>&1; then
119
+ say "no Postgres at $HOST:$PORT and docker is not installed."
120
+ say "Start one and re-run, or set BIFFO_PG_HOST / BIFFO_PG_PORT."
121
+ exit 1
122
+ fi
123
+ if docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER"; then
124
+ say "starting existing container $CONTAINER"
125
+ docker start "$CONTAINER" >/dev/null
126
+ else
127
+ say "creating container $CONTAINER ($IMAGE) on port $PORT"
128
+ docker run -d --name "$CONTAINER" \
129
+ -e POSTGRES_PASSWORD="$PASS" -p "$PORT:5432" "$IMAGE" >/dev/null
130
+ fi
131
+ # Polled, not slept: a cold image pull and a warm restart differ by an order of
132
+ # magnitude, and one fixed sleep is wrong for both.
133
+ _waited=0
134
+ until psql_admin -c 'SELECT 1' >/dev/null 2>&1; do
135
+ _waited=$((_waited + 1))
136
+ if [ "$_waited" -gt 90 ]; then
137
+ say "Postgres did not become ready in 90s"
138
+ exit 1
139
+ fi
140
+ sleep 1
141
+ done
142
+ say "Postgres ready after ${_waited}s"
143
+ fi
144
+
145
+ DSN="postgresql+asyncpg://$USER_:$PASS@$HOST:$PORT/$DB"
146
+ emit() {
147
+ if [ "$EXPORT" -eq 1 ]; then
148
+ echo "export BIFFO_TEST_PG_DSN='$DSN'"
149
+ else
150
+ echo "$DSN"
151
+ fi
152
+ }
153
+
154
+ # --- 2. is the existing schema current? --------------------------------------
155
+ #
156
+ # By CONTENT, not mtime: a branch switch changes content and leaves mtime
157
+ # anywhere. Stored inside the database, so it cannot outlive a drop or describe
158
+ # some other database.
159
+ fingerprint() {
160
+ {
161
+ [ -n "$ALEMBIC_DIR" ] && find "$ALEMBIC_DIR" -name '*.py' -path '*alembic*' -type f 2>/dev/null |
162
+ LC_ALL=C sort | xargs cat 2>/dev/null
163
+ [ -n "$DDL_FILES" ] && echo "$DDL_FILES" | xargs cat 2>/dev/null
164
+ } | sha256sum | cut -d' ' -f1
165
+ }
166
+
167
+ WANT=$(fingerprint)
168
+ HAVE=""
169
+ if [ "$RECREATE" -eq 0 ] &&
170
+ psql_admin -tAc "SELECT 1 FROM pg_database WHERE datname='$DB'" 2>/dev/null | grep -q 1; then
171
+ HAVE=$(psql -tAq -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
172
+ -c "SELECT value FROM biffo_pg_test_fingerprint LIMIT 1" 2>/dev/null || true)
173
+ fi
174
+
175
+ if [ -n "$HAVE" ] && [ "$HAVE" = "$WANT" ]; then
176
+ say "schema is current, reusing $DB"
177
+ emit
178
+ exit 0
179
+ fi
180
+
181
+ [ -n "$HAVE" ] && say "schema inputs changed - rebuilding rather than serving a stale schema"
182
+
183
+ # --- 3. rebuild the way the app and CI do ------------------------------------
184
+ say "rebuilding $DB"
185
+ psql_admin -c "DROP DATABASE IF EXISTS $DB WITH (FORCE)" >/dev/null
186
+ psql_admin -c "CREATE DATABASE $DB" >/dev/null
187
+
188
+ if [ -n "$ALEMBIC_DIR" ]; then
189
+ BIFFO_DATABASE_URL="$DSN" uv run --directory "$ALEMBIC_DIR" alembic upgrade head >/dev/null
190
+ say "alembic upgrade head"
191
+ fi
192
+
193
+ if [ -n "$DDL_FILES" ]; then
194
+ # ONE psql session, sorted by filename, mirroring the API's own DDL import.
195
+ # Session state an early module sets -- typically `SET search_path` in the
196
+ # first file -- has to survive into later ones, so a per-file connection would
197
+ # silently change the meaning of every unqualified name after it. LC_ALL=C
198
+ # keeps the shell's sort byte-ordered to match Python's.
199
+ # shellcheck disable=SC2046
200
+ psql -q -v ON_ERROR_STOP=1 -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
201
+ --single-transaction $(echo "$DDL_FILES" | sed 's/^/-f /' | tr '\n' ' ') >/dev/null
202
+ say "$(echo "$DDL_FILES" | wc -l | tr -d ' ') DDL modules applied"
203
+ fi
204
+
205
+ # --- 4. refuse to bless a half-built schema ----------------------------------
206
+ #
207
+ # The threshold is derived, not guessed: count the policies the DDL declares and
208
+ # require the database to hold at least half. Recording a fingerprint against a
209
+ # partial schema is worse than failing, because the NEXT run would trust it and
210
+ # every failure after that would look like the developer's own change.
211
+ if [ -n "$DDL_FILES" ]; then
212
+ _declared=$(echo "$DDL_FILES" | xargs grep -ciE '^[[:space:]]*CREATE[[:space:]]+POLICY' 2>/dev/null |
213
+ awk -F: '{s+=$NF} END {print s+0}')
214
+ if [ "${_declared:-0}" -gt 0 ]; then
215
+ _actual=$(psql -tAq -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
216
+ -c "SELECT count(*) FROM pg_policies" 2>/dev/null || echo 0)
217
+ if [ "${_actual:-0}" -lt $((_declared / 2)) ]; then
218
+ say "only ${_actual:-0} policies present against $_declared declared - the schema did not build."
219
+ say "Not recording a fingerprint; fix the DDL and re-run."
220
+ exit 1
221
+ fi
222
+ say "$_actual RLS policies ($_declared declared)"
223
+ fi
224
+ fi
225
+
226
+ psql_db \
227
+ -c "CREATE TABLE IF NOT EXISTS biffo_pg_test_fingerprint (value text primary key)" \
228
+ -c "TRUNCATE biffo_pg_test_fingerprint" \
229
+ -c "INSERT INTO biffo_pg_test_fingerprint (value) VALUES ('$WANT')" >/dev/null
230
+
231
+ say "ready"
232
+ emit
@@ -478,6 +478,23 @@ pg_test_run() {
478
478
  }
479
479
 
480
480
  _pg_modules=$(pg_test_modules)
481
+
482
+ # Provision the database rather than requiring the operator to remember.
483
+ #
484
+ # A gate that only runs when you exported the right variable is a gate that runs
485
+ # on the days you did not need it. `scripts/pg-test-db.sh` is idempotent and
486
+ # cheap when the schema is unchanged (~0.3s; ~4s when it genuinely has to
487
+ # rebuild), so calling it is better than warning about it. Failure is silent
488
+ # BECAUSE the WARN below is the honest report of it -- no Docker, no server, no
489
+ # schema all end in the same place: the lane did not run, and the gate says so.
490
+ if [ -z "$PG_TEST_DSN" ] && [ -n "$_pg_modules" ] && [ -z "$LIST" ] && [ -f scripts/pg-test-db.sh ]; then
491
+ PG_TEST_DSN=$(sh scripts/pg-test-db.sh 2>/dev/null | tail -1) || PG_TEST_DSN=""
492
+ case "$PG_TEST_DSN" in
493
+ postgres*) ;;
494
+ *) PG_TEST_DSN="" ;;
495
+ esac
496
+ fi
497
+
481
498
  # Order matters, and getting it wrong made these very tests machine-dependent:
482
499
  # with `uv not installed` checked FIRST, a runner without uv skipped quietly and
483
500
  # the gap warning never printed -- green on a workstation, red on CI, for a
@@ -550,6 +567,7 @@ fi
550
567
  if [ -f scripts/biffo.sh ]; then
551
568
  run_check plugin-tf sh scripts/biffo.sh check plugin-terraform
552
569
  run_check plugin-names sh scripts/biffo.sh check plugin-collisions
570
+ run_check adr-numbering sh scripts/biffo.sh check adr-numbering
553
571
  else
554
572
  skip biffo-guards "no scripts/biffo.sh in this repo"
555
573
  fi
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # Give the Postgres-dependent test lane a database with a CURRENT schema, and
4
+ # print its DSN.
5
+ #
6
+ # ## Why this exists
7
+ #
8
+ # `scripts/verify.sh` grew a `pg-test` check (#1089) because on 2026-08-02 nine
9
+ # of thirteen locally-catchable failing CI steps across the estate were one
10
+ # repo's real-Postgres lane -- a required check with no local counterpart at all.
11
+ # But a gate can only run that lane against a database, and no repo documented
12
+ # how to get one: no compose file, no script, no DSN written down. The container
13
+ # that existed on the workstation had been created ad hoc in some earlier session
14
+ # and held a scatter of scratch databases. That undocumented setup WAS the
15
+ # fail-open, because a gate nobody can run is not a gate.
16
+ #
17
+ # ## Why freshness, not rebuild-every-time
18
+ #
19
+ # The expensive failure is not a slow rebuild, it is a STALE one. Measured on
20
+ # tabsii-platform while writing this: a database built about an hour earlier,
21
+ # before two PRs merged, produced **23 failures** in a module that had nothing to
22
+ # do with the change in hand. Rebuilt from the same tree it passed 336/336, and
23
+ # passed 336 again on an immediate re-run -- so the lane was genuinely
24
+ # re-runnable and every one of those failures was the old schema.
25
+ #
26
+ # That is the worst shape a local gate can have. Twenty-three red tests that are
27
+ # not your fault teach people the gate is unreliable, and an unreliable gate gets
28
+ # bypassed -- which H4 pre-registered as the condition refuting the whole
29
+ # local-gate programme. So the schema inputs are fingerprinted and a rebuild
30
+ # happens only when they actually changed: reuse ~0.3s, rebuild ~4s.
31
+ #
32
+ # ## Why it is generic
33
+ #
34
+ # It adapts to the repo rather than being told about it, for the same reason
35
+ # `verify.sh` does: forks drift, and a per-instance copy of this would drift from
36
+ # the DDL layout it is meant to build. Everything instance-specific is DERIVED --
37
+ # the schema directories from `db/imports/*/`, the engine image from whether the
38
+ # DDL asks for PostGIS, and the did-it-build threshold from the number of
39
+ # policies the DDL itself declares. Nothing here names a product.
40
+ #
41
+ # ## Usage
42
+ #
43
+ # eval "$(sh scripts/pg-test-db.sh --export)" # export BIFFO_TEST_PG_DSN
44
+ # sh scripts/pg-test-db.sh # print the DSN on stdout
45
+ # sh scripts/pg-test-db.sh --recreate # force a rebuild
46
+ #
47
+ # Only the DSN reaches stdout, so it is safe to capture; progress goes to stderr.
48
+ #
49
+ # Overridable: BIFFO_PG_HOST, BIFFO_PG_PORT, BIFFO_PG_USER, BIFFO_PG_PASSWORD,
50
+ # BIFFO_PG_DB, BIFFO_PG_CONTAINER, BIFFO_PG_IMAGE.
51
+
52
+ set -eu
53
+
54
+ HOST="${BIFFO_PG_HOST:-localhost}"
55
+ PORT="${BIFFO_PG_PORT:-55432}"
56
+ USER_="${BIFFO_PG_USER:-postgres}"
57
+ PASS="${BIFFO_PG_PASSWORD:-postgres}"
58
+ DB="${BIFFO_PG_DB:-biffo_test}"
59
+ CONTAINER="${BIFFO_PG_CONTAINER:-biffo-pg-test}"
60
+
61
+ RECREATE=0
62
+ EXPORT=0
63
+ for arg in "$@"; do
64
+ case "$arg" in
65
+ --recreate) RECREATE=1 ;;
66
+ --export) EXPORT=1 ;;
67
+ -h | --help)
68
+ sed -n '2,48p' "$0" | sed 's/^#\{1,2\} \{0,1\}//'
69
+ exit 0
70
+ ;;
71
+ *)
72
+ echo "unknown argument: $arg" >&2
73
+ exit 2
74
+ ;;
75
+ esac
76
+ done
77
+
78
+ say() { echo "pg-test-db: $*" >&2; }
79
+
80
+ REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd)
81
+ cd "$REPO_ROOT"
82
+
83
+ # --- what this repo's schema is made of --------------------------------------
84
+ #
85
+ # `db/imports/<name>/*.sql` is the Biffo DDL-import convention that the API's own
86
+ # `ddl_import.list_sql_files` reads at startup, so deriving from it means this
87
+ # script and the running app agree by construction rather than by someone
88
+ # remembering to update both.
89
+ DDL_FILES=$(find db/imports -mindepth 2 -maxdepth 2 -name '*.sql' 2>/dev/null | LC_ALL=C sort || true)
90
+ ALEMBIC_DIR=""
91
+ for _d in services/api .; do
92
+ [ -f "$_d/alembic.ini" ] && ALEMBIC_DIR="$_d" && break
93
+ done
94
+
95
+ if [ -z "$DDL_FILES" ] && [ -z "$ALEMBIC_DIR" ]; then
96
+ say "no db/imports/*/ DDL and no alembic.ini - this repo has no schema to build"
97
+ exit 1
98
+ fi
99
+
100
+ # PostGIS or plain, decided by what the DDL asks for. A plain `postgres` image
101
+ # fails on the first `CREATE EXTENSION postgis`, and picking the heavier image
102
+ # unconditionally would slow every repo that does not need it.
103
+ if [ -n "$DDL_FILES" ] && echo "$DDL_FILES" | xargs grep -liE 'EXTENSION[[:space:]]+(IF[[:space:]]+NOT[[:space:]]+EXISTS[[:space:]]+)?postgis' >/dev/null 2>&1; then
104
+ IMAGE="${BIFFO_PG_IMAGE:-postgis/postgis:16-3.4}"
105
+ else
106
+ IMAGE="${BIFFO_PG_IMAGE:-postgres:16}"
107
+ fi
108
+
109
+ export PGPASSWORD="$PASS"
110
+ psql_admin() { psql -q -h "$HOST" -p "$PORT" -U "$USER_" -d postgres "$@"; }
111
+ psql_db() { psql -q -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" "$@"; }
112
+
113
+ # --- 1. a reachable server ---------------------------------------------------
114
+ #
115
+ # Started here rather than assumed, because "docker run one yourself" is exactly
116
+ # the tribal knowledge this script replaces. An already-running server is reused.
117
+ if ! psql_admin -c 'SELECT 1' >/dev/null 2>&1; then
118
+ if ! command -v docker >/dev/null 2>&1; then
119
+ say "no Postgres at $HOST:$PORT and docker is not installed."
120
+ say "Start one and re-run, or set BIFFO_PG_HOST / BIFFO_PG_PORT."
121
+ exit 1
122
+ fi
123
+ if docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER"; then
124
+ say "starting existing container $CONTAINER"
125
+ docker start "$CONTAINER" >/dev/null
126
+ else
127
+ say "creating container $CONTAINER ($IMAGE) on port $PORT"
128
+ docker run -d --name "$CONTAINER" \
129
+ -e POSTGRES_PASSWORD="$PASS" -p "$PORT:5432" "$IMAGE" >/dev/null
130
+ fi
131
+ # Polled, not slept: a cold image pull and a warm restart differ by an order of
132
+ # magnitude, and one fixed sleep is wrong for both.
133
+ _waited=0
134
+ until psql_admin -c 'SELECT 1' >/dev/null 2>&1; do
135
+ _waited=$((_waited + 1))
136
+ if [ "$_waited" -gt 90 ]; then
137
+ say "Postgres did not become ready in 90s"
138
+ exit 1
139
+ fi
140
+ sleep 1
141
+ done
142
+ say "Postgres ready after ${_waited}s"
143
+ fi
144
+
145
+ DSN="postgresql+asyncpg://$USER_:$PASS@$HOST:$PORT/$DB"
146
+ emit() {
147
+ if [ "$EXPORT" -eq 1 ]; then
148
+ echo "export BIFFO_TEST_PG_DSN='$DSN'"
149
+ else
150
+ echo "$DSN"
151
+ fi
152
+ }
153
+
154
+ # --- 2. is the existing schema current? --------------------------------------
155
+ #
156
+ # By CONTENT, not mtime: a branch switch changes content and leaves mtime
157
+ # anywhere. Stored inside the database, so it cannot outlive a drop or describe
158
+ # some other database.
159
+ fingerprint() {
160
+ {
161
+ [ -n "$ALEMBIC_DIR" ] && find "$ALEMBIC_DIR" -name '*.py' -path '*alembic*' -type f 2>/dev/null |
162
+ LC_ALL=C sort | xargs cat 2>/dev/null
163
+ [ -n "$DDL_FILES" ] && echo "$DDL_FILES" | xargs cat 2>/dev/null
164
+ } | sha256sum | cut -d' ' -f1
165
+ }
166
+
167
+ WANT=$(fingerprint)
168
+ HAVE=""
169
+ if [ "$RECREATE" -eq 0 ] &&
170
+ psql_admin -tAc "SELECT 1 FROM pg_database WHERE datname='$DB'" 2>/dev/null | grep -q 1; then
171
+ HAVE=$(psql -tAq -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
172
+ -c "SELECT value FROM biffo_pg_test_fingerprint LIMIT 1" 2>/dev/null || true)
173
+ fi
174
+
175
+ if [ -n "$HAVE" ] && [ "$HAVE" = "$WANT" ]; then
176
+ say "schema is current, reusing $DB"
177
+ emit
178
+ exit 0
179
+ fi
180
+
181
+ [ -n "$HAVE" ] && say "schema inputs changed - rebuilding rather than serving a stale schema"
182
+
183
+ # --- 3. rebuild the way the app and CI do ------------------------------------
184
+ say "rebuilding $DB"
185
+ psql_admin -c "DROP DATABASE IF EXISTS $DB WITH (FORCE)" >/dev/null
186
+ psql_admin -c "CREATE DATABASE $DB" >/dev/null
187
+
188
+ if [ -n "$ALEMBIC_DIR" ]; then
189
+ BIFFO_DATABASE_URL="$DSN" uv run --directory "$ALEMBIC_DIR" alembic upgrade head >/dev/null
190
+ say "alembic upgrade head"
191
+ fi
192
+
193
+ if [ -n "$DDL_FILES" ]; then
194
+ # ONE psql session, sorted by filename, mirroring the API's own DDL import.
195
+ # Session state an early module sets -- typically `SET search_path` in the
196
+ # first file -- has to survive into later ones, so a per-file connection would
197
+ # silently change the meaning of every unqualified name after it. LC_ALL=C
198
+ # keeps the shell's sort byte-ordered to match Python's.
199
+ # shellcheck disable=SC2046
200
+ psql -q -v ON_ERROR_STOP=1 -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
201
+ --single-transaction $(echo "$DDL_FILES" | sed 's/^/-f /' | tr '\n' ' ') >/dev/null
202
+ say "$(echo "$DDL_FILES" | wc -l | tr -d ' ') DDL modules applied"
203
+ fi
204
+
205
+ # --- 4. refuse to bless a half-built schema ----------------------------------
206
+ #
207
+ # The threshold is derived, not guessed: count the policies the DDL declares and
208
+ # require the database to hold at least half. Recording a fingerprint against a
209
+ # partial schema is worse than failing, because the NEXT run would trust it and
210
+ # every failure after that would look like the developer's own change.
211
+ if [ -n "$DDL_FILES" ]; then
212
+ _declared=$(echo "$DDL_FILES" | xargs grep -ciE '^[[:space:]]*CREATE[[:space:]]+POLICY' 2>/dev/null |
213
+ awk -F: '{s+=$NF} END {print s+0}')
214
+ if [ "${_declared:-0}" -gt 0 ]; then
215
+ _actual=$(psql -tAq -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
216
+ -c "SELECT count(*) FROM pg_policies" 2>/dev/null || echo 0)
217
+ if [ "${_actual:-0}" -lt $((_declared / 2)) ]; then
218
+ say "only ${_actual:-0} policies present against $_declared declared - the schema did not build."
219
+ say "Not recording a fingerprint; fix the DDL and re-run."
220
+ exit 1
221
+ fi
222
+ say "$_actual RLS policies ($_declared declared)"
223
+ fi
224
+ fi
225
+
226
+ psql_db \
227
+ -c "CREATE TABLE IF NOT EXISTS biffo_pg_test_fingerprint (value text primary key)" \
228
+ -c "TRUNCATE biffo_pg_test_fingerprint" \
229
+ -c "INSERT INTO biffo_pg_test_fingerprint (value) VALUES ('$WANT')" >/dev/null
230
+
231
+ say "ready"
232
+ emit
@@ -478,6 +478,23 @@ pg_test_run() {
478
478
  }
479
479
 
480
480
  _pg_modules=$(pg_test_modules)
481
+
482
+ # Provision the database rather than requiring the operator to remember.
483
+ #
484
+ # A gate that only runs when you exported the right variable is a gate that runs
485
+ # on the days you did not need it. `scripts/pg-test-db.sh` is idempotent and
486
+ # cheap when the schema is unchanged (~0.3s; ~4s when it genuinely has to
487
+ # rebuild), so calling it is better than warning about it. Failure is silent
488
+ # BECAUSE the WARN below is the honest report of it -- no Docker, no server, no
489
+ # schema all end in the same place: the lane did not run, and the gate says so.
490
+ if [ -z "$PG_TEST_DSN" ] && [ -n "$_pg_modules" ] && [ -z "$LIST" ] && [ -f scripts/pg-test-db.sh ]; then
491
+ PG_TEST_DSN=$(sh scripts/pg-test-db.sh 2>/dev/null | tail -1) || PG_TEST_DSN=""
492
+ case "$PG_TEST_DSN" in
493
+ postgres*) ;;
494
+ *) PG_TEST_DSN="" ;;
495
+ esac
496
+ fi
497
+
481
498
  # Order matters, and getting it wrong made these very tests machine-dependent:
482
499
  # with `uv not installed` checked FIRST, a runner without uv skipped quietly and
483
500
  # the gap warning never printed -- green on a workstation, red on CI, for a
@@ -550,6 +567,7 @@ fi
550
567
  if [ -f scripts/biffo.sh ]; then
551
568
  run_check plugin-tf sh scripts/biffo.sh check plugin-terraform
552
569
  run_check plugin-names sh scripts/biffo.sh check plugin-collisions
570
+ run_check adr-numbering sh scripts/biffo.sh check adr-numbering
553
571
  else
554
572
  skip biffo-guards "no scripts/biffo.sh in this repo"
555
573
  fi
package/dist/index.js CHANGED
@@ -9124,9 +9124,61 @@ siblingCommand.addCommand(siblingCheckIdentityCommand);
9124
9124
  // src/commands/check.ts
9125
9125
  import { Command as Command23 } from "commander";
9126
9126
 
9127
+ // src/scripts/check-adr-numbering.ts
9128
+ import { existsSync as existsSync32 } from "fs";
9129
+ import { join as join31 } from "path";
9130
+ import { execa as execa5 } from "execa";
9131
+
9132
+ // src/lib/adr-numbering-guard.ts
9133
+ import { existsSync as existsSync31, readdirSync as readdirSync13 } from "fs";
9134
+ var ADR_FILENAME = /^(\d{4})-.+\.md$/;
9135
+ function adrNumbersIn(adrDir) {
9136
+ const claims = /* @__PURE__ */ new Map();
9137
+ if (!existsSync31(adrDir)) return claims;
9138
+ for (const entry of readdirSync13(adrDir).sort()) {
9139
+ const match = ADR_FILENAME.exec(entry);
9140
+ if (!match) continue;
9141
+ const number = match[1];
9142
+ claims.set(number, [...claims.get(number) ?? [], entry]);
9143
+ }
9144
+ return claims;
9145
+ }
9146
+ function findAdrNumberCollisions(adrDir) {
9147
+ const collisions = [];
9148
+ for (const [number, files] of [...adrNumbersIn(adrDir).entries()].sort()) {
9149
+ if (files.length > 1) collisions.push({ number, files: [...files].sort() });
9150
+ }
9151
+ return collisions;
9152
+ }
9153
+ function formatAdrNumberCollisions(collisions) {
9154
+ return collisions.map(
9155
+ (c) => ` ADR-${c.number} is claimed by: ${c.files.join(", ")}
9156
+ Pick a different number for the newer one \u2014 citing "ADR-${c.number}" is
9157
+ ambiguous while both exist.`
9158
+ ).join("\n");
9159
+ }
9160
+
9161
+ // src/scripts/check-adr-numbering.ts
9162
+ async function runAdrNumberingCheck() {
9163
+ const root = (await execa5("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9164
+ const adrDir = join31(root, "docs", "ADR");
9165
+ if (!existsSync32(adrDir)) {
9166
+ console.log("\u2713 ADR numbering guard: no docs/ADR/ directory \u2014 nothing to compare");
9167
+ return;
9168
+ }
9169
+ const collisions = findAdrNumberCollisions(adrDir);
9170
+ if (collisions.length > 0) {
9171
+ console.error("\u2717 ADR numbering guard: two ADRs in docs/ADR/ share a number\n");
9172
+ console.error(formatAdrNumberCollisions(collisions));
9173
+ console.error("\nSee tabsii-platform#449 for how this class of collision happens.");
9174
+ process.exit(1);
9175
+ }
9176
+ console.log("\u2713 ADR numbering guard: OK");
9177
+ }
9178
+
9127
9179
  // src/scripts/check-branch-protection.ts
9128
9180
  import { Octokit as Octokit2 } from "@octokit/rest";
9129
- import { execa as execa5 } from "execa";
9181
+ import { execa as execa6 } from "execa";
9130
9182
 
9131
9183
  // src/lib/branch-protection-apply.ts
9132
9184
  var CONTEXT_CONSISTENCY_THRESHOLD = 2 / 3;
@@ -9253,7 +9305,7 @@ async function resolveRepo(explicit) {
9253
9305
  }
9254
9306
  return { owner, repo };
9255
9307
  }
9256
- const { stdout } = await execa5("git", ["remote", "get-url", "origin"]);
9308
+ const { stdout } = await execa6("git", ["remote", "get-url", "origin"]);
9257
9309
  const m = /github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/.exec(stdout.trim());
9258
9310
  if (!m?.[1] || !m[2]) {
9259
9311
  console.error(
@@ -9385,7 +9437,7 @@ async function runBranchProtectionCheck(explicitRepo, options = {}) {
9385
9437
  }
9386
9438
 
9387
9439
  // src/scripts/check-core-ownership.ts
9388
- import { execa as execa6 } from "execa";
9440
+ import { execa as execa7 } from "execa";
9389
9441
  var BOLD = "\x1B[1m";
9390
9442
  var DIM = "\x1B[2m";
9391
9443
  var RED = "\x1B[31m";
@@ -9396,7 +9448,7 @@ async function runOwnershipCheck(argv) {
9396
9448
  const stagedFlag = args.indexOf("--staged");
9397
9449
  const staged = stagedFlag !== -1;
9398
9450
  const messageFile = staged ? args[stagedFlag + 1] : void 0;
9399
- const root = (await execa6("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9451
+ const root = (await execa7("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9400
9452
  if (!isInstanceRepo(root)) {
9401
9453
  console.log("\u2713 core ownership guard: skipped \u2014 this is the template, which owns these paths.");
9402
9454
  return;
@@ -9405,11 +9457,11 @@ async function runOwnershipCheck(argv) {
9405
9457
  let deletedFiles = [];
9406
9458
  let commitMessage = "";
9407
9459
  if (staged) {
9408
- const { stdout } = await execa6("git", ["diff", "--cached", "--name-status"], { cwd: root });
9460
+ const { stdout } = await execa7("git", ["diff", "--cached", "--name-status"], { cwd: root });
9409
9461
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
9410
9462
  if (messageFile) {
9411
- const { readFileSync: readFileSync26, existsSync: existsSync35 } = await import("fs");
9412
- if (existsSync35(messageFile)) commitMessage = readFileSync26(messageFile, "utf8");
9463
+ const { readFileSync: readFileSync26, existsSync: existsSync37 } = await import("fs");
9464
+ if (existsSync37(messageFile)) commitMessage = readFileSync26(messageFile, "utf8");
9413
9465
  }
9414
9466
  } else {
9415
9467
  const base = process.env["GITHUB_BASE_REF"] ?? args[0];
@@ -9417,18 +9469,18 @@ async function runOwnershipCheck(argv) {
9417
9469
  console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
9418
9470
  process.exit(2);
9419
9471
  }
9420
- await execa6("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
9421
- const { stdout } = await execa6("git", ["diff", "--name-status", `origin/${base}...HEAD`], {
9472
+ await execa7("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
9473
+ const { stdout } = await execa7("git", ["diff", "--name-status", `origin/${base}...HEAD`], {
9422
9474
  cwd: root
9423
9475
  });
9424
9476
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
9425
- const { stdout: log2 } = await execa6("git", ["log", "--format=%B", `origin/${base}..HEAD`], {
9477
+ const { stdout: log2 } = await execa7("git", ["log", "--format=%B", `origin/${base}..HEAD`], {
9426
9478
  cwd: root,
9427
9479
  reject: false
9428
9480
  });
9429
9481
  commitMessage = log2;
9430
9482
  }
9431
- const { stdout: gitBranch } = await execa6("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
9483
+ const { stdout: gitBranch } = await execa7("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
9432
9484
  cwd: root,
9433
9485
  reject: false
9434
9486
  });
@@ -9510,34 +9562,34 @@ ${BOLD}If the divergence is deliberate${OFF}
9510
9562
  }
9511
9563
 
9512
9564
  // src/scripts/check-plugin-collisions.ts
9513
- import { existsSync as existsSync32 } from "fs";
9514
- import { join as join32 } from "path";
9515
- import { execa as execa7 } from "execa";
9565
+ import { existsSync as existsSync34 } from "fs";
9566
+ import { join as join33 } from "path";
9567
+ import { execa as execa8 } from "execa";
9516
9568
 
9517
9569
  // src/lib/plugin-collision-guard.ts
9518
- import { existsSync as existsSync31, readdirSync as readdirSync13, statSync as statSync7 } from "fs";
9519
- import { join as join31 } from "path";
9570
+ import { existsSync as existsSync33, readdirSync as readdirSync14, statSync as statSync7 } from "fs";
9571
+ import { join as join32 } from "path";
9520
9572
  var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
9521
9573
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
9522
9574
  function subdirectories(dir) {
9523
- if (!existsSync31(dir)) return [];
9524
- return readdirSync13(dir).filter((entry) => {
9575
+ if (!existsSync33(dir)) return [];
9576
+ return readdirSync14(dir).filter((entry) => {
9525
9577
  if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
9526
9578
  try {
9527
- return statSync7(join31(dir, entry)).isDirectory();
9579
+ return statSync7(join32(dir, entry)).isDirectory();
9528
9580
  } catch {
9529
9581
  return false;
9530
9582
  }
9531
9583
  });
9532
9584
  }
9533
9585
  function regularPackagesOf(pluginDir2) {
9534
- return subdirectories(pluginDir2).filter((name) => existsSync31(join31(pluginDir2, name, "__init__.py"))).sort();
9586
+ return subdirectories(pluginDir2).filter((name) => existsSync33(join32(pluginDir2, name, "__init__.py"))).sort();
9535
9587
  }
9536
9588
  function bareTestModulesOf(pluginDir2) {
9537
- const testsDir = join31(pluginDir2, "tests");
9538
- if (!existsSync31(testsDir)) return [];
9539
- if (existsSync31(join31(testsDir, "__init__.py"))) return [];
9540
- return readdirSync13(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
9589
+ const testsDir = join32(pluginDir2, "tests");
9590
+ if (!existsSync33(testsDir)) return [];
9591
+ if (existsSync33(join32(testsDir, "__init__.py"))) return [];
9592
+ return readdirSync14(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
9541
9593
  }
9542
9594
  function findCollisions(servicesDir, pluginDirs) {
9543
9595
  const plugins = (pluginDirs ?? subdirectories(servicesDir)).filter((name) => !name.startsWith("_")).filter((name) => name !== "api").sort();
@@ -9545,7 +9597,7 @@ function findCollisions(servicesDir, pluginDirs) {
9545
9597
  const gather = (kind, namesOf) => {
9546
9598
  const claims = /* @__PURE__ */ new Map();
9547
9599
  for (const plugin of plugins) {
9548
- for (const name of namesOf(join31(servicesDir, plugin))) {
9600
+ for (const name of namesOf(join32(servicesDir, plugin))) {
9549
9601
  claims.set(name, [...claims.get(name) ?? [], plugin]);
9550
9602
  }
9551
9603
  }
@@ -9582,9 +9634,9 @@ function formatCollisions(collisions) {
9582
9634
 
9583
9635
  // src/scripts/check-plugin-collisions.ts
9584
9636
  async function runPluginCollisionCheck() {
9585
- const root = (await execa7("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9586
- const servicesDir = join32(root, "services");
9587
- if (!existsSync32(servicesDir)) {
9637
+ const root = (await execa8("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9638
+ const servicesDir = join33(root, "services");
9639
+ if (!existsSync34(servicesDir)) {
9588
9640
  console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
9589
9641
  return;
9590
9642
  }
@@ -9601,11 +9653,11 @@ async function runPluginCollisionCheck() {
9601
9653
  }
9602
9654
 
9603
9655
  // src/scripts/check-plugin-terraform.ts
9604
- import { execa as execa8 } from "execa";
9656
+ import { execa as execa9 } from "execa";
9605
9657
 
9606
9658
  // src/lib/plugin-terraform-guard.ts
9607
- import { existsSync as existsSync33, readFileSync as readFileSync24, readdirSync as readdirSync14 } from "fs";
9608
- import { dirname as dirname9, join as join33, relative as relative6, sep as sep3 } from "path";
9659
+ import { existsSync as existsSync35, readFileSync as readFileSync24, readdirSync as readdirSync15 } from "fs";
9660
+ import { dirname as dirname9, join as join34, relative as relative6, sep as sep3 } from "path";
9609
9661
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
9610
9662
  var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
9611
9663
  function findPluginManifests(root) {
@@ -9613,16 +9665,16 @@ function findPluginManifests(root) {
9613
9665
  const walk = (dir) => {
9614
9666
  let entries;
9615
9667
  try {
9616
- entries = readdirSync14(dir, { withFileTypes: true });
9668
+ entries = readdirSync15(dir, { withFileTypes: true });
9617
9669
  } catch {
9618
9670
  return;
9619
9671
  }
9620
9672
  for (const entry of entries) {
9621
9673
  if (entry.isDirectory()) {
9622
9674
  if (SKIP_DIRS.has(entry.name)) continue;
9623
- walk(join33(dir, entry.name));
9675
+ walk(join34(dir, entry.name));
9624
9676
  } else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
9625
- found.push(relative6(root, join33(dir, entry.name)).split(sep3).join("/"));
9677
+ found.push(relative6(root, join34(dir, entry.name)).split(sep3).join("/"));
9626
9678
  }
9627
9679
  }
9628
9680
  };
@@ -9647,14 +9699,14 @@ function readSubscriptions(absManifestPath) {
9647
9699
  }
9648
9700
  function checkPluginTerraform(root) {
9649
9701
  const violations = [];
9650
- const coreManifest = existsSync33(join33(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
9702
+ const coreManifest = existsSync35(join34(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
9651
9703
  for (const manifest of findPluginManifests(root)) {
9652
9704
  if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
9653
- const absManifest = join33(root, manifest);
9705
+ const absManifest = join34(root, manifest);
9654
9706
  const subscriptions = readSubscriptions(absManifest);
9655
9707
  if (subscriptions === null) continue;
9656
9708
  const pluginDir2 = dirname9(absManifest);
9657
- if (existsSync33(join33(pluginDir2, "terraform"))) continue;
9709
+ if (existsSync35(join34(pluginDir2, "terraform"))) continue;
9658
9710
  const relPluginDir = relative6(root, pluginDir2).split(sep3).join("/");
9659
9711
  violations.push({
9660
9712
  manifest,
@@ -9674,7 +9726,7 @@ function formatViolations(violations) {
9674
9726
 
9675
9727
  // src/scripts/check-plugin-terraform.ts
9676
9728
  async function runPluginTerraformCheck() {
9677
- const root = (await execa8("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9729
+ const root = (await execa9("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9678
9730
  const violations = checkPluginTerraform(root);
9679
9731
  if (violations.length > 0) {
9680
9732
  console.error("\u2717 plugin Terraform guard: event subscriptions with no infrastructure\n");
@@ -9685,7 +9737,7 @@ async function runPluginTerraformCheck() {
9685
9737
  }
9686
9738
 
9687
9739
  // src/scripts/check-release-subject.ts
9688
- import { execa as execa9 } from "execa";
9740
+ import { execa as execa10 } from "execa";
9689
9741
 
9690
9742
  // src/lib/release-version.ts
9691
9743
  var MINOR_TYPES = /* @__PURE__ */ new Set(["feat"]);
@@ -9723,13 +9775,13 @@ async function runReleaseSubjectCheck(argv) {
9723
9775
  console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
9724
9776
  process.exit(2);
9725
9777
  }
9726
- const root = (await execa9("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9727
- await execa9("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
9728
- const { stdout } = await execa9("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
9778
+ const root = (await execa10("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9779
+ await execa10("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
9780
+ const { stdout } = await execa10("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
9729
9781
  cwd: root
9730
9782
  });
9731
9783
  const changedFiles = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
9732
- const subject = process.env["PR_TITLE"]?.trim() || (await execa9("git", ["log", "-1", "--format=%s"], { cwd: root })).stdout.trim();
9784
+ const subject = process.env["PR_TITLE"]?.trim() || (await execa10("git", ["log", "-1", "--format=%s"], { cwd: root })).stdout.trim();
9733
9785
  const manifest = readCoreManifest(root);
9734
9786
  const { unparseable, bump, templateOwnedChanges, skippedAsInstance } = checkReleaseSubject(
9735
9787
  changedFiles,
@@ -9788,6 +9840,11 @@ checkCommand.command("plugin-collisions").description("Refuse two vendored plugi
9788
9840
  checkCommand.command("plugin-terraform").description("Verify every template-owned plugin declaring infra ships a Terraform module").action(async () => {
9789
9841
  await runPluginTerraformCheck();
9790
9842
  });
9843
+ checkCommand.command("adr-numbering").description(
9844
+ "Refuse two ADRs in this repo's own docs/ADR/ claiming the same number (tabsii-platform#449)"
9845
+ ).action(async () => {
9846
+ await runAdrNumberingCheck();
9847
+ });
9791
9848
  checkCommand.command("branch-protection").description(
9792
9849
  "Verify dev/staging/main are actually protected \u2014 scaffolding skips this on a 403 (#715)"
9793
9850
  ).option("--repo <owner/name>", "Repo to audit; defaults to this checkout's origin remote").option(
@@ -9802,8 +9859,8 @@ function rawArgsAfter(subcommand) {
9802
9859
  }
9803
9860
 
9804
9861
  // src/commands/doctor.ts
9805
- import { existsSync as existsSync34, readFileSync as readFileSync25 } from "fs";
9806
- import { join as join34, resolve as resolve18 } from "path";
9862
+ import { existsSync as existsSync36, readFileSync as readFileSync25 } from "fs";
9863
+ import { join as join35, resolve as resolve18 } from "path";
9807
9864
  import chalk21 from "chalk";
9808
9865
  import { Command as Command24 } from "commander";
9809
9866
 
@@ -9978,8 +10035,8 @@ async function runDoctor(options, deps = { git: new GitAdapter() }) {
9978
10035
  return runDoctorChecks(facts);
9979
10036
  }
9980
10037
  function readLocalCoreVersion(cwd) {
9981
- const path = join34(cwd, INSTANCE_CORE_FILE);
9982
- if (!existsSync34(path)) return null;
10038
+ const path = join35(cwd, INSTANCE_CORE_FILE);
10039
+ if (!existsSync36(path)) return null;
9983
10040
  try {
9984
10041
  return parseCoreRecord(readFileSync25(path, "utf8"));
9985
10042
  } catch {
@@ -9996,8 +10053,8 @@ function parseCoreRecord(contents) {
9996
10053
  }
9997
10054
  }
9998
10055
  function readFossil(cwd) {
9999
- const path = join34(cwd, CORE_VERSION_FILE);
10000
- if (!existsSync34(path)) return null;
10056
+ const path = join35(cwd, CORE_VERSION_FILE);
10057
+ if (!existsSync36(path)) return null;
10001
10058
  try {
10002
10059
  const value = readFileSync25(path, "utf8").trim();
10003
10060
  return value === "" ? null : value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.212.1",
3
+ "version": "0.214.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",