diff --git a/packages/cli/package.json b/packages/cli/package.json index a03ebf2e24..f120829118 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -113,6 +113,7 @@ "minimatch": "^5.1.2", "moo": "^0.5.1", "open": "^8.2.1", + "openai": "^4.11.0", "openapi-diff": "^0.23.6", "ora": "^5.4.1", "parse-diff": "^0.11.1", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index b6e2697bcf..fcb63b1fe7 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -35,6 +35,7 @@ const ArchiveCommand = require('./cmds/archive/archive'); const RestoreCommand = require('./cmds/archive/restore'); const CompareCommand = require('./cmds/compare/compare'); const CompareReportCommand = require('./cmds/compare-report/compareReport'); +const InstallGitHubAction = require('./cmds/install-github-action/install-github-action'); import UploadCommand from './cmds/upload'; import { default as sqlErrorLog } from './lib/sqlErrorLog'; @@ -233,6 +234,7 @@ yargs(process.argv.slice(2)) .command(RestoreCommand) .command(CompareCommand) .command(CompareReportCommand) + .command(InstallGitHubAction) .strict() .demandCommand() .help().argv; diff --git a/packages/cli/src/cmds/agentInstaller/installerUI.ts b/packages/cli/src/cmds/agentInstaller/installerUI.ts index 0abd708122..3e8cc72193 100644 --- a/packages/cli/src/cmds/agentInstaller/installerUI.ts +++ b/packages/cli/src/cmds/agentInstaller/installerUI.ts @@ -1,6 +1,5 @@ import UI from '../userInteraction'; import chalk from 'chalk'; -import { Answers } from 'inquirer'; export enum OverwriteOption { USE_EXISTING = 'Use existing', diff --git a/packages/cli/src/cmds/install-github-action/install-github-action.ts b/packages/cli/src/cmds/install-github-action/install-github-action.ts new file mode 100644 index 0000000000..24e82f2a0d --- /dev/null +++ b/packages/cli/src/cmds/install-github-action/install-github-action.ts @@ -0,0 +1,127 @@ +import yargs from 'yargs'; +import { existsSync } from 'fs'; +import { warn } from 'console'; +import { OpenAI } from 'openai'; +import assert from 'assert'; +import { readFile } from 'fs/promises'; +import { inspect } from 'util'; +import * as diff from 'diff'; + +import { handleWorkingDirectory } from '../../lib/handleWorkingDirectory'; +import { findFiles, verbose } from '../../utils'; +import UI from '../userInteraction'; + +export const command = 'install-github-action'; +export const describe = 'Install the AppMap GitHub action to a project'; + +export default async function buildOpenAIApi(): Promise { + let gptKey = process.env.OPENAI_API_KEY; + if (!gptKey) { + const gptKey = UI.prompt({ + message: 'Enter your OpenAI API key', + mask: '*', + }); + if (!gptKey) return; + } + + return new OpenAI({ apiKey: gptKey }); +} + +export const builder = (args: yargs.Argv) => { + args.option('directory', { + describe: 'program working directory', + type: 'string', + alias: 'd', + }); + + return args.strict(); +}; + +async function installGitHubAction() { + if (!existsSync('.github')) { + warn(`.github directory does not exist`); + return; + } + + const workflowFiles = await findFiles('.github', '.yml'); + if (workflowFiles.length === 0) { + warn(`No workflows found in .github directory`); + return; + } + + let workflowFile: string; + { + const answer = await UI.prompt({ + type: 'list', + name: 'action', + message: 'Choose a workflow file:', + choices: workflowFiles, + }); + if (!answer) return; + + workflowFile = answer.action; + } + const workflowStr = await readFile(workflowFile, 'utf-8'); + + UI.status = `Suggesting AppMap installation to GitHub Workflow ${workflowFile}`; + + const ai = await buildOpenAIApi(); + if (!ai) return; + + const snippet = ` + - name: Prepare bundle for AppMap installation + run: bundle config unset deployment + - name: Install AppMap + id: install-appmap + uses: getappmap/install-action@v1 + with: + build-tool: bundler +` + + const result = await ai.chat.completions.create({ + messages: [ + { + role: 'system', + content: 'Respond with a complete copy of the workflow' + }, + { + role: 'system', + content: 'Include two line breaks before and after the injected snippet' + }, + { + role: 'user', + content: + 'I want to add a step to the following GitHub Action workflow that runs after the language tools are installed, but before the tests are run', + }, + { + role: 'user', + content: `Inject my workflow snippet which is: SNIPPET\n${snippet}END SNIPPET` + } + { + role: 'user', + content: workflowStr, + }, + ], + model: 'gpt-3.5-turbo', + }); + + const response = result.choices + .map((choice) => (assert(choice.message), choice.message.content)) + .filter(Boolean) + .join('\n'); + + UI.success(); + + const patch = diff.createTwoFilesPatch(workflowFile, workflowFile, workflowStr, response); + + warn(patch); +} + +export const handler = async (argv: any) => { + verbose(argv.verbose); + + const { directory } = argv; + handleWorkingDirectory(directory); + + await installGitHubAction(); +}; diff --git a/yarn.lock b/yarn.lock index 663e545498..5449b63512 100644 --- a/yarn.lock +++ b/yarn.lock @@ -183,6 +183,7 @@ __metadata: moo: ^0.5.1 node-fetch: 2.6.7 open: ^8.2.1 + openai: ^4.11.0 openapi-diff: ^0.23.6 openapi-types: ^12.1.0 ora: ^5.4.1 @@ -8360,6 +8361,16 @@ __metadata: languageName: node linkType: hard +"@types/node-fetch@npm:^2.6.4": + version: 2.6.6 + resolution: "@types/node-fetch@npm:2.6.6" + dependencies: + "@types/node": "*" + form-data: ^4.0.0 + checksum: ac66389d9d597ab91f5e5d3724e594965b9f80ae5841ab5da9f0c3bd54ceac084591cfe69b1c413f18bb7efdd97002d05bd7651d58ba0c6c10f804f4fd85e598 + languageName: node + linkType: hard + "@types/node@npm:*, @types/node@npm:^17.0.2": version: 17.0.17 resolution: "@types/node@npm:17.0.17" @@ -8395,6 +8406,13 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^18.11.18": + version: 18.18.1 + resolution: "@types/node@npm:18.18.1" + checksum: 079085afc8615b91727900628410a2077f7fff51c5c3e20c4ab2d9caae4010b035aac13dae1221155e9d6ca46084aebf68121642140527b4009eee17b716d339 + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.0": version: 2.4.1 resolution: "@types/normalize-package-data@npm:2.4.1" @@ -10238,6 +10256,15 @@ __metadata: languageName: node linkType: hard +"abort-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "abort-controller@npm:3.0.0" + dependencies: + event-target-shim: ^5.0.0 + checksum: 170bdba9b47b7e65906a28c8ce4f38a7a369d78e2271706f020849c1bfe0ee2067d4261df8bbb66eb84f79208fd5b710df759d64191db58cfba7ce8ef9c54b75 + languageName: node + linkType: hard + "accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.7": version: 1.3.8 resolution: "accepts@npm:1.3.8" @@ -11951,6 +11978,13 @@ __metadata: languageName: node linkType: hard +"base-64@npm:^0.1.0": + version: 0.1.0 + resolution: "base-64@npm:0.1.0" + checksum: 5a42938f82372ab5392cbacc85a5a78115cbbd9dbef9f7540fa47d78763a3a8bd7d598475f0d92341f66285afd377509851a9bb5c67bbecb89686e9255d5b3eb + languageName: node + linkType: hard + "base64-js@npm:^1.0.2, base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -13013,6 +13047,13 @@ __metadata: languageName: node linkType: hard +"charenc@npm:0.0.2": + version: 0.0.2 + resolution: "charenc@npm:0.0.2" + checksum: 81dcadbe57e861d527faf6dd3855dc857395a1c4d6781f4847288ab23cffb7b3ee80d57c15bba7252ffe3e5e8019db767757ee7975663ad2ca0939bb8fcaf2e5 + languageName: node + linkType: hard + "check-more-types@npm:2.24.0, check-more-types@npm:^2.24.0": version: 2.24.0 resolution: "check-more-types@npm:2.24.0" @@ -14264,6 +14305,13 @@ __metadata: languageName: node linkType: hard +"crypt@npm:0.0.2": + version: 0.0.2 + resolution: "crypt@npm:0.0.2" + checksum: baf4c7bbe05df656ec230018af8cf7dbe8c14b36b98726939cef008d473f6fe7a4fad906cfea4062c93af516f1550a3f43ceb4d6615329612c6511378ed9fe34 + languageName: node + linkType: hard + "crypto-browserify@npm:^3.11.0": version: 3.12.0 resolution: "crypto-browserify@npm:3.12.0" @@ -15654,6 +15702,16 @@ __metadata: languageName: node linkType: hard +"digest-fetch@npm:^1.3.0": + version: 1.3.0 + resolution: "digest-fetch@npm:1.3.0" + dependencies: + base-64: ^0.1.0 + md5: ^2.3.0 + checksum: 8ebdb4b9ef02b1ac0da532d25c7d08388f2552813dfadabfe7c4630e944bb4a48093b997fc926440a10e1ccf4912f2ce9adcf2d6687b0518dab8480e08f22f9d + languageName: node + linkType: hard + "dir-glob@npm:^2.0.0, dir-glob@npm:^2.2.2": version: 2.2.2 resolution: "dir-glob@npm:2.2.2" @@ -17371,6 +17429,13 @@ __metadata: languageName: node linkType: hard +"event-target-shim@npm:^5.0.0": + version: 5.0.1 + resolution: "event-target-shim@npm:5.0.1" + checksum: 1ffe3bb22a6d51bdeb6bf6f7cf97d2ff4a74b017ad12284cc9e6a279e727dc30a5de6bb613e5596ff4dc3e517841339ad09a7eec44266eccb1aa201a30448166 + languageName: node + linkType: hard + "eventemitter2@npm:6.4.7": version: 6.4.7 resolution: "eventemitter2@npm:6.4.7" @@ -18249,6 +18314,13 @@ __metadata: languageName: node linkType: hard +"form-data-encoder@npm:1.7.2": + version: 1.7.2 + resolution: "form-data-encoder@npm:1.7.2" + checksum: aeebd87a1cb009e13cbb5e4e4008e6202ed5f6551eb6d9582ba8a062005178907b90f4887899d3c993de879159b6c0c940af8196725b428b4248cec5af3acf5f + languageName: node + linkType: hard + "form-data@npm:*, form-data@npm:^4.0.0": version: 4.0.0 resolution: "form-data@npm:4.0.0" @@ -18289,6 +18361,16 @@ __metadata: languageName: node linkType: hard +"formdata-node@npm:^4.3.2": + version: 4.4.1 + resolution: "formdata-node@npm:4.4.1" + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + checksum: d91d4f667cfed74827fc281594102c0dabddd03c9f8b426fc97123eedbf73f5060ee43205d89284d6854e2fc5827e030cd352ef68b93beda8decc2d72128c576 + languageName: node + linkType: hard + "forwarded@npm:0.2.0": version: 0.2.0 resolution: "forwarded@npm:0.2.0" @@ -20470,7 +20552,7 @@ __metadata: languageName: node linkType: hard -"is-buffer@npm:^1.1.5": +"is-buffer@npm:^1.1.5, is-buffer@npm:~1.1.6": version: 1.1.6 resolution: "is-buffer@npm:1.1.6" checksum: 4a186d995d8bbf9153b4bd9ff9fd04ae75068fe695d29025d25e592d9488911eeece84eefbd8fa41b8ddcc0711058a71d4c466dcf6f1f6e1d83830052d8ca707 @@ -25075,6 +25157,17 @@ __metadata: languageName: node linkType: hard +"md5@npm:^2.3.0": + version: 2.3.0 + resolution: "md5@npm:2.3.0" + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: ~1.1.6 + checksum: a63cacf4018dc9dee08c36e6f924a64ced735b37826116c905717c41cebeb41a522f7a526ba6ad578f9c80f02cb365033ccd67fe186ffbcc1a1faeb75daa9b6e + languageName: node + linkType: hard + "mdast-squeeze-paragraphs@npm:^4.0.0": version: 4.0.0 resolution: "mdast-squeeze-paragraphs@npm:4.0.0" @@ -26082,6 +26175,13 @@ __metadata: languageName: node linkType: hard +"node-domexception@npm:1.0.0": + version: 1.0.0 + resolution: "node-domexception@npm:1.0.0" + checksum: ee1d37dd2a4eb26a8a92cd6b64dfc29caec72bff5e1ed9aba80c294f57a31ba4895a60fd48347cf17dd6e766da0ae87d75657dfd1f384ebfa60462c2283f5c7f + languageName: node + linkType: hard + "node-emoji@npm:^1.11.0": version: 1.11.0 resolution: "node-emoji@npm:1.11.0" @@ -27035,6 +27135,24 @@ __metadata: languageName: node linkType: hard +"openai@npm:^4.11.0": + version: 4.11.0 + resolution: "openai@npm:4.11.0" + dependencies: + "@types/node": ^18.11.18 + "@types/node-fetch": ^2.6.4 + abort-controller: ^3.0.0 + agentkeepalive: ^4.2.1 + digest-fetch: ^1.3.0 + form-data-encoder: 1.7.2 + formdata-node: ^4.3.2 + node-fetch: ^2.6.7 + bin: + openai: bin/cli + checksum: 4a2609b29d47347f6c4ed03c35787baaa7145b34e57aecbf6b5412f4ac0479e9c46b5ce22d85a972604eef4c0933d8a09c46c2625de8fdad2bd53200888bb04a + languageName: node + linkType: hard + "openapi-diff@npm:^0.23.6": version: 0.23.6 resolution: "openapi-diff@npm:0.23.6" @@ -35167,6 +35285,13 @@ typescript@~4.4.3: languageName: node linkType: hard +"web-streams-polyfill@npm:4.0.0-beta.3": + version: 4.0.0-beta.3 + resolution: "web-streams-polyfill@npm:4.0.0-beta.3" + checksum: dfec1fbf52b9140e4183a941e380487b6c3d5d3838dd1259be81506c1c9f2abfcf5aeb670aeeecfd9dff4271a6d8fef931b193c7bedfb42542a3b05ff36c0d16 + languageName: node + linkType: hard + "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1"