@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.
- package/README.md +1 -1
- package/dist/bin/envmanager.js +452 -53
- package/dist/bin/envmanager.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/bin/envmanager.js
CHANGED
|
@@ -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 = "
|
|
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
|
-
|
|
511
|
-
|
|
513
|
+
const cleaned = cleanProjectInput(input);
|
|
514
|
+
if (UUID_REGEX.test(cleaned)) {
|
|
515
|
+
return cleaned;
|
|
512
516
|
}
|
|
513
|
-
if (/^\d+$/.test(
|
|
514
|
-
const friendlyId = parseInt(
|
|
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",
|
|
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 (
|
|
539
|
-
const
|
|
542
|
+
if (input) {
|
|
543
|
+
const match = memberships.find((m) => {
|
|
540
544
|
const org = m.organizations;
|
|
541
|
-
return org?.name
|
|
542
|
-
})
|
|
543
|
-
|
|
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
|
|
556
|
+
const orgList = memberships.map((m) => {
|
|
546
557
|
const org = m.organizations;
|
|
547
|
-
return org?.name
|
|
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
|
-
|
|
550
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
619
|
-
|
|
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
|
-
|
|
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 =
|
|
626
|
-
const plainCount =
|
|
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
|
|
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:
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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 (
|
|
752
|
-
console.log(chalk6.gray(` ${
|
|
1145
|
+
if (keysToUpdate.length > 0) {
|
|
1146
|
+
console.log(chalk6.gray(` ${keysToInsert.length} inserted, ${keysToUpdate.length} updated`));
|
|
753
1147
|
}
|
|
754
|
-
if (
|
|
755
|
-
console.log(chalk6.gray(`
|
|
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);
|