@penvhq/cli 0.4.0 → 0.5.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.js CHANGED
@@ -8,6 +8,8 @@ import {
8
8
  assertNever,
9
9
  effectiveMeta,
10
10
  formatValueFile as formatValueFile2,
11
+ holdsProjection as holdsProjection3,
12
+ holdsRecords,
11
13
  isPublicVariable,
12
14
  isRequired,
13
15
  isSecret,
@@ -18,9 +20,66 @@ import {
18
20
  tryParseDuration,
19
21
  variableName as variableName3
20
22
  } from "@penvhq/core";
21
- import { createGithubSink as createGithubSink2 } from "@penvhq/sink-github";
22
23
  import { defineCommand as defineCommand3 } from "citty";
23
24
 
25
+ // src/env-flags.ts
26
+ import { PenvError } from "@penvhq/core";
27
+ var BASE_RESERVED = ["help", "version", "h", "v", "_", "--"];
28
+ var COMMAND_FLAGS = [
29
+ ...BASE_RESERVED,
30
+ "env",
31
+ "out",
32
+ "allow-decrypt",
33
+ "destination",
34
+ "dest",
35
+ "d",
36
+ "location",
37
+ "l",
38
+ "yes",
39
+ "y"
40
+ ];
41
+ function shadowedEnvironments(config) {
42
+ const flags = new Set(COMMAND_FLAGS);
43
+ return config.environments.filter((environment) => flags.has(environment));
44
+ }
45
+ function shorthandCandidates(args, declared) {
46
+ const reserved = /* @__PURE__ */ new Set([...BASE_RESERVED, ...declared]);
47
+ return Object.keys(args).filter((key) => !reserved.has(key) && args[key] === true);
48
+ }
49
+ function quoteList(values) {
50
+ return values.map((value) => `\`--${value}\``).join(", ");
51
+ }
52
+ function environmentFromShorthand(config, candidates, explicit) {
53
+ if (candidates.length === 0) {
54
+ return void 0;
55
+ }
56
+ const hits = candidates.filter((candidate) => config.environments.includes(candidate));
57
+ const strangers = candidates.filter((candidate) => !config.environments.includes(candidate));
58
+ if (strangers.length > 0) {
59
+ throw new PenvError(
60
+ "UNKNOWN_FLAG",
61
+ `${quoteList(strangers)} ${strangers.length === 1 ? "is not a flag" : "are not flags"} this command takes, and ${strangers.length === 1 ? "names" : "name"} no declared environment`,
62
+ `Declared environments work as bare flags: ${quoteList(config.environments)}. Anything else needs \`--env <name>\`.`
63
+ );
64
+ }
65
+ if (hits.length > 1) {
66
+ throw new PenvError(
67
+ "ENVIRONMENT_FLAG_AMBIGUOUS",
68
+ `${quoteList(hits)} name two environments at once, and a command acts on exactly one`,
69
+ "Pass a single environment flag, or the canonical `--env <name>`."
70
+ );
71
+ }
72
+ const hit = hits[0];
73
+ if (hit !== void 0 && explicit !== void 0 && explicit !== hit) {
74
+ throw new PenvError(
75
+ "ENVIRONMENT_FLAG_AMBIGUOUS",
76
+ `\`--env ${explicit}\` and \`--${hit}\` name two environments at once`,
77
+ "Drop one of them \u2014 `--env` is the canonical spelling."
78
+ );
79
+ }
80
+ return hit;
81
+ }
82
+
24
83
  // src/project.ts
25
84
  import { dirname as dirname2, resolve as resolve2 } from "path";
26
85
  import {
@@ -30,7 +89,7 @@ import {
30
89
  isReservedToken,
31
90
  loadConfig,
32
91
  openValue,
33
- PenvError as PenvError2,
92
+ PenvError as PenvError3,
34
93
  parameterId,
35
94
  ReservedTokenError,
36
95
  refFromAccessPath,
@@ -43,25 +102,19 @@ import { FilesystemProvider } from "@penvhq/provider-filesystem";
43
102
  import { createRequire } from "module";
44
103
  import { dirname, resolve } from "path";
45
104
  import { pathToFileURL } from "url";
46
- import { PenvError } from "@penvhq/core";
105
+ import { holdsProjection, PenvError as PenvError2 } from "@penvhq/core";
47
106
  import { createFilesystemProvider } from "@penvhq/provider-filesystem";
48
- import { createKubernetesProvider } from "@penvhq/provider-kubernetes";
49
107
  import { createMockProvider } from "@penvhq/provider-mock";
50
- import { createSsmProvider } from "@penvhq/provider-ssm";
51
- import { createVaultProvider } from "@penvhq/provider-vault";
52
108
  var PLUGIN_FACTORY_EXPORT = "penvProviderFactory";
53
- var LOCAL_TREE_TYPE = "filesystem";
109
+ var LOCAL_TREE_TYPE = "@penvhq/provider-filesystem";
54
110
  var REGISTRY = /* @__PURE__ */ new Map([
55
111
  [LOCAL_TREE_TYPE, ({ root, config }) => createFilesystemProvider({ root, config })],
56
- ["vault", ({ providerConfig }) => createVaultProvider({ path: providerConfig?.path ?? "penv" })],
57
- ["ssm", ({ providerConfig }) => createSsmProvider({ path: providerConfig?.path ?? "penv" })],
58
112
  [
59
- "kubernetes",
60
- ({ providerConfig }) => createKubernetesProvider(kubernetesOptions(providerConfig))
61
- ],
62
- ["mock", ({ root }) => createMockProvider({ storePath: resolve(root, ".penv-mock.json") })]
113
+ "@penvhq/provider-mock",
114
+ ({ root }) => createMockProvider({ storePath: resolve(root, ".penv-mock.json") })
115
+ ]
63
116
  ]);
64
- var CONTRACT_METHODS = [
117
+ var RECORD_CONTRACT_METHODS = [
65
118
  "read",
66
119
  "write",
67
120
  "list",
@@ -70,6 +123,7 @@ var CONTRACT_METHODS = [
70
123
  "writeMeta",
71
124
  "removeMeta"
72
125
  ];
126
+ var PROJECTION_CONTRACT_METHODS = ["verify", "push", "list"];
73
127
  var pluginModuleCache = /* @__PURE__ */ new Map();
74
128
  function isProviderRegistered(type) {
75
129
  return REGISTRY.has(type);
@@ -89,31 +143,30 @@ async function createSourceProvider(type, context) {
89
143
  }
90
144
  async function loadPluginProvider(type, context) {
91
145
  const fromDir = resolutionBase(context);
92
- const specifier = pluginSpecifier(context.providerConfig, type);
93
- const resolved = resolvePlugin(specifier, fromDir);
146
+ const resolved = resolvePlugin(type, fromDir);
94
147
  if (resolved === void 0) {
95
- throw unknownProvider(type, context.environment, specifier);
148
+ throw unknownProvider(type, context.environment);
96
149
  }
97
150
  let mod;
98
151
  try {
99
152
  mod = await importPlugin(resolved);
100
153
  } catch {
101
- throw new PenvError(
154
+ throw new PenvError2(
102
155
  "PROVIDER_PLUGIN_LOAD",
103
- `The provider package \`${specifier}\` for type \`${type}\` failed to load`,
156
+ `The provider package \`${type}\` failed to load`,
104
157
  "It resolved but threw while importing. Check it builds and its dependencies are installed."
105
158
  );
106
159
  }
107
160
  const factory = mod[PLUGIN_FACTORY_EXPORT];
108
161
  if (typeof factory !== "function") {
109
- throw new PenvError(
162
+ throw new PenvError2(
110
163
  "PROVIDER_PLUGIN_INVALID",
111
- `\`${specifier}\` does not export \`${PLUGIN_FACTORY_EXPORT}\``,
164
+ `\`${type}\` does not export \`${PLUGIN_FACTORY_EXPORT}\``,
112
165
  `A penv provider package must export \`${PLUGIN_FACTORY_EXPORT}(context) => Provider\`.`
113
166
  );
114
167
  }
115
168
  const provider = await factory(context);
116
- assertSatisfiesContract(provider, specifier);
169
+ assertSatisfiesContract(provider, type);
117
170
  return provider;
118
171
  }
119
172
  function assertProvidersRegistered(config, projectRoot) {
@@ -121,15 +174,11 @@ function assertProvidersRegistered(config, projectRoot) {
121
174
  if (isProviderRegistered(provider.type)) {
122
175
  continue;
123
176
  }
124
- const specifier = pluginSpecifier(provider, provider.type);
125
- if (resolvePlugin(specifier, projectRoot) === void 0) {
126
- throw unknownProvider(provider.type, environment, specifier);
177
+ if (resolvePlugin(provider.type, projectRoot) === void 0) {
178
+ throw unknownProvider(provider.type, environment);
127
179
  }
128
180
  }
129
181
  }
130
- function pluginSpecifier(providerConfig, type) {
131
- return providerConfig?.module ?? `@penvhq/provider-${type}`;
132
- }
133
182
  function resolutionBase(context) {
134
183
  return dirname(context.root);
135
184
  }
@@ -151,32 +200,26 @@ function importPlugin(resolvedPath) {
151
200
  return loading;
152
201
  }
153
202
  function assertSatisfiesContract(provider, specifier) {
154
- for (const method of CONTRACT_METHODS) {
203
+ const projection = holdsProjection(provider);
204
+ const methods = projection ? PROJECTION_CONTRACT_METHODS : RECORD_CONTRACT_METHODS;
205
+ const contract = projection ? "the @penvhq/core ProjectionProvider contract its declared capabilities select" : "the @penvhq/core Provider contract that the filesystem provider defines";
206
+ for (const method of methods) {
155
207
  if (typeof provider[method] !== "function") {
156
- throw new PenvError(
208
+ throw new PenvError2(
157
209
  "PROVIDER_PLUGIN_INVALID",
158
210
  `The provider from \`${specifier}\` is missing \`${method}()\``,
159
- "It must satisfy the @penvhq/core Provider contract that the filesystem provider defines."
211
+ `It must satisfy ${contract}.`
160
212
  );
161
213
  }
162
214
  }
163
215
  }
164
- function kubernetesOptions(providerConfig) {
165
- const path = providerConfig?.path ?? "penv";
166
- const slash = path.indexOf("/");
167
- if (slash === -1) return { secretName: path };
168
- const namespace = path.slice(0, slash);
169
- const secretName = path.slice(slash + 1) || "penv";
170
- return namespace === "" ? { secretName } : { namespace, secretName };
171
- }
172
- function unknownProvider(type, environment, specifier) {
173
- const known = [...REGISTRY.keys()].map((name) => `\`${name}\``).join(", ");
216
+ function unknownProvider(type, environment) {
217
+ const preinstalled = [...REGISTRY.keys()].map((name) => `\`${name}\``).join(", ");
174
218
  const where = environment === void 0 ? "" : ` for environment ${environment}`;
175
- const remedy = specifier === void 0 ? `This build registers ${known}. Name a registered provider, or install the build that carries \`${type}\`.` : `Install its package with \`npm i ${specifier}\`, or name a built-in provider: ${known}.`;
176
- return new PenvError(
219
+ return new PenvError2(
177
220
  "UNKNOWN_PROVIDER",
178
- `The provider type \`${type}\`${where} in penv.config.ts is not one this penv build carries`,
179
- remedy
221
+ `The provider \`${type}\`${where} in penv.config.ts is not installed in this project`,
222
+ `Install it with \`npm i ${type}\` \u2014 a provider's \`type\` is the package penv imports. The CLI ships ${preinstalled} pre-installed.`
180
223
  );
181
224
  }
182
225
 
@@ -197,7 +240,7 @@ function openProject(cwd) {
197
240
  }
198
241
  function localTree(project) {
199
242
  if (!(project.provider instanceof FilesystemProvider)) {
200
- throw new PenvError2(
243
+ throw new PenvError3(
201
244
  "PROVIDER_NOT_LOCAL",
202
245
  `This command reads the local .penv tree synchronously, which the \`${project.provider.type}\` provider is not`,
203
246
  "Run this against a filesystem-backed project, or use a command that speaks the async provider contract."
@@ -217,8 +260,9 @@ async function sourceProviderFor(project, environment) {
217
260
  environment
218
261
  });
219
262
  }
220
- function targetEnvironment(project, explicit) {
221
- return resolveEnvironment(project.config, explicit);
263
+ function targetEnvironment(project, explicit, shorthand) {
264
+ const fromFlags = shorthand === void 0 ? void 0 : environmentFromShorthand(project.config, shorthand, explicit);
265
+ return resolveEnvironment(project.config, explicit ?? fromFlags);
222
266
  }
223
267
  var KEY_SEPARATOR = /[./\\]/;
224
268
  var NO_ENVIRONMENTS = { environments: [], providers: {} };
@@ -226,7 +270,7 @@ function refFromKey(key, config) {
226
270
  const segments = key.split(KEY_SEPARATOR).filter((segment) => segment.length > 0);
227
271
  const name = segments[segments.length - 1];
228
272
  if (name === void 0) {
229
- throw new PenvError2(
273
+ throw new PenvError3(
230
274
  "PARAMETER_KEY",
231
275
  `\`${key}\` names no parameter`,
232
276
  "A key is `<namespace>/<name>` or `<namespace>.<name>`, e.g. `redis/password`."
@@ -245,13 +289,13 @@ function assertWritableKey(key) {
245
289
  const ref = refFromAccessPath(segments);
246
290
  if (ref !== void 0) {
247
291
  const suggestion = [...ref.namespace, ref.name].join("/");
248
- throw new PenvError2(
292
+ throw new PenvError3(
249
293
  "PARAMETER_KEY_CASING",
250
294
  `\`${key}\` is not a canonical parameter name`,
251
295
  `Parameter files are lower-case and hyphenated. Did you mean \`${suggestion}\`? That is the file that backs the \`${key}\` key in your schema.`
252
296
  );
253
297
  }
254
- throw new PenvError2(
298
+ throw new PenvError3(
255
299
  "PARAMETER_KEY_UNREACHABLE",
256
300
  `No value file can be named that reaches \`${key}\``,
257
301
  "Parameter files are lower-case and hyphenated, and this key maps to no such file \u2014 a run of capitals like `apiURL` cannot be reached (use `api-url`, which the schema reads as `apiUrl`). Run `penv validate` or `penv fill` to see the names penv expects."
@@ -540,7 +584,7 @@ function padVisible(text, width) {
540
584
  }
541
585
 
542
586
  // src/ui.ts
543
- import { PenvError as PenvError3 } from "@penvhq/core";
587
+ import { PenvError as PenvError4 } from "@penvhq/core";
544
588
  var CHECK = "\u2713";
545
589
  var WARN = "\u26A0";
546
590
  var CROSS = "\u2717";
@@ -614,7 +658,7 @@ function write(lines) {
614
658
  }
615
659
  }
616
660
  function reportError(error) {
617
- if (error instanceof PenvError3) {
661
+ if (error instanceof PenvError4) {
618
662
  const suffix = error.remedy === void 0 ? void 0 : `
619
663
  ${error.remedy}`;
620
664
  const message = suffix !== void 0 && error.message.endsWith(suffix) ? error.message.slice(0, -suffix.length) : error.message;
@@ -648,31 +692,92 @@ async function guard(run) {
648
692
  }
649
693
 
650
694
  // src/commands/push.ts
651
- import { checkNameCollisions, PenvError as PenvError4, requireValue, variableName as variableName2 } from "@penvhq/core";
652
- import { checkGithubNames, createGithubSink } from "@penvhq/sink-github";
695
+ import {
696
+ checkNameCollisions,
697
+ holdsProjection as holdsProjection2,
698
+ PenvError as PenvError5,
699
+ requireValue,
700
+ variableName as variableName2
701
+ } from "@penvhq/core";
653
702
  import { defineCommand } from "citty";
703
+
704
+ // src/input.ts
705
+ import { createInterface } from "readline";
706
+ function lineReader(input = process.stdin, output = process.stdout) {
707
+ const rl = createInterface({ input, output });
708
+ const buffered = [];
709
+ const waiting = [];
710
+ let closed = false;
711
+ rl.on("line", (line) => {
712
+ const next = waiting.shift();
713
+ if (next === void 0) {
714
+ buffered.push(line);
715
+ } else {
716
+ next(line);
717
+ }
718
+ });
719
+ rl.on("close", () => {
720
+ closed = true;
721
+ for (const next of waiting.splice(0)) {
722
+ next(void 0);
723
+ }
724
+ });
725
+ return {
726
+ ask(question) {
727
+ const early = buffered.shift();
728
+ if (early !== void 0) {
729
+ return Promise.resolve(early);
730
+ }
731
+ if (closed) {
732
+ return Promise.resolve(void 0);
733
+ }
734
+ rl.setPrompt(question);
735
+ rl.prompt();
736
+ return new Promise((resolve7) => {
737
+ waiting.push(resolve7);
738
+ });
739
+ },
740
+ close() {
741
+ rl.close();
742
+ }
743
+ };
744
+ }
745
+
746
+ // src/commands/push.ts
654
747
  var LAST_PUSHED_KEY = "lastPushedAt";
655
- function sinkFor(project, environment, override) {
656
- const declared = project.config.sinks?.[environment];
657
- if (declared === void 0) {
658
- throw new PenvError4(
659
- "NO_SINK",
660
- `Environment ${environment} declares no sink in penv.config.ts, so penv has nowhere to push`,
661
- `Add a \`sinks\` entry, e.g. \`sinks: { ${environment}: { type: "github" } }\`, then run \`penv push --env ${environment}\` again.`
748
+ async function destinationFor(project, environment, options) {
749
+ if (options.destination !== void 0) {
750
+ const providerConfig = {
751
+ type: options.destination,
752
+ ...options.location === void 0 ? {} : { location: options.location }
753
+ };
754
+ const provider2 = options.provider ?? await createSourceProvider(options.destination, {
755
+ root: project.penvDir,
756
+ config: project.config,
757
+ providerConfig,
758
+ environment
759
+ });
760
+ return { provider: provider2, location: options.location };
761
+ }
762
+ const declared = project.config.providers[environment];
763
+ const location = declared?.location;
764
+ if (declared === void 0 || declared.type === LOCAL_TREE_TYPE) {
765
+ throw new PenvError5(
766
+ "NO_DESTINATION",
767
+ `Environment ${environment}'s provider is the local .penv tree itself, so penv has nowhere to push`,
768
+ `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>\`.`
662
769
  );
663
770
  }
664
- const repo = declared.repo;
665
- if (override !== void 0) {
666
- return { sink: override, repo };
667
- }
668
- if (declared.type === "github") {
669
- return { sink: createGithubSink(repo === void 0 ? {} : { repo }), repo };
771
+ if (options.provider !== void 0) {
772
+ return { provider: options.provider, location };
670
773
  }
671
- throw new PenvError4(
672
- "UNKNOWN_SINK",
673
- `Environment ${environment} declares sink type \`${declared.type}\`, which penv does not know`,
674
- 'The only sink in this release is `github`. Set `type: "github"`.'
675
- );
774
+ const provider = await createSourceProvider(declared.type, {
775
+ root: project.penvDir,
776
+ config: project.config,
777
+ providerConfig: declared,
778
+ environment
779
+ });
780
+ return { provider, location };
676
781
  }
677
782
  function plan(resolutions, config, environment, allowDecrypt) {
678
783
  const outbound = [];
@@ -684,10 +789,10 @@ function plan(resolutions, config, environment, allowDecrypt) {
684
789
  let encrypted = false;
685
790
  if (winner.file.encrypted) {
686
791
  if (!allowDecrypt) {
687
- throw new PenvError4(
792
+ throw new PenvError5(
688
793
  "ENCRYPTED_VALUE_REFUSED",
689
- `Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and a push sends plaintext for GitHub to re-seal`,
690
- "Re-run with `--allow-decrypt` to decrypt it locally and push it, or push an environment whose values are plaintext. penv's encryption stops at the sink; the destination seals it under its own key."
794
+ `Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and a push sends plaintext for the destination to re-seal`,
795
+ "Re-run with `--allow-decrypt` to decrypt it locally and push it, or push an environment whose values are plaintext. penv's encryption stops at the projection; the destination seals it under its own key."
691
796
  );
692
797
  }
693
798
  requireValue(resolution, environment);
@@ -717,10 +822,46 @@ function recordPush(tree, ref, environment, iso) {
717
822
  const meta = withLastPushed(tree.readMetaSync(ref), environment, iso);
718
823
  tree.writeMetaSync(ref, meta);
719
824
  }
720
- async function runPush(options) {
721
- const project = openProject(options.cwd);
722
- const environment = targetEnvironment(project, options.environment);
723
- const { sink, repo } = sinkFor(project, environment, options.sink);
825
+ async function terminalConfirm(question) {
826
+ if (process.stdin.isTTY !== true) {
827
+ return false;
828
+ }
829
+ const reader = lineReader();
830
+ try {
831
+ const answer = await reader.ask(`${question} [y/N] `);
832
+ return answer !== void 0 && /^y(es)?$/i.test(answer.trim());
833
+ } finally {
834
+ reader.close();
835
+ }
836
+ }
837
+ async function ensureTargetApproved(provider, environment, options) {
838
+ if (provider.targetExists === void 0) {
839
+ return false;
840
+ }
841
+ if (await provider.targetExists(environment)) {
842
+ return false;
843
+ }
844
+ if (provider.ensureTarget === void 0) {
845
+ throw new PenvError5(
846
+ "MISSING_TARGET",
847
+ `The destination has no environment \`${environment}\` to receive this push, and this provider cannot create one`,
848
+ `Create the environment on the destination side, then run \`penv push --env ${environment}\` again.`
849
+ );
850
+ }
851
+ const approved = options.yes === true || await (options.confirm ?? terminalConfirm)(
852
+ `The destination has no environment \`${environment}\`. Create it?`
853
+ );
854
+ if (!approved) {
855
+ throw new PenvError5(
856
+ "MISSING_TARGET",
857
+ `The destination has no environment \`${environment}\` to receive this push`,
858
+ `Re-run with \`--yes\` to create it, answer \`y\` at the prompt, or create it on the destination side yourself.`
859
+ );
860
+ }
861
+ await provider.ensureTarget(environment);
862
+ return true;
863
+ }
864
+ async function pushProjection(project, provider, environment, location, options) {
724
865
  const tree = localTree(project);
725
866
  const keys = keySourceFor(project, environment);
726
867
  const resolutions = resolveAllSync(tree, environment, keys, true);
@@ -729,16 +870,17 @@ async function runPush(options) {
729
870
  if (collision !== void 0) {
730
871
  throw collision;
731
872
  }
732
- const nameError = checkGithubNames(refs, project.config)[0];
873
+ const nameError = provider.checkNames?.(refs, project.config)[0];
733
874
  if (nameError !== void 0) {
734
875
  throw nameError;
735
876
  }
736
877
  const outbound = plan(resolutions, project.config, environment, options.allowDecrypt === true);
737
- await sink.verify();
878
+ await provider.verify();
879
+ const createdTarget = outbound.some((item) => item.scope.kind === "environment") && await ensureTargetApproved(provider, environment, options);
738
880
  let repositorySecrets = 0;
739
881
  let environmentSecrets = 0;
740
882
  for (const item of outbound) {
741
- await sink.push(item.variable, item.value, item.scope);
883
+ await provider.push(item.variable, item.value, item.scope);
742
884
  if (item.scope.kind === "repository") {
743
885
  repositorySecrets += 1;
744
886
  } else {
@@ -748,15 +890,70 @@ async function runPush(options) {
748
890
  }
749
891
  return {
750
892
  environment,
751
- repo,
893
+ destination: provider.type,
894
+ mode: "projection",
895
+ location,
752
896
  pushed: outbound.length,
897
+ meta: 0,
753
898
  repositorySecrets,
754
899
  environmentSecrets,
755
- decrypted: outbound.filter((item) => item.encrypted).length
900
+ decrypted: outbound.filter((item) => item.encrypted).length,
901
+ createdTarget
902
+ };
903
+ }
904
+ function crossesToRecords(file, environment) {
905
+ const scope = file.scope;
906
+ if (scope.kind === "unscoped") {
907
+ return true;
908
+ }
909
+ return scope.kind === "environment" && scope.environment === environment;
910
+ }
911
+ async function pushRecords(project, provider, environment, location) {
912
+ const tree = localTree(project);
913
+ const files = tree.listSync().filter((file) => crossesToRecords(file, environment));
914
+ let pushed = 0;
915
+ for (const file of files) {
916
+ const value = tree.readSync(file);
917
+ if (value === void 0) {
918
+ continue;
919
+ }
920
+ await provider.write(file, value);
921
+ pushed += 1;
922
+ }
923
+ const refs = refsFrom(files);
924
+ let meta = 0;
925
+ for (const ref of refs) {
926
+ const block = tree.readMetaSync(ref);
927
+ if (block === void 0) {
928
+ continue;
929
+ }
930
+ await provider.writeMeta(ref, block);
931
+ meta += 1;
932
+ }
933
+ return {
934
+ environment,
935
+ destination: provider.type,
936
+ mode: "records",
937
+ location,
938
+ pushed,
939
+ meta,
940
+ repositorySecrets: 0,
941
+ environmentSecrets: 0,
942
+ decrypted: 0,
943
+ createdTarget: false
756
944
  };
757
945
  }
946
+ async function runPush(options) {
947
+ const project = openProject(options.cwd);
948
+ const environment = targetEnvironment(project, options.environment, options.envFlags);
949
+ const { provider, location } = await destinationFor(project, environment, options);
950
+ if (holdsProjection2(provider)) {
951
+ return pushProjection(project, provider, environment, location, options);
952
+ }
953
+ return pushRecords(project, provider, environment, location);
954
+ }
758
955
  function renderPush(result2) {
759
- if (result2.pushed === 0) {
956
+ if (result2.pushed === 0 && result2.meta === 0) {
760
957
  return formatRows([
761
958
  {
762
959
  glyph: CHECK,
@@ -765,7 +962,23 @@ function renderPush(result2) {
765
962
  }
766
963
  ]);
767
964
  }
768
- const target = `GitHub Actions for environment ${result2.environment}${result2.repo === void 0 ? "" : ` (${result2.repo})`}`;
965
+ const target = `${result2.destination} for environment ${result2.environment}${result2.location === void 0 ? "" : ` (${result2.location})`}`;
966
+ if (result2.mode === "records") {
967
+ return formatRows([
968
+ {
969
+ glyph: CHECK,
970
+ label: "Pushed",
971
+ subject: `${result2.pushed} value ${result2.pushed === 1 ? "file" : "files"}, ${result2.meta} meta`,
972
+ detail: `mirrored verbatim to ${target}`
973
+ },
974
+ {
975
+ glyph: CHECK,
976
+ label: "Sealed values",
977
+ subject: "crossed byte-for-byte, still sealed",
978
+ detail: "the destination holds envelopes; no key was needed to push them"
979
+ }
980
+ ]);
981
+ }
769
982
  const rows = [
770
983
  {
771
984
  glyph: CHECK,
@@ -780,12 +993,20 @@ function renderPush(result2) {
780
993
  detail: "environment secrets override repository secrets, as penv's env scope overrides the default"
781
994
  }
782
995
  ];
996
+ if (result2.createdTarget) {
997
+ rows.push({
998
+ glyph: CHECK,
999
+ label: "Created",
1000
+ subject: `the destination environment ${result2.environment}`,
1001
+ detail: "it did not exist yet; created on your approval"
1002
+ });
1003
+ }
783
1004
  if (result2.decrypted > 0) {
784
1005
  rows.push({
785
1006
  glyph: WARN,
786
1007
  label: "Decrypted",
787
1008
  subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
788
- detail: "sent as plaintext for GitHub to re-seal under its own key"
1009
+ detail: "sent as plaintext for the destination to re-seal under its own key"
789
1010
  });
790
1011
  }
791
1012
  return formatRows(rows);
@@ -793,13 +1014,28 @@ function renderPush(result2) {
793
1014
  var pushCommand = defineCommand({
794
1015
  meta: {
795
1016
  name: "push",
796
- description: "Push an environment's resolved values to its sink (GitHub Actions Secrets)"
1017
+ description: "Push an environment's values to its provider"
797
1018
  },
798
1019
  args: {
799
1020
  env: { type: "string", description: "The environment to push" },
800
1021
  "allow-decrypt": {
801
1022
  type: "boolean",
802
- description: "Decrypt sealed values locally and push them as plaintext for GitHub to re-seal"
1023
+ description: "Decrypt sealed values locally and push them as plaintext for the destination to re-seal"
1024
+ },
1025
+ destination: {
1026
+ type: "string",
1027
+ alias: ["dest", "d"],
1028
+ description: "Push once to this provider package instead of the declared one"
1029
+ },
1030
+ location: {
1031
+ type: "string",
1032
+ alias: "l",
1033
+ description: "The destination-side place, when --destination needs one"
1034
+ },
1035
+ yes: {
1036
+ type: "boolean",
1037
+ alias: "y",
1038
+ description: "Create a missing destination-side environment without prompting"
803
1039
  }
804
1040
  },
805
1041
  run({ args }) {
@@ -807,7 +1043,22 @@ var pushCommand = defineCommand({
807
1043
  const result2 = await runPush({
808
1044
  cwd: process.cwd(),
809
1045
  ...args.env === void 0 ? {} : { environment: args.env },
810
- ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
1046
+ ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] },
1047
+ ...args.destination === void 0 ? {} : { destination: args.destination },
1048
+ ...args.location === void 0 ? {} : { location: args.location },
1049
+ ...args.yes === void 0 ? {} : { yes: args.yes },
1050
+ envFlags: shorthandCandidates(args, [
1051
+ "env",
1052
+ "allow-decrypt",
1053
+ "allowDecrypt",
1054
+ "destination",
1055
+ "dest",
1056
+ "d",
1057
+ "location",
1058
+ "l",
1059
+ "yes",
1060
+ "y"
1061
+ ])
811
1062
  });
812
1063
  write(renderPush(result2));
813
1064
  });
@@ -822,7 +1073,7 @@ import {
822
1073
  checkNameCollisions as checkNameCollisions2,
823
1074
  jitiFor,
824
1075
  NameCollisionError,
825
- PenvError as PenvError5,
1076
+ PenvError as PenvError6,
826
1077
  ReservedTokenError as ReservedTokenError2,
827
1078
  resolveAll,
828
1079
  SCHEMA_HARVEST_ENV,
@@ -961,14 +1212,14 @@ async function loadSchemaExclusively(file, schemaPath, environment) {
961
1212
  }
962
1213
  async function runValidate(options) {
963
1214
  const project = openProject(options.cwd);
964
- const environment = targetEnvironment(project, options.environment);
1215
+ const environment = targetEnvironment(project, options.environment, options.envFlags);
965
1216
  const schemaPath = schemaFileOf(project.config);
966
1217
  const issues = [];
967
1218
  let files;
968
1219
  try {
969
1220
  files = await project.provider.list();
970
1221
  } catch (error) {
971
- if (!(error instanceof PenvError5)) {
1222
+ if (!(error instanceof PenvError6)) {
972
1223
  throw error;
973
1224
  }
974
1225
  return {
@@ -1076,7 +1327,8 @@ var validateCommand = defineCommand2({
1076
1327
  return guard(async () => {
1077
1328
  const result2 = await runValidate({
1078
1329
  cwd: process.cwd(),
1079
- ...args.env === void 0 ? {} : { environment: args.env }
1330
+ ...args.env === void 0 ? {} : { environment: args.env },
1331
+ envFlags: shorthandCandidates(args, ["env"])
1080
1332
  });
1081
1333
  write(renderValidate(result2));
1082
1334
  if (!result2.ok) {
@@ -1099,10 +1351,19 @@ function skipped(check, label) {
1099
1351
  }
1100
1352
  async function runDoctor(options) {
1101
1353
  const project = openProject(options.cwd);
1102
- const environment = targetEnvironment(project, options.environment);
1354
+ const environment = targetEnvironment(project, options.environment, options.envFlags);
1103
1355
  const findings = [];
1104
1356
  const now = new Date(options.now ?? (/* @__PURE__ */ new Date()).toISOString());
1105
1357
  const stuckThresholdMs = options.stuckThresholdMs ?? STUCK_THRESHOLD_MS;
1358
+ for (const shadowed of shadowedEnvironments(project.config)) {
1359
+ findings.push({
1360
+ check: "environment-flag-shadow",
1361
+ severity: "warning",
1362
+ label: "Environment shadows a flag",
1363
+ subject: shadowed,
1364
+ detail: `\`--${shadowed}\` is a flag penv defines, so this environment has no shorthand \u2014 \`--env ${shadowed}\` always works`
1365
+ });
1366
+ }
1106
1367
  const { schema, issues } = await loadSchema(project, environment);
1107
1368
  if (schema !== void 0) {
1108
1369
  findings.push({ check: "schema", severity: "pass", label: "Schema valid" });
@@ -1159,7 +1420,7 @@ async function runDoctor(options) {
1159
1420
  findings.push(...stuckFindings(rotation.subjects, environment, now, stuckThresholdMs));
1160
1421
  }
1161
1422
  findings.push(...await providerDriftFindings(project, environment, options.source));
1162
- findings.push(...await sinkFindings(project, environment, options.sink));
1423
+ findings.push(...await projectionFindings(project, environment, options.projection));
1163
1424
  findings.push({
1164
1425
  check: "provider",
1165
1426
  severity: "pass",
@@ -1395,15 +1656,6 @@ function encryptionFindings(subjects, environment) {
1395
1656
  ];
1396
1657
  }
1397
1658
  var EDIT_SKEW_MS = 12e4;
1398
- function buildSink(declared, override) {
1399
- if (override !== void 0) {
1400
- return override;
1401
- }
1402
- if (declared.type === "github") {
1403
- return createGithubSink2(declared.repo === void 0 ? {} : { repo: declared.repo });
1404
- }
1405
- return void 0;
1406
- }
1407
1659
  function errorDetail(error) {
1408
1660
  if (error instanceof Error) {
1409
1661
  return error.message.split("\n")[0] ?? error.message;
@@ -1413,32 +1665,43 @@ function errorDetail(error) {
1413
1665
  function scopeLabel(scope) {
1414
1666
  return scope.kind === "repository" ? "repository secrets" : `environment secrets for ${scope.environment}`;
1415
1667
  }
1416
- async function sinkFindings(project, environment, override) {
1417
- const declared = project.config.sinks?.[environment];
1418
- if (declared === void 0) {
1419
- return [];
1420
- }
1421
- const sink = buildSink(declared, override);
1422
- if (sink === void 0) {
1423
- return [
1424
- {
1425
- check: "sink-unreachable",
1426
- severity: "unknown",
1427
- label: "Sink",
1428
- subject: `sink type \`${declared.type}\` is not one penv knows`,
1429
- detail: "penv cannot check a sink it cannot build"
1430
- }
1431
- ];
1668
+ async function projectionFindings(project, environment, override) {
1669
+ let projection;
1670
+ if (override !== void 0) {
1671
+ projection = override;
1672
+ } else {
1673
+ const declared = project.config.providers[environment];
1674
+ if (declared === void 0 || declared.type === LOCAL_TREE_TYPE) {
1675
+ return [];
1676
+ }
1677
+ let source;
1678
+ try {
1679
+ source = await sourceProviderFor(project, environment);
1680
+ } catch (error) {
1681
+ return [
1682
+ {
1683
+ check: "projection-unreachable",
1684
+ severity: "unknown",
1685
+ label: "Destination",
1686
+ subject: `could not build the ${declared.type} provider for ${environment}`,
1687
+ detail: errorDetail(error)
1688
+ }
1689
+ ];
1690
+ }
1691
+ if (!holdsProjection3(source)) {
1692
+ return [];
1693
+ }
1694
+ projection = source;
1432
1695
  }
1433
1696
  try {
1434
- await sink.verify();
1697
+ await projection.verify();
1435
1698
  } catch (error) {
1436
1699
  return [
1437
1700
  {
1438
- check: "sink-unreachable",
1701
+ check: "projection-unreachable",
1439
1702
  severity: "unknown",
1440
- label: "Sink",
1441
- subject: `could not reach the ${declared.type} sink for ${environment}`,
1703
+ label: "Destination",
1704
+ subject: `could not reach the ${projection.type} destination for ${environment}`,
1442
1705
  detail: errorDetail(error)
1443
1706
  }
1444
1707
  ];
@@ -1446,15 +1709,15 @@ async function sinkFindings(project, environment, override) {
1446
1709
  let repoSecrets;
1447
1710
  let envSecrets;
1448
1711
  try {
1449
- repoSecrets = await sink.list({ kind: "repository" });
1450
- envSecrets = await sink.list({ kind: "environment", environment });
1712
+ repoSecrets = await projection.list({ kind: "repository" });
1713
+ envSecrets = await projection.list({ kind: "environment", environment });
1451
1714
  } catch (error) {
1452
1715
  return [
1453
1716
  {
1454
- check: "sink-unreachable",
1717
+ check: "projection-unreachable",
1455
1718
  severity: "unknown",
1456
- label: "Sink",
1457
- subject: `could not list secrets in the ${declared.type} sink for ${environment}`,
1719
+ label: "Destination",
1720
+ subject: `could not list secrets in the ${projection.type} destination for ${environment}`,
1458
1721
  detail: errorDetail(error)
1459
1722
  }
1460
1723
  ];
@@ -1492,7 +1755,7 @@ async function sinkFindings(project, environment, override) {
1492
1755
  }
1493
1756
  if (!destOf(item.scope).has(key)) {
1494
1757
  nameDrift.push({
1495
- check: "sink-name-drift",
1758
+ check: "projection-name-drift",
1496
1759
  severity: "warning",
1497
1760
  label: "Declared, not pushed",
1498
1761
  subject: item.variable,
@@ -1504,7 +1767,7 @@ async function sinkFindings(project, environment, override) {
1504
1767
  for (const secret of repoSecrets) {
1505
1768
  if (!allVariables.has(upper(secret.name))) {
1506
1769
  nameDrift.push({
1507
- check: "sink-name-drift",
1770
+ check: "projection-name-drift",
1508
1771
  severity: "warning",
1509
1772
  label: "In destination, not declared",
1510
1773
  subject: secret.name,
@@ -1515,7 +1778,7 @@ async function sinkFindings(project, environment, override) {
1515
1778
  for (const secret of envSecrets) {
1516
1779
  if (!expectedEnv.has(upper(secret.name))) {
1517
1780
  nameDrift.push({
1518
- check: "sink-name-drift",
1781
+ check: "projection-name-drift",
1519
1782
  severity: "warning",
1520
1783
  label: "In destination, not declared",
1521
1784
  subject: secret.name,
@@ -1539,7 +1802,7 @@ async function sinkFindings(project, environment, override) {
1539
1802
  continue;
1540
1803
  }
1541
1804
  manualEdits.push({
1542
- check: "sink-manual-edit",
1805
+ check: "projection-manual-edit",
1543
1806
  severity: "warning",
1544
1807
  label: "Edited outside penv",
1545
1808
  subject: item.variable,
@@ -1550,9 +1813,9 @@ async function sinkFindings(project, environment, override) {
1550
1813
  findings.push(
1551
1814
  ...nameDrift.length > 0 ? nameDrift : [
1552
1815
  {
1553
- check: "sink-name-drift",
1816
+ check: "projection-name-drift",
1554
1817
  severity: "pass",
1555
- label: "Sink names",
1818
+ label: "Destination names",
1556
1819
  subject: `every parameter resolving for ${environment} is present, and nothing undeclared is`
1557
1820
  }
1558
1821
  ]
@@ -1560,17 +1823,17 @@ async function sinkFindings(project, environment, override) {
1560
1823
  findings.push(
1561
1824
  ...manualEdits.length > 0 ? manualEdits : [
1562
1825
  {
1563
- check: "sink-manual-edit",
1826
+ check: "projection-manual-edit",
1564
1827
  severity: "pass",
1565
- label: "Sink hand-edits",
1828
+ label: "Destination hand-edits",
1566
1829
  subject: `no secret has changed outside penv since its last push for ${environment}`
1567
1830
  }
1568
1831
  ]
1569
1832
  );
1570
1833
  findings.push({
1571
- check: "sink-value-drift",
1834
+ check: "projection-value-drift",
1572
1835
  severity: "unknown",
1573
- label: "Sink values",
1836
+ label: "Destination values",
1574
1837
  subject: "cannot be read back from a write-only destination",
1575
1838
  detail: "value drift between the tree and the destination is unknowable by design"
1576
1839
  });
@@ -1599,6 +1862,9 @@ async function rotationSubjects(project, environment, local, override) {
1599
1862
  return { kind: "read", subjects: local };
1600
1863
  }
1601
1864
  const source = override ?? await sourceProviderFor(project, environment);
1865
+ if (!holdsRecords(source)) {
1866
+ return { kind: "read", subjects: local };
1867
+ }
1602
1868
  try {
1603
1869
  const subjects = await Promise.all(
1604
1870
  local.map(async ({ resolution }) => ({
@@ -1747,6 +2013,9 @@ async function providerDriftFindings(project, environment, override) {
1747
2013
  ];
1748
2014
  }
1749
2015
  const source = override ?? await sourceProviderFor(project, environment);
2016
+ if (!holdsRecords(source)) {
2017
+ return [];
2018
+ }
1750
2019
  let local;
1751
2020
  let remote;
1752
2021
  try {
@@ -1866,7 +2135,7 @@ function renderDoctor(report) {
1866
2135
  var doctorCommand = defineCommand3({
1867
2136
  meta: {
1868
2137
  name: "doctor",
1869
- description: "Report missing, weak, unused, fallback, plaintext-secret, and sink-drift issues"
2138
+ description: "Report missing, weak, unused, fallback, plaintext-secret, and drift issues"
1870
2139
  },
1871
2140
  args: {
1872
2141
  env: { type: "string", description: "The environment to report on" }
@@ -1875,7 +2144,8 @@ var doctorCommand = defineCommand3({
1875
2144
  return guard(async () => {
1876
2145
  const report = await runDoctor({
1877
2146
  cwd: process.cwd(),
1878
- ...args.env === void 0 ? {} : { environment: args.env }
2147
+ ...args.env === void 0 ? {} : { environment: args.env },
2148
+ envFlags: shorthandCandidates(args, ["env"])
1879
2149
  });
1880
2150
  write(renderDoctor(report));
1881
2151
  if (!report.ok) {
@@ -1890,14 +2160,14 @@ import {
1890
2160
  formatValueFile as formatValueFile4,
1891
2161
  isSecret as isSecret3,
1892
2162
  openValue as openValue3,
1893
- PenvError as PenvError7,
2163
+ PenvError as PenvError8,
1894
2164
  parameterId as parameterId4,
1895
2165
  sealValue as sealValue2
1896
2166
  } from "@penvhq/core";
1897
2167
  import { defineCommand as defineCommand5 } from "citty";
1898
2168
 
1899
2169
  // src/commands/set.ts
1900
- import { formatValueFile as formatValueFile3, isSecret as isSecret2, PenvError as PenvError6, parameterId as parameterId3, sealValue } from "@penvhq/core";
2170
+ import { formatValueFile as formatValueFile3, isSecret as isSecret2, PenvError as PenvError7, parameterId as parameterId3, sealValue } from "@penvhq/core";
1901
2171
  import { defineCommand as defineCommand4 } from "citty";
1902
2172
  function scopeFrom(options) {
1903
2173
  if (options.local === true) {
@@ -1917,7 +2187,7 @@ function targetScope(project, options, key) {
1917
2187
  return scopeFrom(options);
1918
2188
  }
1919
2189
  if (environment.trim().length === 0) {
1920
- throw new PenvError6(
2190
+ throw new PenvError7(
1921
2191
  "ENVIRONMENT_FLAG_EMPTY",
1922
2192
  `\`--env\` for parameter ${key} names no environment`,
1923
2193
  `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.`
@@ -1930,7 +2200,7 @@ function policyEnvironment(project, options) {
1930
2200
  }
1931
2201
  function sealFor(project, file, value, parameter, environment) {
1932
2202
  if (environment === void 0) {
1933
- throw new PenvError6(
2203
+ throw new PenvError7(
1934
2204
  "SECRET_SCOPE_AMBIGUOUS",
1935
2205
  `Parameter ${parameter} is a secret, and ${PENV_DIR}/${formatValueFile3(file)} names no environment`,
1936
2206
  "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."
@@ -2032,7 +2302,7 @@ function twins(project, key, options) {
2032
2302
  }
2033
2303
  function environmentFor(project, options, verb) {
2034
2304
  if (options.environment === void 0) {
2035
- throw new PenvError7(
2305
+ throw new PenvError8(
2036
2306
  "SECRET_SCOPE_AMBIGUOUS",
2037
2307
  `\`penv ${verb}\` names no environment, and keys are declared per environment`,
2038
2308
  "Pass `--env <environment>`. penv cannot tell which environment's key applies to a file that names none, and will not pick one for you."
@@ -2051,7 +2321,7 @@ async function runEncrypt(options) {
2051
2321
  const value = await readOne(project, plain);
2052
2322
  if (value === void 0) {
2053
2323
  const already = await readOne(project, sealed);
2054
- throw new PenvError7(
2324
+ throw new PenvError8(
2055
2325
  "PARAMETER_ABSENT",
2056
2326
  already === void 0 ? `Parameter ${parameter} has no value file at ${PENV_DIR}/${formatValueFile4(plain)}` : `Parameter ${parameter} is already encrypted at ${PENV_DIR}/${formatValueFile4(sealed)}`,
2057
2327
  already === void 0 ? `Write it first with \`penv set ${options.key} --env ${environment}\`, which seals it automatically when the parameter's meta declares it a secret.` : "Nothing to do."
@@ -2072,7 +2342,7 @@ async function runDecrypt(options) {
2072
2342
  const [plain, sealed] = twins(project, options.key, options);
2073
2343
  const parameter = parameterId4(plain);
2074
2344
  if (isSecret3(await project.provider.readMeta(plain), environment)) {
2075
- throw new PenvError7(
2345
+ throw new PenvError8(
2076
2346
  "SECRET_DECRYPT_REFUSED",
2077
2347
  `Parameter ${parameter} is declared a secret for environment ${environment}, so penv will not write it in plaintext`,
2078
2348
  "A secret with a plaintext value file is a `penv doctor` failure. Drop `secret` from the parameter's meta if it is not one, or run `penv generate --allow-decrypt` if you need the plaintext value in a `.env` artifact."
@@ -2080,7 +2350,7 @@ async function runDecrypt(options) {
2080
2350
  }
2081
2351
  const stored = await readOne(project, sealed);
2082
2352
  if (stored === void 0) {
2083
- throw new PenvError7(
2353
+ throw new PenvError8(
2084
2354
  "PARAMETER_ABSENT",
2085
2355
  `Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${formatValueFile4(sealed)}`,
2086
2356
  `Nothing to decrypt. \`penv get ${options.key} --env ${environment} --explain\` shows every file penv looked at.`
@@ -2103,7 +2373,7 @@ async function runDecrypt(options) {
2103
2373
  removed: formatValueFile4(sealed)
2104
2374
  };
2105
2375
  }
2106
- var UndecryptableAt = class extends PenvError7 {
2376
+ var UndecryptableAt = class extends PenvError8 {
2107
2377
  constructor(parameter, environment, location, detail) {
2108
2378
  super(
2109
2379
  "VALUE_UNDECRYPTABLE",
@@ -2158,57 +2428,14 @@ var decryptCommand = defineCommand5({
2158
2428
  });
2159
2429
 
2160
2430
  // src/commands/fill.ts
2161
- import { PenvError as PenvError8 } from "@penvhq/core";
2431
+ import { PenvError as PenvError9 } from "@penvhq/core";
2162
2432
  import { defineCommand as defineCommand6 } from "citty";
2163
-
2164
- // src/input.ts
2165
- import { createInterface } from "readline";
2166
- function lineReader(input = process.stdin, output = process.stdout) {
2167
- const rl = createInterface({ input, output });
2168
- const buffered = [];
2169
- const waiting = [];
2170
- let closed = false;
2171
- rl.on("line", (line) => {
2172
- const next = waiting.shift();
2173
- if (next === void 0) {
2174
- buffered.push(line);
2175
- } else {
2176
- next(line);
2177
- }
2178
- });
2179
- rl.on("close", () => {
2180
- closed = true;
2181
- for (const next of waiting.splice(0)) {
2182
- next(void 0);
2183
- }
2184
- });
2185
- return {
2186
- ask(question) {
2187
- const early = buffered.shift();
2188
- if (early !== void 0) {
2189
- return Promise.resolve(early);
2190
- }
2191
- if (closed) {
2192
- return Promise.resolve(void 0);
2193
- }
2194
- rl.setPrompt(question);
2195
- rl.prompt();
2196
- return new Promise((resolve7) => {
2197
- waiting.push(resolve7);
2198
- });
2199
- },
2200
- close() {
2201
- rl.close();
2202
- }
2203
- };
2204
- }
2205
-
2206
- // src/commands/fill.ts
2207
2433
  var BLOCKING = /* @__PURE__ */ new Set(["config", "collision", "reserved"]);
2208
2434
  async function runFill(options) {
2209
2435
  const validation = await runValidate({
2210
2436
  cwd: options.cwd,
2211
- ...options.environment === void 0 ? {} : { environment: options.environment }
2437
+ ...options.environment === void 0 ? {} : { environment: options.environment },
2438
+ ...options.envFlags === void 0 ? {} : { envFlags: options.envFlags }
2212
2439
  });
2213
2440
  const environment = validation.environment;
2214
2441
  const blockers = validation.issues.filter((issue) => BLOCKING.has(issue.kind));
@@ -2216,7 +2443,7 @@ async function runFill(options) {
2216
2443
  const detail = blockers.map(
2217
2444
  (issue) => ` - ${issue.message}${issue.remedy === void 0 ? "" : ` (${issue.remedy})`}`
2218
2445
  ).join("\n");
2219
- throw new PenvError8(
2446
+ throw new PenvError9(
2220
2447
  "FILL_BLOCKED",
2221
2448
  `penv fill cannot run: environment ${environment} has ${blockers.length} unresolved configuration ${blockers.length === 1 ? "issue" : "issues"}:
2222
2449
  ${detail}`,
@@ -2338,7 +2565,8 @@ var fillCommand = defineCommand6({
2338
2565
  await runFill({
2339
2566
  cwd: process.cwd(),
2340
2567
  ...args.env === void 0 ? {} : { environment: args.env },
2341
- ask
2568
+ ask,
2569
+ envFlags: shorthandCandidates(args, ["env"])
2342
2570
  })
2343
2571
  )
2344
2572
  );
@@ -2355,7 +2583,7 @@ import { isAbsolute, relative, resolve as resolve3 } from "path";
2355
2583
  import {
2356
2584
  checkNameCollisions as checkNameCollisions3,
2357
2585
  effectiveMeta as effectiveMeta2,
2358
- PenvError as PenvError9,
2586
+ PenvError as PenvError10,
2359
2587
  requireValue as requireValue2,
2360
2588
  serializeDotenv,
2361
2589
  variableName as variableName4
@@ -2379,7 +2607,7 @@ function entriesFor(project, environment, allowDecrypt) {
2379
2607
  const winner = resolution.winner;
2380
2608
  if (winner?.file.encrypted === true) {
2381
2609
  if (!allowDecrypt) {
2382
- throw new PenvError9(
2610
+ throw new PenvError10(
2383
2611
  "ENCRYPTED_VALUE_REFUSED",
2384
2612
  `Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and \`penv generate\` writes plaintext`,
2385
2613
  `Re-run with \`--allow-decrypt\` to write the decrypted value into the artifact, or generate for an environment whose values are plaintext. The artifact is gitignored; a committed plaintext secret is a \`penv doctor\` failure.`
@@ -2403,12 +2631,12 @@ function entriesFor(project, environment, allowDecrypt) {
2403
2631
  }
2404
2632
  function generateDotenv(options) {
2405
2633
  const project = openProject(options.cwd);
2406
- const environment = targetEnvironment(project, options.environment);
2634
+ const environment = targetEnvironment(project, options.environment, options.envFlags);
2407
2635
  return serializeDotenv(entriesFor(project, environment, options.allowDecrypt === true).entries);
2408
2636
  }
2409
2637
  function runGenerate(options) {
2410
2638
  const project = openProject(options.cwd);
2411
- const environment = targetEnvironment(project, options.environment);
2639
+ const environment = targetEnvironment(project, options.environment, options.envFlags);
2412
2640
  const { entries, decrypted } = entriesFor(project, environment, options.allowDecrypt === true);
2413
2641
  const file = options.out === void 0 ? resolve3(project.root, DEFAULT_OUTPUT) : isAbsolute(options.out) ? options.out : resolve3(options.cwd, options.out);
2414
2642
  writeFileSync(file, serializeDotenv(entries), "utf8");
@@ -2454,7 +2682,8 @@ var generateCommand = defineCommand7({
2454
2682
  cwd,
2455
2683
  ...args.env === void 0 ? {} : { environment: args.env },
2456
2684
  ...args.out === void 0 ? {} : { out: args.out },
2457
- ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
2685
+ ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] },
2686
+ envFlags: shorthandCandidates(args, ["env", "out", "allow-decrypt", "allowDecrypt"])
2458
2687
  });
2459
2688
  write(renderGenerate(result2, cwd));
2460
2689
  });
@@ -2462,7 +2691,7 @@ var generateCommand = defineCommand7({
2462
2691
  });
2463
2692
 
2464
2693
  // src/commands/get.ts
2465
- import { PenvError as PenvError10, requireValue as requireValue3, resolveParameter } from "@penvhq/core";
2694
+ import { PenvError as PenvError11, requireValue as requireValue3, resolveParameter } from "@penvhq/core";
2466
2695
  import { defineCommand as defineCommand8 } from "citty";
2467
2696
  async function runGet(options) {
2468
2697
  const project = openProject(options.cwd);
@@ -2472,7 +2701,7 @@ async function runGet(options) {
2472
2701
  const resolution = await resolveParameter(ref, environment, project.provider, keys);
2473
2702
  const value = requireValue3(resolution, environment);
2474
2703
  if (value === void 0) {
2475
- throw new PenvError10(
2704
+ throw new PenvError11(
2476
2705
  "PARAMETER_ABSENT",
2477
2706
  `Parameter ${resolution.parameter} resolves to no value for environment ${environment}`,
2478
2707
  `Set it with \`penv set ${options.key} --env ${environment}\`, or run \`penv get ${options.key} --env ${environment} --explain\` to see every file penv looked at.`
@@ -2559,7 +2788,7 @@ import {
2559
2788
  isReservedToken as isReservedToken3,
2560
2789
  loadConfigFrom as loadConfigFrom2,
2561
2790
  lookupEnvironment,
2562
- PenvError as PenvError12,
2791
+ PenvError as PenvError13,
2563
2792
  parseDotenv,
2564
2793
  ReservedTokenError as ReservedTokenError3,
2565
2794
  refFromVariable,
@@ -2679,7 +2908,7 @@ import {
2679
2908
  DEFAULT_SCHEMA_FILE as DEFAULT_SCHEMA_FILE2,
2680
2909
  isLegalEnvironmentName,
2681
2910
  loadConfigFrom,
2682
- PenvError as PenvError11,
2911
+ PenvError as PenvError12,
2683
2912
  RESERVED_TOKENS,
2684
2913
  schemaFileOf as schemaFileOf2,
2685
2914
  schemaInsideTree,
@@ -2723,7 +2952,7 @@ function suggestEnvironments(root) {
2723
2952
  return [...found].sort();
2724
2953
  }
2725
2954
  function emptyFlag(flag) {
2726
- return new PenvError11(
2955
+ return new PenvError12(
2727
2956
  "INIT_FLAG_EMPTY",
2728
2957
  `\`--${flag}\` was given without a value`,
2729
2958
  flag === "schema" ? `Name the module that exports the schema, e.g. \`--schema src/env.ts\`, or drop the flag to use ${DEFAULT_SCHEMA_FILE2}.` : "Name the environment, e.g. `--env production`, or drop the flag to leave the whitelist empty and declare it in penv.config.ts."
@@ -2786,7 +3015,7 @@ function planInit(root, flags = {}) {
2786
3015
  throw emptyFlag("alias");
2787
3016
  }
2788
3017
  if (!ALIAS_NAME.test(alias)) {
2789
- throw new PenvError11(
3018
+ throw new PenvError12(
2790
3019
  "INIT_ALIAS_INVALID",
2791
3020
  `\`${alias}\` is not an alias penv can write`,
2792
3021
  `An alias is \`@name\` \u2014 a tsconfig \`paths\` entry a bundler resolves \u2014 or \`#name\`, a package.json \`imports\` entry Node resolves itself. Those are the two things a module specifier can be that is not a package.`
@@ -2882,6 +3111,15 @@ ${body}
2882
3111
  // code. Importing it loads configuration and throws (naming the parameter and
2883
3112
  // environment) if anything required is missing or invalid.
2884
3113
  export const env = load(schema);
3114
+
3115
+ // Registers the schema's shape with penv's types (erased at runtime, so
3116
+ // nothing cycles). This is what makes \`override\` keys in penv.config.ts
3117
+ // autocomplete from this schema \u2014 a typo'd parameter id is a compile error.
3118
+ declare module "@penvhq/core" {
3119
+ interface PenvSchemaShape {
3120
+ readonly shape: z.infer<typeof schema>;
3121
+ }
3122
+ }
2885
3123
  `;
2886
3124
  }
2887
3125
  function renderEnvironments(decisions) {
@@ -2891,14 +3129,14 @@ function renderEnvironments(decisions) {
2891
3129
  // can read off your codebase, and an environment you do not have is worse
2892
3130
  // than one you have not declared yet. Name yours, and give each a provider:
2893
3131
  // environments: ["development", "production"],
2894
- // providers: { development: { type: "filesystem" }, production: { type: "filesystem" } },
3132
+ // providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-filesystem" } },
2895
3133
  environments: [],
2896
3134
 
2897
3135
  providers: {},
2898
3136
  `;
2899
3137
  }
2900
3138
  const names = decisions.environments.map((name) => JSON.stringify(name)).join(", ");
2901
- const providers = decisions.environments.map((name) => ` ${JSON.stringify(name)}: { type: "filesystem" },
3139
+ const providers = decisions.environments.map((name) => ` ${JSON.stringify(name)}: { type: "@penvhq/provider-filesystem" },
2902
3140
  `).join("");
2903
3141
  return `${shared} environments: [${names}],
2904
3142
 
@@ -3077,7 +3315,7 @@ ${objectIndent}${source.slice(close)}`;
3077
3315
  ${entryIndent}${member},${source.slice(open + 1)}`;
3078
3316
  }
3079
3317
  function shapeError(what, target, alias) {
3080
- return new PenvError11(
3318
+ return new PenvError12(
3081
3319
  "TSCONFIG_SHAPE",
3082
3320
  `penv cannot add the \`${alias}\` path alias to tsconfig.json: ${what}`,
3083
3321
  `Add it by hand: \`{ "compilerOptions": { "paths": { "${alias}": ["${target}"] } } }\`.`
@@ -3357,7 +3595,7 @@ function assertImportable(ref, variable) {
3357
3595
  if (!ref.name.includes(".")) {
3358
3596
  return;
3359
3597
  }
3360
- throw new PenvError12(
3598
+ throw new PenvError13(
3361
3599
  "IMPORT_UNPARSEABLE_NAME",
3362
3600
  `The variable ${variable} becomes the parameter \`${ref.name}\`, whose \`.\` would be read as a scope`,
3363
3601
  `Filenames are split on \`.\`. Rename ${variable} in the source file, then import it again.`
@@ -3376,10 +3614,10 @@ function assertRoundTrips(ref, variable, config) {
3376
3614
  return;
3377
3615
  }
3378
3616
  const generated = variableName5(ref, config);
3379
- throw new PenvError12(
3617
+ throw new PenvError13(
3380
3618
  "IMPORT_LOSSY_NAME",
3381
3619
  `The variable ${variable} becomes the parameter \`${ref.name}\`, which regenerates as ${generated}`,
3382
- `\`penv generate\` would write ${generated}, so anything reading \`process.env["${variable}"]\` would read \`undefined\`. Declare the name you want in the \`names\` block of penv.config.ts \u2014 \`names: { "${ref.name}": "${variable}" }\` \u2014 then import it again. Nothing was imported.`
3620
+ `\`penv generate\` would write ${generated}, so anything reading \`process.env["${variable}"]\` would read \`undefined\`. Declare the name you want in the \`override\` block of penv.config.ts \u2014 \`override: { "${ref.name}": "${variable}" }\` \u2014 then import it again. Nothing was imported.`
3383
3621
  );
3384
3622
  }
3385
3623
  function collisionsIn(refs, config) {
@@ -3460,7 +3698,7 @@ function explicitEnvironment(options, source, config) {
3460
3698
  if (value.length > 0) {
3461
3699
  return value;
3462
3700
  }
3463
- throw new PenvError12(
3701
+ throw new PenvError13(
3464
3702
  "IMPORT_ENV_FLAG_EMPTY",
3465
3703
  `\`--env\` for the import of ${source} names no environment`,
3466
3704
  `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.`
@@ -3470,7 +3708,7 @@ function assertEnvironmentAgrees(derived, explicit, source) {
3470
3708
  if (derived === void 0 || explicit === void 0 || derived === explicit) {
3471
3709
  return;
3472
3710
  }
3473
- throw new PenvError12(
3711
+ throw new PenvError13(
3474
3712
  "IMPORT_ENV_CONFLICT",
3475
3713
  `The file ${source} is scoped to environment ${derived}, but \`--env ${explicit}\` names ${explicit}`,
3476
3714
  `Drop \`--env\` to import ${source} as ${derived}, pass \`--env ${derived}\` to say the same thing twice, or point \`penv import\` at the file that holds ${explicit}'s values. Nothing was imported.`
@@ -3514,7 +3752,7 @@ function importDotenv(options) {
3514
3752
  const cwd = resolve5(options.cwd);
3515
3753
  const file = isAbsolute2(options.file) ? options.file : resolve5(cwd, options.file);
3516
3754
  if (!existsSync3(file)) {
3517
- throw new PenvError12(
3755
+ throw new PenvError13(
3518
3756
  "IMPORT_FILE_MISSING",
3519
3757
  `There is no file at ${file} to import`,
3520
3758
  "Point `penv import` at an existing dotenv file, e.g. `penv import .env`."
@@ -3659,7 +3897,7 @@ var importCommand = defineCommand10({
3659
3897
 
3660
3898
  // src/commands/key.ts
3661
3899
  import { randomBytes } from "crypto";
3662
- import { KEY_BYTES, KEYCHAIN_SERVICE, PenvError as PenvError13 } from "@penvhq/core";
3900
+ import { KEY_BYTES, KEYCHAIN_SERVICE, PenvError as PenvError14 } from "@penvhq/core";
3663
3901
  import { defineCommand as defineCommand11 } from "citty";
3664
3902
 
3665
3903
  // src/keychain.ts
@@ -3692,7 +3930,7 @@ function runKeyCreate(options) {
3692
3930
  const environment = targetEnvironment(project, options.environment);
3693
3931
  const declared = project.config.keys?.[environment];
3694
3932
  if (declared === void 0) {
3695
- throw new PenvError13(
3933
+ throw new PenvError14(
3696
3934
  "KEY_SOURCE_UNDECLARED",
3697
3935
  `Environment ${environment} declares no key source, so penv does not know what a key for it would be`,
3698
3936
  `Add a \`keys\` entry to penv.config.ts \u2014 e.g. \`keys: { ${environment}: { source: "env", id: "${environment}" } }\` \u2014 then run this again.`
@@ -3706,14 +3944,14 @@ function runKeyCreate(options) {
3706
3944
  try {
3707
3945
  existing = keychain.getPassword(KEYCHAIN_SERVICE, declared.id);
3708
3946
  } catch (cause) {
3709
- throw new PenvError13(
3947
+ throw new PenvError14(
3710
3948
  "KEYCHAIN_UNAVAILABLE",
3711
3949
  `penv could not read your OS keychain to check for an existing key \`${declared.id}\``,
3712
3950
  `Unlock your keychain and run this again. Original error: ${cause instanceof Error ? cause.message : String(cause)}`
3713
3951
  );
3714
3952
  }
3715
3953
  if (existing !== null) {
3716
- throw new PenvError13(
3954
+ throw new PenvError14(
3717
3955
  "KEY_EXISTS",
3718
3956
  `Environment ${environment} already has a key \`${declared.id}\` in your OS keychain`,
3719
3957
  "Replacing it would orphan every value already sealed under it \u2014 they could never be decrypted again. Re-run with `--force` only if you are certain nothing is sealed under the current key."
@@ -3872,7 +4110,7 @@ import {
3872
4110
  formatMetaFile,
3873
4111
  formatValueFile as formatValueFile5,
3874
4112
  openValue as openValue4,
3875
- PenvError as PenvError14,
4113
+ PenvError as PenvError15,
3876
4114
  parameterId as parameterId5,
3877
4115
  sealValue as sealValue3
3878
4116
  } from "@penvhq/core";
@@ -3891,7 +4129,7 @@ async function planFile(project, source, target, parameter) {
3891
4129
  }
3892
4130
  const environment = environmentOf2(source);
3893
4131
  if (environment === void 0) {
3894
- throw new PenvError14(
4132
+ throw new PenvError15(
3895
4133
  "SECRET_SCOPE_AMBIGUOUS",
3896
4134
  `${PENV_DIR}/${formatValueFile5(source)} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
3897
4135
  "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."
@@ -3900,7 +4138,7 @@ async function planFile(project, source, target, parameter) {
3900
4138
  const keys = keySourceFor(project, environment);
3901
4139
  const opened = openValue4(source, stored, keys);
3902
4140
  if (opened.kind === "failed") {
3903
- throw new PenvError14(
4141
+ throw new PenvError15(
3904
4142
  "VALUE_UNDECRYPTABLE",
3905
4143
  `${PENV_DIR}/${formatValueFile5(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
3906
4144
  "A sealed value is bound to the file it lives in, so moving it means opening it and sealing it again. Make the key available and run this again. Nothing has been moved."
@@ -3923,7 +4161,7 @@ async function runMove(options) {
3923
4161
  assertWritableKey(options.to);
3924
4162
  const to = refFromKey(options.to, project.config);
3925
4163
  if (parameterId5(from) === parameterId5(to)) {
3926
- throw new PenvError14(
4164
+ throw new PenvError15(
3927
4165
  "PARAMETER_UNCHANGED",
3928
4166
  `\`${options.from}\` and \`${options.to}\` are the same parameter`,
3929
4167
  "Name a different destination, e.g. `penv mv redis-password redis/password`."
@@ -3933,7 +4171,7 @@ async function runMove(options) {
3933
4171
  const sources = filesOf(all, from);
3934
4172
  const meta = await project.provider.readMeta(from);
3935
4173
  if (sources.length === 0 && meta === void 0) {
3936
- throw new PenvError14(
4174
+ throw new PenvError15(
3937
4175
  "PARAMETER_ABSENT",
3938
4176
  `Parameter ${parameterId5(from)} has no value files and no meta, so there is nothing to move`,
3939
4177
  `\`penv list\` shows every parameter penv holds.`
@@ -3941,7 +4179,7 @@ async function runMove(options) {
3941
4179
  }
3942
4180
  const occupied2 = filesOf(all, to);
3943
4181
  if (occupied2.length > 0 || await project.provider.readMeta(to) !== void 0) {
3944
- throw new PenvError14(
4182
+ throw new PenvError15(
3945
4183
  "PARAMETER_EXISTS",
3946
4184
  `Parameter ${parameterId5(to)} already exists, and penv will not merge two parameters into one`,
3947
4185
  `Remove or rename ${parameterId5(to)} first. \`penv get ${options.to} --explain\` shows every file it holds.`
@@ -4023,14 +4261,45 @@ var mvCommand = defineCommand13({
4023
4261
  });
4024
4262
 
4025
4263
  // src/commands/pull.ts
4264
+ import { holdsProjection as holdsProjection4, refFromVariable as refFromVariable2 } from "@penvhq/core";
4026
4265
  import { defineCommand as defineCommand14 } from "citty";
4266
+ async function pullProjection(project, source, environment) {
4267
+ const tree = localTree(project);
4268
+ const names = /* @__PURE__ */ new Set();
4269
+ for (const secret of await source.list({ kind: "repository" })) {
4270
+ names.add(secret.name);
4271
+ }
4272
+ for (const secret of await source.list({ kind: "environment", environment })) {
4273
+ names.add(secret.name);
4274
+ }
4275
+ let meta = 0;
4276
+ for (const name of [...names].sort()) {
4277
+ const ref = refFromVariable2(name);
4278
+ if (tree.readMetaSync(ref) === void 0) {
4279
+ tree.writeMetaSync(ref, {});
4280
+ meta += 1;
4281
+ }
4282
+ }
4283
+ return {
4284
+ environment,
4285
+ source: source.type,
4286
+ localSource: false,
4287
+ values: 0,
4288
+ meta,
4289
+ refs: names.size,
4290
+ valuesUnreadable: true
4291
+ };
4292
+ }
4027
4293
  async function runPull(options) {
4028
4294
  const project = openProject(options.cwd);
4029
- const environment = targetEnvironment(project, options.environment);
4030
- const source = await sourceProviderFor(project, environment);
4295
+ const environment = targetEnvironment(project, options.environment, options.envFlags);
4296
+ const source = options.source ?? await sourceProviderFor(project, environment);
4031
4297
  if (source.type === LOCAL_TREE_TYPE) {
4032
4298
  return { environment, source: source.type, localSource: true, values: 0, meta: 0, refs: 0 };
4033
4299
  }
4300
+ if (holdsProjection4(source)) {
4301
+ return pullProjection(project, source, environment);
4302
+ }
4034
4303
  const tree = localTree(project);
4035
4304
  const files = await source.list();
4036
4305
  let values = 0;
@@ -4065,6 +4334,22 @@ function renderPull(result2) {
4065
4334
  }
4066
4335
  ]);
4067
4336
  }
4337
+ if (result2.valuesUnreadable === true) {
4338
+ return formatRows([
4339
+ {
4340
+ glyph: CHECK,
4341
+ label: "Pulled",
4342
+ subject: `${result2.refs} ${result2.refs === 1 ? "parameter name" : "parameter names"}`,
4343
+ detail: `from ${result2.source} for environment ${result2.environment}`
4344
+ },
4345
+ {
4346
+ glyph: WARN,
4347
+ label: "Values not readable",
4348
+ subject: "this destination never returns a secret's value",
4349
+ detail: "fill them locally with `penv set` or `penv fill` \u2014 `penv validate` names every gap"
4350
+ }
4351
+ ]);
4352
+ }
4068
4353
  return formatRows([
4069
4354
  {
4070
4355
  glyph: CHECK,
@@ -4086,13 +4371,17 @@ var pullCommand = defineCommand14({
4086
4371
  description: "Materialise the local .penv tree from an environment's source-of-truth provider"
4087
4372
  },
4088
4373
  args: {
4089
- env: { type: "string", description: "The environment to pull" }
4374
+ env: {
4375
+ type: "string",
4376
+ description: "The environment to pull (or pass it as a bare flag: --production)"
4377
+ }
4090
4378
  },
4091
4379
  run({ args }) {
4092
4380
  return guard(async () => {
4093
4381
  const result2 = await runPull({
4094
4382
  cwd: process.cwd(),
4095
- ...args.env === void 0 ? {} : { environment: args.env }
4383
+ ...args.env === void 0 ? {} : { environment: args.env },
4384
+ envFlags: shorthandCandidates(args, ["env"])
4096
4385
  });
4097
4386
  write(renderPull(result2));
4098
4387
  });
@@ -4175,7 +4464,8 @@ var removeCommand = defineCommand15({
4175
4464
  import {
4176
4465
  beginRotation,
4177
4466
  completeRotation,
4178
- PenvError as PenvError15,
4467
+ holdsRecords as holdsRecords2,
4468
+ PenvError as PenvError16,
4179
4469
  retainsPrevious,
4180
4470
  rotationOf as rotationOf2
4181
4471
  } from "@penvhq/core";
@@ -4204,7 +4494,7 @@ async function writeRotatedValue(project, provider, ref, environment, value) {
4204
4494
  }
4205
4495
  function requireNewValue(value, phase, key) {
4206
4496
  if (value === void 0) {
4207
- throw new PenvError15(
4497
+ throw new PenvError16(
4208
4498
  "ROTATION_NO_VALUE",
4209
4499
  `A ${phase} rotation of ${key} writes a new value, and none was given`,
4210
4500
  "Pass the new value as the argument \u2014 `penv rotate <key> <value>` \u2014 or pipe it in on stdin."
@@ -4214,7 +4504,7 @@ function requireNewValue(value, phase, key) {
4214
4504
  }
4215
4505
  function requireRetaining(provider, environment) {
4216
4506
  if (!retainsPrevious(provider)) {
4217
- throw new PenvError15(
4507
+ throw new PenvError16(
4218
4508
  "ROTATION_NOT_RETAINING",
4219
4509
  `A dual-valid rotation needs the previous value to stay readable during the grace window, and the \`${provider.type}\` provider for environment ${environment} does not retain it`,
4220
4510
  "Point this environment at a provider that keeps prior versions (its `readPrevious` is what penv reads during the window), or, if a momentary overlap is not required, declare the parameter `atomic-cutover` in its meta and flip it in one step."
@@ -4226,12 +4516,20 @@ async function runRotate(options) {
4226
4516
  const project = openProject(options.cwd);
4227
4517
  const environment = targetEnvironment(project, options.environment);
4228
4518
  const ref = refFromKey(options.key, project.config);
4229
- const provider = await sourceProviderFor(project, environment);
4519
+ const source = await sourceProviderFor(project, environment);
4520
+ if (!holdsRecords2(source)) {
4521
+ throw new PenvError16(
4522
+ "ROTATION_NOT_RECORDS",
4523
+ `Environment ${environment} is backed by \`${source.type}\`, which holds a resolved projection penv cannot rotate in place`,
4524
+ "Rotate the parameter in the environment that holds the records (the local tree, Vault, SSM), then `penv push` the result to this destination."
4525
+ );
4526
+ }
4527
+ const provider = source;
4230
4528
  const nowIso = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
4231
4529
  const before = await provider.readMeta(ref);
4232
4530
  const { mechanism } = rotationOf2(before, environment);
4233
4531
  if (mechanism === void 0) {
4234
- throw new PenvError15(
4532
+ throw new PenvError16(
4235
4533
  "ROTATION_NO_MECHANISM",
4236
4534
  `Parameter ${options.key} declares no rotation mechanism for environment ${environment}, so penv does not know how to rotate it`,
4237
4535
  'Set `rotationMechanism` in the parameter\'s meta to `"dual-valid"` (a grace-window overlap) or `"atomic-cutover"` (a single flip), then run `penv rotate` again.'
@@ -4241,7 +4539,7 @@ async function runRotate(options) {
4241
4539
  const complete = options.complete === true;
4242
4540
  if (mechanism === "atomic-cutover") {
4243
4541
  if (begin || complete) {
4244
- throw new PenvError15(
4542
+ throw new PenvError16(
4245
4543
  "ROTATION_MECHANISM_MISMATCH",
4246
4544
  `Parameter ${options.key} is atomic-cutover, which flips in one step, so \`--begin\`/\`--complete\` do not apply`,
4247
4545
  "Run `penv rotate <key> <value>` with no phase flag to flip it. `--begin`/`--complete` bracket a dual-valid grace window, which atomic-cutover has none of."
@@ -4255,7 +4553,7 @@ async function runRotate(options) {
4255
4553
  }
4256
4554
  const retaining = requireRetaining(provider, environment);
4257
4555
  if (begin === complete) {
4258
- throw new PenvError15(
4556
+ throw new PenvError16(
4259
4557
  "ROTATION_PHASE_REQUIRED",
4260
4558
  `A dual-valid rotation of ${options.key} needs exactly one of \`--begin\` or \`--complete\``,
4261
4559
  "`--begin` writes the new value and opens the grace window; `--complete` closes it once every reader has moved to the new value. Run them in that order, one at a time."