@@ -160,23 +160,46 @@ const extractors: Record<string, Extractor> = {
160160 ( m ) : Dep => ( { type : 'cargo' , name : m [ 1 ] } )
161161 ) ,
162162 'Cargo.toml' : ( content : string ) : Dep [ ] => {
163- // Rust: only extract from [dependencies], [dev-dependencies], [build-dependencies] sections.
164- // Skip [package], [lib], [bin], [workspace], [profile] metadata sections.
163+ // Rust: extract crate names from dep lines.
164+ //
165+ // Two-mode strategy because the hook receives either a full
166+ // Cargo.toml (Write) or a fragment (Edit's new_string, often just
167+ // the added line with no section header):
168+ //
169+ // Full file — scan only [dependencies] / [dev-dependencies] /
170+ // [build-dependencies] (incl. target-specific
171+ // [target.*.dependencies] via the `.<name>` suffix)
172+ // and skip [package], [features], [profile], etc.
173+ // Fragment — no section headers at all → treat the whole
174+ // content as an implicit [dependencies] body and
175+ // match any `name = "..."` or `name = { version = "..." }`.
176+ //
177+ // The lineRe requires the value to look like a version spec
178+ // (string or table with a `version` key), so `[features]`-style
179+ // `key = ["derive"]` array values don't match even in fragment mode.
165180 const deps : Dep [ ] = [ ]
166- const depSectionRe = / ^ \[ (?: (?: d e v - | b u i l d - ) ? d e p e n d e n c i e s (?: \. [ ^ \] ] + ) ? ) \] \s * $ / gm
181+ const depSectionRe = / ^ \[ (?: (?: d e v - | b u i l d - ) ? d e p e n d e n c i e s (?: \. [ ^ \] ] + ) ? | t a r g e t \. [ ^ \] ] + \. (?: d e v - | b u i l d - ) ? d e p e n d e n c i e s (?: \. [ ^ \] ] + ) ? ) \] \s * $ / gm
167182 const anySectionRe = / ^ \[ / gm
183+ const lineRe = / ^ ( \w [ \w - ] * ) \s * = \s * (?: \{ [ ^ } ] * v e r s i o n \s * = \s * " [ ^ " ] * " | \s * " [ ^ " ] * " ) / gm
184+ const push = ( section : string ) => {
185+ let m
186+ while ( ( m = lineRe . exec ( section ) ) !== null ) {
187+ deps . push ( { type : 'cargo' , name : m [ 1 ] } )
188+ }
189+ lineRe . lastIndex = 0
190+ }
191+ const hasAnySection = / ^ \[ / m. test ( content )
192+ if ( ! hasAnySection ) {
193+ push ( content )
194+ return deps
195+ }
168196 let sectionMatch
169197 while ( ( sectionMatch = depSectionRe . exec ( content ) ) !== null ) {
170198 const sectionStart = sectionMatch . index + sectionMatch [ 0 ] . length
171199 anySectionRe . lastIndex = sectionStart
172200 const nextSection = anySectionRe . exec ( content )
173201 const sectionEnd = nextSection ? nextSection . index : content . length
174- const sectionText = content . slice ( sectionStart , sectionEnd )
175- const lineRe = / ^ ( \w [ \w - ] * ) \s * = \s * (?: \{ [ ^ } ] * v e r s i o n \s * = \s * " [ ^ " ] * " | \s * " [ ^ " ] * " ) / gm
176- let m
177- while ( ( m = lineRe . exec ( sectionText ) ) !== null ) {
178- deps . push ( { type : 'cargo' , name : m [ 1 ] } )
179- }
202+ push ( content . slice ( sectionStart , sectionEnd ) )
180203 }
181204 return deps
182205 } ,
@@ -281,21 +304,6 @@ const extractors: Record<string, Extractor> = {
281304 'yarn.lock' : extractNpmLockfile ,
282305}
283306
284- // --- main (only when executed directly, not imported) ---
285-
286- if ( fileURLToPath ( import . meta. url ) === path . resolve ( process . argv [ 1 ] ) ) {
287- // Read the full JSON blob from stdin (piped by Claude Code).
288- let input = ''
289- for await ( const chunk of process . stdin ) input += chunk
290- const hook : HookInput = JSON . parse ( input )
291-
292- if ( hook . tool_name !== 'Edit' && hook . tool_name !== 'Write' ) {
293- process . exitCode = 0
294- } else {
295- process . exitCode = await check ( hook )
296- }
297- }
298-
299307// --- core ---
300308
301309// Orchestrates the full check: extract deps, diff against old, query API.
@@ -729,3 +737,26 @@ export {
729737 extractTerraform ,
730738 findExtractor ,
731739}
740+
741+ // --- main (only when executed directly, not imported) ---
742+ //
743+ // Kept at the bottom because the module uses top-level await
744+ // (`for await (const chunk of process.stdin)`) to read the hook payload.
745+ // Top-level await suspends module evaluation at the suspension point, so
746+ // any `const` declared AFTER the suspending block is still in the TDZ
747+ // when the awaited work calls back into the module (e.g. extractNpm →
748+ // PACKAGE_JSON_METADATA_KEYS). Placing main last guarantees every
749+ // module-level declaration is initialized before main runs.
750+
751+ if ( fileURLToPath ( import . meta. url ) === path . resolve ( process . argv [ 1 ] ) ) {
752+ // Read the full JSON blob from stdin (piped by Claude Code).
753+ let input = ''
754+ for await ( const chunk of process . stdin ) input += chunk
755+ const hook : HookInput = JSON . parse ( input )
756+
757+ if ( hook . tool_name !== 'Edit' && hook . tool_name !== 'Write' ) {
758+ process . exitCode = 0
759+ } else {
760+ process . exitCode = await check ( hook )
761+ }
762+ }
0 commit comments