@envmanager-cli/cli 0.1.10 → 0.2.1

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.
@@ -615,7 +615,9 @@ var ConfigSchema = z.object({
615
615
  api_url: z.string().url().optional(),
616
616
  format: z.enum(EXPORT_FORMATS).optional(),
617
617
  k8s_namespace: z.string().optional(),
618
- k8s_name: z.string().optional()
618
+ k8s_name: z.string().optional(),
619
+ tags: z.array(z.string()).optional(),
620
+ service: z.string().optional()
619
621
  });
620
622
  var CONFIG_FILENAMES = ["envmanager.json", ".envmanagerrc"];
621
623
  function findConfigFile(startDir = process.cwd()) {
@@ -725,6 +727,15 @@ async function detectOrgFromProject(projectInput, client, memberships) {
725
727
  if (data && data.length === 1) return data[0].organization_id;
726
728
  return null;
727
729
  }
730
+ async function resolveServiceId(serviceName, client, projectId) {
731
+ const { data, error } = await client.from("services").select("id").eq("project_id", projectId).ilike("name", serviceName).single();
732
+ if (error || !data) {
733
+ const { data: allServices } = await client.from("services").select("name").eq("project_id", projectId).order("sort_order");
734
+ const available = allServices?.map((s) => s.name).join(", ") || "none";
735
+ throw new Error(`Service "${serviceName}" not found in project. Available services: ${available}`);
736
+ }
737
+ return data.id;
738
+ }
728
739
 
729
740
  // src/lib/variable-references.ts
730
741
  var REFERENCE_PATTERN = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
@@ -885,7 +896,7 @@ function resolveAll(variables) {
885
896
  }
886
897
 
887
898
  // src/commands/pull.ts
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) => {
899
+ 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)").option("--service <name>", "Filter by service name").action(async (options) => {
889
900
  const spinner = ora3("Connecting to EnvManager...").start();
890
901
  try {
891
902
  const config = loadConfig();
@@ -923,6 +934,16 @@ var pullCommand = new Command4("pull").description("Pull environment variables f
923
934
  spinner.fail(error instanceof Error ? error.message : "Failed to resolve project");
924
935
  process.exit(1);
925
936
  }
937
+ const serviceName = options.service || config?.service;
938
+ let serviceId;
939
+ if (serviceName) {
940
+ try {
941
+ serviceId = await resolveServiceId(serviceName, client, projectId);
942
+ } catch (error) {
943
+ spinner.fail(error instanceof Error ? error.message : "Failed to resolve service");
944
+ process.exit(1);
945
+ }
946
+ }
926
947
  spinner.text = "Fetching environment...";
927
948
  const { data: environments, error: envError } = await client.from("environments").select("id, name").eq("project_id", projectId).ilike("name", envName).single();
928
949
  if (envError || !environments) {
@@ -938,7 +959,8 @@ var pullCommand = new Command4("pull").description("Pull environment variables f
938
959
  p_environment_id: environmentId,
939
960
  p_sync_secrets: includeSecrets,
940
961
  p_sync_variables: true,
941
- p_include_fallbacks: shouldFallback || false
962
+ p_include_fallbacks: shouldFallback || false,
963
+ ...serviceId && { p_service_id: serviceId }
942
964
  };
943
965
  const { data: variables, error: varError } = await client.rpc("get_variables_for_sync", rpcParams);
944
966
  if (varError) {
@@ -957,7 +979,11 @@ File ${outputFile} already exists.`));
957
979
  console.log(chalk4.gray("Use --force to overwrite."));
958
980
  process.exit(1);
959
981
  }
960
- const vars = variables.sort((a, b) => a.key.localeCompare(b.key));
982
+ const filterTags = options.tag || config?.tags || [];
983
+ const vars = filterTags.length > 0 ? variables.filter((v) => {
984
+ const t = v.tags || [];
985
+ return t.length === 0 || t.some((tag) => filterTags.includes(tag));
986
+ }).sort((a, b) => a.key.localeCompare(b.key)) : variables.sort((a, b) => a.key.localeCompare(b.key));
961
987
  let resolvedMap = null;
962
988
  if (shouldResolve) {
963
989
  spinner.text = "Resolving variable references...";
@@ -1034,7 +1060,9 @@ File ${outputFile} already exists.`));
1034
1060
  writeFileSync2(outputFile, content + "\n");
1035
1061
  const secretCount = vars.filter((v) => v.is_secret).length;
1036
1062
  const plainCount = vars.length - secretCount;
1037
- spinner.succeed(`Pulled ${variables.length} variables to ${outputFile} (${format})`);
1063
+ const serviceInfo = serviceName ? ` (service: ${serviceName})` : "";
1064
+ const tagInfo = filterTags.length > 0 ? ` (tags: ${filterTags.join(", ")})` : "";
1065
+ spinner.succeed(`Pulled ${vars.length} variables to ${outputFile} (${format})${serviceInfo}${tagInfo}`);
1038
1066
  console.log(chalk4.gray(` ${plainCount} plain, ${secretCount} secrets`));
1039
1067
  client.rpc("log_variable_access", {
1040
1068
  p_environment_id: environmentId,
@@ -1191,7 +1219,7 @@ function validateVariableName(name, config) {
1191
1219
  }
1192
1220
 
1193
1221
  // src/commands/push.ts
1194
- 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) => {
1222
+ 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("--service <name>", "Push to specific service").option("--dry-run", "Show what would be pushed without making changes").action(async (options) => {
1195
1223
  const spinner = ora4("Reading .env file...").start();
1196
1224
  try {
1197
1225
  const config = loadConfig();
@@ -1242,6 +1270,16 @@ var pushCommand = new Command5("push").description("Push local .env file to EnvM
1242
1270
  spinner.fail(error instanceof Error ? error.message : "Failed to resolve project");
1243
1271
  process.exit(1);
1244
1272
  }
1273
+ const serviceName = options.service || config?.service;
1274
+ let serviceId;
1275
+ if (serviceName) {
1276
+ try {
1277
+ serviceId = await resolveServiceId(serviceName, client, projectId);
1278
+ } catch (error) {
1279
+ spinner.fail(error instanceof Error ? error.message : "Failed to resolve service");
1280
+ process.exit(1);
1281
+ }
1282
+ }
1245
1283
  spinner.text = "Fetching environment...";
1246
1284
  const { data: environment, error: envError } = await client.from("environments").select("id, name, project_id").eq("project_id", projectId).ilike("name", envName).single();
1247
1285
  if (envError || !environment) {
@@ -1297,12 +1335,24 @@ var pushCommand = new Command5("push").description("Push local .env file to EnvM
1297
1335
  key: v.key,
1298
1336
  value: v.value
1299
1337
  }));
1300
- const { data: existingVars } = await client.from("variables").select("key").eq("environment_id", environment.id);
1338
+ let existingQuery = client.from("variables").select("key").eq("environment_id", environment.id);
1339
+ if (serviceId) {
1340
+ existingQuery = existingQuery.eq("service_id", serviceId);
1341
+ } else {
1342
+ existingQuery = existingQuery.is("service_id", null);
1343
+ }
1344
+ const { data: existingVars } = await existingQuery;
1301
1345
  const existingKeys = new Set((existingVars || []).map((v) => v.key));
1302
1346
  const keysToUpdate = variablesData.filter((v) => existingKeys.has(v.key));
1303
1347
  const keysToInsert = variablesData.filter((v) => !existingKeys.has(v.key));
1304
1348
  if (keysToUpdate.length > 0) {
1305
- const { error: deleteError } = await client.from("variables").delete().eq("environment_id", environment.id).in("key", keysToUpdate.map((v) => v.key));
1349
+ let deleteQuery = client.from("variables").delete().eq("environment_id", environment.id).in("key", keysToUpdate.map((v) => v.key));
1350
+ if (serviceId) {
1351
+ deleteQuery = deleteQuery.eq("service_id", serviceId);
1352
+ } else {
1353
+ deleteQuery = deleteQuery.is("service_id", null);
1354
+ }
1355
+ const { error: deleteError } = await deleteQuery;
1306
1356
  if (deleteError) {
1307
1357
  spinner.fail("Failed to update existing variables");
1308
1358
  console.error(chalk6.red(deleteError.message));
@@ -1313,14 +1363,16 @@ var pushCommand = new Command5("push").description("Push local .env file to EnvM
1313
1363
  variables_data: variablesData,
1314
1364
  environment_id_param: environment.id,
1315
1365
  organization_id_param: organizationId,
1316
- import_as_secrets: markAsSecrets
1366
+ import_as_secrets: markAsSecrets,
1367
+ ...serviceId && { service_id_param: serviceId }
1317
1368
  });
1318
1369
  if (pushError) {
1319
1370
  spinner.fail("Push failed");
1320
1371
  console.error(chalk6.red(pushError.message));
1321
1372
  process.exit(1);
1322
1373
  }
1323
- spinner.succeed(`Pushed ${vars.length} variables to ${envName}`);
1374
+ const serviceInfo = serviceName ? ` (service: ${serviceName})` : "";
1375
+ spinner.succeed(`Pushed ${vars.length} variables to ${envName}${serviceInfo}`);
1324
1376
  if (keysToUpdate.length > 0) {
1325
1377
  console.log(chalk6.gray(` ${keysToInsert.length} inserted, ${keysToUpdate.length} updated`));
1326
1378
  }
@@ -1340,7 +1392,7 @@ import chalk7 from "chalk";
1340
1392
  import ora5 from "ora";
1341
1393
  import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
1342
1394
  import { resolve as resolve3 } from "path";
1343
- 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) => {
1395
+ 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) => {
1344
1396
  const spinner = ora5("Comparing...").start();
1345
1397
  try {
1346
1398
  const config = loadConfig();
@@ -1392,8 +1444,13 @@ var diffCommand = new Command6("diff").description("Show differences between loc
1392
1444
  console.error(chalk7.red(varError.message));
1393
1445
  process.exit(1);
1394
1446
  }
1447
+ const filterTags = options.tag || config?.tags || [];
1448
+ const filteredRemoteData = filterTags.length > 0 ? (remoteVarsData || []).filter((v) => {
1449
+ const t = v.tags || [];
1450
+ return t.length === 0 || t.some((tag) => filterTags.includes(tag));
1451
+ }) : remoteVarsData || [];
1395
1452
  const remoteVars = /* @__PURE__ */ new Map();
1396
- for (const v of remoteVarsData || []) {
1453
+ for (const v of filteredRemoteData) {
1397
1454
  remoteVars.set(v.key, v);
1398
1455
  }
1399
1456
  spinner.stop();
@@ -1728,14 +1785,16 @@ async function subscribeToVariableChanges(environmentId, onEvent, onStatus) {
1728
1785
  });
1729
1786
  return subscription;
1730
1787
  }
1731
- async function fetchAllVariables(environmentId, includeSecrets = true) {
1788
+ async function fetchAllVariables(environmentId, includeSecrets = true, serviceId) {
1732
1789
  const client = await createClient();
1733
- const { data: variables, error } = await client.rpc("get_variables_for_sync", {
1790
+ const rpcParams = {
1734
1791
  p_environment_id: environmentId,
1735
1792
  p_sync_secrets: includeSecrets,
1736
1793
  p_sync_variables: true,
1737
- p_include_fallbacks: false
1738
- });
1794
+ p_include_fallbacks: false,
1795
+ ...serviceId && { p_service_id: serviceId }
1796
+ };
1797
+ const { data: variables, error } = await client.rpc("get_variables_for_sync", rpcParams);
1739
1798
  if (error) {
1740
1799
  throw new Error(`Failed to fetch variables: ${error.message}`);
1741
1800
  }
@@ -1877,7 +1936,7 @@ function mergeWithRemote(local, remoteVariables, strategy) {
1877
1936
  }
1878
1937
 
1879
1938
  // src/commands/dev.ts
1880
- 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) => {
1939
+ 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)").option("--service <name>", "Filter by service name").action(async (options) => {
1881
1940
  const spinner = ora7("Starting dev mode...").start();
1882
1941
  try {
1883
1942
  const config = loadConfig();
@@ -1907,6 +1966,16 @@ var devCommand = new Command9("dev").description("Start real-time sync daemon -
1907
1966
  spinner.fail(error instanceof Error ? error.message : "Failed to resolve project");
1908
1967
  process.exit(1);
1909
1968
  }
1969
+ const serviceName = options.service || config?.service;
1970
+ let serviceId;
1971
+ if (serviceName) {
1972
+ try {
1973
+ serviceId = await resolveServiceId(serviceName, client, projectId);
1974
+ } catch (error) {
1975
+ spinner.fail(error instanceof Error ? error.message : "Failed to resolve service");
1976
+ process.exit(1);
1977
+ }
1978
+ }
1910
1979
  spinner.text = "Fetching environment...";
1911
1980
  const { data: environment, error: envError } = await client.from("environments").select("id, name, project_id").eq("project_id", projectId).ilike("name", envName).single();
1912
1981
  if (envError || !environment) {
@@ -1914,8 +1983,16 @@ var devCommand = new Command9("dev").description("Start real-time sync daemon -
1914
1983
  process.exit(1);
1915
1984
  }
1916
1985
  const environmentId = environment.id;
1986
+ const filterTags = options.tag || config?.tags || [];
1987
+ const applyTagFilter = (vars) => {
1988
+ if (filterTags.length === 0) return vars;
1989
+ return vars.filter((v) => {
1990
+ const t = v.tags || [];
1991
+ return t.length === 0 || t.some((tag) => filterTags.includes(tag));
1992
+ });
1993
+ };
1917
1994
  spinner.text = "Performing initial sync...";
1918
- const remoteVariables = await fetchAllVariables(environmentId, true);
1995
+ const remoteVariables = applyTagFilter(await fetchAllVariables(environmentId, true, serviceId));
1919
1996
  let localVariables = /* @__PURE__ */ new Map();
1920
1997
  if (existsSync7(outputFile)) {
1921
1998
  const content = readFileSync6(outputFile, "utf-8");
@@ -1932,7 +2009,7 @@ var devCommand = new Command9("dev").description("Start real-time sync daemon -
1932
2009
  let isPaused = false;
1933
2010
  let lastRemoteKeys = null;
1934
2011
  async function syncRemoteToLocal(silent = false) {
1935
- const updatedVariables = await fetchAllVariables(environmentId, true);
2012
+ const updatedVariables = applyTagFilter(await fetchAllVariables(environmentId, true, serviceId));
1936
2013
  const currentLocal = /* @__PURE__ */ new Map();
1937
2014
  if (existsSync7(outputFile)) {
1938
2015
  const content = readFileSync6(outputFile, "utf-8");
@@ -2024,6 +2101,9 @@ Realtime error: ${message || ""}`));
2024
2101
  console.log("");
2025
2102
  console.log(chalk10.cyan(" Project:"), projectId);
2026
2103
  console.log(chalk10.cyan(" Environment:"), envName);
2104
+ if (serviceName) {
2105
+ console.log(chalk10.cyan(" Service:"), serviceName);
2106
+ }
2027
2107
  console.log(chalk10.cyan(" Output:"), outputFile);
2028
2108
  console.log(chalk10.cyan(" Strategy:"), strategy);
2029
2109
  console.log(chalk10.cyan(" Watching:"), watchLocal ? "local + remote" : "remote only");
@@ -2328,7 +2408,7 @@ function generateYamlTemplate(variables, options = {}) {
2328
2408
  }
2329
2409
 
2330
2410
  // src/commands/init.ts
2331
- var initCommand = new Command10("init").description("Initialize project configuration and generate .env.template from EnvManager").option("--org <name>", "Organization name (required if you belong to multiple)").option("-e, --environment <name>", 'Environment name (default: "development")').option("-p, --project <id>", "Project ID").option("--format <type>", "Template format: simple or yaml (default: simple)", "simple").option("-f, --force", "Overwrite existing files without prompting").action(async (options) => {
2411
+ var initCommand = new Command10("init").description("Initialize project configuration and generate .env.template from EnvManager").option("--org <name>", "Organization name (required if you belong to multiple)").option("-e, --environment <name>", 'Environment name (default: "development")').option("-p, --project <id>", "Project ID").option("--format <type>", "Template format: simple or yaml (default: simple)", "simple").option("-f, --force", "Overwrite existing files without prompting").option("--service <name>", "Service name to scope variables to").action(async (options) => {
2332
2412
  const spinner = ora8("Initializing project...").start();
2333
2413
  try {
2334
2414
  const configPath = resolve5("envmanager.json");
@@ -2389,13 +2469,39 @@ Environment "${envName}" not found. Available:`));
2389
2469
  }
2390
2470
  process.exit(1);
2391
2471
  }
2472
+ let selectedServiceName;
2473
+ let serviceIdForSync;
2474
+ const { data: projectServices } = await client.from("services").select("id, name").eq("project_id", projectId).order("sort_order");
2475
+ if (options.service) {
2476
+ spinner.text = "Resolving service...";
2477
+ try {
2478
+ serviceIdForSync = await resolveServiceId(options.service, client, projectId);
2479
+ selectedServiceName = options.service;
2480
+ } catch (error) {
2481
+ spinner.fail(error instanceof Error ? error.message : "Failed to resolve service");
2482
+ process.exit(1);
2483
+ }
2484
+ } else if (projectServices && projectServices.length > 0) {
2485
+ spinner.stop();
2486
+ console.log("");
2487
+ console.log(chalk11.cyan("Services available in this project:"));
2488
+ projectServices.forEach((s) => {
2489
+ console.log(chalk11.gray(` - ${s.name}`));
2490
+ });
2491
+ console.log(chalk11.gray("\n Use --service <name> to scope variables to a specific service."));
2492
+ console.log(chalk11.gray(" Without --service, all variables will be synced."));
2493
+ console.log("");
2494
+ spinner.start("Fetching variables...");
2495
+ }
2392
2496
  spinner.text = "Fetching variables...";
2393
- const { data: variables, error: varError } = await client.rpc("get_variables_for_sync", {
2497
+ const rpcParams = {
2394
2498
  p_environment_id: environment.id,
2395
2499
  p_sync_secrets: false,
2396
2500
  p_sync_variables: true,
2397
- p_include_fallbacks: false
2398
- });
2501
+ p_include_fallbacks: false,
2502
+ ...serviceIdForSync && { p_service_id: serviceIdForSync }
2503
+ };
2504
+ const { data: variables, error: varError } = await client.rpc("get_variables_for_sync", rpcParams);
2399
2505
  if (varError) {
2400
2506
  spinner.fail("Failed to fetch variables");
2401
2507
  console.error(chalk11.red(varError.message));
@@ -2419,6 +2525,9 @@ File ${templatePath} already exists. Use --force to overwrite.`));
2419
2525
  project_id: projectId,
2420
2526
  environment: environment.name
2421
2527
  };
2528
+ if (selectedServiceName) {
2529
+ config.service = selectedServiceName;
2530
+ }
2422
2531
  writeFileSync4(configPath, JSON.stringify(config, null, 2) + "\n");
2423
2532
  const templateVars = varsArray.map((v) => ({
2424
2533
  key: v.key,
@@ -2432,7 +2541,8 @@ File ${templatePath} already exists. Use --force to overwrite.`));
2432
2541
  const simpleContent = generateTemplate(templateVars, { includeDefaults: true });
2433
2542
  writeFileSync4(templatePath, simpleContent);
2434
2543
  }
2435
- spinner.succeed("Project initialized");
2544
+ const serviceInfo = selectedServiceName ? ` (service: ${selectedServiceName})` : "";
2545
+ spinner.succeed(`Project initialized${serviceInfo}`);
2436
2546
  console.log("");
2437
2547
  console.log(chalk11.green("Created:"));
2438
2548
  console.log(chalk11.gray(` ${configPath}`));