@penvhq/cli 0.14.0 → 0.15.0

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/dist/index.cjs CHANGED
@@ -602,22 +602,10 @@ var import_provider_filesystem2 = require("@penvhq/provider-filesystem");
602
602
  // src/env-flags.ts
603
603
  var import_core3 = require("@penvhq/core");
604
604
  var BASE_RESERVED = ["help", "version", "h", "v", "_", "--"];
605
- var COMMAND_FLAGS = [
606
- ...BASE_RESERVED,
607
- "env",
608
- "out",
609
- "allow-decrypt",
610
- "destination",
611
- "dest",
612
- "d",
613
- "location",
614
- "l",
615
- "yes",
616
- "y"
617
- ];
605
+ var COMMAND_FLAGS = [...BASE_RESERVED, "env", "out", "allow-decrypt", "yes", "y"];
618
606
  function shadowedEnvironments(config) {
619
607
  const flags = new Set(COMMAND_FLAGS);
620
- return config.environments.filter((environment) => flags.has(environment));
608
+ return (0, import_core3.environmentNames)(config).filter((environment) => flags.has(environment));
621
609
  }
622
610
  function shorthandCandidates(args, declared) {
623
611
  const reserved = /* @__PURE__ */ new Set([...BASE_RESERVED, ...declared]);
@@ -630,13 +618,14 @@ function environmentFromShorthand(config, candidates, explicit) {
630
618
  if (candidates.length === 0) {
631
619
  return void 0;
632
620
  }
633
- const hits = candidates.filter((candidate) => config.environments.includes(candidate));
634
- const strangers = candidates.filter((candidate) => !config.environments.includes(candidate));
621
+ const declared = (0, import_core3.environmentNames)(config);
622
+ const hits = candidates.filter((candidate) => declared.includes(candidate));
623
+ const strangers = candidates.filter((candidate) => !declared.includes(candidate));
635
624
  if (strangers.length > 0) {
636
625
  throw new import_core3.PenvError(
637
626
  "UNKNOWN_FLAG",
638
627
  `${quoteList(strangers)} ${strangers.length === 1 ? "is not a flag" : "are not flags"} this command takes, and ${strangers.length === 1 ? "names" : "name"} no declared environment`,
639
- `Declared environments work as bare flags: ${quoteList(config.environments)}. Anything else needs \`--env <name>\`.`
628
+ `Declared environments work as bare flags: ${quoteList(declared)}. Anything else needs \`--env <name>\`.`
640
629
  );
641
630
  }
642
631
  if (hits.length > 1) {
@@ -727,14 +716,18 @@ async function loadPluginProvider(type, context) {
727
716
  function assertProvidersRegistered(config, projectRoot, options) {
728
717
  const local = localExtensions(projectRoot);
729
718
  const ci = options?.ci ?? isCi(process.env.CI);
730
- for (const [environment, provider] of Object.entries(config.providers)) {
731
- if (isProviderRegistered(provider.type)) {
719
+ for (const environment of (0, import_core4.environmentNames)(config)) {
720
+ const provider = (0, import_core4.environmentEntry)(config, environment)?.provider;
721
+ if (typeof provider !== "string" || provider.trim().length === 0) {
722
+ throw (0, import_core4.providerMissing)(environment);
723
+ }
724
+ if (isProviderRegistered(provider)) {
732
725
  continue;
733
726
  }
734
- if (ci && local.includes(provider.type)) {
735
- throw localExtensionInCi(provider.type, environment);
727
+ if (ci && local.includes(provider)) {
728
+ throw localExtensionInCi(provider, environment);
736
729
  }
737
- resolveExtension(provider.type, projectRoot, environment, local);
730
+ resolveExtension(provider, projectRoot, environment, local);
738
731
  }
739
732
  }
740
733
  function resolveExtension(type, projectRoot, environment, local = localExtensions(projectRoot)) {
@@ -892,11 +885,11 @@ function schemaShapeFileOf(project) {
892
885
  return selfContainedSchemaModule(project.root, schemaFile) ?? import_core5.SCHEMA_SHAPE_FILE;
893
886
  }
894
887
  async function sourceProviderFor(project, environment) {
895
- const providerConfig = project.config.providers[environment];
888
+ const providerConfig = (0, import_core5.environmentEntry)(project.config, environment);
896
889
  if (providerConfig === void 0) {
897
890
  return createProvider(LOCAL_TREE_TYPE, { root: project.root, config: project.config });
898
891
  }
899
- return createSourceProvider(providerConfig.type, {
892
+ return createSourceProvider(providerConfig.provider, {
900
893
  root: project.root,
901
894
  config: project.config,
902
895
  providerConfig,
@@ -908,7 +901,7 @@ function targetEnvironment(project, explicit, shorthand) {
908
901
  return (0, import_core5.resolveEnvironment)(project.config, explicit ?? fromFlags);
909
902
  }
910
903
  var KEY_SEPARATOR = /[./\\]/;
911
- var NO_ENVIRONMENTS = { environments: [], providers: {} };
904
+ var NO_ENVIRONMENTS = { environments: {} };
912
905
  function refFromKey(key, config) {
913
906
  const segments = key.split(KEY_SEPARATOR).filter((segment) => segment.length > 0);
914
907
  const name = segments[segments.length - 1];
@@ -1232,8 +1225,9 @@ function discoverDotenvFiles(root) {
1232
1225
  return files.sort((a, b) => order(a) < order(b) ? -1 : order(a) > order(b) ? 1 : 0);
1233
1226
  }
1234
1227
  function activeDotenvFiles(root, config) {
1228
+ const declared = (0, import_core7.environmentNames)(config);
1235
1229
  return discoverDotenvFiles(root).filter(
1236
- (file) => file.environment === void 0 || config.environments.includes(file.environment)
1230
+ (file) => file.environment === void 0 || declared.includes(file.environment)
1237
1231
  );
1238
1232
  }
1239
1233
  function environmentsDeclaredBy(files) {
@@ -1974,11 +1968,12 @@ function packageManifest(specifier, root) {
1974
1968
  }
1975
1969
  function declaredCredentials(config, root) {
1976
1970
  const declared = {};
1977
- for (const provider of Object.values(config.providers)) {
1978
- if ((0, import_core11.own)(declared, provider.type) !== void 0) {
1971
+ for (const environment of (0, import_core11.environmentNames)(config)) {
1972
+ const provider = (0, import_core11.environmentEntry)(config, environment)?.provider;
1973
+ if (provider === void 0 || (0, import_core11.own)(declared, provider) !== void 0) {
1979
1974
  continue;
1980
1975
  }
1981
- const penv = packageManifest(provider.type, root)?.penv;
1976
+ const penv = packageManifest(provider, root)?.penv;
1982
1977
  const credentials = isPlainObject(penv) ? penv.credentials : void 0;
1983
1978
  if (credentials === void 0) {
1984
1979
  continue;
@@ -1986,11 +1981,11 @@ function declaredCredentials(config, root) {
1986
1981
  if (!Array.isArray(credentials) || credentials.some((name) => typeof name !== "string" || !VARIABLE.test(name))) {
1987
1982
  throw new import_core11.PenvError(
1988
1983
  "PROVIDER_CREDENTIALS_INVALID",
1989
- `\`${provider.type}\` declares \`penv.credentials\`, and it is not a list of variable names`,
1984
+ `\`${provider}\` declares \`penv.credentials\`, and it is not a list of variable names`,
1990
1985
  `Its package.json should read \`"penv": { "credentials": ["ACME_TOKEN"] }\` \u2014 penv strips exactly what an extension declares, so it will not guess at a declaration it cannot read.`
1991
1986
  );
1992
1987
  }
1993
- declared[provider.type] = credentials;
1988
+ declared[provider] = credentials;
1994
1989
  }
1995
1990
  return declared;
1996
1991
  }
@@ -2682,38 +2677,23 @@ function lineReader(input = process.stdin, output = process.stdout) {
2682
2677
  // src/commands/push.ts
2683
2678
  var LAST_PUSHED_KEY = "lastPushedAt";
2684
2679
  async function destinationFor(project, environment, options) {
2685
- if (options.destination !== void 0) {
2686
- const providerConfig = {
2687
- type: options.destination,
2688
- ...options.location === void 0 ? {} : { location: options.location }
2689
- };
2690
- const provider2 = options.provider ?? await createSourceProvider(options.destination, {
2691
- root: project.root,
2692
- config: project.config,
2693
- providerConfig,
2694
- environment
2695
- });
2696
- return { provider: provider2, location: options.location };
2697
- }
2698
- const declared = project.config.providers[environment];
2699
- const location = declared?.location;
2700
- if (declared === void 0 || declared.type === LOCAL_TREE_TYPE) {
2680
+ const declared = (0, import_core15.environmentEntry)(project.config, environment);
2681
+ if (declared === void 0 || declared.provider === LOCAL_TREE_TYPE) {
2701
2682
  throw new import_core15.PenvError(
2702
2683
  "NO_DESTINATION",
2703
2684
  `Environment ${environment}'s provider is the local records tree itself, so penv has nowhere to push`,
2704
- `Declare a provider for it in penv.config.ts \u2014 e.g. \`${environment}: { type: "@penvhq/provider-github", location: "owner/repo" }\` \u2014 or push somewhere once with \`penv push --env ${environment} --destination <package> --location <place>\`.`
2685
+ `Declare the backend that holds it in penv.config.ts \u2014 e.g. \`${environment}: { provider: "@penvhq/provider-github", repository: "owner/repo" }\` \u2014 and run this again. A push only ever goes where the config says this environment lives.`
2705
2686
  );
2706
2687
  }
2707
2688
  if (options.provider !== void 0) {
2708
- return { provider: options.provider, location };
2689
+ return options.provider;
2709
2690
  }
2710
- const provider = await createSourceProvider(declared.type, {
2691
+ return createSourceProvider(declared.provider, {
2711
2692
  root: project.root,
2712
2693
  config: project.config,
2713
2694
  providerConfig: declared,
2714
2695
  environment
2715
2696
  });
2716
- return { provider, location };
2717
2697
  }
2718
2698
  function plan(resolutions, config, environment, allowDecrypt) {
2719
2699
  const outbound = [];
@@ -2797,7 +2777,7 @@ async function ensureTargetApproved(provider, environment, options) {
2797
2777
  await provider.ensureTarget(environment);
2798
2778
  return true;
2799
2779
  }
2800
- async function pushProjection(project, provider, environment, location, options) {
2780
+ async function pushProjection(project, provider, environment, options) {
2801
2781
  const tree = localTree(project);
2802
2782
  const keys = keySourceFor(project, environment);
2803
2783
  const resolutions = resolveAllSync(tree, environment, keys, true);
@@ -2828,7 +2808,6 @@ async function pushProjection(project, provider, environment, location, options)
2828
2808
  environment,
2829
2809
  destination: provider.type,
2830
2810
  mode: "projection",
2831
- location,
2832
2811
  pushed: outbound.length,
2833
2812
  meta: 0,
2834
2813
  repositorySecrets,
@@ -2844,7 +2823,7 @@ function crossesToRecords(file, environment) {
2844
2823
  }
2845
2824
  return scope.kind === "environment" && scope.environment === environment;
2846
2825
  }
2847
- async function pushRecords(project, provider, environment, location) {
2826
+ async function pushRecords(project, provider, environment) {
2848
2827
  const tree = localTree(project);
2849
2828
  const files = tree.listSync().filter((file) => crossesToRecords(file, environment));
2850
2829
  let pushed = 0;
@@ -2870,7 +2849,6 @@ async function pushRecords(project, provider, environment, location) {
2870
2849
  environment,
2871
2850
  destination: provider.type,
2872
2851
  mode: "records",
2873
- location,
2874
2852
  pushed,
2875
2853
  meta,
2876
2854
  repositorySecrets: 0,
@@ -2882,11 +2860,11 @@ async function pushRecords(project, provider, environment, location) {
2882
2860
  async function runPush(options) {
2883
2861
  const project = openProject(options.cwd);
2884
2862
  const environment = targetEnvironment(project, options.environment, options.envFlags);
2885
- const { provider, location } = await destinationFor(project, environment, options);
2863
+ const provider = await destinationFor(project, environment, options);
2886
2864
  if ((0, import_core15.holdsProjection)(provider)) {
2887
- return pushProjection(project, provider, environment, location, options);
2865
+ return pushProjection(project, provider, environment, options);
2888
2866
  }
2889
- return pushRecords(project, provider, environment, location);
2867
+ return pushRecords(project, provider, environment);
2890
2868
  }
2891
2869
  function renderPush(result2) {
2892
2870
  if (result2.pushed === 0 && result2.meta === 0) {
@@ -2898,7 +2876,7 @@ function renderPush(result2) {
2898
2876
  }
2899
2877
  ]);
2900
2878
  }
2901
- const target = `${result2.destination} for environment ${result2.environment}${result2.location === void 0 ? "" : ` (${result2.location})`}`;
2879
+ const target = `${result2.destination} for environment ${result2.environment}`;
2902
2880
  if (result2.mode === "records") {
2903
2881
  return formatRows([
2904
2882
  {
@@ -2947,6 +2925,19 @@ function renderPush(result2) {
2947
2925
  }
2948
2926
  return formatRows(rows);
2949
2927
  }
2928
+ var REDIRECT_FLAGS = ["destination", "dest", "d", "location", "l"];
2929
+ function assertNoRedirect(args) {
2930
+ const given = REDIRECT_FLAGS.filter((flag) => args[flag] !== void 0);
2931
+ const named = given[0];
2932
+ if (named === void 0) {
2933
+ return;
2934
+ }
2935
+ throw new import_core15.PenvError(
2936
+ "REMOVED_FLAG",
2937
+ `\`--${named}\` is not a flag \`penv push\` takes`,
2938
+ "A push only ever goes where the config says this environment lives. Declare the destination in `environments.<env>` \u2014 the provider package, addressed in that provider's own words \u2014 and push with `--env <name>`."
2939
+ );
2940
+ }
2950
2941
  var pushCommand = (0, import_citty6.defineCommand)({
2951
2942
  meta: {
2952
2943
  name: "push",
@@ -2958,16 +2949,6 @@ var pushCommand = (0, import_citty6.defineCommand)({
2958
2949
  type: "boolean",
2959
2950
  description: "Decrypt sealed values locally and push them as plaintext for the destination to re-seal"
2960
2951
  },
2961
- destination: {
2962
- type: "string",
2963
- alias: ["dest", "d"],
2964
- description: "Push once to this provider package instead of the declared one"
2965
- },
2966
- location: {
2967
- type: "string",
2968
- alias: "l",
2969
- description: "The destination-side place, when --destination needs one"
2970
- },
2971
2952
  yes: {
2972
2953
  type: "boolean",
2973
2954
  alias: "y",
@@ -2976,25 +2957,13 @@ var pushCommand = (0, import_citty6.defineCommand)({
2976
2957
  },
2977
2958
  run({ args }) {
2978
2959
  return guard(async () => {
2960
+ assertNoRedirect(args);
2979
2961
  const result2 = await runPush({
2980
2962
  cwd: process.cwd(),
2981
2963
  ...args.env === void 0 ? {} : { environment: args.env },
2982
2964
  ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] },
2983
- ...args.destination === void 0 ? {} : { destination: args.destination },
2984
- ...args.location === void 0 ? {} : { location: args.location },
2985
2965
  ...args.yes === void 0 ? {} : { yes: args.yes },
2986
- envFlags: shorthandCandidates(args, [
2987
- "env",
2988
- "allow-decrypt",
2989
- "allowDecrypt",
2990
- "destination",
2991
- "dest",
2992
- "d",
2993
- "location",
2994
- "l",
2995
- "yes",
2996
- "y"
2997
- ])
2966
+ envFlags: shorthandCandidates(args, ["env", "allow-decrypt", "allowDecrypt", "yes", "y"])
2998
2967
  });
2999
2968
  write(renderPush(result2));
3000
2969
  });
@@ -3093,7 +3062,7 @@ async function runDoctor(options) {
3093
3062
  check: "provider",
3094
3063
  severity: "pass",
3095
3064
  label: "Provider",
3096
- subject: project.config.providers[environment]?.type ?? project.provider.type
3065
+ subject: (0, import_core16.environmentEntry)(project.config, environment)?.provider ?? project.provider.type
3097
3066
  });
3098
3067
  return {
3099
3068
  environment,
@@ -3363,8 +3332,8 @@ async function projectionFindings(project, environment, override) {
3363
3332
  if (override !== void 0) {
3364
3333
  projection = override;
3365
3334
  } else {
3366
- const declared = project.config.providers[environment];
3367
- if (declared === void 0 || declared.type === LOCAL_TREE_TYPE) {
3335
+ const declared = (0, import_core16.environmentEntry)(project.config, environment);
3336
+ if (declared === void 0 || declared.provider === LOCAL_TREE_TYPE) {
3368
3337
  return [];
3369
3338
  }
3370
3339
  let source;
@@ -3376,7 +3345,7 @@ async function projectionFindings(project, environment, override) {
3376
3345
  check: "projection-unreachable",
3377
3346
  severity: "unknown",
3378
3347
  label: "Destination",
3379
- subject: `could not build the ${declared.type} provider for ${environment}`,
3348
+ subject: `could not build the ${declared.provider} provider for ${environment}`,
3380
3349
  detail: errorDetail(error)
3381
3350
  }
3382
3351
  ];
@@ -3649,8 +3618,8 @@ function refPathOf(ref) {
3649
3618
  return [...ref.namespace, ref.name].join("/");
3650
3619
  }
3651
3620
  async function rotationSubjects(project, environment, local, override) {
3652
- const providerConfig = project.config.providers[environment];
3653
- if (override === void 0 && (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE)) {
3621
+ const providerConfig = (0, import_core16.environmentEntry)(project.config, environment);
3622
+ if (override === void 0 && (providerConfig === void 0 || providerConfig.provider === LOCAL_TREE_TYPE)) {
3654
3623
  return { kind: "read", subjects: local };
3655
3624
  }
3656
3625
  const source = override ?? await sourceProviderFor(project, environment);
@@ -3672,7 +3641,7 @@ async function rotationSubjects(project, environment, local, override) {
3672
3641
  check: "rotation-overdue",
3673
3642
  severity: "unknown",
3674
3643
  label: "Rotation",
3675
- subject: `could not reach the ${providerConfig?.type ?? source.type} source of truth for ${environment}`,
3644
+ subject: `could not reach the ${providerConfig?.provider ?? source.type} source of truth for ${environment}`,
3676
3645
  detail: errorDetail(error)
3677
3646
  }
3678
3647
  };
@@ -3793,8 +3762,8 @@ async function readRelevant(provider, environment) {
3793
3762
  return entries;
3794
3763
  }
3795
3764
  async function providerDriftFindings(project, environment, override) {
3796
- const providerConfig = project.config.providers[environment];
3797
- if (providerConfig === void 0 || providerConfig.type === LOCAL_TREE_TYPE) {
3765
+ const providerConfig = (0, import_core16.environmentEntry)(project.config, environment);
3766
+ if (providerConfig === void 0 || providerConfig.provider === LOCAL_TREE_TYPE) {
3798
3767
  return [
3799
3768
  {
3800
3769
  check: "provider-value-drift",
@@ -3819,7 +3788,7 @@ async function providerDriftFindings(project, environment, override) {
3819
3788
  check: "provider-value-drift",
3820
3789
  severity: "unknown",
3821
3790
  label: "Provider values",
3822
- subject: `could not reach the ${providerConfig.type} provider for ${environment}`,
3791
+ subject: `could not reach the ${providerConfig.provider} provider for ${environment}`,
3823
3792
  detail: errorDetail(error)
3824
3793
  }
3825
3794
  ];
@@ -3838,7 +3807,7 @@ async function providerDriftFindings(project, environment, override) {
3838
3807
  severity: "unknown",
3839
3808
  label: "Provider value unreadable",
3840
3809
  subject: (0, import_core16.formatValueFile)(here.file),
3841
- detail: `the local value is sealed and did not open, so it cannot be compared against the ${providerConfig.type} source of truth`
3810
+ detail: `the local value is sealed and did not open, so it cannot be compared against the ${providerConfig.provider} source of truth`
3842
3811
  });
3843
3812
  continue;
3844
3813
  }
@@ -3848,7 +3817,7 @@ async function providerDriftFindings(project, environment, override) {
3848
3817
  severity: "failure",
3849
3818
  label: "Provider value drift",
3850
3819
  subject: (0, import_core16.formatValueFile)(here.file),
3851
- detail: `the local tree and the ${providerConfig.type} source of truth hold different values`
3820
+ detail: `the local tree and the ${providerConfig.provider} source of truth hold different values`
3852
3821
  });
3853
3822
  }
3854
3823
  continue;
@@ -3862,7 +3831,7 @@ async function providerDriftFindings(project, environment, override) {
3862
3831
  severity: "warning",
3863
3832
  label: here !== void 0 ? "Only in the local tree" : "Only in the source",
3864
3833
  subject: (0, import_core16.formatValueFile)(present.file),
3865
- detail: here !== void 0 ? `present locally, absent from the ${providerConfig.type} source of truth` : `present in the ${providerConfig.type} source of truth, absent from the local tree`
3834
+ detail: here !== void 0 ? `present locally, absent from the ${providerConfig.provider} source of truth` : `present in the ${providerConfig.provider} source of truth, absent from the local tree`
3866
3835
  });
3867
3836
  }
3868
3837
  if (findings.length > 0) {
@@ -3873,7 +3842,7 @@ async function providerDriftFindings(project, environment, override) {
3873
3842
  check: "provider-value-drift",
3874
3843
  severity: "pass",
3875
3844
  label: "Provider values",
3876
- subject: `every value matches the ${providerConfig.type} source of truth for ${environment}`
3845
+ subject: `every value matches the ${providerConfig.provider} source of truth for ${environment}`
3877
3846
  }
3878
3847
  ];
3879
3848
  }
@@ -3975,7 +3944,7 @@ function targetScope(project, options, key) {
3975
3944
  throw new import_core17.PenvError(
3976
3945
  "ENVIRONMENT_FLAG_EMPTY",
3977
3946
  `\`--env\` for parameter ${key} names no environment`,
3978
- `Pass a declared environment \u2014 ${project.config.environments.map((e) => `\`${e}\``).join(", ")} \u2014 e.g. \`--env production\`, or drop \`--env\` to write the scope that has no environment.`
3947
+ `Pass a declared environment \u2014 ${(0, import_core17.environmentNames)(project.config).map((e) => `\`${e}\``).join(", ")} \u2014 e.g. \`--env production\`, or drop \`--env\` to write the scope that has no environment.`
3979
3948
  );
3980
3949
  }
3981
3950
  return scopeFrom({ ...options, environment: targetEnvironment(project, environment) });
@@ -3988,7 +3957,7 @@ function sealFor(project, file, value, parameter, environment) {
3988
3957
  throw new import_core17.PenvError(
3989
3958
  "SECRET_SCOPE_AMBIGUOUS",
3990
3959
  `Parameter ${parameter} is a secret, and ${(0, import_core17.recordPath)((0, import_core17.formatValueFile)(file))} names no environment`,
3991
- "Keys are declared per environment in the `keys` block of penv.config.ts, so penv cannot tell which key should seal a file that every environment reads. Write it at an environment scope \u2014 add `--env <environment>` \u2014 or drop `secret` from the parameter's meta."
3960
+ "A key is declared by its environment's `keySource` in penv.config.ts, so penv cannot tell which key should seal a file that every environment reads. Write it at an environment scope \u2014 add `--env <environment>` \u2014 or drop `secret` from the parameter's meta."
3992
3961
  );
3993
3962
  }
3994
3963
  return (0, import_core17.sealValue)(file, value, keySourceFor(project, environment), parameter, environment);
@@ -5074,8 +5043,14 @@ function environmentsFromFlag(flag) {
5074
5043
  }
5075
5044
  return [...new Set(names)];
5076
5045
  }
5046
+ function environmentsRecord(names) {
5047
+ return Object.fromEntries(names.map((environment) => [environment, LOCAL_PROVIDER]));
5048
+ }
5077
5049
  function configOf(decisions) {
5078
- return { environments: decisions.environments, providers: {}, schemaFile: decisions.schemaFile };
5050
+ return {
5051
+ environments: environmentsRecord(decisions.environments),
5052
+ schemaFile: decisions.schemaFile
5053
+ };
5079
5054
  }
5080
5055
  function declaredIn2(root) {
5081
5056
  const file = (0, import_node_path13.join)(root, CONFIG_FILE);
@@ -5107,7 +5082,7 @@ function planInit(root, flags = {}) {
5107
5082
  if (schemaFile.length === 0) {
5108
5083
  throw emptyFlag("schema");
5109
5084
  }
5110
- const error = (0, import_core25.validateSchemaFile)({ environments: [], providers: {}, schemaFile })[0];
5085
+ const error = (0, import_core25.validateSchemaFile)({ environments: {}, schemaFile })[0];
5111
5086
  if (error !== void 0) {
5112
5087
  throw error;
5113
5088
  }
@@ -5130,7 +5105,7 @@ function planInit(root, flags = {}) {
5130
5105
  }
5131
5106
  const suggestedEnvironments = suggestEnvironments(root);
5132
5107
  const safeDefault = flags.yes === true && declared === void 0;
5133
- const environments = flags.environments ?? declared?.environments ?? (safeDefault ? [DEVELOPMENT] : []);
5108
+ const environments = flags.environments ?? (declared === void 0 ? void 0 : (0, import_core25.environmentNames)(declared)) ?? (safeDefault ? [DEVELOPMENT] : []);
5134
5109
  const defaultEnvironment = declared?.defaultEnvironment ?? (safeDefault ? DEVELOPMENT : void 0);
5135
5110
  if (safeDefault) {
5136
5111
  notes.push(
@@ -5263,27 +5238,27 @@ export { schema };
5263
5238
 
5264
5239
  ` + loadComment + loadCall;
5265
5240
  }
5241
+ function entryKey(name) {
5242
+ return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
5243
+ }
5266
5244
  function renderEnvironments(decisions) {
5267
- const shared = " // Environments are a whitelist. A filename segment is an environment only if\n // it is declared here \u2014 penv never infers one from a folder or a filename.\n";
5245
+ const shared = " // Environments are a whitelist, and each entry is that environment's whole\n // declaration: which provider holds it, in that provider's own words, and\n // which key seals it. A segment is an environment only if it is named here.\n";
5268
5246
  if (decisions.environments.length === 0) {
5269
5247
  return `${shared} // It starts empty because which environments you deploy is not something penv
5270
5248
  // can read off your codebase, and an environment you do not have is worse
5271
- // than one you have not declared yet. Name yours, and give each a provider:
5272
- // environments: ["development", "production"],
5273
- // providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-vault" } },
5274
- environments: [],
5275
-
5276
- providers: {},
5249
+ // than one you have not declared yet. Name yours:
5250
+ // environments: {
5251
+ // development: "@penvhq/provider-filesystem",
5252
+ // production: { provider: "@penvhq/provider-vault", path: "penv", keySource: "env" },
5253
+ // },
5254
+ environments: {},
5277
5255
  `;
5278
5256
  }
5279
- const names = decisions.environments.map((name) => JSON.stringify(name)).join(", ");
5280
- const providers = decisions.environments.map((name) => ` ${JSON.stringify(name)}: { type: "@penvhq/provider-filesystem" },
5257
+ const entries = decisions.environments.map((name) => ` ${entryKey(name)}: ${JSON.stringify(LOCAL_PROVIDER)},
5281
5258
  `).join("");
5282
- return `${shared} environments: [${names}],
5283
-
5284
- // One entry per environment: where that environment's values are read from.
5285
- providers: {
5286
- ${providers} },
5259
+ return `${shared} // A provider that needs no fields is just its package name.
5260
+ environments: {
5261
+ ${entries} },
5287
5262
  `;
5288
5263
  }
5289
5264
  function renderConfigModule(decisions) {
@@ -5802,13 +5777,13 @@ function planCutover(input) {
5802
5777
  const declared = declaredIn2(root);
5803
5778
  const named = environmentsDeclaredBy(selected);
5804
5779
  const chosen = named.length > 0 ? named : [requireEnvironment(input)];
5805
- const environments = declared === void 0 ? chosen : declared.environments;
5780
+ const environments = declared === void 0 ? chosen : (0, import_core25.environmentNames)(declared);
5806
5781
  for (const environment of chosen) {
5807
5782
  if (!environments.includes(environment)) {
5808
5783
  throw new import_core25.PenvError(
5809
5784
  "INIT_ENVIRONMENT_UNDECLARED",
5810
5785
  `${CONFIG_FILE} declares ${describeList(environments)}, and adopting these files needs \`${environment}\` declared too`,
5811
- `Add \`${environment}\` to \`environments\` in ${CONFIG_FILE}, with a provider for it, then run \`penv init\` again. Nothing was changed.`
5786
+ `Add \`${environment}: "${LOCAL_PROVIDER}"\` to \`environments\` in ${CONFIG_FILE}, or name the backend that holds it, then run \`penv init\` again. Nothing was changed.`
5812
5787
  );
5813
5788
  }
5814
5789
  }
@@ -5875,10 +5850,7 @@ function distinct(refs) {
5875
5850
  }
5876
5851
  function configFor(decisions) {
5877
5852
  return {
5878
- environments: decisions.environments,
5879
- providers: Object.fromEntries(
5880
- decisions.environments.map((environment) => [environment, { type: LOCAL_PROVIDER }])
5881
- ),
5853
+ environments: environmentsRecord(decisions.environments),
5882
5854
  schemaFile: decisions.schemaFile,
5883
5855
  ...decisions.defaultEnvironment === void 0 ? {} : { defaultEnvironment: decisions.defaultEnvironment },
5884
5856
  ...decisions.publicPrefixes.length === 0 ? {} : { publicPrefixes: [...decisions.publicPrefixes] }
@@ -6218,7 +6190,8 @@ function offeredEnvironment(root) {
6218
6190
  if (declared === void 0) {
6219
6191
  return DEVELOPMENT;
6220
6192
  }
6221
- return declared.defaultEnvironment ?? (declared.environments.includes(DEVELOPMENT) ? DEVELOPMENT : declared.environments[0] ?? DEVELOPMENT);
6193
+ const names = (0, import_core25.environmentNames)(declared);
6194
+ return declared.defaultEnvironment ?? (names.includes(DEVELOPMENT) ? DEVELOPMENT : names[0] ?? DEVELOPMENT);
6222
6195
  }
6223
6196
  async function cutoverWithYes(root, base, adoption) {
6224
6197
  const selected = selectionForYes(adoption);
@@ -6326,10 +6299,11 @@ var BACKUP_SUFFIX = ".backup";
6326
6299
  var DOTENV_SEGMENT = "env";
6327
6300
  var LOCAL2 = "local";
6328
6301
  function assertDeclared(segment, config) {
6329
- if (config.environments.includes(segment)) {
6302
+ const declared = (0, import_core26.environmentNames)(config);
6303
+ if (declared.includes(segment)) {
6330
6304
  return segment;
6331
6305
  }
6332
- throw new import_core26.UnknownEnvironmentError(segment, config.environments);
6306
+ throw new import_core26.UnknownEnvironmentError(segment, declared);
6333
6307
  }
6334
6308
  function scopeFromFilename(file, config) {
6335
6309
  const name = (0, import_node_path14.basename)(file);
@@ -6399,7 +6373,7 @@ function explicitEnvironment(options, source, config) {
6399
6373
  throw new import_core26.PenvError(
6400
6374
  "IMPORT_ENV_FLAG_EMPTY",
6401
6375
  `\`--env\` for the import of ${source} names no environment`,
6402
- `Pass a declared environment \u2014 ${config.environments.map((e) => `\`${e}\``).join(", ")} \u2014 e.g. \`--env production\`, or drop \`--env\` to import ${source} as the scope that has no environment. Nothing was imported.`
6376
+ `Pass a declared environment \u2014 ${(0, import_core26.environmentNames)(config).map((e) => `\`${e}\``).join(", ")} \u2014 e.g. \`--env production\`, or drop \`--env\` to import ${source} as the scope that has no environment. Nothing was imported.`
6403
6377
  );
6404
6378
  }
6405
6379
  function assertEnvironmentAgrees(derived, explicit, source) {
@@ -6426,7 +6400,7 @@ function environmentNamed(file, explicit) {
6426
6400
  }
6427
6401
  function decisionsOf(config, cwd) {
6428
6402
  return {
6429
- environments: config.environments,
6403
+ environments: (0, import_core26.environmentNames)(config),
6430
6404
  // `import` re-scaffolds env.ts for an existing project; injection is an init
6431
6405
  // choice, so it is not turned on here.
6432
6406
  inject: false,
@@ -6482,7 +6456,7 @@ function importDotenv(options) {
6482
6456
  backup,
6483
6457
  scope,
6484
6458
  environment,
6485
- environments: config.environments,
6459
+ environments: (0, import_core26.environmentNames)(config),
6486
6460
  variables: parsed.entries.length,
6487
6461
  orphanComments: parsed.orphanComments,
6488
6462
  steps
@@ -6630,12 +6604,12 @@ function envVarFor(id) {
6630
6604
  function runKeyCreate(options) {
6631
6605
  const project = openProject(options.cwd);
6632
6606
  const environment = targetEnvironment(project, options.environment);
6633
- const declared = project.config.keys?.[environment];
6607
+ const declared = (0, import_core28.keyConfigFor)(project.config, environment);
6634
6608
  if (declared === void 0) {
6635
6609
  throw new import_core28.PenvError(
6636
6610
  "KEY_SOURCE_UNDECLARED",
6637
6611
  `Environment ${environment} declares no key source, so penv does not know what a key for it would be`,
6638
- `Add a \`keys\` entry to penv.config.ts \u2014 e.g. \`keys: { ${environment}: { source: "env", id: "${environment}" } }\` \u2014 then run this again.`
6612
+ `Give its entry in penv.config.ts a \`keySource\` \u2014 e.g. \`${environment}: { provider: "@penvhq/provider-filesystem", keySource: "env" }\` \u2014 then run this again. The key is named after the environment unless the object form names another.`
6639
6613
  );
6640
6614
  }
6641
6615
  const key = (0, import_node_crypto.randomBytes)(import_core28.KEY_BYTES).toString("base64");
@@ -6978,7 +6952,7 @@ async function planFile(project, source, target, parameter) {
6978
6952
  throw new import_core31.PenvError(
6979
6953
  "SECRET_SCOPE_AMBIGUOUS",
6980
6954
  `${(0, import_core31.recordPath)((0, import_core31.formatValueFile)(source))} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
6981
- "Keys are declared per environment in the `keys` block of penv.config.ts. Decrypt it with `penv decrypt`, move the parameter, then encrypt it again at its new address."
6955
+ "A key is declared by its environment's `keySource` in penv.config.ts. Decrypt it with `penv decrypt`, move the parameter, then encrypt it again at its new address."
6982
6956
  );
6983
6957
  }
6984
6958
  const keys = keySourceFor(project, environment);