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