diff --git a/.changeset/ninety-countries-jam.md b/.changeset/ninety-countries-jam.md new file mode 100644 index 00000000..becd870a --- /dev/null +++ b/.changeset/ninety-countries-jam.md @@ -0,0 +1,6 @@ +--- +"@tryabby/core": patch +"@tryabby/cli": patch +--- + +add feature flag removal command diff --git a/packages/cli/package.json b/packages/cli/package.json index 7962a98d..bab3cf84 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -27,6 +27,7 @@ "dotenv": "^16.0.3", "esbuild": "0.18.17", "figlet": "^1.6.0", + "globby": "^14.0.2", "magicast": "^0.3.2", "msw": "^1.2.2", "node-fetch": "^3.3.1", diff --git a/packages/cli/src/ai.ts b/packages/cli/src/ai.ts new file mode 100644 index 00000000..db0a9e70 --- /dev/null +++ b/packages/cli/src/ai.ts @@ -0,0 +1,58 @@ +import { loadLocalConfig } from "./util"; +import { globby } from "globby"; +import { getUseFeatureFlagRegex } from "@tryabby/core"; +import { readFile } from "fs/promises"; +import chalk from "chalk"; +import { HttpService } from "./http"; + +export async function removeFlagInstance(options: { + flagName: string; + apiKey: string; + path: string; + host?: string; + configPath?: string; +}) { + const files = await globby("**/*.tsx", { + cwd: options.path ?? process.cwd(), + absolute: true, + gitignore: true, + onlyFiles: true, + }); + + const regex = getUseFeatureFlagRegex(options.flagName); + + const filesToUse = ( + await Promise.all( + files.flatMap(async (filePath) => { + const content = await readFile(filePath, "utf-8").then((content) => { + const matches = content.match(regex); + return matches ? content : null; + }); + if (!content) return []; + + return { + filePath, + fileContent: content, + }; + }) + ) + ).flat(); + + await HttpService.getFilesWithFlagsRemoved({ + apiKey: options.apiKey, + files: filesToUse, + flagName: options.flagName, + apiUrl: options.host, + }); + + try { + const { mutableConfig, saveMutableConfig } = await loadLocalConfig( + options.configPath + ); + mutableConfig.flags = mutableConfig.flags.filter((flag: string) => flag !== options.flagName); + await saveMutableConfig(); + } catch (e) { + // fail silently + } + console.log(chalk.green("Flag removed successfully")); +} diff --git a/packages/cli/src/http.ts b/packages/cli/src/http.ts index 2fe384b6..941073a7 100644 --- a/packages/cli/src/http.ts +++ b/packages/cli/src/http.ts @@ -3,6 +3,7 @@ import { ABBY_BASE_URL } from "./consts"; import fetch from "node-fetch"; import { multiLineLog } from "./util"; import chalk from "chalk"; +import { writeFile } from "fs/promises"; export abstract class HttpService { static async getConfigFromServer({ @@ -69,4 +70,54 @@ export abstract class HttpService { throw e; } } + + static async getFilesWithFlagsRemoved({ + apiKey, + files, + flagName, + apiUrl, + }: { + apiKey: string; + files: Array<{ filePath: string; fileContent: string }>; + flagName: string; + apiUrl?: string; + }) { + const url = apiUrl ?? ABBY_BASE_URL; + + try { + const response = await fetch(`${url}/api/ee/v1/abby-ai/flag-removal`, { + method: "POST", + headers: { + Authorization: "Bearer " + apiKey, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + flagName, + files, + }), + }); + + const status = response.status; + + if (status === 200) { + const res = await response.json(); + if (!Array.isArray(res)) { + throw new Error("Invalid response from server"); + } + console.log({ res, files }); + await Promise.all(res.map((file) => writeFile(file.filePath, file.fileContent))); + console.log(chalk.green("All files have been updated successfully")); + } else if (status === 500) { + throw new Error("Internal server error trying to update files"); + } else if (status === 401) { + throw new Error("Invalid API Key"); + } else { + console.log(response); + throw new Error("Push failed"); + } + } catch (e) { + console.log(chalk.red(multiLineLog("Error: " + e))); + throw e; + } + } } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 4f7928d8..4fd3cee3 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -13,6 +13,7 @@ import { initAbbyConfig } from "./init"; import { addCommandTypeSchema } from "./schemas"; import { addFlag } from "./add-flag"; import { addRemoteConfig } from "./add-remote-config"; +import { removeFlagInstance } from "./ai"; const program = new Command(); @@ -184,4 +185,24 @@ program } }); +const aiCommand = program.command("ai").description("Abby AI helpers"); + +aiCommand + .command("remove") + .description("remove a flag from your code") + .argument("", "The directory to scan for") + .argument("", "The flag name to remove") + .addOption(ConfigOption) + .addOption(HostOption) + .action(async (dir: string, flagName: string, options: { config?: string; host?: string }) => { + const files = await removeFlagInstance({ + apiKey: await getToken(), + flagName, + path: dir, + configPath: options.config, + host: options.host, + }); + console.log(files); + }); + program.parse(process.argv); diff --git a/packages/core/src/shared/helpers.ts b/packages/core/src/shared/helpers.ts index 78cfd598..94ec5fea 100644 --- a/packages/core/src/shared/helpers.ts +++ b/packages/core/src/shared/helpers.ts @@ -67,3 +67,6 @@ export function stringifyRemoteConfigValue(value: RemoteConfigValue) { assertUnreachable(value); } } + +export const getUseFeatureFlagRegex = (flagName: string) => + new RegExp(`useFeatureFlag\\s*\\(\\s*['"\`]${flagName}['"\`]\\s*\\)`); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f534b6a7..6773e962 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -691,6 +691,9 @@ importers: figlet: specifier: ^1.6.0 version: 1.6.0 + globby: + specifier: ^14.0.2 + version: 14.0.2 magicast: specifier: ^0.3.2 version: 0.3.2 @@ -8504,7 +8507,7 @@ packages: engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} dependencies: cross-spawn: 7.0.3 - fast-glob: 3.2.12 + fast-glob: 3.3.2 is-glob: 4.0.3 open: 9.1.0 picocolors: 1.0.0 @@ -10234,6 +10237,11 @@ packages: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true + /@sindresorhus/merge-streams@2.3.0: + resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + engines: {node: '>=18'} + dev: false + /@socket.io/component-emitter@3.1.0: resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} @@ -12431,7 +12439,7 @@ packages: '@typescript-eslint/scope-manager': 5.59.9 '@typescript-eslint/types': 5.59.9 '@typescript-eslint/typescript-estree': 5.59.9(typescript@4.9.5) - debug: 4.3.5 + debug: 4.3.4 eslint: 7.32.0 typescript: 4.9.5 transitivePeerDependencies: @@ -12451,7 +12459,7 @@ packages: '@typescript-eslint/scope-manager': 5.59.9 '@typescript-eslint/types': 5.59.9 '@typescript-eslint/typescript-estree': 5.59.9(typescript@4.9.3) - debug: 4.3.5 + debug: 4.3.4 eslint: 8.29.0 typescript: 4.9.3 transitivePeerDependencies: @@ -17519,6 +17527,16 @@ packages: merge2: 1.4.1 micromatch: 4.0.5 + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -18168,7 +18186,7 @@ packages: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.2.12 + fast-glob: 3.3.2 ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 @@ -18183,6 +18201,18 @@ packages: merge2: 1.4.1 slash: 4.0.0 + /globby@14.0.2: + resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} + engines: {node: '>=18'} + dependencies: + '@sindresorhus/merge-streams': 2.3.0 + fast-glob: 3.3.2 + ignore: 5.2.4 + path-type: 5.0.0 + slash: 5.1.0 + unicorn-magic: 0.1.0 + dev: false + /globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} dev: true @@ -23429,6 +23459,11 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + /path-type@5.0.0: + resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} + engines: {node: '>=12'} + dev: false + /pathe@1.1.1: resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} @@ -25598,6 +25633,11 @@ packages: resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} engines: {node: '>=12'} + /slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + dev: false + /slice-ansi@4.0.0: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} @@ -27308,6 +27348,11 @@ packages: resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} engines: {node: '>=4'} + /unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + dev: false + /unified@10.1.2: resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} dependencies: @@ -28135,8 +28180,8 @@ packages: strip-literal: 2.1.0 tinybench: 2.8.0 tinypool: 0.8.4 - vite: 5.3.1(@types/node@20.3.1) - vite-node: 1.5.0(@types/node@20.3.1) + vite: 5.3.1(@types/node@18.16.17) + vite-node: 1.5.0(@types/node@18.16.17) why-is-node-running: 2.2.2 transitivePeerDependencies: - less