6666// "Prefer failing to falling back" (AGENTS.md, route & surface ownership §3):
6767// the prerequisite verdict is a HARD failure that states it checked nothing —
6868// never a skip, and never anything a reader can mistake for "bundles are fine".
69+ //
70+ // The two pure functions that answer it moved to `scripts/cli-build-prerequisite.mjs`
71+ // when #5862 found the same missing precondition in `check-i18n-coverage.mjs`, one
72+ // lint.yml step away. They are imported, not copied: see that module's header.
6973import { spawnSync } from 'node:child_process' ;
7074import { readFileSync , existsSync } from 'node:fs' ;
7175import { readdirSync , statSync } from 'node:fs' ;
7276import { join } from 'node:path' ;
77+ import {
78+ CLI ,
79+ CLI_BUILD_FIX ,
80+ looksLikeMissingCliCommand ,
81+ oclifCommandFileFor ,
82+ resolveCliCommandFile ,
83+ } from './cli-build-prerequisite.mjs' ;
7384
74- const CLI = 'packages/cli/bin/run.js' ;
75- /**
76- * `CLI` is a SOURCE file — four lines handing off to `@oclif/core` — so it is
77- * present in an unbuilt tree and proves nothing. What the gate actually depends
78- * on is the built command surface `bin/run.js` makes oclif resolve, which is why
79- * the prerequisite probe below reads the package rather than the bin stub.
80- */
81- const CLI_PKG = 'packages/cli' ;
8285/** The one command this gate invokes per package, as oclif topic/command parts. */
8386const EXTRACT_COMMAND_ID = [ 'i18n' , 'extract' ] ;
8487const write = process . argv . includes ( '--write' ) ;
@@ -173,55 +176,10 @@ function collectDriftedBundles(text) {
173176 return [ ...String ( text ?? '' ) . matchAll ( / (?: o u t o f d a t e | m i s s i n g ) : \s + ( \S + ) / g) ] . map ( ( m ) => m [ 1 ] ) ;
174177}
175178
176- /**
177- * Where oclif will look for the command this gate runs, derived from the CLI
178- * package's own `oclif.commands.target` (#5217). Pure: takes the parsed
179- * package.json, returns a repo-relative path or a reason it cannot tell.
180- *
181- * Derived rather than hardcoded for the same reason the extract flags come from
182- * each config's docstring: `dist/commands` is the CLI's declaration of where its
183- * commands live, and a gate that restates it would keep probing the old path for
184- * a release after someone moves it — passing while checking nothing.
185- */
186- function oclifCommandFileFor ( pkgJson , commandId ) {
187- const target = pkgJson ?. oclif ?. commands ?. target ?? pkgJson ?. oclif ?. commands ;
188- if ( typeof target !== 'string' || ! target ) {
189- return { unknown : `${ CLI_PKG } /package.json declares no oclif.commands.target` } ;
190- }
191- const rel = target . replace ( / ^ \. \/ / , '' ) . replace ( / \/ + $ / , '' ) ;
192- return { file : join ( CLI_PKG , rel , ...commandId . slice ( 0 , - 1 ) , `${ commandId . at ( - 1 ) } .js` ) } ;
193- }
194-
195- /**
196- * oclif's own "command <id> not found", which is what an unbuilt (or half-built)
197- * CLI answers with. The in-loop safety net for the prerequisite probe, and it has
198- * to survive oclif's line wrapping to be worth anything: oclif hard-wraps that
199- * one sentence across two or three ` › `-prefixed lines, and it wraps at a width
200- * that depends on the config path's length, so the real corpus contains BOTH
201- *
202- * " › Error: command \n › i18n:extract:<path> not \n › found"
203- * " › Error: command i18n:extract:<path-broken\n › -mid-token> not found"
204- *
205- * — the second one split inside the path itself. A per-line regex (the obvious
206- * first implementation, and the one that reads as correct) matches NEITHER. So
207- * the prefixes come off and the whole text is flattened before matching.
208- *
209- * Returns the matched SENTENCE (re-joined into one readable line) so the caller
210- * can quote it as evidence, or '' for no match. Returning the whole flattened
211- * text instead is a trap this returned from once in review: a stale-dist run
212- * also carries a node `Warning:` block above the error, and quoting the flattened
213- * text put that unrelated block in the report while the actual sentence sat past
214- * the truncation.
215- */
216- function looksLikeMissingCliCommand ( text ) {
217- const flat = String ( text ?? '' )
218- . split ( '\n' )
219- . map ( ( l ) => l . replace ( / ^ \s * › \s * / , '' ) )
220- . join ( ' ' )
221- . replace ( / \s + / g, ' ' )
222- . trim ( ) ;
223- return flat . match ( / E r r o r : \s * c o m m a n d \b .* ?\b n o t f o u n d \b / ) ?. [ 0 ] ?? '' ;
224- }
179+ // `oclifCommandFileFor` and `looksLikeMissingCliCommand` — the two classifiers the
180+ // prerequisite is built from — now live in `./cli-build-prerequisite.mjs`, shared
181+ // with `check-i18n-coverage.mjs` (#5862). The self-test below still drives them
182+ // directly, so this gate's corpus keeps proving them from here.
225183
226184/** stderr lines that are neither the lint signature nor blank — pass them through. */
227185function passthroughStderrLines ( text ) {
@@ -404,7 +362,7 @@ function reportPrerequisiteNotMet(headline, detail) {
404362 console . error (
405363 `\ncheck-i18n-bundles: PREREQUISITE NOT MET — ${ headline } \n\n` +
406364 detail . map ( ( l ) => ( l ? ` ${ l } ` : '' ) ) . join ( '\n' ) +
407- `\n\n Fix: pnpm exec turbo run build --filter=@objectstack/cli \n\n` +
365+ `\n\n Fix: ${ CLI_BUILD_FIX } \n\n` +
408366 ` Nothing was checked: no bundle was compared and no config was parsed, so this\n` +
409367 ` result says NOTHING about whether the committed translation bundles are in sync.\n` +
410368 ` (Exit code 1 — but piping this gate reports the PIPE's status, so\n` +
@@ -429,14 +387,7 @@ function reportPrerequisiteNotMet(headline, detail) {
429387 * is only the cheap early answer.
430388 */
431389function checkCliBuildPrerequisite ( ) {
432- let pkgJson ;
433- try {
434- pkgJson = JSON . parse ( readFileSync ( join ( CLI_PKG , 'package.json' ) , 'utf8' ) ) ;
435- } catch ( e ) {
436- console . error ( `check-i18n-bundles: could not read ${ CLI_PKG } /package.json (${ e . message } ) — build prerequisite not pre-checked` ) ;
437- return ;
438- }
439- const resolved = oclifCommandFileFor ( pkgJson , EXTRACT_COMMAND_ID ) ;
390+ const resolved = resolveCliCommandFile ( EXTRACT_COMMAND_ID ) ;
440391 if ( resolved . unknown ) {
441392 console . error ( `check-i18n-bundles: ${ resolved . unknown } — build prerequisite not pre-checked` ) ;
442393 return ;
0 commit comments