@envmanager-cli/cli 0.1.9 → 0.2.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.
@@ -464,6 +464,147 @@ import { resolve } from "path";
464
464
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
465
465
  import { join as join2, dirname } from "path";
466
466
  import { z } from "zod";
467
+
468
+ // src/lib/formatters.ts
469
+ import * as yaml from "js-yaml";
470
+ var EXPORT_FORMATS = [
471
+ "dotenv",
472
+ "docker-compose",
473
+ "k8s-secret",
474
+ "k8s-configmap",
475
+ "vercel",
476
+ "railway",
477
+ "render"
478
+ ];
479
+ function sanitizeK8sName(name) {
480
+ return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "").substring(0, 63);
481
+ }
482
+ function toDotEnv(variables) {
483
+ return variables.map((v) => {
484
+ const needsQuotes = /[\s='"#\n\r]/.test(v.value);
485
+ const value = needsQuotes ? `"${v.value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"` : v.value;
486
+ return `${v.key}=${value}`;
487
+ }).join("\n");
488
+ }
489
+ function toDockerCompose(variables) {
490
+ const envData = {};
491
+ for (const v of variables) {
492
+ envData[v.key] = v.value;
493
+ }
494
+ const snippet = {
495
+ services: {
496
+ app: {
497
+ environment: envData
498
+ }
499
+ }
500
+ };
501
+ const yamlOutput = yaml.dump(snippet, { lineWidth: -1, quotingType: '"' });
502
+ return `# Docker Compose environment section
503
+ # Copy this into your docker-compose.yml and replace 'app' with your service name
504
+ ${yamlOutput}`;
505
+ }
506
+ function toKubernetesSecret(variables, config) {
507
+ const data = {};
508
+ for (const v of variables) {
509
+ data[v.key] = Buffer.from(v.value).toString("base64");
510
+ }
511
+ const manifest = {
512
+ apiVersion: "v1",
513
+ kind: "Secret",
514
+ metadata: {
515
+ name: config.name,
516
+ namespace: config.namespace
517
+ },
518
+ type: "Opaque",
519
+ data
520
+ };
521
+ return yaml.dump(manifest, { lineWidth: -1, quotingType: '"' });
522
+ }
523
+ function toKubernetesConfigMap(variables, config) {
524
+ const data = {};
525
+ for (const v of variables) {
526
+ data[v.key] = v.value;
527
+ }
528
+ const manifest = {
529
+ apiVersion: "v1",
530
+ kind: "ConfigMap",
531
+ metadata: {
532
+ name: config.name,
533
+ namespace: config.namespace
534
+ },
535
+ data
536
+ };
537
+ return yaml.dump(manifest, { lineWidth: -1, quotingType: '"' });
538
+ }
539
+ function toVercelCLI(variables) {
540
+ if (variables.length === 0) return "# No variables to export";
541
+ const header = `# Vercel CLI commands to add environment variables
542
+ # Run these commands in your project directory
543
+ # Docs: https://vercel.com/docs/cli/env
544
+ # Note: You may need to run 'vercel login' first
545
+
546
+ `;
547
+ const commands = variables.map((v) => {
548
+ if (v.value.includes("\n")) {
549
+ const escapedValue = v.value.replace(/'/g, "'\\''");
550
+ return `vercel env add ${v.key} production << 'EOF'
551
+ ${escapedValue}
552
+ EOF`;
553
+ } else {
554
+ const escapedValue = v.value.replace(/'/g, "'\\''");
555
+ return `echo '${escapedValue}' | vercel env add ${v.key} production`;
556
+ }
557
+ }).join("\n\n");
558
+ return header + commands;
559
+ }
560
+ function toRailwayCLI(variables) {
561
+ if (variables.length === 0) return "# No variables to export";
562
+ const header = `# Railway CLI commands to set environment variables
563
+ # Run these commands in your project directory
564
+ # Docs: https://docs.railway.app/reference/cli-api#variables-set
565
+ # Note: You may need to run 'railway login' first
566
+
567
+ `;
568
+ const commands = variables.map((v) => {
569
+ const escapedValue = v.value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
570
+ return `railway variables set ${v.key}="${escapedValue}"`;
571
+ }).join("\n");
572
+ return header + commands;
573
+ }
574
+ function toRenderCLI(variables) {
575
+ if (variables.length === 0) return "# No variables to export";
576
+ const header = `# Render CLI commands to set environment variables
577
+ # Run these commands in your project directory
578
+ # Docs: https://render.com/docs/cli
579
+ # Note: You may need to authenticate first
580
+
581
+ `;
582
+ const commands = variables.map((v) => {
583
+ const escapedValue = v.value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
584
+ return `render env:set ${v.key}="${escapedValue}"`;
585
+ }).join("\n");
586
+ return header + commands;
587
+ }
588
+ function formatVariables(variables, format, k8sConfig) {
589
+ switch (format) {
590
+ case "dotenv":
591
+ return toDotEnv(variables);
592
+ case "docker-compose":
593
+ return toDockerCompose(variables);
594
+ case "k8s-secret":
595
+ return toKubernetesSecret(variables, k8sConfig ?? { name: "app-secrets", namespace: "default" });
596
+ case "k8s-configmap":
597
+ return toKubernetesConfigMap(variables, k8sConfig ?? { name: "app-config", namespace: "default" });
598
+ case "vercel":
599
+ return toVercelCLI(variables);
600
+ case "railway":
601
+ return toRailwayCLI(variables);
602
+ case "render":
603
+ return toRenderCLI(variables);
604
+ }
605
+ }
606
+
607
+ // src/lib/config.ts
467
608
  var ConfigSchema = z.object({
468
609
  project_id: z.string().uuid().optional(),
469
610
  project_name: z.string().optional(),
@@ -471,7 +612,11 @@ var ConfigSchema = z.object({
471
612
  environment_id: z.string().uuid().optional(),
472
613
  organization_id: z.string().uuid().optional(),
473
614
  output: z.string().default(".env"),
474
- api_url: z.string().url().optional()
615
+ api_url: z.string().url().optional(),
616
+ format: z.enum(EXPORT_FORMATS).optional(),
617
+ k8s_namespace: z.string().optional(),
618
+ k8s_name: z.string().optional(),
619
+ tags: z.array(z.string()).optional()
475
620
  });
476
621
  var CONFIG_FILENAMES = ["envmanager.json", ".envmanagerrc"];
477
622
  function findConfigFile(startDir = process.cwd()) {
@@ -741,17 +886,23 @@ function resolveAll(variables) {
741
886
  }
742
887
 
743
888
  // src/commands/pull.ts
744
- var pullCommand = new Command4("pull").description("Pull environment variables from EnvManager to local .env file").option("--org <name>", "Organization name (required if you belong to multiple)").option("-e, --environment <name>", 'Environment name (default: from config or "development")').option("-p, --project <id>", "Project ID (default: from config)").option("-o, --output <file>", "Output file path (default: .env)").option("--no-secrets", "Exclude secret values (will be empty)").option("-f, --force", "Overwrite existing file without prompting").option("-r, --resolve-references", "Resolve ${VAR} references to their values").option("-F, --include-fallbacks", "Include fallback values for empty variables").option("-s, --show-sources", "Show value source as inline comments").action(async (options) => {
889
+ var pullCommand = new Command4("pull").description("Pull environment variables from EnvManager to local .env file").option("--org <name>", "Organization name (required if you belong to multiple)").option("-e, --environment <name>", 'Environment name (default: from config or "development")').option("-p, --project <id>", "Project ID (default: from config)").option("-o, --output <file>", "Output file path (default: .env)").option("--no-secrets", "Exclude secret values (will be empty)").option("-f, --force", "Overwrite existing file without prompting").option("-r, --resolve-references", "Resolve ${VAR} references to their values").option("-F, --include-fallbacks", "Include fallback values for empty variables").option("-s, --show-sources", "Show value source as inline comments").option("--format <type>", `Export format (${EXPORT_FORMATS.join(", ")})`).option("--k8s-namespace <ns>", 'Kubernetes namespace (default: "default")').option("--k8s-name <name>", "Kubernetes resource name").option("--tag <tags...>", "Filter by tags (untagged variables always included)").action(async (options) => {
745
890
  const spinner = ora3("Connecting to EnvManager...").start();
746
891
  try {
747
892
  const config = loadConfig();
748
893
  const projectInput = options.project || config?.project_id;
749
894
  const envName = options.environment || config?.environment || "development";
750
- const outputFile = resolve(options.output || ".env");
895
+ const outputFile = resolve(options.output || config?.output || ".env");
751
896
  const includeSecrets = options.secrets !== false;
752
897
  const shouldResolve = options.resolveReferences === true;
753
898
  const shouldFallback = options.includeFallbacks === true;
754
899
  const shouldShowSources = options.showSources === true;
900
+ const formatInput = options.format || config?.format || "dotenv";
901
+ if (!EXPORT_FORMATS.includes(formatInput)) {
902
+ spinner.fail(`Invalid format "${formatInput}". Valid formats: ${EXPORT_FORMATS.join(", ")}`);
903
+ process.exit(1);
904
+ }
905
+ const format = formatInput;
755
906
  if (!projectInput) {
756
907
  spinner.fail("No project specified");
757
908
  console.log(chalk4.yellow("\nSpecify a project with --project <id-or-name> or create envmanager.json"));
@@ -807,7 +958,11 @@ File ${outputFile} already exists.`));
807
958
  console.log(chalk4.gray("Use --force to overwrite."));
808
959
  process.exit(1);
809
960
  }
810
- const vars = variables.sort((a, b) => a.key.localeCompare(b.key));
961
+ const filterTags = options.tag || config?.tags || [];
962
+ const vars = filterTags.length > 0 ? variables.filter((v) => {
963
+ const t = v.tags || [];
964
+ return t.length === 0 || t.some((tag) => filterTags.includes(tag));
965
+ }).sort((a, b) => a.key.localeCompare(b.key)) : variables.sort((a, b) => a.key.localeCompare(b.key));
811
966
  let resolvedMap = null;
812
967
  if (shouldResolve) {
813
968
  spinner.text = "Resolving variable references...";
@@ -828,39 +983,64 @@ File ${outputFile} already exists.`));
828
983
  const resolved = resolveAll(inputs);
829
984
  resolvedMap = new Map(resolved.map((r) => [r.key, r]));
830
985
  }
831
- spinner.text = "Writing .env file...";
832
- const envContent = vars.map((v) => {
986
+ spinner.text = `Writing ${format} output...`;
987
+ const exportVars = vars.map((v) => {
833
988
  let value;
834
- let source = null;
835
989
  if (resolvedMap) {
836
- const resolved = resolvedMap.get(v.key);
837
- value = resolved.resolvedValue;
838
- source = resolved.source;
990
+ value = resolvedMap.get(v.key).resolvedValue;
839
991
  } else if (shouldFallback && (!v.value || v.value === "") && v.fallback_value) {
840
992
  value = v.fallback_value;
841
- source = "fallback";
842
993
  } else {
843
994
  value = v.value || "";
844
995
  }
845
- const needsQuotes = value.includes(" ") || value.includes("\n") || value.includes('"');
846
- const formattedValue = needsQuotes ? `"${value.replace(/"/g, '\\"')}"` : value;
847
- let line = `${v.key}=${formattedValue}`;
848
- if (shouldShowSources && resolvedMap) {
849
- const resolved = resolvedMap.get(v.key);
850
- if (resolved.references.length > 0 && resolved.source !== "empty") {
851
- line += ` # resolved from ${resolved.rawValue ?? v.value ?? ""}`;
852
- } else if (resolved.source === "fallback") {
996
+ return { key: v.key, value, isSecret: v.is_secret };
997
+ });
998
+ let content;
999
+ if (format === "dotenv" && (shouldShowSources || shouldResolve)) {
1000
+ content = vars.map((v) => {
1001
+ let value;
1002
+ let source = null;
1003
+ if (resolvedMap) {
1004
+ const resolved = resolvedMap.get(v.key);
1005
+ value = resolved.resolvedValue;
1006
+ source = resolved.source;
1007
+ } else if (shouldFallback && (!v.value || v.value === "") && v.fallback_value) {
1008
+ value = v.fallback_value;
1009
+ source = "fallback";
1010
+ } else {
1011
+ value = v.value || "";
1012
+ }
1013
+ const needsQuotes = value.includes(" ") || value.includes("\n") || value.includes('"');
1014
+ const formattedValue = needsQuotes ? `"${value.replace(/"/g, '\\"')}"` : value;
1015
+ let line = `${v.key}=${formattedValue}`;
1016
+ if (shouldShowSources && resolvedMap) {
1017
+ const resolved = resolvedMap.get(v.key);
1018
+ if (resolved.references.length > 0 && resolved.source !== "empty") {
1019
+ line += ` # resolved from ${resolved.rawValue ?? v.value ?? ""}`;
1020
+ } else if (resolved.source === "fallback") {
1021
+ line += ` # fallback value`;
1022
+ }
1023
+ } else if (shouldShowSources && source === "fallback") {
853
1024
  line += ` # fallback value`;
854
1025
  }
855
- } else if (shouldShowSources && source === "fallback") {
856
- line += ` # fallback value`;
1026
+ return line;
1027
+ }).join("\n");
1028
+ } else {
1029
+ let k8sConfig;
1030
+ if (format === "k8s-secret" || format === "k8s-configmap") {
1031
+ const defaultName = format === "k8s-secret" ? "app-secrets" : "app-config";
1032
+ k8sConfig = {
1033
+ name: sanitizeK8sName(options.k8sName || config?.k8s_name || defaultName),
1034
+ namespace: options.k8sNamespace || config?.k8s_namespace || "default"
1035
+ };
857
1036
  }
858
- return line;
859
- }).join("\n");
860
- writeFileSync2(outputFile, envContent + "\n");
1037
+ content = formatVariables(exportVars, format, k8sConfig);
1038
+ }
1039
+ writeFileSync2(outputFile, content + "\n");
861
1040
  const secretCount = vars.filter((v) => v.is_secret).length;
862
1041
  const plainCount = vars.length - secretCount;
863
- spinner.succeed(`Pulled ${variables.length} variables to ${outputFile}`);
1042
+ const tagInfo = filterTags.length > 0 ? ` (tags: ${filterTags.join(", ")})` : "";
1043
+ spinner.succeed(`Pulled ${vars.length} variables to ${outputFile} (${format})${tagInfo}`);
864
1044
  console.log(chalk4.gray(` ${plainCount} plain, ${secretCount} secrets`));
865
1045
  client.rpc("log_variable_access", {
866
1046
  p_environment_id: environmentId,
@@ -1166,7 +1346,7 @@ import chalk7 from "chalk";
1166
1346
  import ora5 from "ora";
1167
1347
  import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
1168
1348
  import { resolve as resolve3 } from "path";
1169
- var diffCommand = new Command6("diff").description("Show differences between local .env and EnvManager").option("--org <name>", "Organization name (required if you belong to multiple)").option("-e, --environment <name>", 'Environment name (default: from config or "development")').option("-p, --project <id>", "Project ID (default: from config)").option("-i, --input <file>", "Input file path (default: .env)").option("--keys-only", "Only show key names, not values").action(async (options) => {
1349
+ var diffCommand = new Command6("diff").description("Show differences between local .env and EnvManager").option("--org <name>", "Organization name (required if you belong to multiple)").option("-e, --environment <name>", 'Environment name (default: from config or "development")').option("-p, --project <id>", "Project ID (default: from config)").option("-i, --input <file>", "Input file path (default: .env)").option("--keys-only", "Only show key names, not values").option("--tag <tags...>", "Filter by tags (untagged variables always included)").action(async (options) => {
1170
1350
  const spinner = ora5("Comparing...").start();
1171
1351
  try {
1172
1352
  const config = loadConfig();
@@ -1218,8 +1398,13 @@ var diffCommand = new Command6("diff").description("Show differences between loc
1218
1398
  console.error(chalk7.red(varError.message));
1219
1399
  process.exit(1);
1220
1400
  }
1401
+ const filterTags = options.tag || config?.tags || [];
1402
+ const filteredRemoteData = filterTags.length > 0 ? (remoteVarsData || []).filter((v) => {
1403
+ const t = v.tags || [];
1404
+ return t.length === 0 || t.some((tag) => filterTags.includes(tag));
1405
+ }) : remoteVarsData || [];
1221
1406
  const remoteVars = /* @__PURE__ */ new Map();
1222
- for (const v of remoteVarsData || []) {
1407
+ for (const v of filteredRemoteData) {
1223
1408
  remoteVars.set(v.key, v);
1224
1409
  }
1225
1410
  spinner.stop();
@@ -1703,7 +1888,7 @@ function mergeWithRemote(local, remoteVariables, strategy) {
1703
1888
  }
1704
1889
 
1705
1890
  // src/commands/dev.ts
1706
- var devCommand = new Command9("dev").description("Start real-time sync daemon - watches for remote variable changes").option("--org <name>", "Organization name (required if you belong to multiple)").option("-e, --environment <name>", 'Environment name (default: from config or "development")').option("-p, --project <id>", "Project ID (default: from config)").option("--output <file>", "Output file path (default: .env)").option("--no-watch", "Disable local file watching").option("--strategy <type>", "Merge strategy: remote_wins, local_wins, merge_new (default: remote_wins)", "remote_wins").action(async (options) => {
1891
+ var devCommand = new Command9("dev").description("Start real-time sync daemon - watches for remote variable changes").option("--org <name>", "Organization name (required if you belong to multiple)").option("-e, --environment <name>", 'Environment name (default: from config or "development")').option("-p, --project <id>", "Project ID (default: from config)").option("--output <file>", "Output file path (default: .env)").option("--no-watch", "Disable local file watching").option("--strategy <type>", "Merge strategy: remote_wins, local_wins, merge_new (default: remote_wins)", "remote_wins").option("--tag <tags...>", "Filter by tags (untagged variables always included)").action(async (options) => {
1707
1892
  const spinner = ora7("Starting dev mode...").start();
1708
1893
  try {
1709
1894
  const config = loadConfig();
@@ -1740,8 +1925,16 @@ var devCommand = new Command9("dev").description("Start real-time sync daemon -
1740
1925
  process.exit(1);
1741
1926
  }
1742
1927
  const environmentId = environment.id;
1928
+ const filterTags = options.tag || config?.tags || [];
1929
+ const applyTagFilter = (vars) => {
1930
+ if (filterTags.length === 0) return vars;
1931
+ return vars.filter((v) => {
1932
+ const t = v.tags || [];
1933
+ return t.length === 0 || t.some((tag) => filterTags.includes(tag));
1934
+ });
1935
+ };
1743
1936
  spinner.text = "Performing initial sync...";
1744
- const remoteVariables = await fetchAllVariables(environmentId, true);
1937
+ const remoteVariables = applyTagFilter(await fetchAllVariables(environmentId, true));
1745
1938
  let localVariables = /* @__PURE__ */ new Map();
1746
1939
  if (existsSync7(outputFile)) {
1747
1940
  const content = readFileSync6(outputFile, "utf-8");
@@ -1758,7 +1951,7 @@ var devCommand = new Command9("dev").description("Start real-time sync daemon -
1758
1951
  let isPaused = false;
1759
1952
  let lastRemoteKeys = null;
1760
1953
  async function syncRemoteToLocal(silent = false) {
1761
- const updatedVariables = await fetchAllVariables(environmentId, true);
1954
+ const updatedVariables = applyTagFilter(await fetchAllVariables(environmentId, true));
1762
1955
  const currentLocal = /* @__PURE__ */ new Map();
1763
1956
  if (existsSync7(outputFile)) {
1764
1957
  const content = readFileSync6(outputFile, "utf-8");
@@ -2003,9 +2196,9 @@ function validateAgainstTemplate(template, env) {
2003
2196
  }
2004
2197
 
2005
2198
  // src/lib/template-yaml.ts
2006
- import yaml from "js-yaml";
2199
+ import yaml2 from "js-yaml";
2007
2200
  function parseYamlTemplate(content) {
2008
- const parsed = yaml.load(content);
2201
+ const parsed = yaml2.load(content);
2009
2202
  if (!parsed || typeof parsed !== "object") {
2010
2203
  throw new Error("Invalid YAML template: must be an object");
2011
2204
  }
@@ -2150,7 +2343,7 @@ function generateYamlTemplate(variables, options = {}) {
2150
2343
  vars[v.key] = varConfig;
2151
2344
  }
2152
2345
  template.variables = vars;
2153
- return yaml.dump(template, { lineWidth: -1, quotingType: '"' });
2346
+ return yaml2.dump(template, { lineWidth: -1, quotingType: '"' });
2154
2347
  }
2155
2348
 
2156
2349
  // src/commands/init.ts