@envmanager-cli/cli 0.1.8 → 0.1.10

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,10 @@ 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()
475
619
  });
476
620
  var CONFIG_FILENAMES = ["envmanager.json", ".envmanagerrc"];
477
621
  function findConfigFile(startDir = process.cwd()) {
@@ -741,17 +885,23 @@ function resolveAll(variables) {
741
885
  }
742
886
 
743
887
  // 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) => {
888
+ 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").action(async (options) => {
745
889
  const spinner = ora3("Connecting to EnvManager...").start();
746
890
  try {
747
891
  const config = loadConfig();
748
892
  const projectInput = options.project || config?.project_id;
749
893
  const envName = options.environment || config?.environment || "development";
750
- const outputFile = resolve(options.output || ".env");
894
+ const outputFile = resolve(options.output || config?.output || ".env");
751
895
  const includeSecrets = options.secrets !== false;
752
896
  const shouldResolve = options.resolveReferences === true;
753
897
  const shouldFallback = options.includeFallbacks === true;
754
898
  const shouldShowSources = options.showSources === true;
899
+ const formatInput = options.format || config?.format || "dotenv";
900
+ if (!EXPORT_FORMATS.includes(formatInput)) {
901
+ spinner.fail(`Invalid format "${formatInput}". Valid formats: ${EXPORT_FORMATS.join(", ")}`);
902
+ process.exit(1);
903
+ }
904
+ const format = formatInput;
755
905
  if (!projectInput) {
756
906
  spinner.fail("No project specified");
757
907
  console.log(chalk4.yellow("\nSpecify a project with --project <id-or-name> or create envmanager.json"));
@@ -828,39 +978,63 @@ File ${outputFile} already exists.`));
828
978
  const resolved = resolveAll(inputs);
829
979
  resolvedMap = new Map(resolved.map((r) => [r.key, r]));
830
980
  }
831
- spinner.text = "Writing .env file...";
832
- const envContent = vars.map((v) => {
981
+ spinner.text = `Writing ${format} output...`;
982
+ const exportVars = vars.map((v) => {
833
983
  let value;
834
- let source = null;
835
984
  if (resolvedMap) {
836
- const resolved = resolvedMap.get(v.key);
837
- value = resolved.resolvedValue;
838
- source = resolved.source;
985
+ value = resolvedMap.get(v.key).resolvedValue;
839
986
  } else if (shouldFallback && (!v.value || v.value === "") && v.fallback_value) {
840
987
  value = v.fallback_value;
841
- source = "fallback";
842
988
  } else {
843
989
  value = v.value || "";
844
990
  }
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") {
991
+ return { key: v.key, value, isSecret: v.is_secret };
992
+ });
993
+ let content;
994
+ if (format === "dotenv" && (shouldShowSources || shouldResolve)) {
995
+ content = vars.map((v) => {
996
+ let value;
997
+ let source = null;
998
+ if (resolvedMap) {
999
+ const resolved = resolvedMap.get(v.key);
1000
+ value = resolved.resolvedValue;
1001
+ source = resolved.source;
1002
+ } else if (shouldFallback && (!v.value || v.value === "") && v.fallback_value) {
1003
+ value = v.fallback_value;
1004
+ source = "fallback";
1005
+ } else {
1006
+ value = v.value || "";
1007
+ }
1008
+ const needsQuotes = value.includes(" ") || value.includes("\n") || value.includes('"');
1009
+ const formattedValue = needsQuotes ? `"${value.replace(/"/g, '\\"')}"` : value;
1010
+ let line = `${v.key}=${formattedValue}`;
1011
+ if (shouldShowSources && resolvedMap) {
1012
+ const resolved = resolvedMap.get(v.key);
1013
+ if (resolved.references.length > 0 && resolved.source !== "empty") {
1014
+ line += ` # resolved from ${resolved.rawValue ?? v.value ?? ""}`;
1015
+ } else if (resolved.source === "fallback") {
1016
+ line += ` # fallback value`;
1017
+ }
1018
+ } else if (shouldShowSources && source === "fallback") {
853
1019
  line += ` # fallback value`;
854
1020
  }
855
- } else if (shouldShowSources && source === "fallback") {
856
- line += ` # fallback value`;
1021
+ return line;
1022
+ }).join("\n");
1023
+ } else {
1024
+ let k8sConfig;
1025
+ if (format === "k8s-secret" || format === "k8s-configmap") {
1026
+ const defaultName = format === "k8s-secret" ? "app-secrets" : "app-config";
1027
+ k8sConfig = {
1028
+ name: sanitizeK8sName(options.k8sName || config?.k8s_name || defaultName),
1029
+ namespace: options.k8sNamespace || config?.k8s_namespace || "default"
1030
+ };
857
1031
  }
858
- return line;
859
- }).join("\n");
860
- writeFileSync2(outputFile, envContent + "\n");
1032
+ content = formatVariables(exportVars, format, k8sConfig);
1033
+ }
1034
+ writeFileSync2(outputFile, content + "\n");
861
1035
  const secretCount = vars.filter((v) => v.is_secret).length;
862
1036
  const plainCount = vars.length - secretCount;
863
- spinner.succeed(`Pulled ${variables.length} variables to ${outputFile}`);
1037
+ spinner.succeed(`Pulled ${variables.length} variables to ${outputFile} (${format})`);
864
1038
  console.log(chalk4.gray(` ${plainCount} plain, ${secretCount} secrets`));
865
1039
  client.rpc("log_variable_access", {
866
1040
  p_environment_id: environmentId,
@@ -1509,6 +1683,12 @@ async function subscribeToVariableChanges(environmentId, onEvent, onStatus) {
1509
1683
  channel.on("broadcast", { event: "variable_change" }, (payload) => {
1510
1684
  onEvent(payload.payload);
1511
1685
  });
1686
+ channel.on("broadcast", { event: "*" }, (payload) => {
1687
+ const data = payload.payload;
1688
+ if (data?.action && data?.key && data?.environment_id) {
1689
+ onEvent(data);
1690
+ }
1691
+ });
1512
1692
  channel.on("system", { event: "*" }, (status) => {
1513
1693
  if (status.event === "connected") {
1514
1694
  reconnectAttempts = 0;
@@ -1750,6 +1930,38 @@ var devCommand = new Command9("dev").description("Start real-time sync daemon -
1750
1930
  spinner.text = "Connecting to realtime...";
1751
1931
  let fileWatcher = null;
1752
1932
  let isPaused = false;
1933
+ let lastRemoteKeys = null;
1934
+ async function syncRemoteToLocal(silent = false) {
1935
+ const updatedVariables = await fetchAllVariables(environmentId, true);
1936
+ const currentLocal = /* @__PURE__ */ new Map();
1937
+ if (existsSync7(outputFile)) {
1938
+ const content = readFileSync6(outputFile, "utf-8");
1939
+ Object.entries(parseDotenv3(content)).forEach(([k, v]) => {
1940
+ currentLocal.set(k, v);
1941
+ });
1942
+ }
1943
+ const newMerged = mergeWithRemote(currentLocal, updatedVariables, strategy);
1944
+ const remoteKeySig = [...newMerged.entries()].sort().map(([k, v]) => `${k}=${v}`).join("\n");
1945
+ if (remoteKeySig === lastRemoteKeys) return false;
1946
+ lastRemoteKeys = remoteKeySig;
1947
+ if (!silent) {
1948
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
1949
+ for (const key of currentLocal.keys()) {
1950
+ if (!newMerged.has(key)) {
1951
+ console.log(chalk10.red(`[${timestamp}] - ${key}`));
1952
+ }
1953
+ }
1954
+ for (const [key] of newMerged) {
1955
+ if (!currentLocal.has(key)) {
1956
+ console.log(chalk10.green(`[${timestamp}] + ${key}`));
1957
+ }
1958
+ }
1959
+ }
1960
+ writeFileSync3(outputFile, formatEnvFile(newMerged));
1961
+ return true;
1962
+ }
1963
+ const initialKeys = [...merged.entries()].sort().map(([k, v]) => `${k}=${v}`).join("\n");
1964
+ lastRemoteKeys = initialKeys;
1753
1965
  const subscription = await subscribeToVariableChanges(
1754
1966
  environmentId,
1755
1967
  async (event) => {
@@ -1772,16 +1984,7 @@ var devCommand = new Command9("dev").description("Start real-time sync daemon -
1772
1984
  }
1773
1985
  isPaused = true;
1774
1986
  try {
1775
- const updatedVariables = await fetchAllVariables(environmentId, true);
1776
- const currentLocal = /* @__PURE__ */ new Map();
1777
- if (existsSync7(outputFile)) {
1778
- const content = readFileSync6(outputFile, "utf-8");
1779
- Object.entries(parseDotenv3(content)).forEach(([k, v]) => {
1780
- currentLocal.set(k, v);
1781
- });
1782
- }
1783
- const newMerged = mergeWithRemote(currentLocal, updatedVariables, strategy);
1784
- writeFileSync3(outputFile, formatEnvFile(newMerged));
1987
+ await syncRemoteToLocal(true);
1785
1988
  } finally {
1786
1989
  isPaused = false;
1787
1990
  }
@@ -1828,6 +2031,17 @@ Realtime error: ${message || ""}`));
1828
2031
  console.log(chalk10.gray("Watching for changes. Press Ctrl+C to stop."));
1829
2032
  console.log("");
1830
2033
  let isCleaningUp = false;
2034
+ const POLL_INTERVAL_MS = 5e3;
2035
+ const pollInterval = setInterval(async () => {
2036
+ if (isPaused) return;
2037
+ isPaused = true;
2038
+ try {
2039
+ await syncRemoteToLocal();
2040
+ } catch {
2041
+ } finally {
2042
+ isPaused = false;
2043
+ }
2044
+ }, POLL_INTERVAL_MS);
1831
2045
  const TOKEN_REFRESH_INTERVAL_MS = 30 * 60 * 1e3;
1832
2046
  const refreshInterval = setInterval(async () => {
1833
2047
  try {
@@ -1842,6 +2056,7 @@ Realtime error: ${message || ""}`));
1842
2056
  }
1843
2057
  isCleaningUp = true;
1844
2058
  console.log(chalk10.gray("\nStopping dev mode..."));
2059
+ clearInterval(pollInterval);
1845
2060
  clearInterval(refreshInterval);
1846
2061
  fileWatcher?.stop();
1847
2062
  subscription.unsubscribe().catch(() => {
@@ -1962,9 +2177,9 @@ function validateAgainstTemplate(template, env) {
1962
2177
  }
1963
2178
 
1964
2179
  // src/lib/template-yaml.ts
1965
- import yaml from "js-yaml";
2180
+ import yaml2 from "js-yaml";
1966
2181
  function parseYamlTemplate(content) {
1967
- const parsed = yaml.load(content);
2182
+ const parsed = yaml2.load(content);
1968
2183
  if (!parsed || typeof parsed !== "object") {
1969
2184
  throw new Error("Invalid YAML template: must be an object");
1970
2185
  }
@@ -2109,7 +2324,7 @@ function generateYamlTemplate(variables, options = {}) {
2109
2324
  vars[v.key] = varConfig;
2110
2325
  }
2111
2326
  template.variables = vars;
2112
- return yaml.dump(template, { lineWidth: -1, quotingType: '"' });
2327
+ return yaml2.dump(template, { lineWidth: -1, quotingType: '"' });
2113
2328
  }
2114
2329
 
2115
2330
  // src/commands/init.ts