@envmanager-cli/cli 0.1.1 → 0.1.3

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.
@@ -339,7 +339,7 @@ import ora2 from "ora";
339
339
  // src/lib/client.ts
340
340
  import { createClient as createSupabaseClient } from "@supabase/supabase-js";
341
341
  var DEFAULT_API_URL2 = "https://rhopfaburfflrdwpowcd.supabase.co";
342
- var DEFAULT_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJob3BmYWJ1cmZmbHJkd3Bvd2NkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Mjk0NjkxNzMsImV4cCI6MjA0NTA0NTE3M30.hpEuNqNRJcbCQ_F5u_u8SYABC0Zr_pKCdHMn3lPXn4M";
342
+ var DEFAULT_ANON_KEY = "sb_publishable_Y2EpPiIN3KPjQMc1GLVXjw__ghRVLC4";
343
343
  var LOCAL_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0";
344
344
  function getApiUrl2() {
345
345
  return process.env.ENVMANAGER_API_URL || getStoredApiUrl() || DEFAULT_API_URL2;
@@ -506,12 +506,16 @@ function getConfigPath() {
506
506
 
507
507
  // src/lib/resolve.ts
508
508
  var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
509
+ function cleanProjectInput(input) {
510
+ return input.startsWith("#") ? input.slice(1) : input;
511
+ }
509
512
  async function resolveProjectId(input, client, organizationId) {
510
- if (UUID_REGEX.test(input)) {
511
- return input;
513
+ const cleaned = cleanProjectInput(input);
514
+ if (UUID_REGEX.test(cleaned)) {
515
+ return cleaned;
512
516
  }
513
- if (/^\d+$/.test(input)) {
514
- const friendlyId = parseInt(input, 10);
517
+ if (/^\d+$/.test(cleaned)) {
518
+ const friendlyId = parseInt(cleaned, 10);
515
519
  if (friendlyId < 1) {
516
520
  throw new Error("Project ID must be 1 or greater");
517
521
  }
@@ -521,13 +525,13 @@ async function resolveProjectId(input, client, organizationId) {
521
525
  }
522
526
  return data2.id;
523
527
  }
524
- const { data, error } = await client.from("projects").select("id").eq("organization_id", organizationId).ilike("name", input).single();
528
+ const { data, error } = await client.from("projects").select("id").eq("organization_id", organizationId).ilike("name", cleaned).single();
525
529
  if (error || !data) {
526
530
  throw new Error(`Project "${input}" not found in this organization`);
527
531
  }
528
532
  return data.id;
529
533
  }
530
- async function resolveOrganizationId(input, client) {
534
+ async function resolveOrganizationId(input, client, projectHint) {
531
535
  const { data: memberships, error: memberError } = await client.from("organization_members").select("organization_id, organizations(id, name)");
532
536
  if (memberError || !memberships || memberships.length === 0) {
533
537
  throw new Error("No organizations found. Create one at envmanager.dev");
@@ -535,25 +539,204 @@ async function resolveOrganizationId(input, client) {
535
539
  if (memberships.length === 1 && !input) {
536
540
  return memberships[0].organization_id;
537
541
  }
538
- if (!input) {
539
- const orgList = memberships.map((m) => {
542
+ if (input) {
543
+ const match = memberships.find((m) => {
540
544
  const org = m.organizations;
541
- return org?.name || "Unknown";
542
- }).join(", ");
543
- throw new Error(`Multiple organizations found. Use --org to specify: ${orgList}`);
545
+ return org?.name?.toLowerCase() === input.toLowerCase();
546
+ });
547
+ if (!match) {
548
+ throw new Error(`Organization "${input}" not found or you don't have access`);
549
+ }
550
+ return match.organization_id;
551
+ }
552
+ if (projectHint) {
553
+ const orgId = await detectOrgFromProject(projectHint, client, memberships);
554
+ if (orgId) return orgId;
544
555
  }
545
- const match = memberships.find((m) => {
556
+ const orgList = memberships.map((m) => {
546
557
  const org = m.organizations;
547
- return org?.name?.toLowerCase() === input.toLowerCase();
558
+ return org?.name || "Unknown";
559
+ }).join(", ");
560
+ throw new Error(`Multiple organizations found. Use --org to specify: ${orgList}`);
561
+ }
562
+ async function detectOrgFromProject(projectInput, client, memberships) {
563
+ const cleaned = cleanProjectInput(projectInput);
564
+ const orgIds = memberships.map((m) => m.organization_id);
565
+ if (UUID_REGEX.test(cleaned)) {
566
+ const { data: data2 } = await client.from("projects").select("organization_id").eq("id", cleaned).in("organization_id", orgIds).single();
567
+ return data2?.organization_id ?? null;
568
+ }
569
+ if (/^\d+$/.test(cleaned)) {
570
+ const friendlyId = parseInt(cleaned, 10);
571
+ const { data: data2 } = await client.from("projects").select("organization_id").in("organization_id", orgIds).eq("friendly_id", friendlyId);
572
+ if (data2 && data2.length === 1) return data2[0].organization_id;
573
+ return null;
574
+ }
575
+ const { data } = await client.from("projects").select("organization_id").in("organization_id", orgIds).ilike("name", cleaned);
576
+ if (data && data.length === 1) return data[0].organization_id;
577
+ return null;
578
+ }
579
+
580
+ // src/lib/variable-references.ts
581
+ var REFERENCE_PATTERN = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
582
+ var MAX_DEPTH = 10;
583
+ function parseReferences(value) {
584
+ const cleaned = value.replace(/\\\$\{/g, "___ESCAPED___");
585
+ const refs = [];
586
+ const pattern = new RegExp(REFERENCE_PATTERN.source, "g");
587
+ let match;
588
+ while ((match = pattern.exec(cleaned)) !== null) {
589
+ refs.push(match[1]);
590
+ }
591
+ return refs;
592
+ }
593
+ function detectCircularReferences(variables) {
594
+ const graph = /* @__PURE__ */ new Map();
595
+ for (const v of variables) {
596
+ const base = v.value || v.fallbackValue || "";
597
+ graph.set(v.key, parseReferences(base));
598
+ }
599
+ const cycles = [];
600
+ const visited = /* @__PURE__ */ new Set();
601
+ const inStack = /* @__PURE__ */ new Set();
602
+ function dfs(key, stack) {
603
+ if (inStack.has(key)) {
604
+ const cycleStart = stack.indexOf(key);
605
+ cycles.push(stack.slice(cycleStart));
606
+ return;
607
+ }
608
+ if (visited.has(key)) return;
609
+ visited.add(key);
610
+ inStack.add(key);
611
+ stack.push(key);
612
+ const deps = graph.get(key) || [];
613
+ for (const dep of deps) {
614
+ if (graph.has(dep)) {
615
+ dfs(dep, stack);
616
+ }
617
+ }
618
+ stack.pop();
619
+ inStack.delete(key);
620
+ }
621
+ for (const key of graph.keys()) {
622
+ dfs(key, []);
623
+ }
624
+ return cycles;
625
+ }
626
+ function resolveValue(key, variables, depth = 0, _stack = /* @__PURE__ */ new Set()) {
627
+ const variable = variables.get(key);
628
+ if (!variable) {
629
+ return {
630
+ key,
631
+ rawValue: null,
632
+ resolvedValue: "",
633
+ source: "empty",
634
+ references: [],
635
+ referencedBy: [],
636
+ unresolvedRefs: [key],
637
+ hasCircularRef: false
638
+ };
639
+ }
640
+ if (variable.isSecret) {
641
+ const base2 = variable.value ?? variable.fallbackValue ?? "";
642
+ const source2 = variable.value ? "explicit" : variable.fallbackValue ? "fallback" : "empty";
643
+ return {
644
+ key,
645
+ rawValue: variable.value,
646
+ resolvedValue: base2,
647
+ source: source2,
648
+ references: parseReferences(base2),
649
+ referencedBy: [],
650
+ unresolvedRefs: [],
651
+ hasCircularRef: false
652
+ };
653
+ }
654
+ let base;
655
+ let source;
656
+ if (variable.value !== null && variable.value !== void 0 && variable.value !== "") {
657
+ base = variable.value;
658
+ source = "explicit";
659
+ } else if (variable.fallbackValue !== null && variable.fallbackValue !== void 0 && variable.fallbackValue !== "") {
660
+ base = variable.fallbackValue;
661
+ source = "fallback";
662
+ } else {
663
+ return {
664
+ key,
665
+ rawValue: variable.value,
666
+ resolvedValue: "",
667
+ source: "empty",
668
+ references: [],
669
+ referencedBy: [],
670
+ unresolvedRefs: [],
671
+ hasCircularRef: false
672
+ };
673
+ }
674
+ const references = parseReferences(base);
675
+ const unresolvedRefs = [];
676
+ let hasCircularRef = false;
677
+ let resolved = base.replace(/\\\$\{/g, "___ESCAPED___");
678
+ const pattern = new RegExp(REFERENCE_PATTERN.source, "g");
679
+ resolved = resolved.replace(pattern, (fullMatch, refKey) => {
680
+ if (_stack.has(refKey)) {
681
+ hasCircularRef = true;
682
+ unresolvedRefs.push(refKey);
683
+ return fullMatch;
684
+ }
685
+ if (depth >= MAX_DEPTH) {
686
+ unresolvedRefs.push(refKey);
687
+ return fullMatch;
688
+ }
689
+ if (!variables.has(refKey)) {
690
+ unresolvedRefs.push(refKey);
691
+ return fullMatch;
692
+ }
693
+ const newStack = new Set(_stack);
694
+ newStack.add(key);
695
+ const inner = resolveValue(refKey, variables, depth + 1, newStack);
696
+ if (inner.hasCircularRef) {
697
+ hasCircularRef = true;
698
+ }
699
+ unresolvedRefs.push(...inner.unresolvedRefs);
700
+ return inner.resolvedValue;
548
701
  });
549
- if (!match) {
550
- throw new Error(`Organization "${input}" not found or you don't have access`);
702
+ resolved = resolved.replace(/___ESCAPED___/g, "${");
703
+ return {
704
+ key,
705
+ rawValue: variable.value,
706
+ resolvedValue: resolved,
707
+ source,
708
+ references,
709
+ referencedBy: [],
710
+ unresolvedRefs: [...new Set(unresolvedRefs)],
711
+ hasCircularRef
712
+ };
713
+ }
714
+ function resolveAll(variables) {
715
+ const varMap = /* @__PURE__ */ new Map();
716
+ for (const v of variables) {
717
+ varMap.set(v.key, v);
718
+ }
719
+ const results = variables.map((v) => resolveValue(v.key, varMap));
720
+ const referencedByMap = /* @__PURE__ */ new Map();
721
+ for (const v of variables) {
722
+ referencedByMap.set(v.key, []);
723
+ }
724
+ for (const result of results) {
725
+ for (const ref of result.references) {
726
+ const existing = referencedByMap.get(ref);
727
+ if (existing) {
728
+ existing.push(result.key);
729
+ }
730
+ }
551
731
  }
552
- return match.organization_id;
732
+ for (const result of results) {
733
+ result.referencedBy = referencedByMap.get(result.key) || [];
734
+ }
735
+ return results;
553
736
  }
554
737
 
555
738
  // src/commands/pull.ts
556
- 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").action(async (options) => {
739
+ 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) => {
557
740
  const spinner = ora3("Connecting to EnvManager...").start();
558
741
  try {
559
742
  const config = loadConfig();
@@ -561,6 +744,9 @@ var pullCommand = new Command4("pull").description("Pull environment variables f
561
744
  const envName = options.environment || config?.environment || "development";
562
745
  const outputFile = resolve(options.output || ".env");
563
746
  const includeSecrets = options.secrets !== false;
747
+ const shouldResolve = options.resolveReferences === true;
748
+ const shouldFallback = options.includeFallbacks === true;
749
+ const shouldShowSources = options.showSources === true;
564
750
  if (!projectInput) {
565
751
  spinner.fail("No project specified");
566
752
  console.log(chalk4.yellow("\nSpecify a project with --project <id-or-name> or create envmanager.json"));
@@ -570,7 +756,7 @@ var pullCommand = new Command4("pull").description("Pull environment variables f
570
756
  const client = await createClient();
571
757
  let organizationId;
572
758
  try {
573
- organizationId = await resolveOrganizationId(options.org, client);
759
+ organizationId = await resolveOrganizationId(options.org, client, projectInput);
574
760
  } catch (error) {
575
761
  spinner.fail(error instanceof Error ? error.message : "Failed to resolve organization");
576
762
  process.exit(1);
@@ -593,11 +779,13 @@ var pullCommand = new Command4("pull").description("Pull environment variables f
593
779
  }
594
780
  const environmentId = environments.id;
595
781
  spinner.text = "Fetching variables...";
596
- const { data: variables, error: varError } = await client.rpc("get_variables_for_sync", {
782
+ const rpcParams = {
597
783
  p_environment_id: environmentId,
598
784
  p_sync_secrets: includeSecrets,
599
- p_sync_variables: true
600
- });
785
+ p_sync_variables: true,
786
+ p_include_fallbacks: shouldFallback || false
787
+ };
788
+ const { data: variables, error: varError } = await client.rpc("get_variables_for_sync", rpcParams);
601
789
  if (varError) {
602
790
  spinner.fail("Failed to fetch variables");
603
791
  console.error(chalk4.red(varError.message));
@@ -614,18 +802,68 @@ File ${outputFile} already exists.`));
614
802
  console.log(chalk4.gray("Use --force to overwrite."));
615
803
  process.exit(1);
616
804
  }
805
+ const vars = variables.sort((a, b) => a.key.localeCompare(b.key));
806
+ let resolvedMap = null;
807
+ if (shouldResolve) {
808
+ spinner.text = "Resolving variable references...";
809
+ const inputs = vars.map((v) => ({
810
+ key: v.key,
811
+ value: v.value,
812
+ fallbackValue: v.fallback_value ?? null,
813
+ isSecret: v.is_secret
814
+ }));
815
+ const cycles = detectCircularReferences(inputs);
816
+ if (cycles.length > 0) {
817
+ spinner.stop();
818
+ for (const cycle of cycles) {
819
+ console.log(chalk4.yellow(`Warning: circular reference detected: ${cycle.join(" -> ")} -> ${cycle[0]}`));
820
+ }
821
+ spinner.start("Resolving variable references...");
822
+ }
823
+ const resolved = resolveAll(inputs);
824
+ resolvedMap = new Map(resolved.map((r) => [r.key, r]));
825
+ }
617
826
  spinner.text = "Writing .env file...";
618
- const envContent = variables.sort((a, b) => a.key.localeCompare(b.key)).map((v) => {
619
- const value = v.value || "";
827
+ const envContent = vars.map((v) => {
828
+ let value;
829
+ let source = null;
830
+ if (resolvedMap) {
831
+ const resolved = resolvedMap.get(v.key);
832
+ value = resolved.resolvedValue;
833
+ source = resolved.source;
834
+ } else if (shouldFallback && (!v.value || v.value === "") && v.fallback_value) {
835
+ value = v.fallback_value;
836
+ source = "fallback";
837
+ } else {
838
+ value = v.value || "";
839
+ }
620
840
  const needsQuotes = value.includes(" ") || value.includes("\n") || value.includes('"');
621
841
  const formattedValue = needsQuotes ? `"${value.replace(/"/g, '\\"')}"` : value;
622
- return `${v.key}=${formattedValue}`;
842
+ let line = `${v.key}=${formattedValue}`;
843
+ if (shouldShowSources && resolvedMap) {
844
+ const resolved = resolvedMap.get(v.key);
845
+ if (resolved.references.length > 0 && resolved.source !== "empty") {
846
+ line += ` # resolved from ${resolved.rawValue ?? v.value ?? ""}`;
847
+ } else if (resolved.source === "fallback") {
848
+ line += ` # fallback value`;
849
+ }
850
+ } else if (shouldShowSources && source === "fallback") {
851
+ line += ` # fallback value`;
852
+ }
853
+ return line;
623
854
  }).join("\n");
624
855
  writeFileSync2(outputFile, envContent + "\n");
625
- const secretCount = variables.filter((v) => v.is_secret).length;
626
- const plainCount = variables.length - secretCount;
856
+ const secretCount = vars.filter((v) => v.is_secret).length;
857
+ const plainCount = vars.length - secretCount;
627
858
  spinner.succeed(`Pulled ${variables.length} variables to ${outputFile}`);
628
859
  console.log(chalk4.gray(` ${plainCount} plain, ${secretCount} secrets`));
860
+ client.rpc("log_variable_access", {
861
+ p_environment_id: environmentId,
862
+ p_access_type: "cli_pull",
863
+ p_metadata: { cli_version: "0.1.1" }
864
+ }).then(() => {
865
+ }, () => {
866
+ });
629
867
  } catch (error) {
630
868
  spinner.fail("Pull failed");
631
869
  console.error(chalk4.red(error instanceof Error ? error.message : "Unknown error"));
@@ -669,6 +907,110 @@ function parseEnvFileAsArray(content) {
669
907
  return Array.from(map.entries()).map(([key, value]) => ({ key, value }));
670
908
  }
671
909
 
910
+ // src/lib/naming-conventions.ts
911
+ function isScreamingSnakeCase(name) {
912
+ return /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/.test(name);
913
+ }
914
+ function isSnakeCase(name) {
915
+ return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
916
+ }
917
+ function isPascalCase(name) {
918
+ return /^[A-Z][a-zA-Z0-9]*$/.test(name);
919
+ }
920
+ function isCamelCase(name) {
921
+ return /^[a-z][a-zA-Z0-9]*$/.test(name);
922
+ }
923
+ function splitIntoWords(name) {
924
+ if (name.includes("_")) {
925
+ return name.split("_").filter(Boolean);
926
+ }
927
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").split("_").filter(Boolean);
928
+ }
929
+ function convertToCase(name, targetCase) {
930
+ const words = splitIntoWords(name);
931
+ if (words.length === 0) return name;
932
+ switch (targetCase) {
933
+ case "SCREAMING_SNAKE_CASE":
934
+ return words.map((w) => w.toUpperCase()).join("_");
935
+ case "snake_case":
936
+ return words.map((w) => w.toLowerCase()).join("_");
937
+ case "PascalCase":
938
+ return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
939
+ case "camelCase":
940
+ return words.map((w, i) => {
941
+ if (i === 0) return w.toLowerCase();
942
+ return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase();
943
+ }).join("");
944
+ default:
945
+ return name;
946
+ }
947
+ }
948
+ function validateVariableName(name, config) {
949
+ const issues = [];
950
+ if (config.rules.case) {
951
+ let caseValid = false;
952
+ switch (config.rules.case) {
953
+ case "SCREAMING_SNAKE_CASE":
954
+ caseValid = isScreamingSnakeCase(name);
955
+ break;
956
+ case "snake_case":
957
+ caseValid = isSnakeCase(name);
958
+ break;
959
+ case "PascalCase":
960
+ caseValid = isPascalCase(name);
961
+ break;
962
+ case "camelCase":
963
+ caseValid = isCamelCase(name);
964
+ break;
965
+ }
966
+ if (!caseValid) {
967
+ const suggestion = convertToCase(name, config.rules.case);
968
+ issues.push({
969
+ type: "case",
970
+ message: `Must be ${config.rules.case}`,
971
+ suggestion
972
+ });
973
+ }
974
+ }
975
+ if (config.rules.patterns) {
976
+ for (const pattern of config.rules.patterns) {
977
+ try {
978
+ const regex = new RegExp(pattern.match);
979
+ if (!regex.test(name)) {
980
+ issues.push({
981
+ type: "pattern",
982
+ message: pattern.description,
983
+ suggestion: pattern.example
984
+ });
985
+ }
986
+ } catch {
987
+ }
988
+ }
989
+ }
990
+ if (config.rules.forbidden) {
991
+ for (const rule of config.rules.forbidden) {
992
+ try {
993
+ const regex = new RegExp(rule.match, "i");
994
+ if (regex.test(name)) {
995
+ issues.push({
996
+ type: "forbidden",
997
+ message: rule.reason
998
+ });
999
+ }
1000
+ } catch {
1001
+ }
1002
+ }
1003
+ }
1004
+ const isBlock = config.enforcement_mode === "block";
1005
+ const suggestions = issues.map((i) => i.suggestion).filter((s) => !!s);
1006
+ return {
1007
+ valid: isBlock ? issues.length === 0 : true,
1008
+ errors: isBlock ? issues : [],
1009
+ warnings: isBlock ? [] : issues,
1010
+ suggestions: [...new Set(suggestions)]
1011
+ };
1012
+ }
1013
+
672
1014
  // src/commands/push.ts
673
1015
  var pushCommand = new Command5("push").description("Push local .env file to 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("--secrets <keys>", "Comma-separated list of keys to mark as secrets").option("--all-secrets", "Mark all variables as secrets").option("--dry-run", "Show what would be pushed without making changes").action(async (options) => {
674
1016
  const spinner = ora4("Reading .env file...").start();
@@ -709,7 +1051,7 @@ var pushCommand = new Command5("push").description("Push local .env file to EnvM
709
1051
  const client = await createClient();
710
1052
  let organizationId;
711
1053
  try {
712
- organizationId = await resolveOrganizationId(options.org, client);
1054
+ organizationId = await resolveOrganizationId(options.org, client, projectInput);
713
1055
  } catch (error) {
714
1056
  spinner.fail(error instanceof Error ? error.message : "Failed to resolve organization");
715
1057
  process.exit(1);
@@ -727,32 +1069,84 @@ var pushCommand = new Command5("push").description("Push local .env file to EnvM
727
1069
  spinner.fail(`Environment "${envName}" not found in project`);
728
1070
  process.exit(1);
729
1071
  }
1072
+ spinner.text = "Checking naming conventions...";
1073
+ const { data: namingRules } = await client.from("naming_conventions").select("*").eq("organization_id", organizationId).eq("project_id", projectId).maybeSingle();
1074
+ let namingConfig = null;
1075
+ if (namingRules) {
1076
+ namingConfig = {
1077
+ rules: namingRules.rules,
1078
+ enforcement_mode: namingRules.enforcement_mode,
1079
+ template_name: namingRules.template_name || void 0
1080
+ };
1081
+ } else {
1082
+ const { data: orgRules } = await client.from("naming_conventions").select("*").eq("organization_id", organizationId).is("project_id", null).maybeSingle();
1083
+ if (orgRules) {
1084
+ namingConfig = {
1085
+ rules: orgRules.rules,
1086
+ enforcement_mode: orgRules.enforcement_mode,
1087
+ template_name: orgRules.template_name || void 0
1088
+ };
1089
+ }
1090
+ }
1091
+ if (namingConfig) {
1092
+ const issues = [];
1093
+ for (const v of vars) {
1094
+ const result = validateVariableName(v.key, namingConfig);
1095
+ const allIssues = [...result.errors, ...result.warnings];
1096
+ for (const issue of allIssues) {
1097
+ issues.push({ key: v.key, message: issue.message, suggestion: issue.suggestion });
1098
+ }
1099
+ }
1100
+ if (issues.length > 0) {
1101
+ const isBlock = namingConfig.enforcement_mode === "block";
1102
+ console.log("");
1103
+ console.log(chalk6[isBlock ? "red" : "yellow"](` Naming convention ${isBlock ? "errors" : "warnings"}:`));
1104
+ for (const issue of issues) {
1105
+ const suggestion = issue.suggestion ? chalk6.gray(` \u2192 ${issue.suggestion}`) : "";
1106
+ console.log(chalk6[isBlock ? "red" : "yellow"](` ${issue.key}: ${issue.message}${suggestion}`));
1107
+ }
1108
+ console.log("");
1109
+ if (isBlock) {
1110
+ spinner.fail("Push blocked by naming convention errors");
1111
+ process.exit(1);
1112
+ }
1113
+ }
1114
+ }
730
1115
  spinner.text = "Pushing variables...";
731
- const variablesToInsert = vars.map((v) => ({
1116
+ const markAsSecrets = allSecrets || secretKeys.length > 0;
1117
+ const variablesData = vars.map((v) => ({
732
1118
  key: v.key,
733
- value: v.value,
734
- is_secret: allSecrets || secretKeys.includes(v.key)
1119
+ value: v.value
735
1120
  }));
736
- const { data: result, error: pushError } = await client.rpc("bulk_insert_variables", {
737
- p_environment_id: environment.id,
738
- p_organization_id: organizationId,
739
- p_variables: variablesToInsert,
740
- p_overwrite: true
1121
+ const { data: existingVars } = await client.from("variables").select("key").eq("environment_id", environment.id);
1122
+ const existingKeys = new Set((existingVars || []).map((v) => v.key));
1123
+ const keysToUpdate = variablesData.filter((v) => existingKeys.has(v.key));
1124
+ const keysToInsert = variablesData.filter((v) => !existingKeys.has(v.key));
1125
+ if (keysToUpdate.length > 0) {
1126
+ const { error: deleteError } = await client.from("variables").delete().eq("environment_id", environment.id).in("key", keysToUpdate.map((v) => v.key));
1127
+ if (deleteError) {
1128
+ spinner.fail("Failed to update existing variables");
1129
+ console.error(chalk6.red(deleteError.message));
1130
+ process.exit(1);
1131
+ }
1132
+ }
1133
+ const { error: pushError } = await client.rpc("bulk_insert_variables", {
1134
+ variables_data: variablesData,
1135
+ environment_id_param: environment.id,
1136
+ organization_id_param: organizationId,
1137
+ import_as_secrets: markAsSecrets
741
1138
  });
742
1139
  if (pushError) {
743
1140
  spinner.fail("Push failed");
744
1141
  console.error(chalk6.red(pushError.message));
745
1142
  process.exit(1);
746
1143
  }
747
- const insertedCount = result?.inserted || vars.length;
748
- const updatedCount = result?.updated || 0;
749
- const secretCount = variablesToInsert.filter((v) => v.is_secret).length;
750
1144
  spinner.succeed(`Pushed ${vars.length} variables to ${envName}`);
751
- if (updatedCount > 0) {
752
- console.log(chalk6.gray(` ${insertedCount} inserted, ${updatedCount} updated`));
1145
+ if (keysToUpdate.length > 0) {
1146
+ console.log(chalk6.gray(` ${keysToInsert.length} inserted, ${keysToUpdate.length} updated`));
753
1147
  }
754
- if (secretCount > 0) {
755
- console.log(chalk6.gray(` ${secretCount} marked as secrets`));
1148
+ if (markAsSecrets) {
1149
+ console.log(chalk6.gray(` All marked as secrets`));
756
1150
  }
757
1151
  } catch (error) {
758
1152
  spinner.fail("Push failed");
@@ -789,7 +1183,7 @@ var diffCommand = new Command6("diff").description("Show differences between loc
789
1183
  const client = await createClient();
790
1184
  let organizationId;
791
1185
  try {
792
- organizationId = await resolveOrganizationId(options.org, client);
1186
+ organizationId = await resolveOrganizationId(options.org, client, projectInput);
793
1187
  } catch (error) {
794
1188
  spinner.fail(error instanceof Error ? error.message : "Failed to resolve organization");
795
1189
  process.exit(1);
@@ -811,7 +1205,8 @@ var diffCommand = new Command6("diff").description("Show differences between loc
811
1205
  const { data: remoteVarsData, error: varError } = await client.rpc("get_variables_for_sync", {
812
1206
  p_environment_id: environment.id,
813
1207
  p_sync_secrets: true,
814
- p_sync_variables: true
1208
+ p_sync_variables: true,
1209
+ p_include_fallbacks: false
815
1210
  });
816
1211
  if (varError) {
817
1212
  spinner.fail("Failed to fetch remote variables");
@@ -940,7 +1335,7 @@ var listCommand = new Command7("list").description("List projects, environments,
940
1335
  }
941
1336
  let organizationId;
942
1337
  try {
943
- organizationId = await resolveOrganizationId(options.org, client);
1338
+ organizationId = await resolveOrganizationId(options.org, client, projectInput);
944
1339
  } catch (error) {
945
1340
  spinner.fail(error instanceof Error ? error.message : "Failed to resolve organization");
946
1341
  process.exit(1);
@@ -984,7 +1379,7 @@ var listCommand = new Command7("list").description("List projects, environments,
984
1379
  }
985
1380
  let organizationId;
986
1381
  try {
987
- organizationId = await resolveOrganizationId(options.org, client);
1382
+ organizationId = await resolveOrganizationId(options.org, client, projectInput);
988
1383
  } catch (error) {
989
1384
  spinner.fail(error instanceof Error ? error.message : "Failed to resolve organization");
990
1385
  process.exit(1);
@@ -1150,7 +1545,8 @@ async function fetchAllVariables(environmentId, includeSecrets = true) {
1150
1545
  const { data: variables, error } = await client.rpc("get_variables_for_sync", {
1151
1546
  p_environment_id: environmentId,
1152
1547
  p_sync_secrets: includeSecrets,
1153
- p_sync_variables: true
1548
+ p_sync_variables: true,
1549
+ p_include_fallbacks: false
1154
1550
  });
1155
1551
  if (error) {
1156
1552
  throw new Error(`Failed to fetch variables: ${error.message}`);
@@ -1307,7 +1703,7 @@ var devCommand = new Command9("dev").description("Start real-time sync daemon -
1307
1703
  const client = await createClient();
1308
1704
  let organizationId;
1309
1705
  try {
1310
- organizationId = await resolveOrganizationId(options.org, client);
1706
+ organizationId = await resolveOrganizationId(options.org, client, projectInput);
1311
1707
  } catch (error) {
1312
1708
  spinner.fail(error instanceof Error ? error.message : "Failed to resolve organization");
1313
1709
  process.exit(1);
@@ -1770,7 +2166,8 @@ Environment "${envName}" not found. Available:`));
1770
2166
  const { data: variables, error: varError } = await client.rpc("get_variables_for_sync", {
1771
2167
  p_environment_id: environment.id,
1772
2168
  p_sync_secrets: false,
1773
- p_sync_variables: true
2169
+ p_sync_variables: true,
2170
+ p_include_fallbacks: false
1774
2171
  });
1775
2172
  if (varError) {
1776
2173
  spinner.fail("Failed to fetch variables");
@@ -1884,7 +2281,8 @@ File ${outputPath} already exists. Use --force to overwrite.`));
1884
2281
  const { data: variables, error: varError } = await client.rpc("get_variables_for_sync", {
1885
2282
  p_environment_id: environment.id,
1886
2283
  p_sync_secrets: true,
1887
- p_sync_variables: true
2284
+ p_sync_variables: true,
2285
+ p_include_fallbacks: false
1888
2286
  });
1889
2287
  if (varError) {
1890
2288
  spinner.fail("Failed to fetch variables");
@@ -1975,7 +2373,8 @@ templateCommand.command("sync").description("Compare template with EnvManager an
1975
2373
  const { data: variables, error: varError } = await client.rpc("get_variables_for_sync", {
1976
2374
  p_environment_id: environment.id,
1977
2375
  p_sync_secrets: false,
1978
- p_sync_variables: true
2376
+ p_sync_variables: true,
2377
+ p_include_fallbacks: false
1979
2378
  });
1980
2379
  if (varError) {
1981
2380
  spinner.fail("Failed to fetch variables");
@@ -2283,7 +2682,7 @@ var validateCommand = new Command12("validate").description("Validate local .env
2283
2682
  const client = await createClient();
2284
2683
  let organizationId;
2285
2684
  try {
2286
- organizationId = await resolveOrganizationId(options.org, client);
2685
+ organizationId = await resolveOrganizationId(options.org, client, projectInput);
2287
2686
  } catch (error) {
2288
2687
  spinner.fail(error instanceof Error ? error.message : "Failed to resolve organization");
2289
2688
  process.exit(1);