@envmanager-cli/cli 0.2.0 → 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.
@@ -616,7 +616,8 @@ var ConfigSchema = z.object({
616
616
  format: z.enum(EXPORT_FORMATS).optional(),
617
617
  k8s_namespace: z.string().optional(),
618
618
  k8s_name: z.string().optional(),
619
- tags: z.array(z.string()).optional()
619
+ tags: z.array(z.string()).optional(),
620
+ service: z.string().optional()
620
621
  });
621
622
  var CONFIG_FILENAMES = ["envmanager.json", ".envmanagerrc"];
622
623
  function findConfigFile(startDir = process.cwd()) {
@@ -726,6 +727,15 @@ async function detectOrgFromProject(projectInput, client, memberships) {
726
727
  if (data && data.length === 1) return data[0].organization_id;
727
728
  return null;
728
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
+ }
729
739
 
730
740
  // src/lib/variable-references.ts
731
741
  var REFERENCE_PATTERN = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
@@ -886,7 +896,7 @@ function resolveAll(variables) {
886
896
  }
887
897
 
888
898
  // src/commands/pull.ts
889
- var pullCommand = new Command4("pull").description("Pull environment variables from EnvManager to local .env file").option("--org <name>", "Organization name (required if you belong to multiple)").option("-e, --environment <name>", 'Environment name (default: from config or "development")').option("-p, --project <id>", "Project ID (default: from config)").option("-o, --output <file>", "Output file path (default: .env)").option("--no-secrets", "Exclude secret values (will be empty)").option("-f, --force", "Overwrite existing file without prompting").option("-r, --resolve-references", "Resolve ${VAR} references to their values").option("-F, --include-fallbacks", "Include fallback values for empty variables").option("-s, --show-sources", "Show value source as inline comments").option("--format <type>", `Export format (${EXPORT_FORMATS.join(", ")})`).option("--k8s-namespace <ns>", 'Kubernetes namespace (default: "default")').option("--k8s-name <name>", "Kubernetes resource name").option("--tag <tags...>", "Filter by tags (untagged variables always included)").action(async (options) => {
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) => {
890
900
  const spinner = ora3("Connecting to EnvManager...").start();
891
901
  try {
892
902
  const config = loadConfig();
@@ -924,6 +934,16 @@ var pullCommand = new Command4("pull").description("Pull environment variables f
924
934
  spinner.fail(error instanceof Error ? error.message : "Failed to resolve project");
925
935
  process.exit(1);
926
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
+ }
927
947
  spinner.text = "Fetching environment...";
928
948
  const { data: environments, error: envError } = await client.from("environments").select("id, name").eq("project_id", projectId).ilike("name", envName).single();
929
949
  if (envError || !environments) {
@@ -939,7 +959,8 @@ var pullCommand = new Command4("pull").description("Pull environment variables f
939
959
  p_environment_id: environmentId,
940
960
  p_sync_secrets: includeSecrets,
941
961
  p_sync_variables: true,
942
- p_include_fallbacks: shouldFallback || false
962
+ p_include_fallbacks: shouldFallback || false,
963
+ ...serviceId && { p_service_id: serviceId }
943
964
  };
944
965
  const { data: variables, error: varError } = await client.rpc("get_variables_for_sync", rpcParams);
945
966
  if (varError) {
@@ -1039,8 +1060,9 @@ File ${outputFile} already exists.`));
1039
1060
  writeFileSync2(outputFile, content + "\n");
1040
1061
  const secretCount = vars.filter((v) => v.is_secret).length;
1041
1062
  const plainCount = vars.length - secretCount;
1063
+ const serviceInfo = serviceName ? ` (service: ${serviceName})` : "";
1042
1064
  const tagInfo = filterTags.length > 0 ? ` (tags: ${filterTags.join(", ")})` : "";
1043
- spinner.succeed(`Pulled ${vars.length} variables to ${outputFile} (${format})${tagInfo}`);
1065
+ spinner.succeed(`Pulled ${vars.length} variables to ${outputFile} (${format})${serviceInfo}${tagInfo}`);
1044
1066
  console.log(chalk4.gray(` ${plainCount} plain, ${secretCount} secrets`));
1045
1067
  client.rpc("log_variable_access", {
1046
1068
  p_environment_id: environmentId,
@@ -1197,7 +1219,7 @@ function validateVariableName(name, config) {
1197
1219
  }
1198
1220
 
1199
1221
  // src/commands/push.ts
1200
- 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) => {
1201
1223
  const spinner = ora4("Reading .env file...").start();
1202
1224
  try {
1203
1225
  const config = loadConfig();
@@ -1248,6 +1270,16 @@ var pushCommand = new Command5("push").description("Push local .env file to EnvM
1248
1270
  spinner.fail(error instanceof Error ? error.message : "Failed to resolve project");
1249
1271
  process.exit(1);
1250
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
+ }
1251
1283
  spinner.text = "Fetching environment...";
1252
1284
  const { data: environment, error: envError } = await client.from("environments").select("id, name, project_id").eq("project_id", projectId).ilike("name", envName).single();
1253
1285
  if (envError || !environment) {
@@ -1303,12 +1335,24 @@ var pushCommand = new Command5("push").description("Push local .env file to EnvM
1303
1335
  key: v.key,
1304
1336
  value: v.value
1305
1337
  }));
1306
- 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;
1307
1345
  const existingKeys = new Set((existingVars || []).map((v) => v.key));
1308
1346
  const keysToUpdate = variablesData.filter((v) => existingKeys.has(v.key));
1309
1347
  const keysToInsert = variablesData.filter((v) => !existingKeys.has(v.key));
1310
1348
  if (keysToUpdate.length > 0) {
1311
- 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;
1312
1356
  if (deleteError) {
1313
1357
  spinner.fail("Failed to update existing variables");
1314
1358
  console.error(chalk6.red(deleteError.message));
@@ -1319,14 +1363,16 @@ var pushCommand = new Command5("push").description("Push local .env file to EnvM
1319
1363
  variables_data: variablesData,
1320
1364
  environment_id_param: environment.id,
1321
1365
  organization_id_param: organizationId,
1322
- import_as_secrets: markAsSecrets
1366
+ import_as_secrets: markAsSecrets,
1367
+ ...serviceId && { service_id_param: serviceId }
1323
1368
  });
1324
1369
  if (pushError) {
1325
1370
  spinner.fail("Push failed");
1326
1371
  console.error(chalk6.red(pushError.message));
1327
1372
  process.exit(1);
1328
1373
  }
1329
- spinner.succeed(`Pushed ${vars.length} variables to ${envName}`);
1374
+ const serviceInfo = serviceName ? ` (service: ${serviceName})` : "";
1375
+ spinner.succeed(`Pushed ${vars.length} variables to ${envName}${serviceInfo}`);
1330
1376
  if (keysToUpdate.length > 0) {
1331
1377
  console.log(chalk6.gray(` ${keysToInsert.length} inserted, ${keysToUpdate.length} updated`));
1332
1378
  }
@@ -1739,14 +1785,16 @@ async function subscribeToVariableChanges(environmentId, onEvent, onStatus) {
1739
1785
  });
1740
1786
  return subscription;
1741
1787
  }
1742
- async function fetchAllVariables(environmentId, includeSecrets = true) {
1788
+ async function fetchAllVariables(environmentId, includeSecrets = true, serviceId) {
1743
1789
  const client = await createClient();
1744
- const { data: variables, error } = await client.rpc("get_variables_for_sync", {
1790
+ const rpcParams = {
1745
1791
  p_environment_id: environmentId,
1746
1792
  p_sync_secrets: includeSecrets,
1747
1793
  p_sync_variables: true,
1748
- p_include_fallbacks: false
1749
- });
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);
1750
1798
  if (error) {
1751
1799
  throw new Error(`Failed to fetch variables: ${error.message}`);
1752
1800
  }
@@ -1888,7 +1936,7 @@ function mergeWithRemote(local, remoteVariables, strategy) {
1888
1936
  }
1889
1937
 
1890
1938
  // src/commands/dev.ts
1891
- var devCommand = new Command9("dev").description("Start real-time sync daemon - watches for remote variable changes").option("--org <name>", "Organization name (required if you belong to multiple)").option("-e, --environment <name>", 'Environment name (default: from config or "development")').option("-p, --project <id>", "Project ID (default: from config)").option("--output <file>", "Output file path (default: .env)").option("--no-watch", "Disable local file watching").option("--strategy <type>", "Merge strategy: remote_wins, local_wins, merge_new (default: remote_wins)", "remote_wins").option("--tag <tags...>", "Filter by tags (untagged variables always included)").action(async (options) => {
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) => {
1892
1940
  const spinner = ora7("Starting dev mode...").start();
1893
1941
  try {
1894
1942
  const config = loadConfig();
@@ -1918,6 +1966,16 @@ var devCommand = new Command9("dev").description("Start real-time sync daemon -
1918
1966
  spinner.fail(error instanceof Error ? error.message : "Failed to resolve project");
1919
1967
  process.exit(1);
1920
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
+ }
1921
1979
  spinner.text = "Fetching environment...";
1922
1980
  const { data: environment, error: envError } = await client.from("environments").select("id, name, project_id").eq("project_id", projectId).ilike("name", envName).single();
1923
1981
  if (envError || !environment) {
@@ -1934,7 +1992,7 @@ var devCommand = new Command9("dev").description("Start real-time sync daemon -
1934
1992
  });
1935
1993
  };
1936
1994
  spinner.text = "Performing initial sync...";
1937
- const remoteVariables = applyTagFilter(await fetchAllVariables(environmentId, true));
1995
+ const remoteVariables = applyTagFilter(await fetchAllVariables(environmentId, true, serviceId));
1938
1996
  let localVariables = /* @__PURE__ */ new Map();
1939
1997
  if (existsSync7(outputFile)) {
1940
1998
  const content = readFileSync6(outputFile, "utf-8");
@@ -1951,7 +2009,7 @@ var devCommand = new Command9("dev").description("Start real-time sync daemon -
1951
2009
  let isPaused = false;
1952
2010
  let lastRemoteKeys = null;
1953
2011
  async function syncRemoteToLocal(silent = false) {
1954
- const updatedVariables = applyTagFilter(await fetchAllVariables(environmentId, true));
2012
+ const updatedVariables = applyTagFilter(await fetchAllVariables(environmentId, true, serviceId));
1955
2013
  const currentLocal = /* @__PURE__ */ new Map();
1956
2014
  if (existsSync7(outputFile)) {
1957
2015
  const content = readFileSync6(outputFile, "utf-8");
@@ -2043,6 +2101,9 @@ Realtime error: ${message || ""}`));
2043
2101
  console.log("");
2044
2102
  console.log(chalk10.cyan(" Project:"), projectId);
2045
2103
  console.log(chalk10.cyan(" Environment:"), envName);
2104
+ if (serviceName) {
2105
+ console.log(chalk10.cyan(" Service:"), serviceName);
2106
+ }
2046
2107
  console.log(chalk10.cyan(" Output:"), outputFile);
2047
2108
  console.log(chalk10.cyan(" Strategy:"), strategy);
2048
2109
  console.log(chalk10.cyan(" Watching:"), watchLocal ? "local + remote" : "remote only");
@@ -2347,7 +2408,7 @@ function generateYamlTemplate(variables, options = {}) {
2347
2408
  }
2348
2409
 
2349
2410
  // src/commands/init.ts
2350
- 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) => {
2351
2412
  const spinner = ora8("Initializing project...").start();
2352
2413
  try {
2353
2414
  const configPath = resolve5("envmanager.json");
@@ -2408,13 +2469,39 @@ Environment "${envName}" not found. Available:`));
2408
2469
  }
2409
2470
  process.exit(1);
2410
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
+ }
2411
2496
  spinner.text = "Fetching variables...";
2412
- const { data: variables, error: varError } = await client.rpc("get_variables_for_sync", {
2497
+ const rpcParams = {
2413
2498
  p_environment_id: environment.id,
2414
2499
  p_sync_secrets: false,
2415
2500
  p_sync_variables: true,
2416
- p_include_fallbacks: false
2417
- });
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);
2418
2505
  if (varError) {
2419
2506
  spinner.fail("Failed to fetch variables");
2420
2507
  console.error(chalk11.red(varError.message));
@@ -2438,6 +2525,9 @@ File ${templatePath} already exists. Use --force to overwrite.`));
2438
2525
  project_id: projectId,
2439
2526
  environment: environment.name
2440
2527
  };
2528
+ if (selectedServiceName) {
2529
+ config.service = selectedServiceName;
2530
+ }
2441
2531
  writeFileSync4(configPath, JSON.stringify(config, null, 2) + "\n");
2442
2532
  const templateVars = varsArray.map((v) => ({
2443
2533
  key: v.key,
@@ -2451,7 +2541,8 @@ File ${templatePath} already exists. Use --force to overwrite.`));
2451
2541
  const simpleContent = generateTemplate(templateVars, { includeDefaults: true });
2452
2542
  writeFileSync4(templatePath, simpleContent);
2453
2543
  }
2454
- spinner.succeed("Project initialized");
2544
+ const serviceInfo = selectedServiceName ? ` (service: ${selectedServiceName})` : "";
2545
+ spinner.succeed(`Project initialized${serviceInfo}`);
2455
2546
  console.log("");
2456
2547
  console.log(chalk11.green("Created:"));
2457
2548
  console.log(chalk11.gray(` ${configPath}`));