@penvhq/cli 0.13.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
@@ -311,6 +311,10 @@ function declaredIn(dir, name) {
311
311
  }
312
312
  return void 0;
313
313
  }
314
+ function blockPresentIn(dir, dev) {
315
+ const block = manifestOf(dir)?.[dev ? "devDependencies" : "dependencies"];
316
+ return block !== null && typeof block === "object" && !Array.isArray(block);
317
+ }
314
318
  function isPnpmWorkspaceRoot(root) {
315
319
  return (0, import_node_fs2.existsSync)((0, import_node_path2.join)(root, PNPM_WORKSPACE)) && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(root, "package.json"));
316
320
  }
@@ -435,6 +439,7 @@ function stepFor(manager, manifest, packages, options) {
435
439
  packages,
436
440
  command: addCommand(manager, options, specs),
437
441
  dev: options.dev,
442
+ blockPresent: blockPresentIn(options.dir, options.dev),
438
443
  satisfied: pending.length === 0
439
444
  };
440
445
  }
@@ -475,31 +480,35 @@ function planInstall(root, version = engineVersion()) {
475
480
  const steps = [
476
481
  stepFor(manager, "package.json", packages, {
477
482
  workspaceRoot,
483
+ dir: root,
478
484
  dev: runtime?.dev === true && pending.every((entry) => entry.name === RUNTIME_PACKAGE) && pending.length > 0
479
485
  }),
480
- stepFor(manager, "package.json", [typesPackage], { workspaceRoot, dev: true })
486
+ stepFor(manager, "package.json", [typesPackage], { workspaceRoot, dir: root, dev: true })
481
487
  ];
482
- for (const dir of workspaceMembers(root, RUNTIME_PACKAGE)) {
483
- const declared = declaredIn(dir, RUNTIME_PACKAGE);
484
- steps.push(
485
- stepFor(
486
- manager,
487
- manifestPathOf(root, dir),
488
- [
488
+ for (const name of [RUNTIME_PACKAGE, TYPES_PACKAGE]) {
489
+ for (const dir of workspaceMembers(root, name)) {
490
+ const declared = declaredIn(dir, name);
491
+ steps.push(
492
+ stepFor(
493
+ manager,
494
+ manifestPathOf(root, dir),
495
+ [
496
+ {
497
+ name,
498
+ version,
499
+ declared: declared.version,
500
+ satisfied: declared.version === version
501
+ }
502
+ ],
489
503
  {
490
- name: RUNTIME_PACKAGE,
491
- version,
492
- declared: declared.version,
493
- satisfied: declared.version === version
504
+ filter: `./${(0, import_node_path2.relative)(root, dir).split(import_node_path2.sep).join("/")}`,
505
+ workspaceRoot: false,
506
+ dir,
507
+ dev: declared.dev
494
508
  }
495
- ],
496
- {
497
- filter: `./${(0, import_node_path2.relative)(root, dir).split(import_node_path2.sep).join("/")}`,
498
- workspaceRoot: false,
499
- dev: declared.dev
500
- }
501
- )
502
- );
509
+ )
510
+ );
511
+ }
503
512
  }
504
513
  return {
505
514
  root,
@@ -528,12 +537,13 @@ function renderStep(step) {
528
537
  const added = pending.filter((entry) => entry.declared === void 0);
529
538
  const replaced = pending.filter((entry) => entry.declared !== void 0);
530
539
  const block = step.dev ? "devDependencies" : "dependencies";
540
+ const edge = step.blockPresent ? " " : " +";
531
541
  return [
532
542
  step.manifest,
533
543
  ...added.length === 0 ? [] : [
534
- ` + "${block}": {`,
544
+ `${edge} "${block}": {`,
535
545
  ...added.map((entry) => ` + "${entry.name}": "${entry.version}"`),
536
- " + }"
546
+ `${edge} }`
537
547
  ],
538
548
  ...replaced.flatMap((entry) => [
539
549
  ` - "${entry.name}": "${entry.declared}"`,
@@ -592,22 +602,10 @@ var import_provider_filesystem2 = require("@penvhq/provider-filesystem");
592
602
  // src/env-flags.ts
593
603
  var import_core3 = require("@penvhq/core");
594
604
  var BASE_RESERVED = ["help", "version", "h", "v", "_", "--"];
595
- var COMMAND_FLAGS = [
596
- ...BASE_RESERVED,
597
- "env",
598
- "out",
599
- "allow-decrypt",
600
- "destination",
601
- "dest",
602
- "d",
603
- "location",
604
- "l",
605
- "yes",
606
- "y"
607
- ];
605
+ var COMMAND_FLAGS = [...BASE_RESERVED, "env", "out", "allow-decrypt", "yes", "y"];
608
606
  function shadowedEnvironments(config) {
609
607
  const flags = new Set(COMMAND_FLAGS);
610
- return config.environments.filter((environment) => flags.has(environment));
608
+ return (0, import_core3.environmentNames)(config).filter((environment) => flags.has(environment));
611
609
  }
612
610
  function shorthandCandidates(args, declared) {
613
611
  const reserved = /* @__PURE__ */ new Set([...BASE_RESERVED, ...declared]);
@@ -620,13 +618,14 @@ function environmentFromShorthand(config, candidates, explicit) {
620
618
  if (candidates.length === 0) {
621
619
  return void 0;
622
620
  }
623
- const hits = candidates.filter((candidate) => config.environments.includes(candidate));
624
- 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));
625
624
  if (strangers.length > 0) {
626
625
  throw new import_core3.PenvError(
627
626
  "UNKNOWN_FLAG",
628
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`,
629
- `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>\`.`
630
629
  );
631
630
  }
632
631
  if (hits.length > 1) {
@@ -717,14 +716,18 @@ async function loadPluginProvider(type, context) {
717
716
  function assertProvidersRegistered(config, projectRoot, options) {
718
717
  const local = localExtensions(projectRoot);
719
718
  const ci = options?.ci ?? isCi(process.env.CI);
720
- for (const [environment, provider] of Object.entries(config.providers)) {
721
- 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)) {
722
725
  continue;
723
726
  }
724
- if (ci && local.includes(provider.type)) {
725
- throw localExtensionInCi(provider.type, environment);
727
+ if (ci && local.includes(provider)) {
728
+ throw localExtensionInCi(provider, environment);
726
729
  }
727
- resolveExtension(provider.type, projectRoot, environment, local);
730
+ resolveExtension(provider, projectRoot, environment, local);
728
731
  }
729
732
  }
730
733
  function resolveExtension(type, projectRoot, environment, local = localExtensions(projectRoot)) {
@@ -882,11 +885,11 @@ function schemaShapeFileOf(project) {
882
885
  return selfContainedSchemaModule(project.root, schemaFile) ?? import_core5.SCHEMA_SHAPE_FILE;
883
886
  }
884
887
  async function sourceProviderFor(project, environment) {
885
- const providerConfig = project.config.providers[environment];
888
+ const providerConfig = (0, import_core5.environmentEntry)(project.config, environment);
886
889
  if (providerConfig === void 0) {
887
890
  return createProvider(LOCAL_TREE_TYPE, { root: project.root, config: project.config });
888
891
  }
889
- return createSourceProvider(providerConfig.type, {
892
+ return createSourceProvider(providerConfig.provider, {
890
893
  root: project.root,
891
894
  config: project.config,
892
895
  providerConfig,
@@ -898,7 +901,7 @@ function targetEnvironment(project, explicit, shorthand) {
898
901
  return (0, import_core5.resolveEnvironment)(project.config, explicit ?? fromFlags);
899
902
  }
900
903
  var KEY_SEPARATOR = /[./\\]/;
901
- var NO_ENVIRONMENTS = { environments: [], providers: {} };
904
+ var NO_ENVIRONMENTS = { environments: {} };
902
905
  function refFromKey(key, config) {
903
906
  const segments = key.split(KEY_SEPARATOR).filter((segment) => segment.length > 0);
904
907
  const name = segments[segments.length - 1];
@@ -1222,8 +1225,9 @@ function discoverDotenvFiles(root) {
1222
1225
  return files.sort((a, b) => order(a) < order(b) ? -1 : order(a) > order(b) ? 1 : 0);
1223
1226
  }
1224
1227
  function activeDotenvFiles(root, config) {
1228
+ const declared = (0, import_core7.environmentNames)(config);
1225
1229
  return discoverDotenvFiles(root).filter(
1226
- (file) => file.environment === void 0 || config.environments.includes(file.environment)
1230
+ (file) => file.environment === void 0 || declared.includes(file.environment)
1227
1231
  );
1228
1232
  }
1229
1233
  function environmentsDeclaredBy(files) {
@@ -1964,11 +1968,12 @@ function packageManifest(specifier, root) {
1964
1968
  }
1965
1969
  function declaredCredentials(config, root) {
1966
1970
  const declared = {};
1967
- for (const provider of Object.values(config.providers)) {
1968
- 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) {
1969
1974
  continue;
1970
1975
  }
1971
- const penv = packageManifest(provider.type, root)?.penv;
1976
+ const penv = packageManifest(provider, root)?.penv;
1972
1977
  const credentials = isPlainObject(penv) ? penv.credentials : void 0;
1973
1978
  if (credentials === void 0) {
1974
1979
  continue;
@@ -1976,11 +1981,11 @@ function declaredCredentials(config, root) {
1976
1981
  if (!Array.isArray(credentials) || credentials.some((name) => typeof name !== "string" || !VARIABLE.test(name))) {
1977
1982
  throw new import_core11.PenvError(
1978
1983
  "PROVIDER_CREDENTIALS_INVALID",
1979
- `\`${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`,
1980
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.`
1981
1986
  );
1982
1987
  }
1983
- declared[provider.type] = credentials;
1988
+ declared[provider] = credentials;
1984
1989
  }
1985
1990
  return declared;
1986
1991
  }
@@ -2672,38 +2677,23 @@ function lineReader(input = process.stdin, output = process.stdout) {
2672
2677
  // src/commands/push.ts
2673
2678
  var LAST_PUSHED_KEY = "lastPushedAt";
2674
2679
  async function destinationFor(project, environment, options) {
2675
- if (options.destination !== void 0) {
2676
- const providerConfig = {
2677
- type: options.destination,
2678
- ...options.location === void 0 ? {} : { location: options.location }
2679
- };
2680
- const provider2 = options.provider ?? await createSourceProvider(options.destination, {
2681
- root: project.root,
2682
- config: project.config,
2683
- providerConfig,
2684
- environment
2685
- });
2686
- return { provider: provider2, location: options.location };
2687
- }
2688
- const declared = project.config.providers[environment];
2689
- const location = declared?.location;
2690
- 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) {
2691
2682
  throw new import_core15.PenvError(
2692
2683
  "NO_DESTINATION",
2693
2684
  `Environment ${environment}'s provider is the local records tree itself, so penv has nowhere to push`,
2694
- `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.`
2695
2686
  );
2696
2687
  }
2697
2688
  if (options.provider !== void 0) {
2698
- return { provider: options.provider, location };
2689
+ return options.provider;
2699
2690
  }
2700
- const provider = await createSourceProvider(declared.type, {
2691
+ return createSourceProvider(declared.provider, {
2701
2692
  root: project.root,
2702
2693
  config: project.config,
2703
2694
  providerConfig: declared,
2704
2695
  environment
2705
2696
  });
2706
- return { provider, location };
2707
2697
  }
2708
2698
  function plan(resolutions, config, environment, allowDecrypt) {
2709
2699
  const outbound = [];
@@ -2787,7 +2777,7 @@ async function ensureTargetApproved(provider, environment, options) {
2787
2777
  await provider.ensureTarget(environment);
2788
2778
  return true;
2789
2779
  }
2790
- async function pushProjection(project, provider, environment, location, options) {
2780
+ async function pushProjection(project, provider, environment, options) {
2791
2781
  const tree = localTree(project);
2792
2782
  const keys = keySourceFor(project, environment);
2793
2783
  const resolutions = resolveAllSync(tree, environment, keys, true);
@@ -2818,7 +2808,6 @@ async function pushProjection(project, provider, environment, location, options)
2818
2808
  environment,
2819
2809
  destination: provider.type,
2820
2810
  mode: "projection",
2821
- location,
2822
2811
  pushed: outbound.length,
2823
2812
  meta: 0,
2824
2813
  repositorySecrets,
@@ -2834,7 +2823,7 @@ function crossesToRecords(file, environment) {
2834
2823
  }
2835
2824
  return scope.kind === "environment" && scope.environment === environment;
2836
2825
  }
2837
- async function pushRecords(project, provider, environment, location) {
2826
+ async function pushRecords(project, provider, environment) {
2838
2827
  const tree = localTree(project);
2839
2828
  const files = tree.listSync().filter((file) => crossesToRecords(file, environment));
2840
2829
  let pushed = 0;
@@ -2860,7 +2849,6 @@ async function pushRecords(project, provider, environment, location) {
2860
2849
  environment,
2861
2850
  destination: provider.type,
2862
2851
  mode: "records",
2863
- location,
2864
2852
  pushed,
2865
2853
  meta,
2866
2854
  repositorySecrets: 0,
@@ -2872,11 +2860,11 @@ async function pushRecords(project, provider, environment, location) {
2872
2860
  async function runPush(options) {
2873
2861
  const project = openProject(options.cwd);
2874
2862
  const environment = targetEnvironment(project, options.environment, options.envFlags);
2875
- const { provider, location } = await destinationFor(project, environment, options);
2863
+ const provider = await destinationFor(project, environment, options);
2876
2864
  if ((0, import_core15.holdsProjection)(provider)) {
2877
- return pushProjection(project, provider, environment, location, options);
2865
+ return pushProjection(project, provider, environment, options);
2878
2866
  }
2879
- return pushRecords(project, provider, environment, location);
2867
+ return pushRecords(project, provider, environment);
2880
2868
  }
2881
2869
  function renderPush(result2) {
2882
2870
  if (result2.pushed === 0 && result2.meta === 0) {
@@ -2888,7 +2876,7 @@ function renderPush(result2) {
2888
2876
  }
2889
2877
  ]);
2890
2878
  }
2891
- const target = `${result2.destination} for environment ${result2.environment}${result2.location === void 0 ? "" : ` (${result2.location})`}`;
2879
+ const target = `${result2.destination} for environment ${result2.environment}`;
2892
2880
  if (result2.mode === "records") {
2893
2881
  return formatRows([
2894
2882
  {
@@ -2937,6 +2925,19 @@ function renderPush(result2) {
2937
2925
  }
2938
2926
  return formatRows(rows);
2939
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
+ }
2940
2941
  var pushCommand = (0, import_citty6.defineCommand)({
2941
2942
  meta: {
2942
2943
  name: "push",
@@ -2948,16 +2949,6 @@ var pushCommand = (0, import_citty6.defineCommand)({
2948
2949
  type: "boolean",
2949
2950
  description: "Decrypt sealed values locally and push them as plaintext for the destination to re-seal"
2950
2951
  },
2951
- destination: {
2952
- type: "string",
2953
- alias: ["dest", "d"],
2954
- description: "Push once to this provider package instead of the declared one"
2955
- },
2956
- location: {
2957
- type: "string",
2958
- alias: "l",
2959
- description: "The destination-side place, when --destination needs one"
2960
- },
2961
2952
  yes: {
2962
2953
  type: "boolean",
2963
2954
  alias: "y",
@@ -2966,25 +2957,13 @@ var pushCommand = (0, import_citty6.defineCommand)({
2966
2957
  },
2967
2958
  run({ args }) {
2968
2959
  return guard(async () => {
2960
+ assertNoRedirect(args);
2969
2961
  const result2 = await runPush({
2970
2962
  cwd: process.cwd(),
2971
2963
  ...args.env === void 0 ? {} : { environment: args.env },
2972
2964
  ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] },
2973
- ...args.destination === void 0 ? {} : { destination: args.destination },
2974
- ...args.location === void 0 ? {} : { location: args.location },
2975
2965
  ...args.yes === void 0 ? {} : { yes: args.yes },
2976
- envFlags: shorthandCandidates(args, [
2977
- "env",
2978
- "allow-decrypt",
2979
- "allowDecrypt",
2980
- "destination",
2981
- "dest",
2982
- "d",
2983
- "location",
2984
- "l",
2985
- "yes",
2986
- "y"
2987
- ])
2966
+ envFlags: shorthandCandidates(args, ["env", "allow-decrypt", "allowDecrypt", "yes", "y"])
2988
2967
  });
2989
2968
  write(renderPush(result2));
2990
2969
  });
@@ -3083,7 +3062,7 @@ async function runDoctor(options) {
3083
3062
  check: "provider",
3084
3063
  severity: "pass",
3085
3064
  label: "Provider",
3086
- subject: project.config.providers[environment]?.type ?? project.provider.type
3065
+ subject: (0, import_core16.environmentEntry)(project.config, environment)?.provider ?? project.provider.type
3087
3066
  });
3088
3067
  return {
3089
3068
  environment,
@@ -3353,8 +3332,8 @@ async function projectionFindings(project, environment, override) {
3353
3332
  if (override !== void 0) {
3354
3333
  projection = override;
3355
3334
  } else {
3356
- const declared = project.config.providers[environment];
3357
- 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) {
3358
3337
  return [];
3359
3338
  }
3360
3339
  let source;
@@ -3366,7 +3345,7 @@ async function projectionFindings(project, environment, override) {
3366
3345
  check: "projection-unreachable",
3367
3346
  severity: "unknown",
3368
3347
  label: "Destination",
3369
- subject: `could not build the ${declared.type} provider for ${environment}`,
3348
+ subject: `could not build the ${declared.provider} provider for ${environment}`,
3370
3349
  detail: errorDetail(error)
3371
3350
  }
3372
3351
  ];
@@ -3639,8 +3618,8 @@ function refPathOf(ref) {
3639
3618
  return [...ref.namespace, ref.name].join("/");
3640
3619
  }
3641
3620
  async function rotationSubjects(project, environment, local, override) {
3642
- const providerConfig = project.config.providers[environment];
3643
- 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)) {
3644
3623
  return { kind: "read", subjects: local };
3645
3624
  }
3646
3625
  const source = override ?? await sourceProviderFor(project, environment);
@@ -3662,7 +3641,7 @@ async function rotationSubjects(project, environment, local, override) {
3662
3641
  check: "rotation-overdue",
3663
3642
  severity: "unknown",
3664
3643
  label: "Rotation",
3665
- 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}`,
3666
3645
  detail: errorDetail(error)
3667
3646
  }
3668
3647
  };
@@ -3783,8 +3762,8 @@ async function readRelevant(provider, environment) {
3783
3762
  return entries;
3784
3763
  }
3785
3764
  async function providerDriftFindings(project, environment, override) {
3786
- const providerConfig = project.config.providers[environment];
3787
- 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) {
3788
3767
  return [
3789
3768
  {
3790
3769
  check: "provider-value-drift",
@@ -3809,7 +3788,7 @@ async function providerDriftFindings(project, environment, override) {
3809
3788
  check: "provider-value-drift",
3810
3789
  severity: "unknown",
3811
3790
  label: "Provider values",
3812
- subject: `could not reach the ${providerConfig.type} provider for ${environment}`,
3791
+ subject: `could not reach the ${providerConfig.provider} provider for ${environment}`,
3813
3792
  detail: errorDetail(error)
3814
3793
  }
3815
3794
  ];
@@ -3828,7 +3807,7 @@ async function providerDriftFindings(project, environment, override) {
3828
3807
  severity: "unknown",
3829
3808
  label: "Provider value unreadable",
3830
3809
  subject: (0, import_core16.formatValueFile)(here.file),
3831
- 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`
3832
3811
  });
3833
3812
  continue;
3834
3813
  }
@@ -3838,7 +3817,7 @@ async function providerDriftFindings(project, environment, override) {
3838
3817
  severity: "failure",
3839
3818
  label: "Provider value drift",
3840
3819
  subject: (0, import_core16.formatValueFile)(here.file),
3841
- 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`
3842
3821
  });
3843
3822
  }
3844
3823
  continue;
@@ -3852,7 +3831,7 @@ async function providerDriftFindings(project, environment, override) {
3852
3831
  severity: "warning",
3853
3832
  label: here !== void 0 ? "Only in the local tree" : "Only in the source",
3854
3833
  subject: (0, import_core16.formatValueFile)(present.file),
3855
- 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`
3856
3835
  });
3857
3836
  }
3858
3837
  if (findings.length > 0) {
@@ -3863,7 +3842,7 @@ async function providerDriftFindings(project, environment, override) {
3863
3842
  check: "provider-value-drift",
3864
3843
  severity: "pass",
3865
3844
  label: "Provider values",
3866
- 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}`
3867
3846
  }
3868
3847
  ];
3869
3848
  }
@@ -3965,7 +3944,7 @@ function targetScope(project, options, key) {
3965
3944
  throw new import_core17.PenvError(
3966
3945
  "ENVIRONMENT_FLAG_EMPTY",
3967
3946
  `\`--env\` for parameter ${key} names no environment`,
3968
- `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.`
3969
3948
  );
3970
3949
  }
3971
3950
  return scopeFrom({ ...options, environment: targetEnvironment(project, environment) });
@@ -3978,7 +3957,7 @@ function sealFor(project, file, value, parameter, environment) {
3978
3957
  throw new import_core17.PenvError(
3979
3958
  "SECRET_SCOPE_AMBIGUOUS",
3980
3959
  `Parameter ${parameter} is a secret, and ${(0, import_core17.recordPath)((0, import_core17.formatValueFile)(file))} names no environment`,
3981
- "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."
3982
3961
  );
3983
3962
  }
3984
3963
  return (0, import_core17.sealValue)(file, value, keySourceFor(project, environment), parameter, environment);
@@ -5064,8 +5043,14 @@ function environmentsFromFlag(flag) {
5064
5043
  }
5065
5044
  return [...new Set(names)];
5066
5045
  }
5046
+ function environmentsRecord(names) {
5047
+ return Object.fromEntries(names.map((environment) => [environment, LOCAL_PROVIDER]));
5048
+ }
5067
5049
  function configOf(decisions) {
5068
- return { environments: decisions.environments, providers: {}, schemaFile: decisions.schemaFile };
5050
+ return {
5051
+ environments: environmentsRecord(decisions.environments),
5052
+ schemaFile: decisions.schemaFile
5053
+ };
5069
5054
  }
5070
5055
  function declaredIn2(root) {
5071
5056
  const file = (0, import_node_path13.join)(root, CONFIG_FILE);
@@ -5097,7 +5082,7 @@ function planInit(root, flags = {}) {
5097
5082
  if (schemaFile.length === 0) {
5098
5083
  throw emptyFlag("schema");
5099
5084
  }
5100
- const error = (0, import_core25.validateSchemaFile)({ environments: [], providers: {}, schemaFile })[0];
5085
+ const error = (0, import_core25.validateSchemaFile)({ environments: {}, schemaFile })[0];
5101
5086
  if (error !== void 0) {
5102
5087
  throw error;
5103
5088
  }
@@ -5120,7 +5105,7 @@ function planInit(root, flags = {}) {
5120
5105
  }
5121
5106
  const suggestedEnvironments = suggestEnvironments(root);
5122
5107
  const safeDefault = flags.yes === true && declared === void 0;
5123
- 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] : []);
5124
5109
  const defaultEnvironment = declared?.defaultEnvironment ?? (safeDefault ? DEVELOPMENT : void 0);
5125
5110
  if (safeDefault) {
5126
5111
  notes.push(
@@ -5253,27 +5238,27 @@ export { schema };
5253
5238
 
5254
5239
  ` + loadComment + loadCall;
5255
5240
  }
5241
+ function entryKey(name) {
5242
+ return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
5243
+ }
5256
5244
  function renderEnvironments(decisions) {
5257
- 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";
5258
5246
  if (decisions.environments.length === 0) {
5259
5247
  return `${shared} // It starts empty because which environments you deploy is not something penv
5260
5248
  // can read off your codebase, and an environment you do not have is worse
5261
- // than one you have not declared yet. Name yours, and give each a provider:
5262
- // environments: ["development", "production"],
5263
- // providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-vault" } },
5264
- environments: [],
5265
-
5266
- 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: {},
5267
5255
  `;
5268
5256
  }
5269
- const names = decisions.environments.map((name) => JSON.stringify(name)).join(", ");
5270
- 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)},
5271
5258
  `).join("");
5272
- return `${shared} environments: [${names}],
5273
-
5274
- // One entry per environment: where that environment's values are read from.
5275
- providers: {
5276
- ${providers} },
5259
+ return `${shared} // A provider that needs no fields is just its package name.
5260
+ environments: {
5261
+ ${entries} },
5277
5262
  `;
5278
5263
  }
5279
5264
  function renderConfigModule(decisions) {
@@ -5792,13 +5777,13 @@ function planCutover(input) {
5792
5777
  const declared = declaredIn2(root);
5793
5778
  const named = environmentsDeclaredBy(selected);
5794
5779
  const chosen = named.length > 0 ? named : [requireEnvironment(input)];
5795
- const environments = declared === void 0 ? chosen : declared.environments;
5780
+ const environments = declared === void 0 ? chosen : (0, import_core25.environmentNames)(declared);
5796
5781
  for (const environment of chosen) {
5797
5782
  if (!environments.includes(environment)) {
5798
5783
  throw new import_core25.PenvError(
5799
5784
  "INIT_ENVIRONMENT_UNDECLARED",
5800
5785
  `${CONFIG_FILE} declares ${describeList(environments)}, and adopting these files needs \`${environment}\` declared too`,
5801
- `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.`
5802
5787
  );
5803
5788
  }
5804
5789
  }
@@ -5865,10 +5850,7 @@ function distinct(refs) {
5865
5850
  }
5866
5851
  function configFor(decisions) {
5867
5852
  return {
5868
- environments: decisions.environments,
5869
- providers: Object.fromEntries(
5870
- decisions.environments.map((environment) => [environment, { type: LOCAL_PROVIDER }])
5871
- ),
5853
+ environments: environmentsRecord(decisions.environments),
5872
5854
  schemaFile: decisions.schemaFile,
5873
5855
  ...decisions.defaultEnvironment === void 0 ? {} : { defaultEnvironment: decisions.defaultEnvironment },
5874
5856
  ...decisions.publicPrefixes.length === 0 ? {} : { publicPrefixes: [...decisions.publicPrefixes] }
@@ -6208,7 +6190,8 @@ function offeredEnvironment(root) {
6208
6190
  if (declared === void 0) {
6209
6191
  return DEVELOPMENT;
6210
6192
  }
6211
- 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);
6212
6195
  }
6213
6196
  async function cutoverWithYes(root, base, adoption) {
6214
6197
  const selected = selectionForYes(adoption);
@@ -6316,10 +6299,11 @@ var BACKUP_SUFFIX = ".backup";
6316
6299
  var DOTENV_SEGMENT = "env";
6317
6300
  var LOCAL2 = "local";
6318
6301
  function assertDeclared(segment, config) {
6319
- if (config.environments.includes(segment)) {
6302
+ const declared = (0, import_core26.environmentNames)(config);
6303
+ if (declared.includes(segment)) {
6320
6304
  return segment;
6321
6305
  }
6322
- throw new import_core26.UnknownEnvironmentError(segment, config.environments);
6306
+ throw new import_core26.UnknownEnvironmentError(segment, declared);
6323
6307
  }
6324
6308
  function scopeFromFilename(file, config) {
6325
6309
  const name = (0, import_node_path14.basename)(file);
@@ -6389,7 +6373,7 @@ function explicitEnvironment(options, source, config) {
6389
6373
  throw new import_core26.PenvError(
6390
6374
  "IMPORT_ENV_FLAG_EMPTY",
6391
6375
  `\`--env\` for the import of ${source} names no environment`,
6392
- `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.`
6393
6377
  );
6394
6378
  }
6395
6379
  function assertEnvironmentAgrees(derived, explicit, source) {
@@ -6416,7 +6400,7 @@ function environmentNamed(file, explicit) {
6416
6400
  }
6417
6401
  function decisionsOf(config, cwd) {
6418
6402
  return {
6419
- environments: config.environments,
6403
+ environments: (0, import_core26.environmentNames)(config),
6420
6404
  // `import` re-scaffolds env.ts for an existing project; injection is an init
6421
6405
  // choice, so it is not turned on here.
6422
6406
  inject: false,
@@ -6472,7 +6456,7 @@ function importDotenv(options) {
6472
6456
  backup,
6473
6457
  scope,
6474
6458
  environment,
6475
- environments: config.environments,
6459
+ environments: (0, import_core26.environmentNames)(config),
6476
6460
  variables: parsed.entries.length,
6477
6461
  orphanComments: parsed.orphanComments,
6478
6462
  steps
@@ -6620,12 +6604,12 @@ function envVarFor(id) {
6620
6604
  function runKeyCreate(options) {
6621
6605
  const project = openProject(options.cwd);
6622
6606
  const environment = targetEnvironment(project, options.environment);
6623
- const declared = project.config.keys?.[environment];
6607
+ const declared = (0, import_core28.keyConfigFor)(project.config, environment);
6624
6608
  if (declared === void 0) {
6625
6609
  throw new import_core28.PenvError(
6626
6610
  "KEY_SOURCE_UNDECLARED",
6627
6611
  `Environment ${environment} declares no key source, so penv does not know what a key for it would be`,
6628
- `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.`
6629
6613
  );
6630
6614
  }
6631
6615
  const key = (0, import_node_crypto.randomBytes)(import_core28.KEY_BYTES).toString("base64");
@@ -6968,7 +6952,7 @@ async function planFile(project, source, target, parameter) {
6968
6952
  throw new import_core31.PenvError(
6969
6953
  "SECRET_SCOPE_AMBIGUOUS",
6970
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`,
6971
- "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."
6972
6956
  );
6973
6957
  }
6974
6958
  const keys = keySourceFor(project, environment);