@rowlabs/ev 0.4.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +394 -18
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6708,7 +6708,7 @@ async function getDecryptedProjectKey(projectId) {
|
|
|
6708
6708
|
async function resolveEnvironmentId(projectId, appName, envName) {
|
|
6709
6709
|
const client = await createApiClient();
|
|
6710
6710
|
const project = await client.getProject(projectId);
|
|
6711
|
-
let app = appName ? project.apps.find((a2) => a2.name === appName) : project.apps.find((a2) => a2.name === "default") ?? project.apps[0];
|
|
6711
|
+
let app = appName ? project.apps.find((a2) => a2.name.toLowerCase() === appName.toLowerCase()) : project.apps.find((a2) => a2.name === "default") ?? project.apps[0];
|
|
6712
6712
|
if (!app && appName) {
|
|
6713
6713
|
app = await client.createApp(projectId, appName);
|
|
6714
6714
|
}
|
|
@@ -6717,7 +6717,7 @@ async function resolveEnvironmentId(projectId, appName, envName) {
|
|
|
6717
6717
|
process.exit(1);
|
|
6718
6718
|
}
|
|
6719
6719
|
const envs = await client.listEnvironments(app.id);
|
|
6720
|
-
let env = envs.find((e) => e.name === envName);
|
|
6720
|
+
let env = envs.find((e) => e.name.toLowerCase() === envName.toLowerCase());
|
|
6721
6721
|
if (!env) {
|
|
6722
6722
|
env = await client.createEnvironment(app.id, envName);
|
|
6723
6723
|
}
|
|
@@ -6735,7 +6735,7 @@ var init_auth = __esm({
|
|
|
6735
6735
|
|
|
6736
6736
|
// src/index.ts
|
|
6737
6737
|
init_dist();
|
|
6738
|
-
import { Command as
|
|
6738
|
+
import { Command as Command22 } from "commander";
|
|
6739
6739
|
|
|
6740
6740
|
// src/commands/login.ts
|
|
6741
6741
|
init_dist();
|
|
@@ -7573,7 +7573,7 @@ envCommand.command("create <name>").description("Create a new environment").acti
|
|
|
7573
7573
|
async ({ context, spinner }) => {
|
|
7574
7574
|
const client = await createApiClient();
|
|
7575
7575
|
const project = await client.getProject(context.project);
|
|
7576
|
-
const app = context.app ? project.apps.find((a2) => a2.name === context.app) : project.apps.find((a2) => a2.name === "default") ?? project.apps[0];
|
|
7576
|
+
const app = context.app ? project.apps.find((a2) => a2.name.toLowerCase() === context.app.toLowerCase()) : project.apps.find((a2) => a2.name === "default") ?? project.apps[0];
|
|
7577
7577
|
if (!app) {
|
|
7578
7578
|
spinner.fail(chalk10.red("App not found"));
|
|
7579
7579
|
process.exit(1);
|
|
@@ -7605,13 +7605,13 @@ envCommand.command("delete <name>").description("Delete an environment").action(
|
|
|
7605
7605
|
async ({ context, spinner }) => {
|
|
7606
7606
|
const client = await createApiClient();
|
|
7607
7607
|
const project = await client.getProject(context.project);
|
|
7608
|
-
const app = context.app ? project.apps.find((a2) => a2.name === context.app) : project.apps.find((a2) => a2.name === "default") ?? project.apps[0];
|
|
7608
|
+
const app = context.app ? project.apps.find((a2) => a2.name.toLowerCase() === context.app.toLowerCase()) : project.apps.find((a2) => a2.name === "default") ?? project.apps[0];
|
|
7609
7609
|
if (!app) {
|
|
7610
7610
|
spinner.fail(chalk10.red("App not found"));
|
|
7611
7611
|
process.exit(1);
|
|
7612
7612
|
}
|
|
7613
7613
|
const envs = await client.listEnvironments(app.id);
|
|
7614
|
-
const env = envs.find((e) => e.name === name);
|
|
7614
|
+
const env = envs.find((e) => e.name.toLowerCase() === name.toLowerCase());
|
|
7615
7615
|
if (!env) {
|
|
7616
7616
|
spinner.fail(chalk10.red(`Environment "${name}" not found`));
|
|
7617
7617
|
process.exit(1);
|
|
@@ -7957,7 +7957,9 @@ async function getSecretValue(projectId, appName, envName, key, isEvBackend, bac
|
|
|
7957
7957
|
const envId = await resolveEnvironmentId(projectId, appName, envName);
|
|
7958
7958
|
const client = await createApiClient(backendConfig);
|
|
7959
7959
|
const response = await client.pullSecrets(envId);
|
|
7960
|
-
const secret = response.secrets.find(
|
|
7960
|
+
const secret = response.secrets.find(
|
|
7961
|
+
(s2) => s2.key.toLowerCase() === key.toLowerCase()
|
|
7962
|
+
);
|
|
7961
7963
|
if (!secret) return null;
|
|
7962
7964
|
if (isEvBackend) {
|
|
7963
7965
|
const projectKey = await getDecryptedProjectKey(projectId);
|
|
@@ -8640,12 +8642,16 @@ var completionsCommand = new Command16("completions").description("Generate shel
|
|
|
8640
8642
|
});
|
|
8641
8643
|
|
|
8642
8644
|
// src/commands/scan.ts
|
|
8643
|
-
init_dist();
|
|
8644
8645
|
init_api_client();
|
|
8645
8646
|
init_config();
|
|
8646
8647
|
import { Command as Command17 } from "commander";
|
|
8647
8648
|
import chalk19 from "chalk";
|
|
8648
8649
|
import ora11 from "ora";
|
|
8650
|
+
import { stat as stat2 } from "fs/promises";
|
|
8651
|
+
import { join as join9 } from "path";
|
|
8652
|
+
|
|
8653
|
+
// src/lib/scanner.ts
|
|
8654
|
+
init_dist();
|
|
8649
8655
|
import { readdir, readFile as readFile6, stat } from "fs/promises";
|
|
8650
8656
|
import { existsSync as existsSync6 } from "fs";
|
|
8651
8657
|
import { join as join8, extname } from "path";
|
|
@@ -8750,6 +8756,8 @@ async function getLocalEnvKeys(dir, configEnvFiles) {
|
|
|
8750
8756
|
}
|
|
8751
8757
|
return keys;
|
|
8752
8758
|
}
|
|
8759
|
+
|
|
8760
|
+
// src/commands/scan.ts
|
|
8753
8761
|
function printTable(label, refs, envData, repoRoot, showFiles, localKeys) {
|
|
8754
8762
|
const varNames = [...refs.keys()].sort();
|
|
8755
8763
|
if (varNames.length === 0) return 0;
|
|
@@ -8829,15 +8837,15 @@ var scanCommand = new Command17("scan").description("Scan codebase for env var r
|
|
|
8829
8837
|
let totalMissing = 0;
|
|
8830
8838
|
let totalVars = 0;
|
|
8831
8839
|
for (const [appName, appConfig] of Object.entries(config.apps)) {
|
|
8832
|
-
const appDir =
|
|
8840
|
+
const appDir = join9(repoRoot, appConfig.path);
|
|
8833
8841
|
try {
|
|
8834
|
-
await
|
|
8842
|
+
await stat2(appDir);
|
|
8835
8843
|
} catch {
|
|
8836
8844
|
continue;
|
|
8837
8845
|
}
|
|
8838
8846
|
const refs = await scanDir(appDir);
|
|
8839
8847
|
if (refs.size === 0) continue;
|
|
8840
|
-
const serverApp = project.apps.find((a2) => a2.name === appName);
|
|
8848
|
+
const serverApp = project.apps.find((a2) => a2.name.toLowerCase() === appName.toLowerCase());
|
|
8841
8849
|
let envData;
|
|
8842
8850
|
if (serverApp) {
|
|
8843
8851
|
envData = [];
|
|
@@ -8883,7 +8891,7 @@ var scanCommand = new Command17("scan").description("Scan codebase for env var r
|
|
|
8883
8891
|
}
|
|
8884
8892
|
console.log();
|
|
8885
8893
|
} else {
|
|
8886
|
-
const scanRoot = context.app && config.apps?.[context.app] ?
|
|
8894
|
+
const scanRoot = context.app && config.apps?.[context.app] ? join9(repoRoot, config.apps[context.app].path) : cwd;
|
|
8887
8895
|
const refs = await scanDir(scanRoot);
|
|
8888
8896
|
if (refs.size === 0) {
|
|
8889
8897
|
spinner.succeed(
|
|
@@ -8932,7 +8940,7 @@ var updateCommand = new Command18("update").description("Update ev to the latest
|
|
|
8932
8940
|
const spinner = ora12("Checking for updates...").start();
|
|
8933
8941
|
try {
|
|
8934
8942
|
const latest = execSync2("npm view @rowlabs/ev version", { encoding: "utf-8" }).trim();
|
|
8935
|
-
const current = "0.
|
|
8943
|
+
const current = "0.5.0";
|
|
8936
8944
|
if (current === latest) {
|
|
8937
8945
|
spinner.succeed(chalk20.green(`Already on the latest version (${current})`));
|
|
8938
8946
|
return;
|
|
@@ -8965,13 +8973,15 @@ var deleteCommand = new Command19("delete").description("Delete an app or enviro
|
|
|
8965
8973
|
const project = await client.getProject(context.project);
|
|
8966
8974
|
if (target.includes(":")) {
|
|
8967
8975
|
const [appName, envName] = target.split(":");
|
|
8968
|
-
const app = project.apps.find(
|
|
8976
|
+
const app = project.apps.find(
|
|
8977
|
+
(a2) => a2.name.toLowerCase() === appName.toLowerCase()
|
|
8978
|
+
);
|
|
8969
8979
|
if (!app) {
|
|
8970
8980
|
console.error(chalk21.red(`App "${appName}" not found`));
|
|
8971
8981
|
process.exit(1);
|
|
8972
8982
|
}
|
|
8973
8983
|
const envs = await client.listEnvironments(app.id);
|
|
8974
|
-
const env = envs.find((e) => e.name === envName);
|
|
8984
|
+
const env = envs.find((e) => e.name.toLowerCase() === envName.toLowerCase());
|
|
8975
8985
|
if (!env) {
|
|
8976
8986
|
console.error(chalk21.red(`Environment "${envName}" not found in app "${appName}"`));
|
|
8977
8987
|
process.exit(1);
|
|
@@ -8988,7 +8998,9 @@ var deleteCommand = new Command19("delete").description("Delete an app or enviro
|
|
|
8988
8998
|
spinner.succeed(chalk21.green(`Deleted environment "${appName}:${envName}"`));
|
|
8989
8999
|
} else {
|
|
8990
9000
|
const appName = target;
|
|
8991
|
-
const app = project.apps.find(
|
|
9001
|
+
const app = project.apps.find(
|
|
9002
|
+
(a2) => a2.name.toLowerCase() === appName.toLowerCase()
|
|
9003
|
+
);
|
|
8992
9004
|
if (!app) {
|
|
8993
9005
|
console.error(chalk21.red(`App "${appName}" not found`));
|
|
8994
9006
|
process.exit(1);
|
|
@@ -9023,9 +9035,371 @@ function prompt2(question) {
|
|
|
9023
9035
|
});
|
|
9024
9036
|
}
|
|
9025
9037
|
|
|
9038
|
+
// src/commands/audit.ts
|
|
9039
|
+
init_api_client();
|
|
9040
|
+
init_config();
|
|
9041
|
+
import { Command as Command20 } from "commander";
|
|
9042
|
+
import chalk22 from "chalk";
|
|
9043
|
+
import ora14 from "ora";
|
|
9044
|
+
import { stat as stat3 } from "fs/promises";
|
|
9045
|
+
import { join as join10 } from "path";
|
|
9046
|
+
var isTTY = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
9047
|
+
function stripAnsi(str) {
|
|
9048
|
+
return str.replace(/\x1B\[[0-9;]*m/g, "");
|
|
9049
|
+
}
|
|
9050
|
+
function log(msg) {
|
|
9051
|
+
console.log(isTTY ? msg : stripAnsi(msg));
|
|
9052
|
+
}
|
|
9053
|
+
async function auditApp(appName, envName, scanPath, projectId) {
|
|
9054
|
+
const client = await createApiClient();
|
|
9055
|
+
const project = await client.getProject(projectId);
|
|
9056
|
+
const serverApp = project.apps.find((a2) => a2.name.toLowerCase() === appName.toLowerCase());
|
|
9057
|
+
if (!serverApp) {
|
|
9058
|
+
log(isTTY ? chalk22.red(` App "${appName}" not found on server`) : `ERROR: App "${appName}" not found on server`);
|
|
9059
|
+
process.exit(1);
|
|
9060
|
+
}
|
|
9061
|
+
const envs = await client.listEnvironments(serverApp.id);
|
|
9062
|
+
const serverEnv = envs.find((e) => e.name.toLowerCase() === envName.toLowerCase());
|
|
9063
|
+
if (!serverEnv) {
|
|
9064
|
+
log(isTTY ? chalk22.red(` Environment "${envName}" not found for app "${appName}"`) : `ERROR: Environment "${envName}" not found for app "${appName}"`);
|
|
9065
|
+
process.exit(1);
|
|
9066
|
+
}
|
|
9067
|
+
const response = await client.pullSecrets(serverEnv.id);
|
|
9068
|
+
const remoteKeys = new Set(response.secrets.map((s2) => s2.key));
|
|
9069
|
+
const refs = await scanDir(scanPath);
|
|
9070
|
+
const referencedVars = [...refs.keys()];
|
|
9071
|
+
const missingVars = referencedVars.filter((v2) => !remoteKeys.has(v2));
|
|
9072
|
+
return {
|
|
9073
|
+
label: `${appName}:${envName}`,
|
|
9074
|
+
total: referencedVars.length,
|
|
9075
|
+
missing: missingVars
|
|
9076
|
+
};
|
|
9077
|
+
}
|
|
9078
|
+
function printResult(result) {
|
|
9079
|
+
if (result.missing.length === 0) {
|
|
9080
|
+
const msg = ` ${result.label} \u2014 ${result.total} variable${result.total === 1 ? "" : "s"}, all present \u2714`;
|
|
9081
|
+
log(isTTY ? chalk22.green(msg) : msg);
|
|
9082
|
+
} else {
|
|
9083
|
+
const header = ` ${result.label} \u2014 ${result.missing.length} missing`;
|
|
9084
|
+
log(isTTY ? chalk22.yellow(header) : header);
|
|
9085
|
+
log("");
|
|
9086
|
+
for (const v2 of result.missing) {
|
|
9087
|
+
log(isTTY ? ` ${chalk22.red(v2)}` : ` ${v2}`);
|
|
9088
|
+
}
|
|
9089
|
+
}
|
|
9090
|
+
}
|
|
9091
|
+
var auditCommand = new Command20("audit").description("Check that all env vars referenced in code exist in a remote environment").argument("[target]", "Target environment name (defaults to defaultEnv from ev.yaml)").action(async (target) => {
|
|
9092
|
+
const spinner = isTTY ? ora14("Resolving context...").start() : null;
|
|
9093
|
+
try {
|
|
9094
|
+
const resolved = await resolveCurrentContext(target);
|
|
9095
|
+
if (!resolved) {
|
|
9096
|
+
if (spinner) spinner.fail(chalk22.red("No ev.yaml found. Run `ev init` first."));
|
|
9097
|
+
else log("ERROR: No ev.yaml found. Run `ev init` first.");
|
|
9098
|
+
process.exit(1);
|
|
9099
|
+
}
|
|
9100
|
+
const { context, repoRoot } = resolved;
|
|
9101
|
+
const envName = target ?? context.env;
|
|
9102
|
+
const config = await loadEvConfig(process.cwd());
|
|
9103
|
+
if (!config) {
|
|
9104
|
+
if (spinner) spinner.fail(chalk22.red("Could not load ev.yaml"));
|
|
9105
|
+
else log("ERROR: Could not load ev.yaml");
|
|
9106
|
+
process.exit(1);
|
|
9107
|
+
}
|
|
9108
|
+
const cwd = process.cwd();
|
|
9109
|
+
const isAtRoot = cwd === repoRoot;
|
|
9110
|
+
const hasApps = config.apps && Object.keys(config.apps).length > 0;
|
|
9111
|
+
const results = [];
|
|
9112
|
+
if (isAtRoot && hasApps) {
|
|
9113
|
+
if (spinner) spinner.text = "Auditing all apps...";
|
|
9114
|
+
for (const [appName, appConfig] of Object.entries(config.apps)) {
|
|
9115
|
+
const appDir = join10(repoRoot, appConfig.path);
|
|
9116
|
+
try {
|
|
9117
|
+
await stat3(appDir);
|
|
9118
|
+
} catch {
|
|
9119
|
+
continue;
|
|
9120
|
+
}
|
|
9121
|
+
if (spinner) spinner.text = `Auditing ${appName}:${envName}...`;
|
|
9122
|
+
const result = await auditApp(appName, envName, appDir, context.project);
|
|
9123
|
+
results.push(result);
|
|
9124
|
+
}
|
|
9125
|
+
} else {
|
|
9126
|
+
const appName = context.app ?? "default";
|
|
9127
|
+
const scanPath = context.app && config.apps?.[context.app] ? join10(repoRoot, config.apps[context.app].path) : cwd;
|
|
9128
|
+
if (spinner) spinner.text = `Auditing ${appName}:${envName}...`;
|
|
9129
|
+
const result = await auditApp(appName, envName, scanPath, context.project);
|
|
9130
|
+
results.push(result);
|
|
9131
|
+
}
|
|
9132
|
+
if (spinner) spinner.stop();
|
|
9133
|
+
const totalVars = results.reduce((sum, r2) => sum + r2.total, 0);
|
|
9134
|
+
if (totalVars === 0) {
|
|
9135
|
+
log(isTTY ? chalk22.green(" No environment variable references found in code.") : "No environment variable references found in code.");
|
|
9136
|
+
process.exit(0);
|
|
9137
|
+
}
|
|
9138
|
+
log("");
|
|
9139
|
+
for (const result of results) {
|
|
9140
|
+
printResult(result);
|
|
9141
|
+
log("");
|
|
9142
|
+
}
|
|
9143
|
+
const anyMissing = results.some((r2) => r2.missing.length > 0);
|
|
9144
|
+
if (anyMissing) {
|
|
9145
|
+
log(isTTY ? chalk22.red(" \u2717 Audit failed") : "FAIL");
|
|
9146
|
+
process.exit(1);
|
|
9147
|
+
} else {
|
|
9148
|
+
process.exit(0);
|
|
9149
|
+
}
|
|
9150
|
+
} catch (err) {
|
|
9151
|
+
if (spinner) spinner.fail(chalk22.red(`Audit failed: ${err.message}`));
|
|
9152
|
+
else log(`ERROR: Audit failed: ${err.message}`);
|
|
9153
|
+
process.exit(1);
|
|
9154
|
+
}
|
|
9155
|
+
});
|
|
9156
|
+
|
|
9157
|
+
// src/commands/clean.ts
|
|
9158
|
+
init_dist();
|
|
9159
|
+
init_api_client();
|
|
9160
|
+
init_config();
|
|
9161
|
+
init_auth();
|
|
9162
|
+
import { Command as Command21 } from "commander";
|
|
9163
|
+
import chalk23 from "chalk";
|
|
9164
|
+
import ora15 from "ora";
|
|
9165
|
+
import { join as join11 } from "path";
|
|
9166
|
+
import { readFile as readFile7, writeFile as writeFile6, stat as stat4 } from "fs/promises";
|
|
9167
|
+
import { existsSync as existsSync7 } from "fs";
|
|
9168
|
+
var isTTY2 = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
9169
|
+
function stripAnsi2(str) {
|
|
9170
|
+
return str.replace(/\x1B\[[0-9;]*m/g, "");
|
|
9171
|
+
}
|
|
9172
|
+
function log2(msg) {
|
|
9173
|
+
console.log(isTTY2 ? msg : stripAnsi2(msg));
|
|
9174
|
+
}
|
|
9175
|
+
async function findDeadSecrets(appName, envName, scanPath, projectId, envFiles) {
|
|
9176
|
+
const client = await createApiClient();
|
|
9177
|
+
const project = await client.getProject(projectId);
|
|
9178
|
+
const serverApp = project.apps.find((a2) => a2.name.toLowerCase() === appName.toLowerCase());
|
|
9179
|
+
if (!serverApp) {
|
|
9180
|
+
log2(
|
|
9181
|
+
isTTY2 ? chalk23.red(` App "${appName}" not found on server`) : `ERROR: App "${appName}" not found on server`
|
|
9182
|
+
);
|
|
9183
|
+
process.exit(1);
|
|
9184
|
+
}
|
|
9185
|
+
const envs = await client.listEnvironments(serverApp.id);
|
|
9186
|
+
const serverEnv = envs.find((e) => e.name.toLowerCase() === envName.toLowerCase());
|
|
9187
|
+
if (!serverEnv) {
|
|
9188
|
+
log2(
|
|
9189
|
+
isTTY2 ? chalk23.red(` Environment "${envName}" not found for app "${appName}"`) : `ERROR: Environment "${envName}" not found for app "${appName}"`
|
|
9190
|
+
);
|
|
9191
|
+
process.exit(1);
|
|
9192
|
+
}
|
|
9193
|
+
const refs = await scanDir(scanPath);
|
|
9194
|
+
const referencedKeys = new Set(refs.keys());
|
|
9195
|
+
const localKeyFiles = /* @__PURE__ */ new Map();
|
|
9196
|
+
for (const envFile of envFiles) {
|
|
9197
|
+
const filePath = join11(scanPath, envFile);
|
|
9198
|
+
if (existsSync7(filePath)) {
|
|
9199
|
+
const content = await readFile7(filePath, "utf-8");
|
|
9200
|
+
const parsed = parseEnvFile(content);
|
|
9201
|
+
for (const key of Object.keys(parsed)) {
|
|
9202
|
+
if (!localKeyFiles.has(key)) localKeyFiles.set(key, []);
|
|
9203
|
+
localKeyFiles.get(key).push(filePath);
|
|
9204
|
+
}
|
|
9205
|
+
}
|
|
9206
|
+
}
|
|
9207
|
+
const response = await client.pullSecrets(serverEnv.id);
|
|
9208
|
+
const remoteKeys = new Set(response.secrets.map((s2) => s2.key));
|
|
9209
|
+
const allKeys = /* @__PURE__ */ new Set([...localKeyFiles.keys(), ...remoteKeys]);
|
|
9210
|
+
const dead = [];
|
|
9211
|
+
for (const key of allKeys) {
|
|
9212
|
+
if (!referencedKeys.has(key)) {
|
|
9213
|
+
dead.push({
|
|
9214
|
+
key,
|
|
9215
|
+
inLocal: localKeyFiles.has(key),
|
|
9216
|
+
inRemote: remoteKeys.has(key)
|
|
9217
|
+
});
|
|
9218
|
+
}
|
|
9219
|
+
}
|
|
9220
|
+
const affectedFiles = /* @__PURE__ */ new Set();
|
|
9221
|
+
for (const d2 of dead) {
|
|
9222
|
+
if (d2.inLocal) {
|
|
9223
|
+
for (const f2 of localKeyFiles.get(d2.key) ?? []) {
|
|
9224
|
+
affectedFiles.add(f2);
|
|
9225
|
+
}
|
|
9226
|
+
}
|
|
9227
|
+
}
|
|
9228
|
+
return {
|
|
9229
|
+
label: `${appName}:${envName}`,
|
|
9230
|
+
dead,
|
|
9231
|
+
localFiles: [...affectedFiles],
|
|
9232
|
+
envName: serverEnv.id,
|
|
9233
|
+
projectId,
|
|
9234
|
+
appName,
|
|
9235
|
+
codeRefCount: referencedKeys.size
|
|
9236
|
+
};
|
|
9237
|
+
}
|
|
9238
|
+
function printDeadTable(result) {
|
|
9239
|
+
const { label, dead } = result;
|
|
9240
|
+
const count = dead.length;
|
|
9241
|
+
if (count === 0) {
|
|
9242
|
+
const msg = ` ${label} \u2014 no unused secrets found`;
|
|
9243
|
+
log2(isTTY2 ? chalk23.green(msg) : msg);
|
|
9244
|
+
return;
|
|
9245
|
+
}
|
|
9246
|
+
const header = ` ${label} \u2014 ${count} unused secret${count === 1 ? "" : "s"} found`;
|
|
9247
|
+
log2(isTTY2 ? chalk23.yellow(header) : header);
|
|
9248
|
+
log2("");
|
|
9249
|
+
const maxKeyLen = Math.max(8, ...dead.map((d2) => d2.key.length));
|
|
9250
|
+
const keyCol = maxKeyLen + 2;
|
|
9251
|
+
const headerLine = " " + "Variable".padEnd(keyCol) + "local remote";
|
|
9252
|
+
log2(isTTY2 ? chalk23.dim(headerLine) : headerLine);
|
|
9253
|
+
const divider = " " + "\u2500".repeat(keyCol + 16);
|
|
9254
|
+
log2(isTTY2 ? chalk23.dim(divider) : divider);
|
|
9255
|
+
for (const d2 of dead) {
|
|
9256
|
+
const localMark = d2.inLocal ? isTTY2 ? chalk23.green("\u2713") : "\u2713" : isTTY2 ? chalk23.dim("\u2013") : "\u2013";
|
|
9257
|
+
const remoteMark = d2.inRemote ? isTTY2 ? chalk23.green("\u2713") : "\u2713" : isTTY2 ? chalk23.dim("\u2013") : "\u2013";
|
|
9258
|
+
const paddedKey = d2.key.padEnd(keyCol);
|
|
9259
|
+
log2(` ${isTTY2 ? chalk23.red(d2.key) + " ".repeat(keyCol - d2.key.length) : paddedKey}${localMark} ${remoteMark}`);
|
|
9260
|
+
}
|
|
9261
|
+
log2("");
|
|
9262
|
+
}
|
|
9263
|
+
async function cleanApp(result, deadSet, envFiles, scanPath, backendConfig) {
|
|
9264
|
+
const isEvBackend = !backendConfig || backendConfig.type === "ev";
|
|
9265
|
+
let localCleaned = 0;
|
|
9266
|
+
for (const envFile of envFiles) {
|
|
9267
|
+
const filePath = join11(scanPath, envFile);
|
|
9268
|
+
if (!existsSync7(filePath)) continue;
|
|
9269
|
+
const content = await readFile7(filePath, "utf-8");
|
|
9270
|
+
const parsed = parseEnvFile(content);
|
|
9271
|
+
let changed = false;
|
|
9272
|
+
for (const key of deadSet) {
|
|
9273
|
+
if (key in parsed) {
|
|
9274
|
+
delete parsed[key];
|
|
9275
|
+
changed = true;
|
|
9276
|
+
}
|
|
9277
|
+
}
|
|
9278
|
+
if (changed) {
|
|
9279
|
+
await writeFile6(filePath, serializeEnv(parsed), "utf-8");
|
|
9280
|
+
const display = envFile;
|
|
9281
|
+
log2(isTTY2 ? ` ${chalk23.green("\u2713")} Cleaned ${display}` : ` \u2713 Cleaned ${display}`);
|
|
9282
|
+
localCleaned++;
|
|
9283
|
+
}
|
|
9284
|
+
}
|
|
9285
|
+
const deadRemoteKeys = result.dead.filter((d2) => d2.inRemote).map((d2) => d2.key);
|
|
9286
|
+
if (deadRemoteKeys.length > 0) {
|
|
9287
|
+
const client = await createApiClient();
|
|
9288
|
+
const pullResponse = await client.pullSecrets(result.envName);
|
|
9289
|
+
const kept = pullResponse.secrets.filter((s2) => !deadSet.has(s2.key));
|
|
9290
|
+
let secrets;
|
|
9291
|
+
if (isEvBackend) {
|
|
9292
|
+
const projectKey = await getDecryptedProjectKey(result.projectId);
|
|
9293
|
+
secrets = kept.map((s2) => {
|
|
9294
|
+
const decrypted = decryptSecret(s2.encryptedValue, projectKey);
|
|
9295
|
+
const reEncrypted = encryptSecret(decrypted, projectKey);
|
|
9296
|
+
return { key: s2.key, encryptedValue: reEncrypted };
|
|
9297
|
+
});
|
|
9298
|
+
} else {
|
|
9299
|
+
secrets = kept.map((s2) => ({ key: s2.key, encryptedValue: s2.encryptedValue }));
|
|
9300
|
+
}
|
|
9301
|
+
await client.pushSecrets(result.envName, {
|
|
9302
|
+
secrets,
|
|
9303
|
+
prune: true,
|
|
9304
|
+
message: "ev clean: removed unused secrets"
|
|
9305
|
+
});
|
|
9306
|
+
log2(
|
|
9307
|
+
isTTY2 ? ` ${chalk23.green("\u2713")} Removed ${deadRemoteKeys.length} from ${result.label} remote` : ` \u2713 Removed ${deadRemoteKeys.length} from ${result.label} remote`
|
|
9308
|
+
);
|
|
9309
|
+
}
|
|
9310
|
+
}
|
|
9311
|
+
var cleanCommand = new Command21("clean").description("Find and remove unused secrets from local env files and remote environments").argument("[target]", "Target environment name (defaults to defaultEnv from ev.yaml)").action(async (target) => {
|
|
9312
|
+
const spinner = isTTY2 ? ora15("Resolving context...").start() : null;
|
|
9313
|
+
try {
|
|
9314
|
+
const resolved = await resolveCurrentContext(target);
|
|
9315
|
+
if (!resolved) {
|
|
9316
|
+
if (spinner) spinner.fail(chalk23.red("No ev.yaml found. Run `ev init` first."));
|
|
9317
|
+
else log2("ERROR: No ev.yaml found. Run `ev init` first.");
|
|
9318
|
+
process.exit(1);
|
|
9319
|
+
}
|
|
9320
|
+
const { context, repoRoot, backendConfig, envFiles } = resolved;
|
|
9321
|
+
const envName = target ?? context.env;
|
|
9322
|
+
const config = await loadEvConfig(process.cwd());
|
|
9323
|
+
if (!config) {
|
|
9324
|
+
if (spinner) spinner.fail(chalk23.red("Could not load ev.yaml"));
|
|
9325
|
+
else log2("ERROR: Could not load ev.yaml");
|
|
9326
|
+
process.exit(1);
|
|
9327
|
+
}
|
|
9328
|
+
const cwd = process.cwd();
|
|
9329
|
+
const isAtRoot = cwd === repoRoot;
|
|
9330
|
+
const hasApps = config.apps && Object.keys(config.apps).length > 0;
|
|
9331
|
+
const results = [];
|
|
9332
|
+
const scanPaths = /* @__PURE__ */ new Map();
|
|
9333
|
+
if (isAtRoot && hasApps) {
|
|
9334
|
+
if (spinner) spinner.text = "Scanning all apps...";
|
|
9335
|
+
for (const [appName, appConfig] of Object.entries(config.apps)) {
|
|
9336
|
+
const appDir = join11(repoRoot, appConfig.path);
|
|
9337
|
+
try {
|
|
9338
|
+
await stat4(appDir);
|
|
9339
|
+
} catch {
|
|
9340
|
+
continue;
|
|
9341
|
+
}
|
|
9342
|
+
if (spinner) spinner.text = `Scanning ${appName}:${envName}...`;
|
|
9343
|
+
const result = await findDeadSecrets(
|
|
9344
|
+
appName,
|
|
9345
|
+
envName,
|
|
9346
|
+
appDir,
|
|
9347
|
+
context.project,
|
|
9348
|
+
envFiles
|
|
9349
|
+
);
|
|
9350
|
+
results.push(result);
|
|
9351
|
+
scanPaths.set(appName, appDir);
|
|
9352
|
+
}
|
|
9353
|
+
} else {
|
|
9354
|
+
const appName = context.app ?? "default";
|
|
9355
|
+
const scanPath = context.app && config.apps?.[context.app] ? join11(repoRoot, config.apps[context.app].path) : cwd;
|
|
9356
|
+
if (spinner) spinner.text = `Scanning ${appName}:${envName}...`;
|
|
9357
|
+
const result = await findDeadSecrets(
|
|
9358
|
+
appName,
|
|
9359
|
+
envName,
|
|
9360
|
+
scanPath,
|
|
9361
|
+
context.project,
|
|
9362
|
+
envFiles
|
|
9363
|
+
);
|
|
9364
|
+
results.push(result);
|
|
9365
|
+
scanPaths.set(appName, scanPath);
|
|
9366
|
+
}
|
|
9367
|
+
if (spinner) spinner.stop();
|
|
9368
|
+
const totalCodeRefs = results.reduce((sum, r2) => sum + r2.codeRefCount, 0);
|
|
9369
|
+
if (totalCodeRefs === 0) {
|
|
9370
|
+
log2(
|
|
9371
|
+
"No env var references found in code \u2014 can't determine what's unused."
|
|
9372
|
+
);
|
|
9373
|
+
process.exit(0);
|
|
9374
|
+
}
|
|
9375
|
+
log2("");
|
|
9376
|
+
for (const result of results) {
|
|
9377
|
+
printDeadTable(result);
|
|
9378
|
+
if (result.dead.length === 0) continue;
|
|
9379
|
+
const deadSet = new Set(result.dead.map((d2) => d2.key));
|
|
9380
|
+
const appScanPath = scanPaths.get(result.appName) ?? repoRoot;
|
|
9381
|
+
const answer = await prompt(
|
|
9382
|
+
isTTY2 ? ` Remove these ${result.dead.length} secret${result.dead.length === 1 ? "" : "s"}? (y/N) ` : `Remove these ${result.dead.length} secrets? (y/N) `
|
|
9383
|
+
);
|
|
9384
|
+
if (answer.toLowerCase() !== "y") {
|
|
9385
|
+
log2(isTTY2 ? chalk23.dim(" Skipped.") : " Skipped.");
|
|
9386
|
+
log2("");
|
|
9387
|
+
continue;
|
|
9388
|
+
}
|
|
9389
|
+
log2("");
|
|
9390
|
+
await cleanApp(result, deadSet, envFiles, appScanPath, backendConfig);
|
|
9391
|
+
log2("");
|
|
9392
|
+
}
|
|
9393
|
+
} catch (err) {
|
|
9394
|
+
if (spinner) spinner.fail(chalk23.red(`Clean failed: ${err.message}`));
|
|
9395
|
+
else log2(`ERROR: Clean failed: ${err.message}`);
|
|
9396
|
+
process.exit(1);
|
|
9397
|
+
}
|
|
9398
|
+
});
|
|
9399
|
+
|
|
9026
9400
|
// src/index.ts
|
|
9027
|
-
var program = new
|
|
9028
|
-
program.name("ev").description("Git for env vars \u2014 sync environment variables across teams securely").version("0.
|
|
9401
|
+
var program = new Command22();
|
|
9402
|
+
program.name("ev").description("Git for env vars \u2014 sync environment variables across teams securely").version("0.5.0");
|
|
9029
9403
|
program.addCommand(loginCommand);
|
|
9030
9404
|
program.addCommand(initCommand);
|
|
9031
9405
|
program.addCommand(pushCommand);
|
|
@@ -9045,6 +9419,8 @@ program.addCommand(completionsCommand);
|
|
|
9045
9419
|
program.addCommand(scanCommand);
|
|
9046
9420
|
program.addCommand(updateCommand);
|
|
9047
9421
|
program.addCommand(deleteCommand);
|
|
9422
|
+
program.addCommand(auditCommand);
|
|
9423
|
+
program.addCommand(cleanCommand);
|
|
9048
9424
|
async function main() {
|
|
9049
9425
|
await initCrypto();
|
|
9050
9426
|
program.parse();
|