@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.
Files changed (2) hide show
  1. package/dist/index.js +394 -18
  2. 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 Command20 } from "commander";
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((s2) => s2.key === key);
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 = join8(repoRoot, appConfig.path);
8840
+ const appDir = join9(repoRoot, appConfig.path);
8833
8841
  try {
8834
- await stat(appDir);
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] ? join8(repoRoot, config.apps[context.app].path) : cwd;
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.4.4";
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((a2) => a2.name === appName);
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((a2) => a2.name === appName);
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 Command20();
9028
- program.name("ev").description("Git for env vars \u2014 sync environment variables across teams securely").version("0.4.4");
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rowlabs/ev",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "Git for env vars — sync environment variables across teams securely",
5
5
  "type": "module",
6
6
  "bin": {