@rowlabs/ev 0.4.5 → 0.5.1
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 +380 -12
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -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.toLowerCase() === context.app
|
|
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,7 +7605,7 @@ 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.toLowerCase() === context.app
|
|
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);
|
|
@@ -8642,12 +8642,16 @@ var completionsCommand = new Command16("completions").description("Generate shel
|
|
|
8642
8642
|
});
|
|
8643
8643
|
|
|
8644
8644
|
// src/commands/scan.ts
|
|
8645
|
-
init_dist();
|
|
8646
8645
|
init_api_client();
|
|
8647
8646
|
init_config();
|
|
8648
8647
|
import { Command as Command17 } from "commander";
|
|
8649
8648
|
import chalk19 from "chalk";
|
|
8650
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();
|
|
8651
8655
|
import { readdir, readFile as readFile6, stat } from "fs/promises";
|
|
8652
8656
|
import { existsSync as existsSync6 } from "fs";
|
|
8653
8657
|
import { join as join8, extname } from "path";
|
|
@@ -8729,7 +8733,7 @@ async function scanDir(dir) {
|
|
|
8729
8733
|
const varName = match[1];
|
|
8730
8734
|
if (IGNORE_VARS.has(varName)) continue;
|
|
8731
8735
|
if (!results.has(varName)) results.set(varName, /* @__PURE__ */ new Set());
|
|
8732
|
-
results.get(varName)
|
|
8736
|
+
results.get(varName)?.add(fullPath);
|
|
8733
8737
|
}
|
|
8734
8738
|
}
|
|
8735
8739
|
}
|
|
@@ -8752,6 +8756,8 @@ async function getLocalEnvKeys(dir, configEnvFiles) {
|
|
|
8752
8756
|
}
|
|
8753
8757
|
return keys;
|
|
8754
8758
|
}
|
|
8759
|
+
|
|
8760
|
+
// src/commands/scan.ts
|
|
8755
8761
|
function printTable(label, refs, envData, repoRoot, showFiles, localKeys) {
|
|
8756
8762
|
const varNames = [...refs.keys()].sort();
|
|
8757
8763
|
if (varNames.length === 0) return 0;
|
|
@@ -8787,7 +8793,7 @@ function printTable(label, refs, envData, repoRoot, showFiles, localKeys) {
|
|
|
8787
8793
|
if (!allPresent) missingCount++;
|
|
8788
8794
|
console.log(row);
|
|
8789
8795
|
if (showFiles) {
|
|
8790
|
-
const files = refs.get(varName);
|
|
8796
|
+
const files = refs.get(varName) ?? [];
|
|
8791
8797
|
for (const file of files) {
|
|
8792
8798
|
const relative = file.replace(repoRoot + "/", "");
|
|
8793
8799
|
console.log(chalk19.dim(` \u2192 ${relative}`));
|
|
@@ -8831,9 +8837,9 @@ var scanCommand = new Command17("scan").description("Scan codebase for env var r
|
|
|
8831
8837
|
let totalMissing = 0;
|
|
8832
8838
|
let totalVars = 0;
|
|
8833
8839
|
for (const [appName, appConfig] of Object.entries(config.apps)) {
|
|
8834
|
-
const appDir =
|
|
8840
|
+
const appDir = join9(repoRoot, appConfig.path);
|
|
8835
8841
|
try {
|
|
8836
|
-
await
|
|
8842
|
+
await stat2(appDir);
|
|
8837
8843
|
} catch {
|
|
8838
8844
|
continue;
|
|
8839
8845
|
}
|
|
@@ -8885,7 +8891,7 @@ var scanCommand = new Command17("scan").description("Scan codebase for env var r
|
|
|
8885
8891
|
}
|
|
8886
8892
|
console.log();
|
|
8887
8893
|
} else {
|
|
8888
|
-
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;
|
|
8889
8895
|
const refs = await scanDir(scanRoot);
|
|
8890
8896
|
if (refs.size === 0) {
|
|
8891
8897
|
spinner.succeed(
|
|
@@ -8934,7 +8940,7 @@ var updateCommand = new Command18("update").description("Update ev to the latest
|
|
|
8934
8940
|
const spinner = ora12("Checking for updates...").start();
|
|
8935
8941
|
try {
|
|
8936
8942
|
const latest = execSync2("npm view @rowlabs/ev version", { encoding: "utf-8" }).trim();
|
|
8937
|
-
const current = "0.
|
|
8943
|
+
const current = "0.5.1";
|
|
8938
8944
|
if (current === latest) {
|
|
8939
8945
|
spinner.succeed(chalk20.green(`Already on the latest version (${current})`));
|
|
8940
8946
|
return;
|
|
@@ -9029,9 +9035,369 @@ function prompt2(question) {
|
|
|
9029
9035
|
});
|
|
9030
9036
|
}
|
|
9031
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
|
+
const ESC = String.fromCharCode(27);
|
|
9049
|
+
return str.replace(new RegExp(`${ESC}\\[[0-9;]*m`, "g"), "");
|
|
9050
|
+
}
|
|
9051
|
+
function log(msg) {
|
|
9052
|
+
console.log(isTTY ? msg : stripAnsi(msg));
|
|
9053
|
+
}
|
|
9054
|
+
async function auditApp(appName, envName, scanPath, projectId) {
|
|
9055
|
+
const client = await createApiClient();
|
|
9056
|
+
const project = await client.getProject(projectId);
|
|
9057
|
+
const serverApp = project.apps.find((a2) => a2.name.toLowerCase() === appName.toLowerCase());
|
|
9058
|
+
if (!serverApp) {
|
|
9059
|
+
log(isTTY ? chalk22.red(` App "${appName}" not found on server`) : `ERROR: App "${appName}" not found on server`);
|
|
9060
|
+
process.exit(1);
|
|
9061
|
+
}
|
|
9062
|
+
const envs = await client.listEnvironments(serverApp.id);
|
|
9063
|
+
const serverEnv = envs.find((e) => e.name.toLowerCase() === envName.toLowerCase());
|
|
9064
|
+
if (!serverEnv) {
|
|
9065
|
+
log(isTTY ? chalk22.red(` Environment "${envName}" not found for app "${appName}"`) : `ERROR: Environment "${envName}" not found for app "${appName}"`);
|
|
9066
|
+
process.exit(1);
|
|
9067
|
+
}
|
|
9068
|
+
const response = await client.pullSecrets(serverEnv.id);
|
|
9069
|
+
const remoteKeys = new Set(response.secrets.map((s2) => s2.key));
|
|
9070
|
+
const refs = await scanDir(scanPath);
|
|
9071
|
+
const referencedVars = [...refs.keys()];
|
|
9072
|
+
const missingVars = referencedVars.filter((v2) => !remoteKeys.has(v2));
|
|
9073
|
+
return {
|
|
9074
|
+
label: `${appName}:${envName}`,
|
|
9075
|
+
total: referencedVars.length,
|
|
9076
|
+
missing: missingVars
|
|
9077
|
+
};
|
|
9078
|
+
}
|
|
9079
|
+
function printResult(result) {
|
|
9080
|
+
if (result.missing.length === 0) {
|
|
9081
|
+
const msg = ` ${result.label} \u2014 ${result.total} variable${result.total === 1 ? "" : "s"}, all present \u2714`;
|
|
9082
|
+
log(isTTY ? chalk22.green(msg) : msg);
|
|
9083
|
+
} else {
|
|
9084
|
+
const header = ` ${result.label} \u2014 ${result.missing.length} missing`;
|
|
9085
|
+
log(isTTY ? chalk22.yellow(header) : header);
|
|
9086
|
+
log("");
|
|
9087
|
+
for (const v2 of result.missing) {
|
|
9088
|
+
log(isTTY ? ` ${chalk22.red(v2)}` : ` ${v2}`);
|
|
9089
|
+
}
|
|
9090
|
+
}
|
|
9091
|
+
}
|
|
9092
|
+
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) => {
|
|
9093
|
+
const spinner = isTTY ? ora14("Resolving context...").start() : null;
|
|
9094
|
+
try {
|
|
9095
|
+
const resolved = await resolveCurrentContext(target);
|
|
9096
|
+
if (!resolved) {
|
|
9097
|
+
if (spinner) spinner.fail(chalk22.red("No ev.yaml found. Run `ev init` first."));
|
|
9098
|
+
else log("ERROR: No ev.yaml found. Run `ev init` first.");
|
|
9099
|
+
process.exit(1);
|
|
9100
|
+
}
|
|
9101
|
+
const { context, repoRoot } = resolved;
|
|
9102
|
+
const envName = target ?? context.env;
|
|
9103
|
+
const config = await loadEvConfig(process.cwd());
|
|
9104
|
+
if (!config) {
|
|
9105
|
+
if (spinner) spinner.fail(chalk22.red("Could not load ev.yaml"));
|
|
9106
|
+
else log("ERROR: Could not load ev.yaml");
|
|
9107
|
+
process.exit(1);
|
|
9108
|
+
}
|
|
9109
|
+
const cwd = process.cwd();
|
|
9110
|
+
const isAtRoot = cwd === repoRoot;
|
|
9111
|
+
const hasApps = config.apps && Object.keys(config.apps).length > 0;
|
|
9112
|
+
const results = [];
|
|
9113
|
+
if (isAtRoot && hasApps) {
|
|
9114
|
+
if (spinner) spinner.text = "Auditing all apps...";
|
|
9115
|
+
for (const [appName, appConfig] of Object.entries(config.apps ?? {})) {
|
|
9116
|
+
const appDir = join10(repoRoot, appConfig.path);
|
|
9117
|
+
try {
|
|
9118
|
+
await stat3(appDir);
|
|
9119
|
+
} catch {
|
|
9120
|
+
continue;
|
|
9121
|
+
}
|
|
9122
|
+
if (spinner) spinner.text = `Auditing ${appName}:${envName}...`;
|
|
9123
|
+
const result = await auditApp(appName, envName, appDir, context.project);
|
|
9124
|
+
results.push(result);
|
|
9125
|
+
}
|
|
9126
|
+
} else {
|
|
9127
|
+
const appName = context.app ?? "default";
|
|
9128
|
+
const scanPath = context.app && config.apps?.[context.app] ? join10(repoRoot, config.apps[context.app].path) : cwd;
|
|
9129
|
+
if (spinner) spinner.text = `Auditing ${appName}:${envName}...`;
|
|
9130
|
+
const result = await auditApp(appName, envName, scanPath, context.project);
|
|
9131
|
+
results.push(result);
|
|
9132
|
+
}
|
|
9133
|
+
if (spinner) spinner.stop();
|
|
9134
|
+
const totalVars = results.reduce((sum, r2) => sum + r2.total, 0);
|
|
9135
|
+
if (totalVars === 0) {
|
|
9136
|
+
log(isTTY ? chalk22.green(" No environment variable references found in code.") : "No environment variable references found in code.");
|
|
9137
|
+
process.exit(0);
|
|
9138
|
+
}
|
|
9139
|
+
log("");
|
|
9140
|
+
for (const result of results) {
|
|
9141
|
+
printResult(result);
|
|
9142
|
+
log("");
|
|
9143
|
+
}
|
|
9144
|
+
const anyMissing = results.some((r2) => r2.missing.length > 0);
|
|
9145
|
+
if (anyMissing) {
|
|
9146
|
+
log(isTTY ? chalk22.red(" \u2717 Audit failed") : "FAIL");
|
|
9147
|
+
process.exit(1);
|
|
9148
|
+
} else {
|
|
9149
|
+
process.exit(0);
|
|
9150
|
+
}
|
|
9151
|
+
} catch (err) {
|
|
9152
|
+
if (spinner) spinner.fail(chalk22.red(`Audit failed: ${err.message}`));
|
|
9153
|
+
else log(`ERROR: Audit failed: ${err.message}`);
|
|
9154
|
+
process.exit(1);
|
|
9155
|
+
}
|
|
9156
|
+
});
|
|
9157
|
+
|
|
9158
|
+
// src/commands/clean.ts
|
|
9159
|
+
init_dist();
|
|
9160
|
+
init_api_client();
|
|
9161
|
+
init_config();
|
|
9162
|
+
init_auth();
|
|
9163
|
+
import { Command as Command21 } from "commander";
|
|
9164
|
+
import chalk23 from "chalk";
|
|
9165
|
+
import ora15 from "ora";
|
|
9166
|
+
import { join as join11 } from "path";
|
|
9167
|
+
import { readFile as readFile7, writeFile as writeFile6, stat as stat4 } from "fs/promises";
|
|
9168
|
+
import { existsSync as existsSync7 } from "fs";
|
|
9169
|
+
var isTTY2 = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
9170
|
+
function stripAnsi2(str) {
|
|
9171
|
+
const ESC = String.fromCharCode(27);
|
|
9172
|
+
return str.replace(new RegExp(`${ESC}\\[[0-9;]*m`, "g"), "");
|
|
9173
|
+
}
|
|
9174
|
+
function log2(msg) {
|
|
9175
|
+
console.log(isTTY2 ? msg : stripAnsi2(msg));
|
|
9176
|
+
}
|
|
9177
|
+
async function findDeadSecrets(appName, envName, envDir, repoRoot, projectId, envFiles) {
|
|
9178
|
+
const client = await createApiClient();
|
|
9179
|
+
const project = await client.getProject(projectId);
|
|
9180
|
+
const serverApp = project.apps.find((a2) => a2.name.toLowerCase() === appName.toLowerCase());
|
|
9181
|
+
if (!serverApp) {
|
|
9182
|
+
log2(
|
|
9183
|
+
isTTY2 ? chalk23.red(` App "${appName}" not found on server`) : `ERROR: App "${appName}" not found on server`
|
|
9184
|
+
);
|
|
9185
|
+
process.exit(1);
|
|
9186
|
+
}
|
|
9187
|
+
const envs = await client.listEnvironments(serverApp.id);
|
|
9188
|
+
const serverEnv = envs.find((e) => e.name.toLowerCase() === envName.toLowerCase());
|
|
9189
|
+
if (!serverEnv) {
|
|
9190
|
+
log2(
|
|
9191
|
+
isTTY2 ? chalk23.red(` Environment "${envName}" not found for app "${appName}"`) : `ERROR: Environment "${envName}" not found for app "${appName}"`
|
|
9192
|
+
);
|
|
9193
|
+
process.exit(1);
|
|
9194
|
+
}
|
|
9195
|
+
const refs = await scanDir(repoRoot);
|
|
9196
|
+
const referencedKeys = new Set(refs.keys());
|
|
9197
|
+
const localKeyFiles = /* @__PURE__ */ new Map();
|
|
9198
|
+
for (const envFile of envFiles) {
|
|
9199
|
+
const filePath = join11(envDir, envFile);
|
|
9200
|
+
if (existsSync7(filePath)) {
|
|
9201
|
+
const content = await readFile7(filePath, "utf-8");
|
|
9202
|
+
const parsed = parseEnvFile(content);
|
|
9203
|
+
for (const key of Object.keys(parsed)) {
|
|
9204
|
+
if (!localKeyFiles.has(key)) localKeyFiles.set(key, []);
|
|
9205
|
+
localKeyFiles.get(key)?.push(filePath);
|
|
9206
|
+
}
|
|
9207
|
+
}
|
|
9208
|
+
}
|
|
9209
|
+
const response = await client.pullSecrets(serverEnv.id);
|
|
9210
|
+
const remoteKeys = new Set(response.secrets.map((s2) => s2.key));
|
|
9211
|
+
const allKeys = /* @__PURE__ */ new Set([...localKeyFiles.keys(), ...remoteKeys]);
|
|
9212
|
+
const dead = [];
|
|
9213
|
+
for (const key of allKeys) {
|
|
9214
|
+
if (!referencedKeys.has(key)) {
|
|
9215
|
+
dead.push({
|
|
9216
|
+
key,
|
|
9217
|
+
inLocal: localKeyFiles.has(key),
|
|
9218
|
+
inRemote: remoteKeys.has(key)
|
|
9219
|
+
});
|
|
9220
|
+
}
|
|
9221
|
+
}
|
|
9222
|
+
const affectedFiles = /* @__PURE__ */ new Set();
|
|
9223
|
+
for (const d2 of dead) {
|
|
9224
|
+
if (d2.inLocal) {
|
|
9225
|
+
for (const f2 of localKeyFiles.get(d2.key) ?? []) {
|
|
9226
|
+
affectedFiles.add(f2);
|
|
9227
|
+
}
|
|
9228
|
+
}
|
|
9229
|
+
}
|
|
9230
|
+
return {
|
|
9231
|
+
label: `${appName}:${envName}`,
|
|
9232
|
+
dead,
|
|
9233
|
+
localFiles: [...affectedFiles],
|
|
9234
|
+
envName: serverEnv.id,
|
|
9235
|
+
projectId,
|
|
9236
|
+
appName,
|
|
9237
|
+
codeRefCount: referencedKeys.size
|
|
9238
|
+
};
|
|
9239
|
+
}
|
|
9240
|
+
function printDeadTable(result) {
|
|
9241
|
+
const { label, dead } = result;
|
|
9242
|
+
const count = dead.length;
|
|
9243
|
+
if (count === 0) {
|
|
9244
|
+
const msg = ` ${label} \u2014 no unused secrets found`;
|
|
9245
|
+
log2(isTTY2 ? chalk23.green(msg) : msg);
|
|
9246
|
+
return;
|
|
9247
|
+
}
|
|
9248
|
+
const header = ` ${label} \u2014 ${count} unused secret${count === 1 ? "" : "s"} found`;
|
|
9249
|
+
log2(isTTY2 ? chalk23.yellow(header) : header);
|
|
9250
|
+
log2("");
|
|
9251
|
+
const maxKeyLen = Math.max(8, ...dead.map((d2) => d2.key.length));
|
|
9252
|
+
const keyCol = maxKeyLen + 2;
|
|
9253
|
+
const headerLine = " " + "Variable".padEnd(keyCol) + "local remote";
|
|
9254
|
+
log2(isTTY2 ? chalk23.dim(headerLine) : headerLine);
|
|
9255
|
+
const divider = " " + "\u2500".repeat(keyCol + 16);
|
|
9256
|
+
log2(isTTY2 ? chalk23.dim(divider) : divider);
|
|
9257
|
+
for (const d2 of dead) {
|
|
9258
|
+
const localMark = d2.inLocal ? isTTY2 ? chalk23.green("\u2713") : "\u2713" : isTTY2 ? chalk23.dim("\u2013") : "\u2013";
|
|
9259
|
+
const remoteMark = d2.inRemote ? isTTY2 ? chalk23.green("\u2713") : "\u2713" : isTTY2 ? chalk23.dim("\u2013") : "\u2013";
|
|
9260
|
+
const paddedKey = d2.key.padEnd(keyCol);
|
|
9261
|
+
log2(` ${isTTY2 ? chalk23.red(d2.key) + " ".repeat(keyCol - d2.key.length) : paddedKey}${localMark} ${remoteMark}`);
|
|
9262
|
+
}
|
|
9263
|
+
log2("");
|
|
9264
|
+
}
|
|
9265
|
+
async function cleanApp(result, deadSet, envFiles, scanPath, backendConfig) {
|
|
9266
|
+
const isEvBackend = !backendConfig || backendConfig.type === "ev";
|
|
9267
|
+
for (const envFile of envFiles) {
|
|
9268
|
+
const filePath = join11(scanPath, envFile);
|
|
9269
|
+
if (!existsSync7(filePath)) continue;
|
|
9270
|
+
const content = await readFile7(filePath, "utf-8");
|
|
9271
|
+
const parsed = parseEnvFile(content);
|
|
9272
|
+
const filtered = {};
|
|
9273
|
+
for (const [k2, v2] of Object.entries(parsed)) {
|
|
9274
|
+
if (!deadSet.has(k2)) filtered[k2] = v2;
|
|
9275
|
+
}
|
|
9276
|
+
if (Object.keys(filtered).length < Object.keys(parsed).length) {
|
|
9277
|
+
await writeFile6(filePath, serializeEnv(filtered), "utf-8");
|
|
9278
|
+
log2(isTTY2 ? ` ${chalk23.green("\u2713")} Cleaned ${envFile}` : ` \u2713 Cleaned ${envFile}`);
|
|
9279
|
+
}
|
|
9280
|
+
}
|
|
9281
|
+
const deadRemoteKeys = result.dead.filter((d2) => d2.inRemote).map((d2) => d2.key);
|
|
9282
|
+
if (deadRemoteKeys.length > 0) {
|
|
9283
|
+
const client = await createApiClient();
|
|
9284
|
+
const pullResponse = await client.pullSecrets(result.envName);
|
|
9285
|
+
const kept = pullResponse.secrets.filter((s2) => !deadSet.has(s2.key));
|
|
9286
|
+
let secrets;
|
|
9287
|
+
if (isEvBackend) {
|
|
9288
|
+
const projectKey = await getDecryptedProjectKey(result.projectId);
|
|
9289
|
+
secrets = kept.map((s2) => {
|
|
9290
|
+
const decrypted = decryptSecret(s2.encryptedValue, projectKey);
|
|
9291
|
+
const reEncrypted = encryptSecret(decrypted, projectKey);
|
|
9292
|
+
return { key: s2.key, encryptedValue: reEncrypted };
|
|
9293
|
+
});
|
|
9294
|
+
} else {
|
|
9295
|
+
secrets = kept.map((s2) => ({ key: s2.key, encryptedValue: s2.encryptedValue }));
|
|
9296
|
+
}
|
|
9297
|
+
await client.pushSecrets(result.envName, {
|
|
9298
|
+
secrets,
|
|
9299
|
+
prune: true,
|
|
9300
|
+
message: "ev clean: removed unused secrets"
|
|
9301
|
+
});
|
|
9302
|
+
log2(
|
|
9303
|
+
isTTY2 ? ` ${chalk23.green("\u2713")} Removed ${deadRemoteKeys.length} from ${result.label} remote` : ` \u2713 Removed ${deadRemoteKeys.length} from ${result.label} remote`
|
|
9304
|
+
);
|
|
9305
|
+
}
|
|
9306
|
+
}
|
|
9307
|
+
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) => {
|
|
9308
|
+
const spinner = isTTY2 ? ora15("Resolving context...").start() : null;
|
|
9309
|
+
try {
|
|
9310
|
+
const resolved = await resolveCurrentContext(target);
|
|
9311
|
+
if (!resolved) {
|
|
9312
|
+
if (spinner) spinner.fail(chalk23.red("No ev.yaml found. Run `ev init` first."));
|
|
9313
|
+
else log2("ERROR: No ev.yaml found. Run `ev init` first.");
|
|
9314
|
+
process.exit(1);
|
|
9315
|
+
}
|
|
9316
|
+
const { context, repoRoot, backendConfig, envFiles } = resolved;
|
|
9317
|
+
const envName = target ?? context.env;
|
|
9318
|
+
const config = await loadEvConfig(process.cwd());
|
|
9319
|
+
if (!config) {
|
|
9320
|
+
if (spinner) spinner.fail(chalk23.red("Could not load ev.yaml"));
|
|
9321
|
+
else log2("ERROR: Could not load ev.yaml");
|
|
9322
|
+
process.exit(1);
|
|
9323
|
+
}
|
|
9324
|
+
const cwd = process.cwd();
|
|
9325
|
+
const isAtRoot = cwd === repoRoot;
|
|
9326
|
+
const hasApps = config.apps && Object.keys(config.apps).length > 0;
|
|
9327
|
+
const results = [];
|
|
9328
|
+
const scanPaths = /* @__PURE__ */ new Map();
|
|
9329
|
+
if (isAtRoot && hasApps) {
|
|
9330
|
+
if (spinner) spinner.text = "Scanning all apps...";
|
|
9331
|
+
for (const [appName, appConfig] of Object.entries(config.apps ?? {})) {
|
|
9332
|
+
const appDir = join11(repoRoot, appConfig.path);
|
|
9333
|
+
try {
|
|
9334
|
+
await stat4(appDir);
|
|
9335
|
+
} catch {
|
|
9336
|
+
continue;
|
|
9337
|
+
}
|
|
9338
|
+
if (spinner) spinner.text = `Scanning ${appName}:${envName}...`;
|
|
9339
|
+
const result = await findDeadSecrets(
|
|
9340
|
+
appName,
|
|
9341
|
+
envName,
|
|
9342
|
+
appDir,
|
|
9343
|
+
repoRoot,
|
|
9344
|
+
context.project,
|
|
9345
|
+
envFiles
|
|
9346
|
+
);
|
|
9347
|
+
results.push(result);
|
|
9348
|
+
scanPaths.set(appName, appDir);
|
|
9349
|
+
}
|
|
9350
|
+
} else {
|
|
9351
|
+
const appName = context.app ?? "default";
|
|
9352
|
+
const scanPath = context.app && config.apps?.[context.app] ? join11(repoRoot, config.apps[context.app].path) : cwd;
|
|
9353
|
+
if (spinner) spinner.text = `Scanning ${appName}:${envName}...`;
|
|
9354
|
+
const result = await findDeadSecrets(
|
|
9355
|
+
appName,
|
|
9356
|
+
envName,
|
|
9357
|
+
scanPath,
|
|
9358
|
+
repoRoot,
|
|
9359
|
+
context.project,
|
|
9360
|
+
envFiles
|
|
9361
|
+
);
|
|
9362
|
+
results.push(result);
|
|
9363
|
+
scanPaths.set(appName, scanPath);
|
|
9364
|
+
}
|
|
9365
|
+
if (spinner) spinner.stop();
|
|
9366
|
+
const totalCodeRefs = results.reduce((sum, r2) => sum + r2.codeRefCount, 0);
|
|
9367
|
+
if (totalCodeRefs === 0) {
|
|
9368
|
+
log2(
|
|
9369
|
+
"No env var references found in code \u2014 can't determine what's unused."
|
|
9370
|
+
);
|
|
9371
|
+
process.exit(0);
|
|
9372
|
+
}
|
|
9373
|
+
log2("");
|
|
9374
|
+
for (const result of results) {
|
|
9375
|
+
printDeadTable(result);
|
|
9376
|
+
if (result.dead.length === 0) continue;
|
|
9377
|
+
const deadSet = new Set(result.dead.map((d2) => d2.key));
|
|
9378
|
+
const appScanPath = scanPaths.get(result.appName) ?? repoRoot;
|
|
9379
|
+
const answer = await prompt(
|
|
9380
|
+
isTTY2 ? ` Remove these ${result.dead.length} secret${result.dead.length === 1 ? "" : "s"}? (y/N) ` : `Remove these ${result.dead.length} secrets? (y/N) `
|
|
9381
|
+
);
|
|
9382
|
+
if (answer.toLowerCase() !== "y") {
|
|
9383
|
+
log2(isTTY2 ? chalk23.dim(" Skipped.") : " Skipped.");
|
|
9384
|
+
log2("");
|
|
9385
|
+
continue;
|
|
9386
|
+
}
|
|
9387
|
+
log2("");
|
|
9388
|
+
await cleanApp(result, deadSet, envFiles, appScanPath, backendConfig);
|
|
9389
|
+
log2("");
|
|
9390
|
+
}
|
|
9391
|
+
} catch (err) {
|
|
9392
|
+
if (spinner) spinner.fail(chalk23.red(`Clean failed: ${err.message}`));
|
|
9393
|
+
else log2(`ERROR: Clean failed: ${err.message}`);
|
|
9394
|
+
process.exit(1);
|
|
9395
|
+
}
|
|
9396
|
+
});
|
|
9397
|
+
|
|
9032
9398
|
// src/index.ts
|
|
9033
|
-
var program = new
|
|
9034
|
-
program.name("ev").description("Git for env vars \u2014 sync environment variables across teams securely").version("0.
|
|
9399
|
+
var program = new Command22();
|
|
9400
|
+
program.name("ev").description("Git for env vars \u2014 sync environment variables across teams securely").version("0.5.1");
|
|
9035
9401
|
program.addCommand(loginCommand);
|
|
9036
9402
|
program.addCommand(initCommand);
|
|
9037
9403
|
program.addCommand(pushCommand);
|
|
@@ -9051,6 +9417,8 @@ program.addCommand(completionsCommand);
|
|
|
9051
9417
|
program.addCommand(scanCommand);
|
|
9052
9418
|
program.addCommand(updateCommand);
|
|
9053
9419
|
program.addCommand(deleteCommand);
|
|
9420
|
+
program.addCommand(auditCommand);
|
|
9421
|
+
program.addCommand(cleanCommand);
|
|
9054
9422
|
async function main() {
|
|
9055
9423
|
await initCrypto();
|
|
9056
9424
|
program.parse();
|