@@ -38,9 +38,11 @@ import type { VariantAnalysisResultsManager } from "./variant-analysis-results-m
3838import {
3939 getAutofixPath ,
4040 getAutofixModel ,
41+ getAutofixCapiDevKey ,
4142 downloadTimeout ,
4243 AUTOFIX_PATH ,
4344 AUTOFIX_MODEL ,
45+ AUTOFIX_CAPI_DEV_KEY ,
4446} from "../config" ;
4547import { asError , getErrorMessage } from "../common/helpers-pure" ;
4648import { createTimeoutSignal } from "../common/fetch-stream" ;
@@ -155,6 +157,39 @@ async function findLocalAutofix(): Promise<string> {
155157 return localAutofixPath ;
156158}
157159
160+ /**
161+ * Finds and resolves the Copilot API dev key from the `codeQL.autofix.capiDevKey` setting.
162+ * The key can be specified as an environment variable reference (e.g., `env:MY_ENV_VAR`)
163+ * or a 1Password secret reference (e.g., `op://vault/item/field`). By default, it uses
164+ * the environment variable `CAPI_DEV_KEY`.
165+ *
166+ * @returns The resolved Copilot API dev key.
167+ * @throws Error if the Copilot API dev key is not found or invalid.
168+ */
169+ async function findCapiDevKey ( ) : Promise < string > {
170+ let capiDevKey = getAutofixCapiDevKey ( ) || "env:CAPI_DEV_KEY" ;
171+
172+ if ( ! capiDevKey . startsWith ( "env:" ) && ! capiDevKey . startsWith ( "op://" ) ) {
173+ // Don't allow literal keys in config.json for security reasons
174+ throw new Error (
175+ `Invalid CAPI dev key format. Use 'env:<ENV_VAR_NAME>' or 'op://<1PASSWORD_SECRET_REFERENCE>'.` ,
176+ ) ;
177+ }
178+ if ( capiDevKey . startsWith ( "env:" ) ) {
179+ const envVarName = capiDevKey . substring ( "env:" . length ) ;
180+ capiDevKey = process . env [ envVarName ] || "" ;
181+ }
182+ if ( capiDevKey . startsWith ( "op://" ) ) {
183+ capiDevKey = await opRead ( capiDevKey ) ;
184+ }
185+ if ( ! capiDevKey ) {
186+ throw new Error (
187+ `Copilot API dev key not found. Make sure ${ AUTOFIX_CAPI_DEV_KEY . qualifiedName } is set correctly.` ,
188+ ) ;
189+ }
190+ return capiDevKey ;
191+ }
192+
158193/**
159194 * Overrides the query help from a given variant analysis
160195 * at a location within the `localAutofixPath` directory .
@@ -210,19 +245,37 @@ async function overrideQueryHelp(
210245 // Use `replaceAll` since some query IDs have multiple slashes.
211246 const queryIdWithDash = queryId . replaceAll ( "/" , "-" ) ;
212247
213- // Get the path to the output directory for overriding the query help.
214- // Note: the path to this directory may change in the future.
215- const queryHelpOverrideDirectory = join (
248+ // Get the path to the output file for overriding the query help.
249+ // Note: the path to this file may change in the future.
250+ const queryHelpOverrideFile = join (
216251 localAutofixPath ,
217- "prompt-templates" ,
252+ "pkg" ,
253+ "autofix" ,
254+ "prompt" ,
218255 "qhelps" ,
219256 `${ queryIdWithDash } .md` ,
220257 ) ;
221258
222- await cliServer . generateQueryHelp (
223- queryHelpFilePath ,
224- queryHelpOverrideDirectory ,
225- ) ;
259+ // If the file already exists, slurp it so that we can check if it has changed.
260+ let existingContents : string | null = null ;
261+ if ( await pathExists ( queryHelpOverrideFile ) ) {
262+ existingContents = await readFile ( queryHelpOverrideFile , "utf8" ) ;
263+ }
264+
265+ // Generate the query help and output it to the override directory.
266+ await cliServer . generateQueryHelp ( queryHelpFilePath , queryHelpOverrideFile ) ;
267+
268+ // If the contents of `queryHelpOverrideFile` have changed, recompile autofix
269+ // to include the new query help.
270+ if ( existingContents !== null ) {
271+ const newContents = await readFile ( queryHelpOverrideFile , "utf8" ) ;
272+ if ( existingContents !== newContents ) {
273+ void extLogger . log (
274+ `Query help for query ID ${ queryId } has changed. Recompiling autofix...` ,
275+ ) ;
276+ await recompileAutofix ( localAutofixPath ) ;
277+ }
278+ }
226279}
227280
228281/**
@@ -607,9 +660,9 @@ async function runAutofixForRepository(
607660 } = await getRepoStoragePaths ( autofixOutputStoragePath , nwo ) ;
608661
609662 // Get autofix binary.
610- // Switch to Go binary in the future and have user pass full path
663+ // In the future, have user pass full path
611664 // in an environment variable instead of hardcoding part here.
612- const cocofixBin = join ( process . cwd ( ) , localAutofixPath , "bin" , "cocofix.js " ) ;
665+ const autofixBin = join ( process . cwd ( ) , localAutofixPath , "bin" , "autofix " ) ;
613666
614667 // Limit number of fixes generated.
615668 const limitFixesBoolean : boolean = resultCount > MAX_NUM_FIXES ;
@@ -642,7 +695,7 @@ async function runAutofixForRepository(
642695 transcriptFiles . push ( tempTranscriptFilePath ) ;
643696
644697 await runAutofixOnResults (
645- cocofixBin ,
698+ autofixBin ,
646699 sarifFile ,
647700 srcRootPath ,
648701 tempOutputTextFilePath ,
@@ -661,7 +714,7 @@ async function runAutofixForRepository(
661714 } else {
662715 // Run autofix once for all alerts.
663716 await runAutofixOnResults (
664- cocofixBin ,
717+ autofixBin ,
665718 sarifFile ,
666719 srcRootPath ,
667720 outputTextFilePath ,
@@ -707,7 +760,7 @@ async function getRepoStoragePaths(
707760 * Runs autofix on the results in the given SARIF file.
708761 */
709762async function runAutofixOnResults (
710- cocofixBin : string ,
763+ autofixBin : string ,
711764 sarifFile : string ,
712765 srcRootPath : string ,
713766 outputTextFilePath : string ,
@@ -736,7 +789,7 @@ async function runAutofixOnResults(
736789 "--format" ,
737790 "text" ,
738791 "--diff-style" ,
739- "diff " , // could do "text" instead if want line of "=" between fixes
792+ "git " , // auto|color|plain|git|unified
740793 "--output" ,
741794 outputTextFilePath ,
742795 "--fix-description" ,
@@ -751,12 +804,12 @@ async function runAutofixOnResults(
751804 }
752805
753806 await execAutofix (
754- cocofixBin ,
807+ autofixBin ,
755808 autofixArgs ,
756809 {
757810 cwd : workDir ,
758811 env : {
759- CAPI_DEV_KEY : process . env . CAPI_DEV_KEY ,
812+ CAPI_DEV_KEY : await findCapiDevKey ( ) ,
760813 PATH : process . env . PATH ,
761814 } ,
762815 } ,
@@ -765,14 +818,14 @@ async function runAutofixOnResults(
765818}
766819
767820/**
768- * Executes the autofix command .
821+ * Spawns an external process and collects its output .
769822 */
770- function execAutofix (
823+ function execCommand (
771824 bin : string ,
772825 args : string [ ] ,
773826 options : Parameters < typeof execFileSync > [ 2 ] ,
774827 showCommand ?: boolean ,
775- ) : Promise < void > {
828+ ) : Promise < { code : number | null ; stdout : string ; stderr : string } > {
776829 return new Promise ( ( resolve , reject ) => {
777830 try {
778831 const cwd = options ?. cwd || process . cwd ( ) ;
@@ -805,27 +858,87 @@ function execAutofix(
805858
806859 // Listen for process exit
807860 p . on ( "exit" , ( code ) => {
808- // Log collected output
809- if ( stdoutBuffer . trim ( ) ) {
810- void extLogger . log ( `Autofix stdout:\n${ stdoutBuffer . trim ( ) } ` ) ;
811- }
812-
813- if ( stderrBuffer . trim ( ) ) {
814- void extLogger . log ( `Autofix stderr:\n${ stderrBuffer . trim ( ) } ` ) ;
815- }
816-
817- if ( code === 0 ) {
818- resolve ( ) ;
819- } else {
820- reject ( new Error ( `Autofix process exited with code ${ code } .` ) ) ;
821- }
861+ resolve ( {
862+ code,
863+ stdout : stdoutBuffer . trim ( ) ,
864+ stderr : stderrBuffer . trim ( ) ,
865+ } ) ;
822866 } ) ;
823867 } catch ( e ) {
824868 reject ( asError ( e ) ) ;
825869 }
826870 } ) ;
827871}
828872
873+ /**
874+ * Executes the autofix command.
875+ */
876+ async function execAutofix (
877+ bin : string ,
878+ args : string [ ] ,
879+ options : Parameters < typeof execFileSync > [ 2 ] ,
880+ showCommand ?: boolean ,
881+ ) : Promise < void > {
882+ const { code, stdout, stderr } = await execCommand (
883+ bin ,
884+ args ,
885+ options ,
886+ showCommand ,
887+ ) ;
888+
889+ if ( code !== 0 ) throw new Error ( `Autofix process exited with code ${ code } .` ) ;
890+
891+ // Log collected output
892+ if ( stdout ) {
893+ void extLogger . log ( `Autofix stdout:\n${ stdout } ` ) ;
894+ }
895+ if ( stderr ) {
896+ void extLogger . log ( `Autofix stderr:\n${ stderr } ` ) ;
897+ }
898+ }
899+
900+ /** Execute the 1Password CLI command `op read <secretReference>`, if the `op` command exists on the PATH. */
901+ async function opRead ( secretReference : string ) : Promise < string > {
902+ try {
903+ const { code, stdout, stderr } = await execCommand (
904+ "op" ,
905+ [ "read" , secretReference ] ,
906+ { } ,
907+ false ,
908+ ) ;
909+
910+ if ( code === 0 ) {
911+ return stdout ;
912+ } else {
913+ throw new Error (
914+ `1Password CLI exited with code ${ code } . Stderr: ${ stderr } ` ,
915+ ) ;
916+ }
917+ } catch ( e ) {
918+ const error = asError ( e ) ;
919+ if ( "code" in error && error . code === "ENOENT" ) {
920+ throw new Error ( "1Password CLI (op) not found in PATH" ) ;
921+ }
922+ throw e ;
923+ }
924+ }
925+
926+ /** Recompile the Autofix binary. */
927+ async function recompileAutofix ( localAutofixPath : string ) : Promise < void > {
928+ const { code, stderr } = await execCommand (
929+ "make" ,
930+ [ "build" ] ,
931+ { cwd : localAutofixPath } ,
932+ false ,
933+ ) ;
934+
935+ if ( code !== 0 ) {
936+ throw new Error (
937+ `Failed to recompile autofix after query help change. Exit code: ${ code } . Stderr: ${ stderr } ` ,
938+ ) ;
939+ }
940+ }
941+
829942/**
830943 * Creates a new file path by appending the given suffix.
831944 * @param filePath The original file path.
@@ -902,9 +1015,11 @@ async function formatWithMarkdown(
9021015 const backFormatting : string =
9031016 "```\n\n</details>\n\n ### Notes\n - notes placeholder\n\n" ;
9041017
905- // Format the content with Markdown
906- // Replace ``` in the content with \``` to avoid breaking the Markdown code block
907- const formattedContent = `## ${ header } \n\n${ frontFormatting } ${ content . replaceAll ( "```" , "\\```" ) } ${ backFormatting } ` ;
1018+ // Format the content with Markdown:
1019+ // - Replace ``` in the content with \``` to avoid breaking the Markdown code block
1020+ // - Remove raw terminal escape sequences if any (workaround until `--diff-style plain` is handled by autofix)
1021+ // eslint-disable-next-line no-control-regex
1022+ const formattedContent = `## ${ header } \n\n${ frontFormatting } ${ content . replaceAll ( "```" , "\\```" ) . replaceAll ( / \x1b \[ [ 0 - 9 ; ] * m / g, "" ) } ${ backFormatting } ` ;
9081023
9091024 // Write the formatted content back to the file
9101025 await writeFile ( inputFile , formattedContent ) ;
0 commit comments