@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.cjs CHANGED
@@ -52,42 +52,93 @@ __export(index_exports, {
52
52
  runWatch: () => runWatch
53
53
  });
54
54
  module.exports = __toCommonJS(index_exports);
55
- var import_core22 = require("@penvhq/core");
55
+ var import_core24 = require("@penvhq/core");
56
56
  var import_citty18 = require("citty");
57
57
 
58
58
  // src/commands/doctor.ts
59
- var import_core7 = require("@penvhq/core");
60
- var import_sink_github2 = require("@penvhq/sink-github");
59
+ var import_core8 = require("@penvhq/core");
61
60
  var import_citty3 = require("citty");
62
61
 
62
+ // src/env-flags.ts
63
+ var import_core = require("@penvhq/core");
64
+ var BASE_RESERVED = ["help", "version", "h", "v", "_", "--"];
65
+ var COMMAND_FLAGS = [
66
+ ...BASE_RESERVED,
67
+ "env",
68
+ "out",
69
+ "allow-decrypt",
70
+ "destination",
71
+ "dest",
72
+ "d",
73
+ "location",
74
+ "l",
75
+ "yes",
76
+ "y"
77
+ ];
78
+ function shadowedEnvironments(config) {
79
+ const flags = new Set(COMMAND_FLAGS);
80
+ return config.environments.filter((environment) => flags.has(environment));
81
+ }
82
+ function shorthandCandidates(args, declared) {
83
+ const reserved = /* @__PURE__ */ new Set([...BASE_RESERVED, ...declared]);
84
+ return Object.keys(args).filter((key) => !reserved.has(key) && args[key] === true);
85
+ }
86
+ function quoteList(values) {
87
+ return values.map((value) => `\`--${value}\``).join(", ");
88
+ }
89
+ function environmentFromShorthand(config, candidates, explicit) {
90
+ if (candidates.length === 0) {
91
+ return void 0;
92
+ }
93
+ const hits = candidates.filter((candidate) => config.environments.includes(candidate));
94
+ const strangers = candidates.filter((candidate) => !config.environments.includes(candidate));
95
+ if (strangers.length > 0) {
96
+ throw new import_core.PenvError(
97
+ "UNKNOWN_FLAG",
98
+ `${quoteList(strangers)} ${strangers.length === 1 ? "is not a flag" : "are not flags"} this command takes, and ${strangers.length === 1 ? "names" : "name"} no declared environment`,
99
+ `Declared environments work as bare flags: ${quoteList(config.environments)}. Anything else needs \`--env <name>\`.`
100
+ );
101
+ }
102
+ if (hits.length > 1) {
103
+ throw new import_core.PenvError(
104
+ "ENVIRONMENT_FLAG_AMBIGUOUS",
105
+ `${quoteList(hits)} name two environments at once, and a command acts on exactly one`,
106
+ "Pass a single environment flag, or the canonical `--env <name>`."
107
+ );
108
+ }
109
+ const hit = hits[0];
110
+ if (hit !== void 0 && explicit !== void 0 && explicit !== hit) {
111
+ throw new import_core.PenvError(
112
+ "ENVIRONMENT_FLAG_AMBIGUOUS",
113
+ `\`--env ${explicit}\` and \`--${hit}\` name two environments at once`,
114
+ "Drop one of them \u2014 `--env` is the canonical spelling."
115
+ );
116
+ }
117
+ return hit;
118
+ }
119
+
63
120
  // src/project.ts
64
121
  var import_node_path2 = require("path");
65
- var import_core2 = require("@penvhq/core");
122
+ var import_core3 = require("@penvhq/core");
66
123
  var import_provider_filesystem2 = require("@penvhq/provider-filesystem");
67
124
 
68
125
  // src/registry.ts
69
126
  var import_node_module = require("module");
70
127
  var import_node_path = require("path");
71
128
  var import_node_url = require("url");
72
- var import_core = require("@penvhq/core");
129
+ var import_core2 = require("@penvhq/core");
73
130
  var import_provider_filesystem = require("@penvhq/provider-filesystem");
74
- var import_provider_kubernetes = require("@penvhq/provider-kubernetes");
75
131
  var import_provider_mock = require("@penvhq/provider-mock");
76
- var import_provider_ssm = require("@penvhq/provider-ssm");
77
- var import_provider_vault = require("@penvhq/provider-vault");
78
132
  var PLUGIN_FACTORY_EXPORT = "penvProviderFactory";
79
- var LOCAL_TREE_TYPE = "filesystem";
133
+ var LOCAL_TREE_TYPE = "@penvhq/provider-filesystem";
80
134
  var REGISTRY = /* @__PURE__ */ new Map([
81
135
  [LOCAL_TREE_TYPE, ({ root, config }) => (0, import_provider_filesystem.createFilesystemProvider)({ root, config })],
82
- ["vault", ({ providerConfig }) => (0, import_provider_vault.createVaultProvider)({ path: providerConfig?.path ?? "penv" })],
83
- ["ssm", ({ providerConfig }) => (0, import_provider_ssm.createSsmProvider)({ path: providerConfig?.path ?? "penv" })],
84
136
  [
85
- "kubernetes",
86
- ({ providerConfig }) => (0, import_provider_kubernetes.createKubernetesProvider)(kubernetesOptions(providerConfig))
87
- ],
88
- ["mock", ({ root }) => (0, import_provider_mock.createMockProvider)({ storePath: (0, import_node_path.resolve)(root, ".penv-mock.json") })]
137
+ "@penvhq/provider-mock",
138
+ ({ root }) => (0, import_provider_mock.createMockProvider)({ storePath: (0, import_node_path.resolve)(root, ".penv-mock.json") })
139
+ ]
89
140
  ]);
90
- var CONTRACT_METHODS = [
141
+ var RECORD_CONTRACT_METHODS = [
91
142
  "read",
92
143
  "write",
93
144
  "list",
@@ -96,6 +147,7 @@ var CONTRACT_METHODS = [
96
147
  "writeMeta",
97
148
  "removeMeta"
98
149
  ];
150
+ var PROJECTION_CONTRACT_METHODS = ["verify", "push", "list"];
99
151
  var pluginModuleCache = /* @__PURE__ */ new Map();
100
152
  function isProviderRegistered(type) {
101
153
  return REGISTRY.has(type);
@@ -115,31 +167,30 @@ async function createSourceProvider(type, context) {
115
167
  }
116
168
  async function loadPluginProvider(type, context) {
117
169
  const fromDir = resolutionBase(context);
118
- const specifier = pluginSpecifier(context.providerConfig, type);
119
- const resolved = resolvePlugin(specifier, fromDir);
170
+ const resolved = resolvePlugin(type, fromDir);
120
171
  if (resolved === void 0) {
121
- throw unknownProvider(type, context.environment, specifier);
172
+ throw unknownProvider(type, context.environment);
122
173
  }
123
174
  let mod;
124
175
  try {
125
176
  mod = await importPlugin(resolved);
126
177
  } catch {
127
- throw new import_core.PenvError(
178
+ throw new import_core2.PenvError(
128
179
  "PROVIDER_PLUGIN_LOAD",
129
- `The provider package \`${specifier}\` for type \`${type}\` failed to load`,
180
+ `The provider package \`${type}\` failed to load`,
130
181
  "It resolved but threw while importing. Check it builds and its dependencies are installed."
131
182
  );
132
183
  }
133
184
  const factory = mod[PLUGIN_FACTORY_EXPORT];
134
185
  if (typeof factory !== "function") {
135
- throw new import_core.PenvError(
186
+ throw new import_core2.PenvError(
136
187
  "PROVIDER_PLUGIN_INVALID",
137
- `\`${specifier}\` does not export \`${PLUGIN_FACTORY_EXPORT}\``,
188
+ `\`${type}\` does not export \`${PLUGIN_FACTORY_EXPORT}\``,
138
189
  `A penv provider package must export \`${PLUGIN_FACTORY_EXPORT}(context) => Provider\`.`
139
190
  );
140
191
  }
141
192
  const provider = await factory(context);
142
- assertSatisfiesContract(provider, specifier);
193
+ assertSatisfiesContract(provider, type);
143
194
  return provider;
144
195
  }
145
196
  function assertProvidersRegistered(config, projectRoot) {
@@ -147,15 +198,11 @@ function assertProvidersRegistered(config, projectRoot) {
147
198
  if (isProviderRegistered(provider.type)) {
148
199
  continue;
149
200
  }
150
- const specifier = pluginSpecifier(provider, provider.type);
151
- if (resolvePlugin(specifier, projectRoot) === void 0) {
152
- throw unknownProvider(provider.type, environment, specifier);
201
+ if (resolvePlugin(provider.type, projectRoot) === void 0) {
202
+ throw unknownProvider(provider.type, environment);
153
203
  }
154
204
  }
155
205
  }
156
- function pluginSpecifier(providerConfig, type) {
157
- return providerConfig?.module ?? `@penvhq/provider-${type}`;
158
- }
159
206
  function resolutionBase(context) {
160
207
  return (0, import_node_path.dirname)(context.root);
161
208
  }
@@ -177,39 +224,33 @@ function importPlugin(resolvedPath) {
177
224
  return loading;
178
225
  }
179
226
  function assertSatisfiesContract(provider, specifier) {
180
- for (const method of CONTRACT_METHODS) {
227
+ const projection = (0, import_core2.holdsProjection)(provider);
228
+ const methods = projection ? PROJECTION_CONTRACT_METHODS : RECORD_CONTRACT_METHODS;
229
+ const contract = projection ? "the @penvhq/core ProjectionProvider contract its declared capabilities select" : "the @penvhq/core Provider contract that the filesystem provider defines";
230
+ for (const method of methods) {
181
231
  if (typeof provider[method] !== "function") {
182
- throw new import_core.PenvError(
232
+ throw new import_core2.PenvError(
183
233
  "PROVIDER_PLUGIN_INVALID",
184
234
  `The provider from \`${specifier}\` is missing \`${method}()\``,
185
- "It must satisfy the @penvhq/core Provider contract that the filesystem provider defines."
235
+ `It must satisfy ${contract}.`
186
236
  );
187
237
  }
188
238
  }
189
239
  }
190
- function kubernetesOptions(providerConfig) {
191
- const path = providerConfig?.path ?? "penv";
192
- const slash = path.indexOf("/");
193
- if (slash === -1) return { secretName: path };
194
- const namespace = path.slice(0, slash);
195
- const secretName = path.slice(slash + 1) || "penv";
196
- return namespace === "" ? { secretName } : { namespace, secretName };
197
- }
198
- function unknownProvider(type, environment, specifier) {
199
- const known = [...REGISTRY.keys()].map((name) => `\`${name}\``).join(", ");
240
+ function unknownProvider(type, environment) {
241
+ const preinstalled = [...REGISTRY.keys()].map((name) => `\`${name}\``).join(", ");
200
242
  const where = environment === void 0 ? "" : ` for environment ${environment}`;
201
- 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}.`;
202
- return new import_core.PenvError(
243
+ return new import_core2.PenvError(
203
244
  "UNKNOWN_PROVIDER",
204
- `The provider type \`${type}\`${where} in penv.config.ts is not one this penv build carries`,
205
- remedy
245
+ `The provider \`${type}\`${where} in penv.config.ts is not installed in this project`,
246
+ `Install it with \`npm i ${type}\` \u2014 a provider's \`type\` is the package penv imports. The CLI ships ${preinstalled} pre-installed.`
206
247
  );
207
248
  }
208
249
 
209
250
  // src/project.ts
210
251
  var PENV_DIR = ".penv";
211
252
  function openProject(cwd) {
212
- const { config, file } = (0, import_core2.loadConfig)(cwd);
253
+ const { config, file } = (0, import_core3.loadConfig)(cwd);
213
254
  const root = (0, import_node_path2.dirname)(file);
214
255
  const penvDir = (0, import_node_path2.resolve)(root, PENV_DIR);
215
256
  assertProvidersRegistered(config, root);
@@ -223,7 +264,7 @@ function openProject(cwd) {
223
264
  }
224
265
  function localTree(project) {
225
266
  if (!(project.provider instanceof import_provider_filesystem2.FilesystemProvider)) {
226
- throw new import_core2.PenvError(
267
+ throw new import_core3.PenvError(
227
268
  "PROVIDER_NOT_LOCAL",
228
269
  `This command reads the local .penv tree synchronously, which the \`${project.provider.type}\` provider is not`,
229
270
  "Run this against a filesystem-backed project, or use a command that speaks the async provider contract."
@@ -243,8 +284,9 @@ async function sourceProviderFor(project, environment) {
243
284
  environment
244
285
  });
245
286
  }
246
- function targetEnvironment(project, explicit) {
247
- return (0, import_core2.resolveEnvironment)(project.config, explicit);
287
+ function targetEnvironment(project, explicit, shorthand) {
288
+ const fromFlags = shorthand === void 0 ? void 0 : environmentFromShorthand(project.config, shorthand, explicit);
289
+ return (0, import_core3.resolveEnvironment)(project.config, explicit ?? fromFlags);
248
290
  }
249
291
  var KEY_SEPARATOR = /[./\\]/;
250
292
  var NO_ENVIRONMENTS = { environments: [], providers: {} };
@@ -252,56 +294,56 @@ function refFromKey(key, config) {
252
294
  const segments = key.split(KEY_SEPARATOR).filter((segment) => segment.length > 0);
253
295
  const name = segments[segments.length - 1];
254
296
  if (name === void 0) {
255
- throw new import_core2.PenvError(
297
+ throw new import_core3.PenvError(
256
298
  "PARAMETER_KEY",
257
299
  `\`${key}\` names no parameter`,
258
300
  "A key is `<namespace>/<name>` or `<namespace>.<name>`, e.g. `redis/password`."
259
301
  );
260
302
  }
261
- if ((0, import_core2.isReservedToken)(name, config ?? NO_ENVIRONMENTS)) {
262
- throw new import_core2.ReservedTokenError("parameter", name, key);
303
+ if ((0, import_core3.isReservedToken)(name, config ?? NO_ENVIRONMENTS)) {
304
+ throw new import_core3.ReservedTokenError("parameter", name, key);
263
305
  }
264
306
  return { namespace: segments.slice(0, -1), name };
265
307
  }
266
308
  function assertWritableKey(key) {
267
309
  const segments = key.split(KEY_SEPARATOR).filter((s) => s.length > 0);
268
- if (segments.length === 0 || segments.every((s) => (0, import_core2.isCanonicalSegment)(s))) {
310
+ if (segments.length === 0 || segments.every((s) => (0, import_core3.isCanonicalSegment)(s))) {
269
311
  return;
270
312
  }
271
- const ref = (0, import_core2.refFromAccessPath)(segments);
313
+ const ref = (0, import_core3.refFromAccessPath)(segments);
272
314
  if (ref !== void 0) {
273
315
  const suggestion = [...ref.namespace, ref.name].join("/");
274
- throw new import_core2.PenvError(
316
+ throw new import_core3.PenvError(
275
317
  "PARAMETER_KEY_CASING",
276
318
  `\`${key}\` is not a canonical parameter name`,
277
319
  `Parameter files are lower-case and hyphenated. Did you mean \`${suggestion}\`? That is the file that backs the \`${key}\` key in your schema.`
278
320
  );
279
321
  }
280
- throw new import_core2.PenvError(
322
+ throw new import_core3.PenvError(
281
323
  "PARAMETER_KEY_UNREACHABLE",
282
324
  `No value file can be named that reaches \`${key}\``,
283
325
  "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."
284
326
  );
285
327
  }
286
328
  function keySourceFor(project, environment) {
287
- return (0, import_core2.resolveKeySource)(project.config, environment);
329
+ return (0, import_core3.resolveKeySource)(project.config, environment);
288
330
  }
289
331
  function resolveSync(provider, ref, environment, keys, skipPersonal) {
290
- for (const file of (0, import_core2.candidatesFor)(ref, environment, skipPersonal)) {
332
+ for (const file of (0, import_core3.candidatesFor)(ref, environment, skipPersonal)) {
291
333
  const read = provider.readSync(file);
292
334
  if (read === void 0) {
293
335
  continue;
294
336
  }
295
- const opened = (0, import_core2.openValue)(file, read, keys);
337
+ const opened = (0, import_core3.openValue)(file, read, keys);
296
338
  return {
297
339
  ref,
298
- parameter: (0, import_core2.parameterId)(ref),
340
+ parameter: (0, import_core3.parameterId)(ref),
299
341
  value: opened.kind === "plaintext" ? opened.value : void 0,
300
342
  ...opened.kind === "failed" ? { undecryptable: opened.failure } : {},
301
- winner: { file, location: (0, import_core2.formatValueFile)(file), present: true }
343
+ winner: { file, location: (0, import_core3.formatValueFile)(file), present: true }
302
344
  };
303
345
  }
304
- return { ref, parameter: (0, import_core2.parameterId)(ref), value: void 0, winner: void 0 };
346
+ return { ref, parameter: (0, import_core3.parameterId)(ref), value: void 0, winner: void 0 };
305
347
  }
306
348
  function resolveAllSync(provider, environment, keys, skipPersonal) {
307
349
  return refsFrom(provider.listSync()).map(
@@ -312,20 +354,20 @@ function refsFrom(files) {
312
354
  const refs = /* @__PURE__ */ new Map();
313
355
  for (const file of files) {
314
356
  const ref = { namespace: file.namespace, name: file.name };
315
- const id = (0, import_core2.parameterId)(ref);
357
+ const id = (0, import_core3.parameterId)(ref);
316
358
  if (!refs.has(id)) {
317
359
  refs.set(id, ref);
318
360
  }
319
361
  }
320
362
  return [...refs.values()].sort((a, b) => {
321
- const left = (0, import_core2.parameterId)(a);
322
- const right = (0, import_core2.parameterId)(b);
363
+ const left = (0, import_core3.parameterId)(a);
364
+ const right = (0, import_core3.parameterId)(b);
323
365
  return left < right ? -1 : left > right ? 1 : 0;
324
366
  });
325
367
  }
326
368
 
327
369
  // src/schema.ts
328
- var import_core3 = require("@penvhq/core");
370
+ var import_core4 = require("@penvhq/core");
329
371
  function defOf(node) {
330
372
  if (typeof node !== "object" || node === null) {
331
373
  return void 0;
@@ -464,9 +506,9 @@ function computeDrift(input) {
464
506
  continue;
465
507
  }
466
508
  const path = leaf.path.join(".");
467
- const ref = (0, import_core3.refFromAccessPath)(leaf.path);
509
+ const ref = (0, import_core4.refFromAccessPath)(leaf.path);
468
510
  const renameRemedy = `Rename the \`${path}\` key in .penv/env.ts \u2014 a parameter name is lower-case, hyphenated, and never a reserved token, so no value file reaches this key.`;
469
- if (ref === void 0 || (0, import_core3.isReservedToken)(ref.name, config)) {
511
+ if (ref === void 0 || (0, import_core4.isReservedToken)(ref.name, config)) {
470
512
  if (leaf.absencePermitted) {
471
513
  optional.push({ subject: path, remedy: renameRemedy });
472
514
  } else {
@@ -478,13 +520,13 @@ function computeDrift(input) {
478
520
  }
479
521
  continue;
480
522
  }
481
- if (valued.has((0, import_core3.parameterId)(ref))) {
523
+ if (valued.has((0, import_core4.parameterId)(ref))) {
482
524
  continue;
483
525
  }
484
526
  if (leaf.absencePermitted) {
485
527
  const defaultValue = defaultValueOf(leaf.node);
486
528
  optional.push({
487
- subject: (0, import_core3.parameterId)(ref),
529
+ subject: (0, import_core4.parameterId)(ref),
488
530
  ref,
489
531
  ...defaultValue === void 0 ? {} : { defaultValue },
490
532
  remedy: `penv set ${[...ref.namespace, ref.name].join("/")} --env ${environment}`
@@ -492,7 +534,7 @@ function computeDrift(input) {
492
534
  continue;
493
535
  }
494
536
  declared.push({
495
- subject: (0, import_core3.parameterId)(ref),
537
+ subject: (0, import_core4.parameterId)(ref),
496
538
  ref,
497
539
  remedy: `penv set ${[...ref.namespace, ref.name].join("/")} --env ${environment}`,
498
540
  detail: `declared in .penv/env.ts, no value for ${environment}`
@@ -500,12 +542,12 @@ function computeDrift(input) {
500
542
  }
501
543
  const undeclared = [];
502
544
  for (const resolution of resolutions) {
503
- if (lookup(schema, (0, import_core3.accessPath)(resolution.ref)).kind !== "absent") {
545
+ if (lookup(schema, (0, import_core4.accessPath)(resolution.ref)).kind !== "absent") {
504
546
  continue;
505
547
  }
506
548
  undeclared.push({
507
549
  ref: resolution.ref,
508
- variable: (0, import_core3.variableName)(resolution.ref, config)
550
+ variable: (0, import_core4.variableName)(resolution.ref, config)
509
551
  });
510
552
  }
511
553
  return { declared, undeclared, optional };
@@ -560,7 +602,7 @@ function padVisible(text, width) {
560
602
  }
561
603
 
562
604
  // src/ui.ts
563
- var import_core4 = require("@penvhq/core");
605
+ var import_core5 = require("@penvhq/core");
564
606
  var CHECK = "\u2713";
565
607
  var WARN = "\u26A0";
566
608
  var CROSS = "\u2717";
@@ -634,7 +676,7 @@ function write(lines) {
634
676
  }
635
677
  }
636
678
  function reportError(error) {
637
- if (error instanceof import_core4.PenvError) {
679
+ if (error instanceof import_core5.PenvError) {
638
680
  const suffix = error.remedy === void 0 ? void 0 : `
639
681
  ${error.remedy}`;
640
682
  const message = suffix !== void 0 && error.message.endsWith(suffix) ? error.message.slice(0, -suffix.length) : error.message;
@@ -668,31 +710,86 @@ async function guard(run) {
668
710
  }
669
711
 
670
712
  // src/commands/push.ts
671
- var import_core5 = require("@penvhq/core");
672
- var import_sink_github = require("@penvhq/sink-github");
713
+ var import_core6 = require("@penvhq/core");
673
714
  var import_citty = require("citty");
715
+
716
+ // src/input.ts
717
+ var import_node_readline = require("readline");
718
+ function lineReader(input = process.stdin, output = process.stdout) {
719
+ const rl = (0, import_node_readline.createInterface)({ input, output });
720
+ const buffered = [];
721
+ const waiting = [];
722
+ let closed = false;
723
+ rl.on("line", (line) => {
724
+ const next = waiting.shift();
725
+ if (next === void 0) {
726
+ buffered.push(line);
727
+ } else {
728
+ next(line);
729
+ }
730
+ });
731
+ rl.on("close", () => {
732
+ closed = true;
733
+ for (const next of waiting.splice(0)) {
734
+ next(void 0);
735
+ }
736
+ });
737
+ return {
738
+ ask(question) {
739
+ const early = buffered.shift();
740
+ if (early !== void 0) {
741
+ return Promise.resolve(early);
742
+ }
743
+ if (closed) {
744
+ return Promise.resolve(void 0);
745
+ }
746
+ rl.setPrompt(question);
747
+ rl.prompt();
748
+ return new Promise((resolve7) => {
749
+ waiting.push(resolve7);
750
+ });
751
+ },
752
+ close() {
753
+ rl.close();
754
+ }
755
+ };
756
+ }
757
+
758
+ // src/commands/push.ts
674
759
  var LAST_PUSHED_KEY = "lastPushedAt";
675
- function sinkFor(project, environment, override) {
676
- const declared = project.config.sinks?.[environment];
677
- if (declared === void 0) {
678
- throw new import_core5.PenvError(
679
- "NO_SINK",
680
- `Environment ${environment} declares no sink in penv.config.ts, so penv has nowhere to push`,
681
- `Add a \`sinks\` entry, e.g. \`sinks: { ${environment}: { type: "github" } }\`, then run \`penv push --env ${environment}\` again.`
760
+ async function destinationFor(project, environment, options) {
761
+ if (options.destination !== void 0) {
762
+ const providerConfig = {
763
+ type: options.destination,
764
+ ...options.location === void 0 ? {} : { location: options.location }
765
+ };
766
+ const provider2 = options.provider ?? await createSourceProvider(options.destination, {
767
+ root: project.penvDir,
768
+ config: project.config,
769
+ providerConfig,
770
+ environment
771
+ });
772
+ return { provider: provider2, location: options.location };
773
+ }
774
+ const declared = project.config.providers[environment];
775
+ const location = declared?.location;
776
+ if (declared === void 0 || declared.type === LOCAL_TREE_TYPE) {
777
+ throw new import_core6.PenvError(
778
+ "NO_DESTINATION",
779
+ `Environment ${environment}'s provider is the local .penv tree itself, so penv has nowhere to push`,
780
+ `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>\`.`
682
781
  );
683
782
  }
684
- const repo = declared.repo;
685
- if (override !== void 0) {
686
- return { sink: override, repo };
783
+ if (options.provider !== void 0) {
784
+ return { provider: options.provider, location };
687
785
  }
688
- if (declared.type === "github") {
689
- return { sink: (0, import_sink_github.createGithubSink)(repo === void 0 ? {} : { repo }), repo };
690
- }
691
- throw new import_core5.PenvError(
692
- "UNKNOWN_SINK",
693
- `Environment ${environment} declares sink type \`${declared.type}\`, which penv does not know`,
694
- 'The only sink in this release is `github`. Set `type: "github"`.'
695
- );
786
+ const provider = await createSourceProvider(declared.type, {
787
+ root: project.penvDir,
788
+ config: project.config,
789
+ providerConfig: declared,
790
+ environment
791
+ });
792
+ return { provider, location };
696
793
  }
697
794
  function plan(resolutions, config, environment, allowDecrypt) {
698
795
  const outbound = [];
@@ -704,13 +801,13 @@ function plan(resolutions, config, environment, allowDecrypt) {
704
801
  let encrypted = false;
705
802
  if (winner.file.encrypted) {
706
803
  if (!allowDecrypt) {
707
- throw new import_core5.PenvError(
804
+ throw new import_core6.PenvError(
708
805
  "ENCRYPTED_VALUE_REFUSED",
709
- `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`,
710
- "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."
806
+ `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`,
807
+ "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."
711
808
  );
712
809
  }
713
- (0, import_core5.requireValue)(resolution, environment);
810
+ (0, import_core6.requireValue)(resolution, environment);
714
811
  encrypted = true;
715
812
  }
716
813
  if (resolution.value === void 0) {
@@ -719,7 +816,7 @@ function plan(resolutions, config, environment, allowDecrypt) {
719
816
  const scope = winner.file.scope.kind === "unscoped" ? { kind: "repository" } : { kind: "environment", environment };
720
817
  outbound.push({
721
818
  ref: resolution.ref,
722
- variable: (0, import_core5.variableName)(resolution.ref, config),
819
+ variable: (0, import_core6.variableName)(resolution.ref, config),
723
820
  value: resolution.value,
724
821
  scope,
725
822
  encrypted
@@ -737,28 +834,65 @@ function recordPush(tree, ref, environment, iso) {
737
834
  const meta = withLastPushed(tree.readMetaSync(ref), environment, iso);
738
835
  tree.writeMetaSync(ref, meta);
739
836
  }
740
- async function runPush(options) {
741
- const project = openProject(options.cwd);
742
- const environment = targetEnvironment(project, options.environment);
743
- const { sink, repo } = sinkFor(project, environment, options.sink);
837
+ async function terminalConfirm(question) {
838
+ if (process.stdin.isTTY !== true) {
839
+ return false;
840
+ }
841
+ const reader = lineReader();
842
+ try {
843
+ const answer = await reader.ask(`${question} [y/N] `);
844
+ return answer !== void 0 && /^y(es)?$/i.test(answer.trim());
845
+ } finally {
846
+ reader.close();
847
+ }
848
+ }
849
+ async function ensureTargetApproved(provider, environment, options) {
850
+ if (provider.targetExists === void 0) {
851
+ return false;
852
+ }
853
+ if (await provider.targetExists(environment)) {
854
+ return false;
855
+ }
856
+ if (provider.ensureTarget === void 0) {
857
+ throw new import_core6.PenvError(
858
+ "MISSING_TARGET",
859
+ `The destination has no environment \`${environment}\` to receive this push, and this provider cannot create one`,
860
+ `Create the environment on the destination side, then run \`penv push --env ${environment}\` again.`
861
+ );
862
+ }
863
+ const approved = options.yes === true || await (options.confirm ?? terminalConfirm)(
864
+ `The destination has no environment \`${environment}\`. Create it?`
865
+ );
866
+ if (!approved) {
867
+ throw new import_core6.PenvError(
868
+ "MISSING_TARGET",
869
+ `The destination has no environment \`${environment}\` to receive this push`,
870
+ `Re-run with \`--yes\` to create it, answer \`y\` at the prompt, or create it on the destination side yourself.`
871
+ );
872
+ }
873
+ await provider.ensureTarget(environment);
874
+ return true;
875
+ }
876
+ async function pushProjection(project, provider, environment, location, options) {
744
877
  const tree = localTree(project);
745
878
  const keys = keySourceFor(project, environment);
746
879
  const resolutions = resolveAllSync(tree, environment, keys, true);
747
880
  const refs = refsFrom(resolutions.map((resolution) => resolution.ref));
748
- const collision = (0, import_core5.checkNameCollisions)(refs, project.config)[0];
881
+ const collision = (0, import_core6.checkNameCollisions)(refs, project.config)[0];
749
882
  if (collision !== void 0) {
750
883
  throw collision;
751
884
  }
752
- const nameError = (0, import_sink_github.checkGithubNames)(refs, project.config)[0];
885
+ const nameError = provider.checkNames?.(refs, project.config)[0];
753
886
  if (nameError !== void 0) {
754
887
  throw nameError;
755
888
  }
756
889
  const outbound = plan(resolutions, project.config, environment, options.allowDecrypt === true);
757
- await sink.verify();
890
+ await provider.verify();
891
+ const createdTarget = outbound.some((item) => item.scope.kind === "environment") && await ensureTargetApproved(provider, environment, options);
758
892
  let repositorySecrets = 0;
759
893
  let environmentSecrets = 0;
760
894
  for (const item of outbound) {
761
- await sink.push(item.variable, item.value, item.scope);
895
+ await provider.push(item.variable, item.value, item.scope);
762
896
  if (item.scope.kind === "repository") {
763
897
  repositorySecrets += 1;
764
898
  } else {
@@ -768,15 +902,70 @@ async function runPush(options) {
768
902
  }
769
903
  return {
770
904
  environment,
771
- repo,
905
+ destination: provider.type,
906
+ mode: "projection",
907
+ location,
772
908
  pushed: outbound.length,
909
+ meta: 0,
773
910
  repositorySecrets,
774
911
  environmentSecrets,
775
- decrypted: outbound.filter((item) => item.encrypted).length
912
+ decrypted: outbound.filter((item) => item.encrypted).length,
913
+ createdTarget
776
914
  };
777
915
  }
916
+ function crossesToRecords(file, environment) {
917
+ const scope = file.scope;
918
+ if (scope.kind === "unscoped") {
919
+ return true;
920
+ }
921
+ return scope.kind === "environment" && scope.environment === environment;
922
+ }
923
+ async function pushRecords(project, provider, environment, location) {
924
+ const tree = localTree(project);
925
+ const files = tree.listSync().filter((file) => crossesToRecords(file, environment));
926
+ let pushed = 0;
927
+ for (const file of files) {
928
+ const value = tree.readSync(file);
929
+ if (value === void 0) {
930
+ continue;
931
+ }
932
+ await provider.write(file, value);
933
+ pushed += 1;
934
+ }
935
+ const refs = refsFrom(files);
936
+ let meta = 0;
937
+ for (const ref of refs) {
938
+ const block = tree.readMetaSync(ref);
939
+ if (block === void 0) {
940
+ continue;
941
+ }
942
+ await provider.writeMeta(ref, block);
943
+ meta += 1;
944
+ }
945
+ return {
946
+ environment,
947
+ destination: provider.type,
948
+ mode: "records",
949
+ location,
950
+ pushed,
951
+ meta,
952
+ repositorySecrets: 0,
953
+ environmentSecrets: 0,
954
+ decrypted: 0,
955
+ createdTarget: false
956
+ };
957
+ }
958
+ async function runPush(options) {
959
+ const project = openProject(options.cwd);
960
+ const environment = targetEnvironment(project, options.environment, options.envFlags);
961
+ const { provider, location } = await destinationFor(project, environment, options);
962
+ if ((0, import_core6.holdsProjection)(provider)) {
963
+ return pushProjection(project, provider, environment, location, options);
964
+ }
965
+ return pushRecords(project, provider, environment, location);
966
+ }
778
967
  function renderPush(result2) {
779
- if (result2.pushed === 0) {
968
+ if (result2.pushed === 0 && result2.meta === 0) {
780
969
  return formatRows([
781
970
  {
782
971
  glyph: CHECK,
@@ -785,7 +974,23 @@ function renderPush(result2) {
785
974
  }
786
975
  ]);
787
976
  }
788
- const target = `GitHub Actions for environment ${result2.environment}${result2.repo === void 0 ? "" : ` (${result2.repo})`}`;
977
+ const target = `${result2.destination} for environment ${result2.environment}${result2.location === void 0 ? "" : ` (${result2.location})`}`;
978
+ if (result2.mode === "records") {
979
+ return formatRows([
980
+ {
981
+ glyph: CHECK,
982
+ label: "Pushed",
983
+ subject: `${result2.pushed} value ${result2.pushed === 1 ? "file" : "files"}, ${result2.meta} meta`,
984
+ detail: `mirrored verbatim to ${target}`
985
+ },
986
+ {
987
+ glyph: CHECK,
988
+ label: "Sealed values",
989
+ subject: "crossed byte-for-byte, still sealed",
990
+ detail: "the destination holds envelopes; no key was needed to push them"
991
+ }
992
+ ]);
993
+ }
789
994
  const rows = [
790
995
  {
791
996
  glyph: CHECK,
@@ -800,12 +1005,20 @@ function renderPush(result2) {
800
1005
  detail: "environment secrets override repository secrets, as penv's env scope overrides the default"
801
1006
  }
802
1007
  ];
1008
+ if (result2.createdTarget) {
1009
+ rows.push({
1010
+ glyph: CHECK,
1011
+ label: "Created",
1012
+ subject: `the destination environment ${result2.environment}`,
1013
+ detail: "it did not exist yet; created on your approval"
1014
+ });
1015
+ }
803
1016
  if (result2.decrypted > 0) {
804
1017
  rows.push({
805
1018
  glyph: WARN,
806
1019
  label: "Decrypted",
807
1020
  subject: `${result2.decrypted} ${result2.decrypted === 1 ? "secret" : "secrets"}`,
808
- detail: "sent as plaintext for GitHub to re-seal under its own key"
1021
+ detail: "sent as plaintext for the destination to re-seal under its own key"
809
1022
  });
810
1023
  }
811
1024
  return formatRows(rows);
@@ -813,13 +1026,28 @@ function renderPush(result2) {
813
1026
  var pushCommand = (0, import_citty.defineCommand)({
814
1027
  meta: {
815
1028
  name: "push",
816
- description: "Push an environment's resolved values to its sink (GitHub Actions Secrets)"
1029
+ description: "Push an environment's values to its provider"
817
1030
  },
818
1031
  args: {
819
1032
  env: { type: "string", description: "The environment to push" },
820
1033
  "allow-decrypt": {
821
1034
  type: "boolean",
822
- description: "Decrypt sealed values locally and push them as plaintext for GitHub to re-seal"
1035
+ description: "Decrypt sealed values locally and push them as plaintext for the destination to re-seal"
1036
+ },
1037
+ destination: {
1038
+ type: "string",
1039
+ alias: ["dest", "d"],
1040
+ description: "Push once to this provider package instead of the declared one"
1041
+ },
1042
+ location: {
1043
+ type: "string",
1044
+ alias: "l",
1045
+ description: "The destination-side place, when --destination needs one"
1046
+ },
1047
+ yes: {
1048
+ type: "boolean",
1049
+ alias: "y",
1050
+ description: "Create a missing destination-side environment without prompting"
823
1051
  }
824
1052
  },
825
1053
  run({ args }) {
@@ -827,7 +1055,22 @@ var pushCommand = (0, import_citty.defineCommand)({
827
1055
  const result2 = await runPush({
828
1056
  cwd: process.cwd(),
829
1057
  ...args.env === void 0 ? {} : { environment: args.env },
830
- ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
1058
+ ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] },
1059
+ ...args.destination === void 0 ? {} : { destination: args.destination },
1060
+ ...args.location === void 0 ? {} : { location: args.location },
1061
+ ...args.yes === void 0 ? {} : { yes: args.yes },
1062
+ envFlags: shorthandCandidates(args, [
1063
+ "env",
1064
+ "allow-decrypt",
1065
+ "allowDecrypt",
1066
+ "destination",
1067
+ "dest",
1068
+ "d",
1069
+ "location",
1070
+ "l",
1071
+ "yes",
1072
+ "y"
1073
+ ])
831
1074
  });
832
1075
  write(renderPush(result2));
833
1076
  });
@@ -837,7 +1080,7 @@ var pushCommand = (0, import_citty.defineCommand)({
837
1080
  // src/commands/validate.ts
838
1081
  var import_node_path3 = require("path");
839
1082
  var import_node_url2 = require("url");
840
- var import_core6 = require("@penvhq/core");
1083
+ var import_core7 = require("@penvhq/core");
841
1084
  var import_citty2 = require("citty");
842
1085
  var SCHEMA_EXPORT = "schema";
843
1086
  var LABELS = {
@@ -855,10 +1098,10 @@ function issueFrom(error, fallbackSubject) {
855
1098
  message: firstLine(error.message),
856
1099
  ...error.remedy === void 0 ? {} : { remedy: error.remedy }
857
1100
  };
858
- if (error instanceof import_core6.ReservedTokenError) {
1101
+ if (error instanceof import_core7.ReservedTokenError) {
859
1102
  return { kind: "reserved", subject: error.token, ...base };
860
1103
  }
861
- if (error instanceof import_core6.NameCollisionError) {
1104
+ if (error instanceof import_core7.NameCollisionError) {
862
1105
  return { kind: "collision", subject: error.variable, ...base };
863
1106
  }
864
1107
  return { kind: "config", subject: fallbackSubject, ...base };
@@ -912,16 +1155,16 @@ function exclusively(work) {
912
1155
  return result2;
913
1156
  }
914
1157
  function loadSchema(project, environment) {
915
- const schemaPath = (0, import_core6.schemaFileOf)(project.config);
1158
+ const schemaPath = (0, import_core7.schemaFileOf)(project.config);
916
1159
  const file = (0, import_node_url2.pathToFileURL)((0, import_node_path3.resolve)(project.root, schemaPath)).href;
917
1160
  return exclusively(() => loadSchemaExclusively(file, schemaPath, environment));
918
1161
  }
919
1162
  async function loadSchemaExclusively(file, schemaPath, environment) {
920
- const jiti = (0, import_core6.jitiFor)(file);
1163
+ const jiti = (0, import_core7.jitiFor)(file);
921
1164
  const previous = process.env.PENV_ENV;
922
- const previousHarvest = process.env[import_core6.SCHEMA_HARVEST_ENV];
1165
+ const previousHarvest = process.env[import_core7.SCHEMA_HARVEST_ENV];
923
1166
  process.env.PENV_ENV = environment;
924
- process.env[import_core6.SCHEMA_HARVEST_ENV] = "1";
1167
+ process.env[import_core7.SCHEMA_HARVEST_ENV] = "1";
925
1168
  let loaded;
926
1169
  try {
927
1170
  loaded = await jiti.import(file);
@@ -948,9 +1191,9 @@ async function loadSchemaExclusively(file, schemaPath, environment) {
948
1191
  process.env.PENV_ENV = previous;
949
1192
  }
950
1193
  if (previousHarvest === void 0) {
951
- delete process.env[import_core6.SCHEMA_HARVEST_ENV];
1194
+ delete process.env[import_core7.SCHEMA_HARVEST_ENV];
952
1195
  } else {
953
- process.env[import_core6.SCHEMA_HARVEST_ENV] = previousHarvest;
1196
+ process.env[import_core7.SCHEMA_HARVEST_ENV] = previousHarvest;
954
1197
  }
955
1198
  }
956
1199
  const exported = typeof loaded === "object" && loaded !== null ? loaded[SCHEMA_EXPORT] : void 0;
@@ -970,14 +1213,14 @@ async function loadSchemaExclusively(file, schemaPath, environment) {
970
1213
  }
971
1214
  async function runValidate(options) {
972
1215
  const project = openProject(options.cwd);
973
- const environment = targetEnvironment(project, options.environment);
974
- const schemaPath = (0, import_core6.schemaFileOf)(project.config);
1216
+ const environment = targetEnvironment(project, options.environment, options.envFlags);
1217
+ const schemaPath = (0, import_core7.schemaFileOf)(project.config);
975
1218
  const issues = [];
976
1219
  let files;
977
1220
  try {
978
1221
  files = await project.provider.list();
979
1222
  } catch (error) {
980
- if (!(error instanceof import_core6.PenvError)) {
1223
+ if (!(error instanceof import_core7.PenvError)) {
981
1224
  throw error;
982
1225
  }
983
1226
  return {
@@ -989,17 +1232,17 @@ async function runValidate(options) {
989
1232
  };
990
1233
  }
991
1234
  const refs = refsFrom(files);
992
- for (const error of (0, import_core6.validateConfig)(project.config)) {
1235
+ for (const error of (0, import_core7.validateConfig)(project.config)) {
993
1236
  issues.push(issueFrom(error, "penv.config.ts"));
994
1237
  }
995
- for (const collision of (0, import_core6.checkNameCollisions)(refs, project.config)) {
1238
+ for (const collision of (0, import_core7.checkNameCollisions)(refs, project.config)) {
996
1239
  issues.push(issueFrom(collision, collision.variable));
997
1240
  }
998
1241
  const { schema, issues: schemaIssues } = await loadSchema(project, environment);
999
1242
  issues.push(...schemaIssues);
1000
1243
  let drift = EMPTY_DRIFT;
1001
1244
  if (schema !== void 0) {
1002
- const resolutions = await (0, import_core6.resolveAll)(
1245
+ const resolutions = await (0, import_core7.resolveAll)(
1003
1246
  environment,
1004
1247
  project.provider,
1005
1248
  keySourceFor(project, environment)
@@ -1008,7 +1251,7 @@ async function runValidate(options) {
1008
1251
  const undecryptable = /* @__PURE__ */ new Set();
1009
1252
  for (const resolution of resolutions) {
1010
1253
  if (resolution.undecryptable !== void 0) {
1011
- undecryptable.add((0, import_core6.accessPath)(resolution.ref).join("."));
1254
+ undecryptable.add((0, import_core7.accessPath)(resolution.ref).join("."));
1012
1255
  issues.push({
1013
1256
  kind: "undecryptable",
1014
1257
  subject: resolution.parameter,
@@ -1018,7 +1261,7 @@ async function runValidate(options) {
1018
1261
  continue;
1019
1262
  }
1020
1263
  if (resolution.value !== void 0) {
1021
- place(object, (0, import_core6.accessPath)(resolution.ref), resolution.value);
1264
+ place(object, (0, import_core7.accessPath)(resolution.ref), resolution.value);
1022
1265
  }
1023
1266
  }
1024
1267
  drift = computeDrift({ schema, resolutions, config: project.config, environment });
@@ -1085,7 +1328,8 @@ var validateCommand = (0, import_citty2.defineCommand)({
1085
1328
  return guard(async () => {
1086
1329
  const result2 = await runValidate({
1087
1330
  cwd: process.cwd(),
1088
- ...args.env === void 0 ? {} : { environment: args.env }
1331
+ ...args.env === void 0 ? {} : { environment: args.env },
1332
+ envFlags: shorthandCandidates(args, ["env"])
1089
1333
  });
1090
1334
  write(renderValidate(result2));
1091
1335
  if (!result2.ok) {
@@ -1108,10 +1352,19 @@ function skipped(check, label) {
1108
1352
  }
1109
1353
  async function runDoctor(options) {
1110
1354
  const project = openProject(options.cwd);
1111
- const environment = targetEnvironment(project, options.environment);
1355
+ const environment = targetEnvironment(project, options.environment, options.envFlags);
1112
1356
  const findings = [];
1113
1357
  const now = new Date(options.now ?? (/* @__PURE__ */ new Date()).toISOString());
1114
1358
  const stuckThresholdMs = options.stuckThresholdMs ?? STUCK_THRESHOLD_MS;
1359
+ for (const shadowed of shadowedEnvironments(project.config)) {
1360
+ findings.push({
1361
+ check: "environment-flag-shadow",
1362
+ severity: "warning",
1363
+ label: "Environment shadows a flag",
1364
+ subject: shadowed,
1365
+ detail: `\`--${shadowed}\` is a flag penv defines, so this environment has no shorthand \u2014 \`--env ${shadowed}\` always works`
1366
+ });
1367
+ }
1115
1368
  const { schema, issues } = await loadSchema(project, environment);
1116
1369
  if (schema !== void 0) {
1117
1370
  findings.push({ check: "schema", severity: "pass", label: "Schema valid" });
@@ -1126,7 +1379,7 @@ async function runDoctor(options) {
1126
1379
  });
1127
1380
  }
1128
1381
  }
1129
- const resolutions = await (0, import_core7.resolveAll)(
1382
+ const resolutions = await (0, import_core8.resolveAll)(
1130
1383
  environment,
1131
1384
  project.provider,
1132
1385
  keySourceFor(project, environment)
@@ -1168,7 +1421,7 @@ async function runDoctor(options) {
1168
1421
  findings.push(...stuckFindings(rotation.subjects, environment, now, stuckThresholdMs));
1169
1422
  }
1170
1423
  findings.push(...await providerDriftFindings(project, environment, options.source));
1171
- findings.push(...await sinkFindings(project, environment, options.sink));
1424
+ findings.push(...await projectionFindings(project, environment, options.projection));
1172
1425
  findings.push({
1173
1426
  check: "provider",
1174
1427
  severity: "pass",
@@ -1182,7 +1435,7 @@ async function runDoctor(options) {
1182
1435
  };
1183
1436
  }
1184
1437
  function missingFindings(subjects, environment) {
1185
- const required = subjects.filter(({ meta }) => (0, import_core7.isRequired)(meta, environment));
1438
+ const required = subjects.filter(({ meta }) => (0, import_core8.isRequired)(meta, environment));
1186
1439
  const findings = required.filter(({ resolution }) => resolution.winner === void 0).map(({ resolution }) => ({
1187
1440
  check: "missing",
1188
1441
  severity: "failure",
@@ -1233,7 +1486,7 @@ function weakFindings(subjects, schema) {
1233
1486
  if (value === void 0) {
1234
1487
  continue;
1235
1488
  }
1236
- const field = lookup(schema, (0, import_core7.accessPath)(resolution.ref));
1489
+ const field = lookup(schema, (0, import_core8.accessPath)(resolution.ref));
1237
1490
  if (field.kind !== "found") {
1238
1491
  continue;
1239
1492
  }
@@ -1306,7 +1559,7 @@ function fallbackFindings(subjects, environment) {
1306
1559
  ];
1307
1560
  }
1308
1561
  function plaintextSecretFindings(subjects, environment) {
1309
- const secrets = subjects.filter(({ meta }) => (0, import_core7.isSecret)(meta, environment));
1562
+ const secrets = subjects.filter(({ meta }) => (0, import_core8.isSecret)(meta, environment));
1310
1563
  const findings = [];
1311
1564
  for (const { resolution } of secrets) {
1312
1565
  const winner = resolution.winner;
@@ -1346,11 +1599,11 @@ function publicSecretFindings(subjects, environment, config) {
1346
1599
  }
1347
1600
  ];
1348
1601
  }
1349
- const secrets = subjects.filter(({ meta }) => (0, import_core7.isSecret)(meta, environment));
1602
+ const secrets = subjects.filter(({ meta }) => (0, import_core8.isSecret)(meta, environment));
1350
1603
  const findings = [];
1351
1604
  for (const { resolution } of secrets) {
1352
- const variable = (0, import_core7.variableName)(resolution.ref, config);
1353
- if (!(0, import_core7.isPublicVariable)(variable, config)) {
1605
+ const variable = (0, import_core8.variableName)(resolution.ref, config);
1606
+ if (!(0, import_core8.isPublicVariable)(variable, config)) {
1354
1607
  continue;
1355
1608
  }
1356
1609
  const prefix = prefixes.find((candidate) => variable.startsWith(candidate));
@@ -1404,15 +1657,6 @@ function encryptionFindings(subjects, environment) {
1404
1657
  ];
1405
1658
  }
1406
1659
  var EDIT_SKEW_MS = 12e4;
1407
- function buildSink(declared, override) {
1408
- if (override !== void 0) {
1409
- return override;
1410
- }
1411
- if (declared.type === "github") {
1412
- return (0, import_sink_github2.createGithubSink)(declared.repo === void 0 ? {} : { repo: declared.repo });
1413
- }
1414
- return void 0;
1415
- }
1416
1660
  function errorDetail(error) {
1417
1661
  if (error instanceof Error) {
1418
1662
  return error.message.split("\n")[0] ?? error.message;
@@ -1422,32 +1666,43 @@ function errorDetail(error) {
1422
1666
  function scopeLabel(scope) {
1423
1667
  return scope.kind === "repository" ? "repository secrets" : `environment secrets for ${scope.environment}`;
1424
1668
  }
1425
- async function sinkFindings(project, environment, override) {
1426
- const declared = project.config.sinks?.[environment];
1427
- if (declared === void 0) {
1428
- return [];
1429
- }
1430
- const sink = buildSink(declared, override);
1431
- if (sink === void 0) {
1432
- return [
1433
- {
1434
- check: "sink-unreachable",
1435
- severity: "unknown",
1436
- label: "Sink",
1437
- subject: `sink type \`${declared.type}\` is not one penv knows`,
1438
- detail: "penv cannot check a sink it cannot build"
1439
- }
1440
- ];
1669
+ async function projectionFindings(project, environment, override) {
1670
+ let projection;
1671
+ if (override !== void 0) {
1672
+ projection = override;
1673
+ } else {
1674
+ const declared = project.config.providers[environment];
1675
+ if (declared === void 0 || declared.type === LOCAL_TREE_TYPE) {
1676
+ return [];
1677
+ }
1678
+ let source;
1679
+ try {
1680
+ source = await sourceProviderFor(project, environment);
1681
+ } catch (error) {
1682
+ return [
1683
+ {
1684
+ check: "projection-unreachable",
1685
+ severity: "unknown",
1686
+ label: "Destination",
1687
+ subject: `could not build the ${declared.type} provider for ${environment}`,
1688
+ detail: errorDetail(error)
1689
+ }
1690
+ ];
1691
+ }
1692
+ if (!(0, import_core8.holdsProjection)(source)) {
1693
+ return [];
1694
+ }
1695
+ projection = source;
1441
1696
  }
1442
1697
  try {
1443
- await sink.verify();
1698
+ await projection.verify();
1444
1699
  } catch (error) {
1445
1700
  return [
1446
1701
  {
1447
- check: "sink-unreachable",
1702
+ check: "projection-unreachable",
1448
1703
  severity: "unknown",
1449
- label: "Sink",
1450
- subject: `could not reach the ${declared.type} sink for ${environment}`,
1704
+ label: "Destination",
1705
+ subject: `could not reach the ${projection.type} destination for ${environment}`,
1451
1706
  detail: errorDetail(error)
1452
1707
  }
1453
1708
  ];
@@ -1455,20 +1710,20 @@ async function sinkFindings(project, environment, override) {
1455
1710
  let repoSecrets;
1456
1711
  let envSecrets;
1457
1712
  try {
1458
- repoSecrets = await sink.list({ kind: "repository" });
1459
- envSecrets = await sink.list({ kind: "environment", environment });
1713
+ repoSecrets = await projection.list({ kind: "repository" });
1714
+ envSecrets = await projection.list({ kind: "environment", environment });
1460
1715
  } catch (error) {
1461
1716
  return [
1462
1717
  {
1463
- check: "sink-unreachable",
1718
+ check: "projection-unreachable",
1464
1719
  severity: "unknown",
1465
- label: "Sink",
1466
- subject: `could not list secrets in the ${declared.type} sink for ${environment}`,
1720
+ label: "Destination",
1721
+ subject: `could not list secrets in the ${projection.type} destination for ${environment}`,
1467
1722
  detail: errorDetail(error)
1468
1723
  }
1469
1724
  ];
1470
1725
  }
1471
- const resolutions = await (0, import_core7.resolveAll)(
1726
+ const resolutions = await (0, import_core8.resolveAll)(
1472
1727
  environment,
1473
1728
  project.provider,
1474
1729
  keySourceFor(project, environment),
@@ -1482,7 +1737,7 @@ async function sinkFindings(project, environment, override) {
1482
1737
  }
1483
1738
  expected.push({
1484
1739
  ref: resolution.ref,
1485
- variable: (0, import_core7.variableName)(resolution.ref, project.config),
1740
+ variable: (0, import_core8.variableName)(resolution.ref, project.config),
1486
1741
  scope: winner.file.scope.kind === "unscoped" ? { kind: "repository" } : { kind: "environment", environment }
1487
1742
  });
1488
1743
  }
@@ -1501,7 +1756,7 @@ async function sinkFindings(project, environment, override) {
1501
1756
  }
1502
1757
  if (!destOf(item.scope).has(key)) {
1503
1758
  nameDrift.push({
1504
- check: "sink-name-drift",
1759
+ check: "projection-name-drift",
1505
1760
  severity: "warning",
1506
1761
  label: "Declared, not pushed",
1507
1762
  subject: item.variable,
@@ -1513,7 +1768,7 @@ async function sinkFindings(project, environment, override) {
1513
1768
  for (const secret of repoSecrets) {
1514
1769
  if (!allVariables.has(upper(secret.name))) {
1515
1770
  nameDrift.push({
1516
- check: "sink-name-drift",
1771
+ check: "projection-name-drift",
1517
1772
  severity: "warning",
1518
1773
  label: "In destination, not declared",
1519
1774
  subject: secret.name,
@@ -1524,7 +1779,7 @@ async function sinkFindings(project, environment, override) {
1524
1779
  for (const secret of envSecrets) {
1525
1780
  if (!expectedEnv.has(upper(secret.name))) {
1526
1781
  nameDrift.push({
1527
- check: "sink-name-drift",
1782
+ check: "projection-name-drift",
1528
1783
  severity: "warning",
1529
1784
  label: "In destination, not declared",
1530
1785
  subject: secret.name,
@@ -1538,7 +1793,7 @@ async function sinkFindings(project, environment, override) {
1538
1793
  if (secret === void 0) {
1539
1794
  continue;
1540
1795
  }
1541
- const pushed = (0, import_core7.effectiveMeta)(await project.provider.readMeta(item.ref), environment)[LAST_PUSHED_KEY];
1796
+ const pushed = (0, import_core8.effectiveMeta)(await project.provider.readMeta(item.ref), environment)[LAST_PUSHED_KEY];
1542
1797
  if (typeof pushed !== "string") {
1543
1798
  continue;
1544
1799
  }
@@ -1548,7 +1803,7 @@ async function sinkFindings(project, environment, override) {
1548
1803
  continue;
1549
1804
  }
1550
1805
  manualEdits.push({
1551
- check: "sink-manual-edit",
1806
+ check: "projection-manual-edit",
1552
1807
  severity: "warning",
1553
1808
  label: "Edited outside penv",
1554
1809
  subject: item.variable,
@@ -1559,9 +1814,9 @@ async function sinkFindings(project, environment, override) {
1559
1814
  findings.push(
1560
1815
  ...nameDrift.length > 0 ? nameDrift : [
1561
1816
  {
1562
- check: "sink-name-drift",
1817
+ check: "projection-name-drift",
1563
1818
  severity: "pass",
1564
- label: "Sink names",
1819
+ label: "Destination names",
1565
1820
  subject: `every parameter resolving for ${environment} is present, and nothing undeclared is`
1566
1821
  }
1567
1822
  ]
@@ -1569,17 +1824,17 @@ async function sinkFindings(project, environment, override) {
1569
1824
  findings.push(
1570
1825
  ...manualEdits.length > 0 ? manualEdits : [
1571
1826
  {
1572
- check: "sink-manual-edit",
1827
+ check: "projection-manual-edit",
1573
1828
  severity: "pass",
1574
- label: "Sink hand-edits",
1829
+ label: "Destination hand-edits",
1575
1830
  subject: `no secret has changed outside penv since its last push for ${environment}`
1576
1831
  }
1577
1832
  ]
1578
1833
  );
1579
1834
  findings.push({
1580
- check: "sink-value-drift",
1835
+ check: "projection-value-drift",
1581
1836
  severity: "unknown",
1582
- label: "Sink values",
1837
+ label: "Destination values",
1583
1838
  subject: "cannot be read back from a write-only destination",
1584
1839
  detail: "value drift between the tree and the destination is unknowable by design"
1585
1840
  });
@@ -1608,6 +1863,9 @@ async function rotationSubjects(project, environment, local, override) {
1608
1863
  return { kind: "read", subjects: local };
1609
1864
  }
1610
1865
  const source = override ?? await sourceProviderFor(project, environment);
1866
+ if (!(0, import_core8.holdsRecords)(source)) {
1867
+ return { kind: "read", subjects: local };
1868
+ }
1611
1869
  try {
1612
1870
  const subjects = await Promise.all(
1613
1871
  local.map(async ({ resolution }) => ({
@@ -1632,11 +1890,11 @@ async function rotationSubjects(project, environment, local, override) {
1632
1890
  function overdueFindings(subjects, environment, now) {
1633
1891
  const findings = [];
1634
1892
  for (const { resolution, meta } of subjects) {
1635
- const { policy, lastRotated } = (0, import_core7.rotationOf)(meta, environment);
1893
+ const { policy, lastRotated } = (0, import_core8.rotationOf)(meta, environment);
1636
1894
  if (policy === void 0 || lastRotated === null) {
1637
1895
  continue;
1638
1896
  }
1639
- const interval = (0, import_core7.tryParseDuration)(policy);
1897
+ const interval = (0, import_core8.tryParseDuration)(policy);
1640
1898
  if (interval === void 0) {
1641
1899
  findings.push({
1642
1900
  check: "rotation-overdue",
@@ -1679,10 +1937,10 @@ function overdueFindings(subjects, environment, now) {
1679
1937
  function stuckFindings(subjects, environment, now, stuckThresholdMs) {
1680
1938
  const findings = [];
1681
1939
  for (const { resolution, meta } of subjects) {
1682
- if (!(0, import_core7.isStuck)(meta, environment, now, stuckThresholdMs)) {
1940
+ if (!(0, import_core8.isStuck)(meta, environment, now, stuckThresholdMs)) {
1683
1941
  continue;
1684
1942
  }
1685
- const { rotatingSince } = (0, import_core7.rotationOf)(meta, environment);
1943
+ const { rotatingSince } = (0, import_core8.rotationOf)(meta, environment);
1686
1944
  if (rotatingSince === null) {
1687
1945
  continue;
1688
1946
  }
@@ -1722,7 +1980,7 @@ function scopeKey(scope) {
1722
1980
  case "environment-local":
1723
1981
  return `environment-local:${scope.environment}`;
1724
1982
  default:
1725
- return (0, import_core7.assertNever)(scope, "scope");
1983
+ return (0, import_core8.assertNever)(scope, "scope");
1726
1984
  }
1727
1985
  }
1728
1986
  function relevantToEnvironment(file, environment) {
@@ -1756,6 +2014,9 @@ async function providerDriftFindings(project, environment, override) {
1756
2014
  ];
1757
2015
  }
1758
2016
  const source = override ?? await sourceProviderFor(project, environment);
2017
+ if (!(0, import_core8.holdsRecords)(source)) {
2018
+ return [];
2019
+ }
1759
2020
  let local;
1760
2021
  let remote;
1761
2022
  try {
@@ -1779,13 +2040,13 @@ async function providerDriftFindings(project, environment, override) {
1779
2040
  const here = local.get(address);
1780
2041
  const there = remote.get(address);
1781
2042
  if (here !== void 0 && there !== void 0) {
1782
- const opened = (0, import_core7.openValue)(here.file, here.stored, keys);
2043
+ const opened = (0, import_core8.openValue)(here.file, here.stored, keys);
1783
2044
  if (opened.kind !== "plaintext") {
1784
2045
  findings.push({
1785
2046
  check: "provider-value-drift",
1786
2047
  severity: "unknown",
1787
2048
  label: "Provider value unreadable",
1788
- subject: (0, import_core7.formatValueFile)(here.file),
2049
+ subject: (0, import_core8.formatValueFile)(here.file),
1789
2050
  detail: `the local value is sealed and did not open, so it cannot be compared against the ${providerConfig.type} source of truth`
1790
2051
  });
1791
2052
  continue;
@@ -1795,7 +2056,7 @@ async function providerDriftFindings(project, environment, override) {
1795
2056
  check: "provider-value-drift",
1796
2057
  severity: "failure",
1797
2058
  label: "Provider value drift",
1798
- subject: (0, import_core7.formatValueFile)(here.file),
2059
+ subject: (0, import_core8.formatValueFile)(here.file),
1799
2060
  detail: `the local tree and the ${providerConfig.type} source of truth hold different values`
1800
2061
  });
1801
2062
  }
@@ -1809,7 +2070,7 @@ async function providerDriftFindings(project, environment, override) {
1809
2070
  check: "provider-value-drift",
1810
2071
  severity: "warning",
1811
2072
  label: here !== void 0 ? "Only in the local tree" : "Only in the source",
1812
- subject: (0, import_core7.formatValueFile)(present.file),
2073
+ subject: (0, import_core8.formatValueFile)(present.file),
1813
2074
  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`
1814
2075
  });
1815
2076
  }
@@ -1875,7 +2136,7 @@ function renderDoctor(report) {
1875
2136
  var doctorCommand = (0, import_citty3.defineCommand)({
1876
2137
  meta: {
1877
2138
  name: "doctor",
1878
- description: "Report missing, weak, unused, fallback, plaintext-secret, and sink-drift issues"
2139
+ description: "Report missing, weak, unused, fallback, plaintext-secret, and drift issues"
1879
2140
  },
1880
2141
  args: {
1881
2142
  env: { type: "string", description: "The environment to report on" }
@@ -1884,7 +2145,8 @@ var doctorCommand = (0, import_citty3.defineCommand)({
1884
2145
  return guard(async () => {
1885
2146
  const report = await runDoctor({
1886
2147
  cwd: process.cwd(),
1887
- ...args.env === void 0 ? {} : { environment: args.env }
2148
+ ...args.env === void 0 ? {} : { environment: args.env },
2149
+ envFlags: shorthandCandidates(args, ["env"])
1888
2150
  });
1889
2151
  write(renderDoctor(report));
1890
2152
  if (!report.ok) {
@@ -1895,11 +2157,11 @@ var doctorCommand = (0, import_citty3.defineCommand)({
1895
2157
  });
1896
2158
 
1897
2159
  // src/commands/encrypt.ts
1898
- var import_core9 = require("@penvhq/core");
2160
+ var import_core10 = require("@penvhq/core");
1899
2161
  var import_citty5 = require("citty");
1900
2162
 
1901
2163
  // src/commands/set.ts
1902
- var import_core8 = require("@penvhq/core");
2164
+ var import_core9 = require("@penvhq/core");
1903
2165
  var import_citty4 = require("citty");
1904
2166
  function scopeFrom(options) {
1905
2167
  if (options.local === true) {
@@ -1919,7 +2181,7 @@ function targetScope(project, options, key) {
1919
2181
  return scopeFrom(options);
1920
2182
  }
1921
2183
  if (environment.trim().length === 0) {
1922
- throw new import_core8.PenvError(
2184
+ throw new import_core9.PenvError(
1923
2185
  "ENVIRONMENT_FLAG_EMPTY",
1924
2186
  `\`--env\` for parameter ${key} names no environment`,
1925
2187
  `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.`
@@ -1932,27 +2194,27 @@ function policyEnvironment(project, options) {
1932
2194
  }
1933
2195
  function sealFor(project, file, value, parameter, environment) {
1934
2196
  if (environment === void 0) {
1935
- throw new import_core8.PenvError(
2197
+ throw new import_core9.PenvError(
1936
2198
  "SECRET_SCOPE_AMBIGUOUS",
1937
- `Parameter ${parameter} is a secret, and ${PENV_DIR}/${(0, import_core8.formatValueFile)(file)} names no environment`,
2199
+ `Parameter ${parameter} is a secret, and ${PENV_DIR}/${(0, import_core9.formatValueFile)(file)} names no environment`,
1938
2200
  "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."
1939
2201
  );
1940
2202
  }
1941
- return (0, import_core8.sealValue)(file, value, keySourceFor(project, environment), parameter, environment);
2203
+ return (0, import_core9.sealValue)(file, value, keySourceFor(project, environment), parameter, environment);
1942
2204
  }
1943
2205
  async function sealAwareWrite(options) {
1944
2206
  const { project, provider, ref, scope, value, environment } = options;
1945
- const secret = (0, import_core8.isSecret)(await provider.readMeta(ref), environment);
2207
+ const secret = (0, import_core9.isSecret)(await provider.readMeta(ref), environment);
1946
2208
  const file = {
1947
2209
  namespace: ref.namespace,
1948
2210
  name: ref.name,
1949
2211
  scope,
1950
2212
  encrypted: secret
1951
2213
  };
1952
- const stored = secret ? sealFor(project, file, value, (0, import_core8.parameterId)(ref), environment) : value;
2214
+ const stored = secret ? sealFor(project, file, value, (0, import_core9.parameterId)(ref), environment) : value;
1953
2215
  await provider.write(file, stored);
1954
2216
  await provider.remove({ ...file, encrypted: !secret });
1955
- return { encrypted: secret, location: (0, import_core8.formatValueFile)(file) };
2217
+ return { encrypted: secret, location: (0, import_core9.formatValueFile)(file) };
1956
2218
  }
1957
2219
  async function runSet(options) {
1958
2220
  const project = openProject(options.cwd);
@@ -2034,7 +2296,7 @@ function twins(project, key, options) {
2034
2296
  }
2035
2297
  function environmentFor(project, options, verb) {
2036
2298
  if (options.environment === void 0) {
2037
- throw new import_core9.PenvError(
2299
+ throw new import_core10.PenvError(
2038
2300
  "SECRET_SCOPE_AMBIGUOUS",
2039
2301
  `\`penv ${verb}\` names no environment, and keys are declared per environment`,
2040
2302
  "Pass `--env <environment>`. penv cannot tell which environment's key applies to a file that names none, and will not pick one for you."
@@ -2049,32 +2311,32 @@ async function runEncrypt(options) {
2049
2311
  const project = openProject(options.cwd);
2050
2312
  const environment = environmentFor(project, options, "encrypt");
2051
2313
  const [plain, sealed] = twins(project, options.key, options);
2052
- const parameter = (0, import_core9.parameterId)(plain);
2314
+ const parameter = (0, import_core10.parameterId)(plain);
2053
2315
  const value = await readOne(project, plain);
2054
2316
  if (value === void 0) {
2055
2317
  const already = await readOne(project, sealed);
2056
- throw new import_core9.PenvError(
2318
+ throw new import_core10.PenvError(
2057
2319
  "PARAMETER_ABSENT",
2058
- already === void 0 ? `Parameter ${parameter} has no value file at ${PENV_DIR}/${(0, import_core9.formatValueFile)(plain)}` : `Parameter ${parameter} is already encrypted at ${PENV_DIR}/${(0, import_core9.formatValueFile)(sealed)}`,
2320
+ already === void 0 ? `Parameter ${parameter} has no value file at ${PENV_DIR}/${(0, import_core10.formatValueFile)(plain)}` : `Parameter ${parameter} is already encrypted at ${PENV_DIR}/${(0, import_core10.formatValueFile)(sealed)}`,
2059
2321
  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."
2060
2322
  );
2061
2323
  }
2062
- const text = (0, import_core9.sealValue)(sealed, value, keySourceFor(project, environment), parameter, environment);
2324
+ const text = (0, import_core10.sealValue)(sealed, value, keySourceFor(project, environment), parameter, environment);
2063
2325
  await project.provider.write(sealed, text);
2064
2326
  await project.provider.remove(plain);
2065
2327
  return {
2066
2328
  parameter,
2067
- location: (0, import_core9.formatValueFile)(sealed),
2068
- removed: (0, import_core9.formatValueFile)(plain)
2329
+ location: (0, import_core10.formatValueFile)(sealed),
2330
+ removed: (0, import_core10.formatValueFile)(plain)
2069
2331
  };
2070
2332
  }
2071
2333
  async function runDecrypt(options) {
2072
2334
  const project = openProject(options.cwd);
2073
2335
  const environment = environmentFor(project, options, "decrypt");
2074
2336
  const [plain, sealed] = twins(project, options.key, options);
2075
- const parameter = (0, import_core9.parameterId)(plain);
2076
- if ((0, import_core9.isSecret)(await project.provider.readMeta(plain), environment)) {
2077
- throw new import_core9.PenvError(
2337
+ const parameter = (0, import_core10.parameterId)(plain);
2338
+ if ((0, import_core10.isSecret)(await project.provider.readMeta(plain), environment)) {
2339
+ throw new import_core10.PenvError(
2078
2340
  "SECRET_DECRYPT_REFUSED",
2079
2341
  `Parameter ${parameter} is declared a secret for environment ${environment}, so penv will not write it in plaintext`,
2080
2342
  "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."
@@ -2082,18 +2344,18 @@ async function runDecrypt(options) {
2082
2344
  }
2083
2345
  const stored = await readOne(project, sealed);
2084
2346
  if (stored === void 0) {
2085
- throw new import_core9.PenvError(
2347
+ throw new import_core10.PenvError(
2086
2348
  "PARAMETER_ABSENT",
2087
- `Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${(0, import_core9.formatValueFile)(sealed)}`,
2349
+ `Parameter ${parameter} has no encrypted value file at ${PENV_DIR}/${(0, import_core10.formatValueFile)(sealed)}`,
2088
2350
  `Nothing to decrypt. \`penv get ${options.key} --env ${environment} --explain\` shows every file penv looked at.`
2089
2351
  );
2090
2352
  }
2091
- const opened = (0, import_core9.openValue)(sealed, stored, keySourceFor(project, environment));
2353
+ const opened = (0, import_core10.openValue)(sealed, stored, keySourceFor(project, environment));
2092
2354
  if (opened.kind === "failed") {
2093
2355
  throw new UndecryptableAt(
2094
2356
  parameter,
2095
2357
  environment,
2096
- (0, import_core9.formatValueFile)(sealed),
2358
+ (0, import_core10.formatValueFile)(sealed),
2097
2359
  opened.failure.detail
2098
2360
  );
2099
2361
  }
@@ -2101,11 +2363,11 @@ async function runDecrypt(options) {
2101
2363
  await project.provider.remove(sealed);
2102
2364
  return {
2103
2365
  parameter,
2104
- location: (0, import_core9.formatValueFile)(plain),
2105
- removed: (0, import_core9.formatValueFile)(sealed)
2366
+ location: (0, import_core10.formatValueFile)(plain),
2367
+ removed: (0, import_core10.formatValueFile)(sealed)
2106
2368
  };
2107
2369
  }
2108
- var UndecryptableAt = class extends import_core9.PenvError {
2370
+ var UndecryptableAt = class extends import_core10.PenvError {
2109
2371
  constructor(parameter, environment, location, detail) {
2110
2372
  super(
2111
2373
  "VALUE_UNDECRYPTABLE",
@@ -2160,57 +2422,14 @@ var decryptCommand = (0, import_citty5.defineCommand)({
2160
2422
  });
2161
2423
 
2162
2424
  // src/commands/fill.ts
2163
- var import_core10 = require("@penvhq/core");
2425
+ var import_core11 = require("@penvhq/core");
2164
2426
  var import_citty6 = require("citty");
2165
-
2166
- // src/input.ts
2167
- var import_node_readline = require("readline");
2168
- function lineReader(input = process.stdin, output = process.stdout) {
2169
- const rl = (0, import_node_readline.createInterface)({ input, output });
2170
- const buffered = [];
2171
- const waiting = [];
2172
- let closed = false;
2173
- rl.on("line", (line) => {
2174
- const next = waiting.shift();
2175
- if (next === void 0) {
2176
- buffered.push(line);
2177
- } else {
2178
- next(line);
2179
- }
2180
- });
2181
- rl.on("close", () => {
2182
- closed = true;
2183
- for (const next of waiting.splice(0)) {
2184
- next(void 0);
2185
- }
2186
- });
2187
- return {
2188
- ask(question) {
2189
- const early = buffered.shift();
2190
- if (early !== void 0) {
2191
- return Promise.resolve(early);
2192
- }
2193
- if (closed) {
2194
- return Promise.resolve(void 0);
2195
- }
2196
- rl.setPrompt(question);
2197
- rl.prompt();
2198
- return new Promise((resolve7) => {
2199
- waiting.push(resolve7);
2200
- });
2201
- },
2202
- close() {
2203
- rl.close();
2204
- }
2205
- };
2206
- }
2207
-
2208
- // src/commands/fill.ts
2209
2427
  var BLOCKING = /* @__PURE__ */ new Set(["config", "collision", "reserved"]);
2210
2428
  async function runFill(options) {
2211
2429
  const validation = await runValidate({
2212
2430
  cwd: options.cwd,
2213
- ...options.environment === void 0 ? {} : { environment: options.environment }
2431
+ ...options.environment === void 0 ? {} : { environment: options.environment },
2432
+ ...options.envFlags === void 0 ? {} : { envFlags: options.envFlags }
2214
2433
  });
2215
2434
  const environment = validation.environment;
2216
2435
  const blockers = validation.issues.filter((issue) => BLOCKING.has(issue.kind));
@@ -2218,7 +2437,7 @@ async function runFill(options) {
2218
2437
  const detail = blockers.map(
2219
2438
  (issue) => ` - ${issue.message}${issue.remedy === void 0 ? "" : ` (${issue.remedy})`}`
2220
2439
  ).join("\n");
2221
- throw new import_core10.PenvError(
2440
+ throw new import_core11.PenvError(
2222
2441
  "FILL_BLOCKED",
2223
2442
  `penv fill cannot run: environment ${environment} has ${blockers.length} unresolved configuration ${blockers.length === 1 ? "issue" : "issues"}:
2224
2443
  ${detail}`,
@@ -2340,7 +2559,8 @@ var fillCommand = (0, import_citty6.defineCommand)({
2340
2559
  await runFill({
2341
2560
  cwd: process.cwd(),
2342
2561
  ...args.env === void 0 ? {} : { environment: args.env },
2343
- ask
2562
+ ask,
2563
+ envFlags: shorthandCandidates(args, ["env"])
2344
2564
  })
2345
2565
  )
2346
2566
  );
@@ -2354,14 +2574,14 @@ var fillCommand = (0, import_citty6.defineCommand)({
2354
2574
  // src/commands/generate.ts
2355
2575
  var import_node_fs = require("fs");
2356
2576
  var import_node_path4 = require("path");
2357
- var import_core11 = require("@penvhq/core");
2577
+ var import_core12 = require("@penvhq/core");
2358
2578
  var import_citty7 = require("citty");
2359
2579
  var DEFAULT_OUTPUT = ".env";
2360
2580
  function entriesFor(project, environment, allowDecrypt) {
2361
2581
  const keys = keySourceFor(project, environment);
2362
2582
  const tree = localTree(project);
2363
2583
  const resolutions = resolveAllSync(tree, environment, keys);
2364
- const collision = (0, import_core11.checkNameCollisions)(
2584
+ const collision = (0, import_core12.checkNameCollisions)(
2365
2585
  refsFrom(resolutions.map((resolution) => resolution.ref)),
2366
2586
  project.config
2367
2587
  )[0];
@@ -2374,21 +2594,21 @@ function entriesFor(project, environment, allowDecrypt) {
2374
2594
  const winner = resolution.winner;
2375
2595
  if (winner?.file.encrypted === true) {
2376
2596
  if (!allowDecrypt) {
2377
- throw new import_core11.PenvError(
2597
+ throw new import_core12.PenvError(
2378
2598
  "ENCRYPTED_VALUE_REFUSED",
2379
2599
  `Parameter ${resolution.parameter} for environment ${environment} resolves to the encrypted value file ${PENV_DIR}/${winner.location}, and \`penv generate\` writes plaintext`,
2380
2600
  `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.`
2381
2601
  );
2382
2602
  }
2383
- (0, import_core11.requireValue)(resolution, environment);
2603
+ (0, import_core12.requireValue)(resolution, environment);
2384
2604
  decrypted += 1;
2385
2605
  }
2386
2606
  if (resolution.value === void 0) {
2387
2607
  continue;
2388
2608
  }
2389
- const description = (0, import_core11.effectiveMeta)(tree.readMetaSync(resolution.ref), environment).description;
2609
+ const description = (0, import_core12.effectiveMeta)(tree.readMetaSync(resolution.ref), environment).description;
2390
2610
  entries.push({
2391
- key: (0, import_core11.variableName)(resolution.ref, project.config),
2611
+ key: (0, import_core12.variableName)(resolution.ref, project.config),
2392
2612
  value: resolution.value,
2393
2613
  ...typeof description === "string" ? { description } : {}
2394
2614
  });
@@ -2398,15 +2618,15 @@ function entriesFor(project, environment, allowDecrypt) {
2398
2618
  }
2399
2619
  function generateDotenv(options) {
2400
2620
  const project = openProject(options.cwd);
2401
- const environment = targetEnvironment(project, options.environment);
2402
- return (0, import_core11.serializeDotenv)(entriesFor(project, environment, options.allowDecrypt === true).entries);
2621
+ const environment = targetEnvironment(project, options.environment, options.envFlags);
2622
+ return (0, import_core12.serializeDotenv)(entriesFor(project, environment, options.allowDecrypt === true).entries);
2403
2623
  }
2404
2624
  function runGenerate(options) {
2405
2625
  const project = openProject(options.cwd);
2406
- const environment = targetEnvironment(project, options.environment);
2626
+ const environment = targetEnvironment(project, options.environment, options.envFlags);
2407
2627
  const { entries, decrypted } = entriesFor(project, environment, options.allowDecrypt === true);
2408
2628
  const file = options.out === void 0 ? (0, import_node_path4.resolve)(project.root, DEFAULT_OUTPUT) : (0, import_node_path4.isAbsolute)(options.out) ? options.out : (0, import_node_path4.resolve)(options.cwd, options.out);
2409
- (0, import_node_fs.writeFileSync)(file, (0, import_core11.serializeDotenv)(entries), "utf8");
2629
+ (0, import_node_fs.writeFileSync)(file, (0, import_core12.serializeDotenv)(entries), "utf8");
2410
2630
  return { file, environment, entries: entries.length, decrypted };
2411
2631
  }
2412
2632
  function displayPath(cwd, file) {
@@ -2449,7 +2669,8 @@ var generateCommand = (0, import_citty7.defineCommand)({
2449
2669
  cwd,
2450
2670
  ...args.env === void 0 ? {} : { environment: args.env },
2451
2671
  ...args.out === void 0 ? {} : { out: args.out },
2452
- ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] }
2672
+ ...args["allow-decrypt"] === void 0 ? {} : { allowDecrypt: args["allow-decrypt"] },
2673
+ envFlags: shorthandCandidates(args, ["env", "out", "allow-decrypt", "allowDecrypt"])
2453
2674
  });
2454
2675
  write(renderGenerate(result2, cwd));
2455
2676
  });
@@ -2457,17 +2678,17 @@ var generateCommand = (0, import_citty7.defineCommand)({
2457
2678
  });
2458
2679
 
2459
2680
  // src/commands/get.ts
2460
- var import_core12 = require("@penvhq/core");
2681
+ var import_core13 = require("@penvhq/core");
2461
2682
  var import_citty8 = require("citty");
2462
2683
  async function runGet(options) {
2463
2684
  const project = openProject(options.cwd);
2464
2685
  const environment = targetEnvironment(project, options.environment);
2465
2686
  const ref = refFromKey(options.key);
2466
2687
  const keys = keySourceFor(project, environment);
2467
- const resolution = await (0, import_core12.resolveParameter)(ref, environment, project.provider, keys);
2468
- const value = (0, import_core12.requireValue)(resolution, environment);
2688
+ const resolution = await (0, import_core13.resolveParameter)(ref, environment, project.provider, keys);
2689
+ const value = (0, import_core13.requireValue)(resolution, environment);
2469
2690
  if (value === void 0) {
2470
- throw new import_core12.PenvError(
2691
+ throw new import_core13.PenvError(
2471
2692
  "PARAMETER_ABSENT",
2472
2693
  `Parameter ${resolution.parameter} resolves to no value for environment ${environment}`,
2473
2694
  `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.`
@@ -2489,7 +2710,7 @@ async function runExplain(options) {
2489
2710
  const environment = targetEnvironment(project, options.environment);
2490
2711
  const ref = refFromKey(options.key);
2491
2712
  const keys = keySourceFor(project, environment);
2492
- const resolution = await (0, import_core12.resolveParameter)(ref, environment, project.provider, keys);
2713
+ const resolution = await (0, import_core13.resolveParameter)(ref, environment, project.provider, keys);
2493
2714
  const winner = resolution.winner;
2494
2715
  return {
2495
2716
  parameter: resolution.parameter,
@@ -2545,13 +2766,13 @@ var getCommand = (0, import_citty8.defineCommand)({
2545
2766
  // src/commands/import.ts
2546
2767
  var import_node_fs4 = require("fs");
2547
2768
  var import_node_path7 = require("path");
2548
- var import_core15 = require("@penvhq/core");
2769
+ var import_core16 = require("@penvhq/core");
2549
2770
  var import_citty10 = require("citty");
2550
2771
 
2551
2772
  // src/detect.ts
2552
2773
  var import_node_fs2 = require("fs");
2553
2774
  var import_node_path5 = require("path");
2554
- var import_core13 = require("@penvhq/core");
2775
+ var import_core14 = require("@penvhq/core");
2555
2776
  var SIGNATURES = [
2556
2777
  { name: "Next.js", packages: ["next"], publicPrefixes: ["NEXT_PUBLIC_"] },
2557
2778
  {
@@ -2589,7 +2810,7 @@ function schemaFileFor(cwd) {
2589
2810
  if (!occupied(cwd, beside)) {
2590
2811
  return { file: beside, displaced: preferred };
2591
2812
  }
2592
- return { file: import_core13.DEFAULT_SCHEMA_FILE, displaced: preferred };
2813
+ return { file: import_core14.DEFAULT_SCHEMA_FILE, displaced: preferred };
2593
2814
  }
2594
2815
  function dependenciesOf(cwd) {
2595
2816
  const manifest = manifestOf(cwd);
@@ -2653,7 +2874,7 @@ function hasImportsBlock(cwd) {
2653
2874
  var import_node_fs3 = require("fs");
2654
2875
  var import_node_path6 = require("path");
2655
2876
  var import_promises = require("readline/promises");
2656
- var import_core14 = require("@penvhq/core");
2877
+ var import_core15 = require("@penvhq/core");
2657
2878
  var import_citty9 = require("citty");
2658
2879
  var CONFIG_FILE = "penv.config.ts";
2659
2880
  var TSCONFIG_FILE = "tsconfig.json";
@@ -2664,11 +2885,11 @@ var IMPORTS_PREFIX = "#";
2664
2885
  var PACKAGE_FILE = "package.json";
2665
2886
  var DEFAULT_DECISIONS = {
2666
2887
  environments: [],
2667
- schemaFile: import_core14.DEFAULT_SCHEMA_FILE,
2888
+ schemaFile: import_core15.DEFAULT_SCHEMA_FILE,
2668
2889
  publicPrefixes: [],
2669
2890
  alias: DEFAULT_ALIAS
2670
2891
  };
2671
- var NOT_ENVIRONMENTS = [...import_core14.RESERVED_TOKENS, "example", "sample", "template"];
2892
+ var NOT_ENVIRONMENTS = [...import_core15.RESERVED_TOKENS, "example", "sample", "template"];
2672
2893
  function suggestEnvironments(root) {
2673
2894
  let entries;
2674
2895
  try {
@@ -2684,7 +2905,7 @@ function suggestEnvironments(root) {
2684
2905
  const segments = entry.slice(".env.".length).split(".");
2685
2906
  const withoutLocal = segments.at(-1) === "local" ? segments.slice(0, -1) : segments;
2686
2907
  const name = withoutLocal.length === 1 ? withoutLocal[0] : void 0;
2687
- if (name === void 0 || !(0, import_core14.isLegalEnvironmentName)(name) || NOT_ENVIRONMENTS.includes(name)) {
2908
+ if (name === void 0 || !(0, import_core15.isLegalEnvironmentName)(name) || NOT_ENVIRONMENTS.includes(name)) {
2688
2909
  continue;
2689
2910
  }
2690
2911
  found.add(name);
@@ -2692,10 +2913,10 @@ function suggestEnvironments(root) {
2692
2913
  return [...found].sort();
2693
2914
  }
2694
2915
  function emptyFlag(flag) {
2695
- return new import_core14.PenvError(
2916
+ return new import_core15.PenvError(
2696
2917
  "INIT_FLAG_EMPTY",
2697
2918
  `\`--${flag}\` was given without a value`,
2698
- flag === "schema" ? `Name the module that exports the schema, e.g. \`--schema src/env.ts\`, or drop the flag to use ${import_core14.DEFAULT_SCHEMA_FILE}.` : "Name the environment, e.g. `--env production`, or drop the flag to leave the whitelist empty and declare it in penv.config.ts."
2919
+ flag === "schema" ? `Name the module that exports the schema, e.g. \`--schema src/env.ts\`, or drop the flag to use ${import_core15.DEFAULT_SCHEMA_FILE}.` : "Name the environment, e.g. `--env production`, or drop the flag to leave the whitelist empty and declare it in penv.config.ts."
2699
2920
  );
2700
2921
  }
2701
2922
  function splitEnvironments(value) {
@@ -2720,7 +2941,7 @@ function declaredIn(root) {
2720
2941
  if (!(0, import_node_fs3.existsSync)(file)) {
2721
2942
  return void 0;
2722
2943
  }
2723
- return (0, import_core14.loadConfigFrom)(file);
2944
+ return (0, import_core15.loadConfigFrom)(file);
2724
2945
  }
2725
2946
  function planInit(root, flags = {}) {
2726
2947
  const declared = declaredIn(root);
@@ -2730,7 +2951,7 @@ function planInit(root, flags = {}) {
2730
2951
  notes.push(`${CONFIG_FILE} already exists \u2014 init keeps every decision it records.`);
2731
2952
  } else if (detected === void 0) {
2732
2953
  notes.push(
2733
- `No framework detected in package.json \u2014 the schema goes to ${import_core14.DEFAULT_SCHEMA_FILE}.`
2954
+ `No framework detected in package.json \u2014 the schema goes to ${import_core15.DEFAULT_SCHEMA_FILE}.`
2734
2955
  );
2735
2956
  } else {
2736
2957
  notes.push(`Detected ${detected.name}.`);
@@ -2740,12 +2961,12 @@ function planInit(root, flags = {}) {
2740
2961
  );
2741
2962
  }
2742
2963
  }
2743
- const schemaFile = flags.schema === void 0 ? declared !== void 0 ? (0, import_core14.schemaFileOf)(declared) : detected?.schemaFile ?? import_core14.DEFAULT_SCHEMA_FILE : flags.schema.trim();
2964
+ const schemaFile = flags.schema === void 0 ? declared !== void 0 ? (0, import_core15.schemaFileOf)(declared) : detected?.schemaFile ?? import_core15.DEFAULT_SCHEMA_FILE : flags.schema.trim();
2744
2965
  if (flags.schema !== void 0) {
2745
2966
  if (schemaFile.length === 0) {
2746
2967
  throw emptyFlag("schema");
2747
2968
  }
2748
- const error = (0, import_core14.validateSchemaFile)({ environments: [], providers: {}, schemaFile })[0];
2969
+ const error = (0, import_core15.validateSchemaFile)({ environments: [], providers: {}, schemaFile })[0];
2749
2970
  if (error !== void 0) {
2750
2971
  throw error;
2751
2972
  }
@@ -2755,7 +2976,7 @@ function planInit(root, flags = {}) {
2755
2976
  throw emptyFlag("alias");
2756
2977
  }
2757
2978
  if (!ALIAS_NAME.test(alias)) {
2758
- throw new import_core14.PenvError(
2979
+ throw new import_core15.PenvError(
2759
2980
  "INIT_ALIAS_INVALID",
2760
2981
  `\`${alias}\` is not an alias penv can write`,
2761
2982
  `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.`
@@ -2802,7 +3023,7 @@ function renderPlan(plan2) {
2802
3023
  rows.push([
2803
3024
  ` ${out.dim("schemaFile")}`,
2804
3025
  plan2.decisions.schemaFile,
2805
- plan2.decisions.schemaFile === import_core14.DEFAULT_SCHEMA_FILE ? "" : out.dim(`(default: ${import_core14.DEFAULT_SCHEMA_FILE})`)
3026
+ plan2.decisions.schemaFile === import_core15.DEFAULT_SCHEMA_FILE ? "" : out.dim(`(default: ${import_core15.DEFAULT_SCHEMA_FILE})`)
2806
3027
  ]);
2807
3028
  for (const prefix of plan2.decisions.publicPrefixes) {
2808
3029
  rows.push([` ${out.dim("publicPrefix")}`, prefix, ""]);
@@ -2851,6 +3072,15 @@ ${body}
2851
3072
  // code. Importing it loads configuration and throws (naming the parameter and
2852
3073
  // environment) if anything required is missing or invalid.
2853
3074
  export const env = load(schema);
3075
+
3076
+ // Registers the schema's shape with penv's types (erased at runtime, so
3077
+ // nothing cycles). This is what makes \`override\` keys in penv.config.ts
3078
+ // autocomplete from this schema \u2014 a typo'd parameter id is a compile error.
3079
+ declare module "@penvhq/core" {
3080
+ interface PenvSchemaShape {
3081
+ readonly shape: z.infer<typeof schema>;
3082
+ }
3083
+ }
2854
3084
  `;
2855
3085
  }
2856
3086
  function renderEnvironments(decisions) {
@@ -2860,14 +3090,14 @@ function renderEnvironments(decisions) {
2860
3090
  // can read off your codebase, and an environment you do not have is worse
2861
3091
  // than one you have not declared yet. Name yours, and give each a provider:
2862
3092
  // environments: ["development", "production"],
2863
- // providers: { development: { type: "filesystem" }, production: { type: "filesystem" } },
3093
+ // providers: { development: { type: "@penvhq/provider-filesystem" }, production: { type: "@penvhq/provider-filesystem" } },
2864
3094
  environments: [],
2865
3095
 
2866
3096
  providers: {},
2867
3097
  `;
2868
3098
  }
2869
3099
  const names = decisions.environments.map((name) => JSON.stringify(name)).join(", ");
2870
- const providers = decisions.environments.map((name) => ` ${JSON.stringify(name)}: { type: "filesystem" },
3100
+ const providers = decisions.environments.map((name) => ` ${JSON.stringify(name)}: { type: "@penvhq/provider-filesystem" },
2871
3101
  `).join("");
2872
3102
  return `${shared} environments: [${names}],
2873
3103
 
@@ -2878,7 +3108,7 @@ ${providers} },
2878
3108
  }
2879
3109
  function renderConfigModule(decisions) {
2880
3110
  let body = renderEnvironments(decisions);
2881
- if (decisions.schemaFile !== import_core14.DEFAULT_SCHEMA_FILE) {
3111
+ if (decisions.schemaFile !== import_core15.DEFAULT_SCHEMA_FILE) {
2882
3112
  body += `
2883
3113
  // The module that exports the schema. It is yours \u2014 penv scaffolds it once
2884
3114
  // and never regenerates it \u2014 so this says where you keep it.
@@ -2901,7 +3131,7 @@ ${body}});
2901
3131
  `;
2902
3132
  }
2903
3133
  function renderGitignore(decisions) {
2904
- const inside = (0, import_core14.schemaInsideTree)(configOf(decisions));
3134
+ const inside = (0, import_core15.schemaInsideTree)(configOf(decisions));
2905
3135
  const listed = inside === void 0 ? "" : `${inside}, `;
2906
3136
  return `# Written by penv. Value files hold configuration values and are never
2907
3137
  # committed; only the structure, ${listed}meta, and config are.
@@ -3046,13 +3276,13 @@ ${objectIndent}${source.slice(close)}`;
3046
3276
  ${entryIndent}${member},${source.slice(open + 1)}`;
3047
3277
  }
3048
3278
  function shapeError(what, target, alias) {
3049
- return new import_core14.PenvError(
3279
+ return new import_core15.PenvError(
3050
3280
  "TSCONFIG_SHAPE",
3051
3281
  `penv cannot add the \`${alias}\` path alias to tsconfig.json: ${what}`,
3052
3282
  `Add it by hand: \`{ "compilerOptions": { "paths": { "${alias}": ["${target}"] } } }\`.`
3053
3283
  );
3054
3284
  }
3055
- function insertEnvAlias(source, target = import_core14.DEFAULT_SCHEMA_FILE, name = DEFAULT_ALIAS) {
3285
+ function insertEnvAlias(source, target = import_core15.DEFAULT_SCHEMA_FILE, name = DEFAULT_ALIAS) {
3056
3286
  const alias = `"${name}": ["${target}"]`;
3057
3287
  const root = skipTrivia(source, 0);
3058
3288
  if (source.charAt(root) !== "{") {
@@ -3261,7 +3491,7 @@ var initCommand = (0, import_citty9.defineCommand)({
3261
3491
  },
3262
3492
  schema: {
3263
3493
  type: "string",
3264
- description: `Where the schema module goes, e.g. src/env.ts (default: ${import_core14.DEFAULT_SCHEMA_FILE})`
3494
+ description: `Where the schema module goes, e.g. src/env.ts (default: ${import_core15.DEFAULT_SCHEMA_FILE})`
3265
3495
  },
3266
3496
  alias: {
3267
3497
  type: "string",
@@ -3315,7 +3545,7 @@ function inferType(value) {
3315
3545
  }
3316
3546
  function draftFields(entries) {
3317
3547
  return entries.map((entry) => {
3318
- const key = (0, import_core15.accessPath)((0, import_core15.refFromVariable)(entry.key)).join(".");
3548
+ const key = (0, import_core16.accessPath)((0, import_core16.refFromVariable)(entry.key)).join(".");
3319
3549
  return {
3320
3550
  key: IDENTIFIER.test(key) ? key : JSON.stringify(key),
3321
3551
  type: inferType(entry.value)
@@ -3326,33 +3556,33 @@ function assertImportable(ref, variable) {
3326
3556
  if (!ref.name.includes(".")) {
3327
3557
  return;
3328
3558
  }
3329
- throw new import_core15.PenvError(
3559
+ throw new import_core16.PenvError(
3330
3560
  "IMPORT_UNPARSEABLE_NAME",
3331
3561
  `The variable ${variable} becomes the parameter \`${ref.name}\`, whose \`.\` would be read as a scope`,
3332
3562
  `Filenames are split on \`.\`. Rename ${variable} in the source file, then import it again.`
3333
3563
  );
3334
3564
  }
3335
3565
  function assertNotReserved(ref, variable, where, config) {
3336
- if ((0, import_core15.isReservedToken)(ref.name, config)) {
3337
- throw new import_core15.ReservedTokenError("parameter", variable, where);
3566
+ if ((0, import_core16.isReservedToken)(ref.name, config)) {
3567
+ throw new import_core16.ReservedTokenError("parameter", variable, where);
3338
3568
  }
3339
3569
  }
3340
3570
  function assertRoundTrips(ref, variable, config) {
3341
- if ((0, import_core15.roundTripsCleanly)(variable)) {
3571
+ if ((0, import_core16.roundTripsCleanly)(variable)) {
3342
3572
  return;
3343
3573
  }
3344
- if ((0, import_core15.variableName)(ref, config) === variable) {
3574
+ if ((0, import_core16.variableName)(ref, config) === variable) {
3345
3575
  return;
3346
3576
  }
3347
- const generated = (0, import_core15.variableName)(ref, config);
3348
- throw new import_core15.PenvError(
3577
+ const generated = (0, import_core16.variableName)(ref, config);
3578
+ throw new import_core16.PenvError(
3349
3579
  "IMPORT_LOSSY_NAME",
3350
3580
  `The variable ${variable} becomes the parameter \`${ref.name}\`, which regenerates as ${generated}`,
3351
- `\`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.`
3581
+ `\`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.`
3352
3582
  );
3353
3583
  }
3354
3584
  function collisionsIn(refs, config) {
3355
- const errors = (0, import_core15.checkNameCollisions)(refs, config);
3585
+ const errors = (0, import_core16.checkNameCollisions)(refs, config);
3356
3586
  const first = errors[0];
3357
3587
  if (first !== void 0) {
3358
3588
  throw first;
@@ -3362,7 +3592,7 @@ function assertDeclared(segment, config) {
3362
3592
  if (config.environments.includes(segment)) {
3363
3593
  return segment;
3364
3594
  }
3365
- throw new import_core15.UnknownEnvironmentError(segment, config.environments);
3595
+ throw new import_core16.UnknownEnvironmentError(segment, config.environments);
3366
3596
  }
3367
3597
  function scopeFromFilename(file, config) {
3368
3598
  const name = (0, import_node_path7.basename)(file);
@@ -3387,13 +3617,13 @@ function scopeFromFilename(file, config) {
3387
3617
  return { kind: "environment-local", environment: assertDeclared(first, config) };
3388
3618
  }
3389
3619
  if (first === LOCAL && rest.length === 2) {
3390
- throw new import_core15.FilenameGrammarError(
3620
+ throw new import_core16.FilenameGrammarError(
3391
3621
  name,
3392
3622
  "`local` precedes the environment segment",
3393
3623
  `The environment segment always precedes \`local\` \u2014 \`.env.${second}.${LOCAL}\` is the file Next.js and Vite read, and \`.env.${LOCAL}.${second}\` is not a synonym for it. Rename it to \`.env.${second}.${LOCAL}\`, then import it again. Nothing was imported.`
3394
3624
  );
3395
3625
  }
3396
- throw new import_core15.FilenameGrammarError(
3626
+ throw new import_core16.FilenameGrammarError(
3397
3627
  name,
3398
3628
  `\`${rest.join("` and `")}\` are ${rest.length} scope segments`,
3399
3629
  "A dotenv file carries exactly one scope: `.env`, `.env.<environment>`, `.env.local`, or `.env.<environment>.local`. Point `penv import` at one of those. Nothing was imported."
@@ -3408,7 +3638,7 @@ function environmentOf(scope) {
3408
3638
  case "local":
3409
3639
  return void 0;
3410
3640
  default:
3411
- return (0, import_core15.assertNever)(scope, "scope");
3641
+ return (0, import_core16.assertNever)(scope, "scope");
3412
3642
  }
3413
3643
  }
3414
3644
  function scopeWithEnvironment(scope, environment) {
@@ -3421,7 +3651,7 @@ function scopeWithEnvironment(scope, environment) {
3421
3651
  case "environment-local":
3422
3652
  return scope;
3423
3653
  default:
3424
- return (0, import_core15.assertNever)(scope, "scope");
3654
+ return (0, import_core16.assertNever)(scope, "scope");
3425
3655
  }
3426
3656
  }
3427
3657
  function explicitEnvironment(options, source, config) {
@@ -3429,7 +3659,7 @@ function explicitEnvironment(options, source, config) {
3429
3659
  if (value.length > 0) {
3430
3660
  return value;
3431
3661
  }
3432
- throw new import_core15.PenvError(
3662
+ throw new import_core16.PenvError(
3433
3663
  "IMPORT_ENV_FLAG_EMPTY",
3434
3664
  `\`--env\` for the import of ${source} names no environment`,
3435
3665
  `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.`
@@ -3439,7 +3669,7 @@ function assertEnvironmentAgrees(derived, explicit, source) {
3439
3669
  if (derived === void 0 || explicit === void 0 || derived === explicit) {
3440
3670
  return;
3441
3671
  }
3442
- throw new import_core15.PenvError(
3672
+ throw new import_core16.PenvError(
3443
3673
  "IMPORT_ENV_CONFLICT",
3444
3674
  `The file ${source} is scoped to environment ${derived}, but \`--env ${explicit}\` names ${explicit}`,
3445
3675
  `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.`
@@ -3460,15 +3690,15 @@ function environmentNamed(file, explicit) {
3460
3690
  function decisionsOf(config, cwd) {
3461
3691
  return {
3462
3692
  environments: config.environments,
3463
- schemaFile: (0, import_core15.schemaFileOf)(config),
3693
+ schemaFile: (0, import_core16.schemaFileOf)(config),
3464
3694
  publicPrefixes: config.publicPrefixes ?? [],
3465
3695
  alias: detectAlias(cwd)
3466
3696
  };
3467
3697
  }
3468
3698
  function configInEffect(cwd, environment) {
3469
- const existing = (0, import_core15.findConfigFile)(cwd);
3699
+ const existing = (0, import_core16.findConfigFile)(cwd);
3470
3700
  if (existing !== void 0) {
3471
- const config = (0, import_core15.loadConfigFrom)(existing);
3701
+ const config = (0, import_core16.loadConfigFrom)(existing);
3472
3702
  return { config, decisions: decisionsOf(config, cwd) };
3473
3703
  }
3474
3704
  const planned = planInit(cwd).decisions;
@@ -3483,13 +3713,13 @@ function importDotenv(options) {
3483
3713
  const cwd = (0, import_node_path7.resolve)(options.cwd);
3484
3714
  const file = (0, import_node_path7.isAbsolute)(options.file) ? options.file : (0, import_node_path7.resolve)(cwd, options.file);
3485
3715
  if (!(0, import_node_fs4.existsSync)(file)) {
3486
- throw new import_core15.PenvError(
3716
+ throw new import_core16.PenvError(
3487
3717
  "IMPORT_FILE_MISSING",
3488
3718
  `There is no file at ${file} to import`,
3489
3719
  "Point `penv import` at an existing dotenv file, e.g. `penv import .env`."
3490
3720
  );
3491
3721
  }
3492
- const parsed = (0, import_core15.parseDotenv)((0, import_node_fs4.readFileSync)(file, "utf8"));
3722
+ const parsed = (0, import_core16.parseDotenv)((0, import_node_fs4.readFileSync)(file, "utf8"));
3493
3723
  const { config, decisions } = configInEffect(cwd, environmentNamed(file, options.environment));
3494
3724
  const source = displayPath2(cwd, file);
3495
3725
  const named = scopeFromFilename(file, config);
@@ -3497,10 +3727,10 @@ function importDotenv(options) {
3497
3727
  const explicit = options.environment === void 0 ? void 0 : explicitEnvironment(options, source, config);
3498
3728
  assertEnvironmentAgrees(derived, explicit, source);
3499
3729
  const scope = explicit === void 0 ? named : scopeWithEnvironment(named, assertDeclared(explicit, config));
3500
- const environment = (0, import_core15.lookupEnvironment)(config, explicit ?? derived);
3730
+ const environment = (0, import_core16.lookupEnvironment)(config, explicit ?? derived);
3501
3731
  const refs = [];
3502
3732
  for (const entry of parsed.entries) {
3503
- const ref = (0, import_core15.refFromVariable)(entry.key);
3733
+ const ref = (0, import_core16.refFromVariable)(entry.key);
3504
3734
  assertImportable(ref, entry.key);
3505
3735
  assertNotReserved(ref, entry.key, source, config);
3506
3736
  assertRoundTrips(ref, entry.key, config);
@@ -3628,7 +3858,7 @@ var importCommand = (0, import_citty10.defineCommand)({
3628
3858
 
3629
3859
  // src/commands/key.ts
3630
3860
  var import_node_crypto = require("crypto");
3631
- var import_core16 = require("@penvhq/core");
3861
+ var import_core17 = require("@penvhq/core");
3632
3862
  var import_citty11 = require("citty");
3633
3863
 
3634
3864
  // src/keychain.ts
@@ -3662,35 +3892,35 @@ function runKeyCreate(options) {
3662
3892
  const environment = targetEnvironment(project, options.environment);
3663
3893
  const declared = project.config.keys?.[environment];
3664
3894
  if (declared === void 0) {
3665
- throw new import_core16.PenvError(
3895
+ throw new import_core17.PenvError(
3666
3896
  "KEY_SOURCE_UNDECLARED",
3667
3897
  `Environment ${environment} declares no key source, so penv does not know what a key for it would be`,
3668
3898
  `Add a \`keys\` entry to penv.config.ts \u2014 e.g. \`keys: { ${environment}: { source: "env", id: "${environment}" } }\` \u2014 then run this again.`
3669
3899
  );
3670
3900
  }
3671
- const key = (0, import_node_crypto.randomBytes)(import_core16.KEY_BYTES).toString("base64");
3901
+ const key = (0, import_node_crypto.randomBytes)(import_core17.KEY_BYTES).toString("base64");
3672
3902
  if (declared.source === "keychain") {
3673
3903
  const keychain = options.keychain ?? defaultKeychain;
3674
3904
  if (options.force !== true) {
3675
3905
  let existing;
3676
3906
  try {
3677
- existing = keychain.getPassword(import_core16.KEYCHAIN_SERVICE, declared.id);
3907
+ existing = keychain.getPassword(import_core17.KEYCHAIN_SERVICE, declared.id);
3678
3908
  } catch (cause) {
3679
- throw new import_core16.PenvError(
3909
+ throw new import_core17.PenvError(
3680
3910
  "KEYCHAIN_UNAVAILABLE",
3681
3911
  `penv could not read your OS keychain to check for an existing key \`${declared.id}\``,
3682
3912
  `Unlock your keychain and run this again. Original error: ${cause instanceof Error ? cause.message : String(cause)}`
3683
3913
  );
3684
3914
  }
3685
3915
  if (existing !== null) {
3686
- throw new import_core16.PenvError(
3916
+ throw new import_core17.PenvError(
3687
3917
  "KEY_EXISTS",
3688
3918
  `Environment ${environment} already has a key \`${declared.id}\` in your OS keychain`,
3689
3919
  "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."
3690
3920
  );
3691
3921
  }
3692
3922
  }
3693
- keychain.setPassword(import_core16.KEYCHAIN_SERVICE, declared.id, key);
3923
+ keychain.setPassword(import_core17.KEYCHAIN_SERVICE, declared.id, key);
3694
3924
  return { source: "keychain", environment, id: declared.id };
3695
3925
  }
3696
3926
  return {
@@ -3758,7 +3988,7 @@ var keyCommand = (0, import_citty11.defineCommand)({
3758
3988
  });
3759
3989
 
3760
3990
  // src/commands/list.ts
3761
- var import_core17 = require("@penvhq/core");
3991
+ var import_core18 = require("@penvhq/core");
3762
3992
  var import_citty12 = require("citty");
3763
3993
  function scopeLabel2(scope) {
3764
3994
  switch (scope.kind) {
@@ -3771,7 +4001,7 @@ function scopeLabel2(scope) {
3771
4001
  case "unscoped":
3772
4002
  return "default";
3773
4003
  default:
3774
- return (0, import_core17.assertNever)(scope, "scope");
4004
+ return (0, import_core18.assertNever)(scope, "scope");
3775
4005
  }
3776
4006
  }
3777
4007
  async function runList(options) {
@@ -3779,12 +4009,12 @@ async function runList(options) {
3779
4009
  const environment = targetEnvironment(project, options.environment);
3780
4010
  const keys = keySourceFor(project, environment);
3781
4011
  const parameters = [];
3782
- for (const resolution of await (0, import_core17.resolveAll)(environment, project.provider, keys)) {
4012
+ for (const resolution of await (0, import_core18.resolveAll)(environment, project.provider, keys)) {
3783
4013
  const winner = resolution.winner;
3784
4014
  const scope = winner?.file.scope;
3785
4015
  parameters.push({
3786
4016
  parameter: resolution.parameter,
3787
- variable: (0, import_core17.variableName)(resolution.ref, project.config),
4017
+ variable: (0, import_core18.variableName)(resolution.ref, project.config),
3788
4018
  scope: scope === void 0 ? "absent" : scopeLabel2(scope),
3789
4019
  location: winner?.location,
3790
4020
  encrypted: winner?.file.encrypted === true,
@@ -3837,7 +4067,7 @@ var listCommand = (0, import_citty12.defineCommand)({
3837
4067
  });
3838
4068
 
3839
4069
  // src/commands/mv.ts
3840
- var import_core18 = require("@penvhq/core");
4070
+ var import_core19 = require("@penvhq/core");
3841
4071
  var import_citty13 = require("citty");
3842
4072
  function environmentOf2(file) {
3843
4073
  const scope = file.scope;
@@ -3853,39 +4083,39 @@ async function planFile(project, source, target, parameter) {
3853
4083
  }
3854
4084
  const environment = environmentOf2(source);
3855
4085
  if (environment === void 0) {
3856
- throw new import_core18.PenvError(
4086
+ throw new import_core19.PenvError(
3857
4087
  "SECRET_SCOPE_AMBIGUOUS",
3858
- `${PENV_DIR}/${(0, import_core18.formatValueFile)(source)} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
4088
+ `${PENV_DIR}/${(0, import_core19.formatValueFile)(source)} is encrypted at a scope that names no environment, so penv cannot tell which key would re-seal it`,
3859
4089
  "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."
3860
4090
  );
3861
4091
  }
3862
4092
  const keys = keySourceFor(project, environment);
3863
- const opened = (0, import_core18.openValue)(source, stored, keys);
4093
+ const opened = (0, import_core19.openValue)(source, stored, keys);
3864
4094
  if (opened.kind === "failed") {
3865
- throw new import_core18.PenvError(
4095
+ throw new import_core19.PenvError(
3866
4096
  "VALUE_UNDECRYPTABLE",
3867
- `${PENV_DIR}/${(0, import_core18.formatValueFile)(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
4097
+ `${PENV_DIR}/${(0, import_core19.formatValueFile)(source)} could not be decrypted, so penv cannot re-seal it at its new address: ${opened.failure.detail}`,
3868
4098
  "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."
3869
4099
  );
3870
4100
  }
3871
4101
  return {
3872
4102
  source,
3873
4103
  target,
3874
- contents: (0, import_core18.sealValue)(target, opened.value, keys, parameter, environment),
4104
+ contents: (0, import_core19.sealValue)(target, opened.value, keys, parameter, environment),
3875
4105
  resealed: true
3876
4106
  };
3877
4107
  }
3878
4108
  function filesOf(all, ref) {
3879
- const id = (0, import_core18.parameterId)(ref);
3880
- return all.filter((file) => (0, import_core18.parameterId)(file) === id);
4109
+ const id = (0, import_core19.parameterId)(ref);
4110
+ return all.filter((file) => (0, import_core19.parameterId)(file) === id);
3881
4111
  }
3882
4112
  async function runMove(options) {
3883
4113
  const project = openProject(options.cwd);
3884
4114
  const from = refFromKey(options.from, project.config);
3885
4115
  assertWritableKey(options.to);
3886
4116
  const to = refFromKey(options.to, project.config);
3887
- if ((0, import_core18.parameterId)(from) === (0, import_core18.parameterId)(to)) {
3888
- throw new import_core18.PenvError(
4117
+ if ((0, import_core19.parameterId)(from) === (0, import_core19.parameterId)(to)) {
4118
+ throw new import_core19.PenvError(
3889
4119
  "PARAMETER_UNCHANGED",
3890
4120
  `\`${options.from}\` and \`${options.to}\` are the same parameter`,
3891
4121
  "Name a different destination, e.g. `penv mv redis-password redis/password`."
@@ -3895,24 +4125,24 @@ async function runMove(options) {
3895
4125
  const sources = filesOf(all, from);
3896
4126
  const meta = await project.provider.readMeta(from);
3897
4127
  if (sources.length === 0 && meta === void 0) {
3898
- throw new import_core18.PenvError(
4128
+ throw new import_core19.PenvError(
3899
4129
  "PARAMETER_ABSENT",
3900
- `Parameter ${(0, import_core18.parameterId)(from)} has no value files and no meta, so there is nothing to move`,
4130
+ `Parameter ${(0, import_core19.parameterId)(from)} has no value files and no meta, so there is nothing to move`,
3901
4131
  `\`penv list\` shows every parameter penv holds.`
3902
4132
  );
3903
4133
  }
3904
4134
  const occupied2 = filesOf(all, to);
3905
4135
  if (occupied2.length > 0 || await project.provider.readMeta(to) !== void 0) {
3906
- throw new import_core18.PenvError(
4136
+ throw new import_core19.PenvError(
3907
4137
  "PARAMETER_EXISTS",
3908
- `Parameter ${(0, import_core18.parameterId)(to)} already exists, and penv will not merge two parameters into one`,
3909
- `Remove or rename ${(0, import_core18.parameterId)(to)} first. \`penv get ${options.to} --explain\` shows every file it holds.`
4138
+ `Parameter ${(0, import_core19.parameterId)(to)} already exists, and penv will not merge two parameters into one`,
4139
+ `Remove or rename ${(0, import_core19.parameterId)(to)} first. \`penv get ${options.to} --explain\` shows every file it holds.`
3910
4140
  );
3911
4141
  }
3912
4142
  const planned = [];
3913
4143
  for (const source of sources) {
3914
4144
  const target = { ...source, namespace: to.namespace, name: to.name };
3915
- const one = await planFile(project, source, target, (0, import_core18.parameterId)(to));
4145
+ const one = await planFile(project, source, target, (0, import_core19.parameterId)(to));
3916
4146
  if (one !== void 0) {
3917
4147
  planned.push(one);
3918
4148
  }
@@ -3930,18 +4160,18 @@ async function runMove(options) {
3930
4160
  await project.provider.removeMeta(from);
3931
4161
  }
3932
4162
  return {
3933
- from: (0, import_core18.parameterId)(from),
3934
- to: (0, import_core18.parameterId)(to),
4163
+ from: (0, import_core19.parameterId)(from),
4164
+ to: (0, import_core19.parameterId)(to),
3935
4165
  files: planned.map((file) => ({
3936
- from: (0, import_core18.formatValueFile)(file.source),
3937
- to: (0, import_core18.formatValueFile)(file.target),
4166
+ from: (0, import_core19.formatValueFile)(file.source),
4167
+ to: (0, import_core19.formatValueFile)(file.target),
3938
4168
  resealed: file.resealed
3939
4169
  })),
3940
4170
  // The meta's path, not the parameter's dotted id: `redis.password` is what
3941
4171
  // the schema calls it and `redis/password.json` is the file, and a report
3942
4172
  // that printed the first while moving the second names no file on disk.
3943
- meta: meta === void 0 ? void 0 : (0, import_core18.formatMetaFile)({ ...to, format: "json" }),
3944
- schema: { was: (0, import_core18.accessPath)(from).join("."), now: (0, import_core18.accessPath)(to).join(".") }
4173
+ meta: meta === void 0 ? void 0 : (0, import_core19.formatMetaFile)({ ...to, format: "json" }),
4174
+ schema: { was: (0, import_core19.accessPath)(from).join("."), now: (0, import_core19.accessPath)(to).join(".") }
3945
4175
  };
3946
4176
  }
3947
4177
  function renderMove(result2) {
@@ -3985,14 +4215,45 @@ var mvCommand = (0, import_citty13.defineCommand)({
3985
4215
  });
3986
4216
 
3987
4217
  // src/commands/pull.ts
4218
+ var import_core20 = require("@penvhq/core");
3988
4219
  var import_citty14 = require("citty");
4220
+ async function pullProjection(project, source, environment) {
4221
+ const tree = localTree(project);
4222
+ const names = /* @__PURE__ */ new Set();
4223
+ for (const secret of await source.list({ kind: "repository" })) {
4224
+ names.add(secret.name);
4225
+ }
4226
+ for (const secret of await source.list({ kind: "environment", environment })) {
4227
+ names.add(secret.name);
4228
+ }
4229
+ let meta = 0;
4230
+ for (const name of [...names].sort()) {
4231
+ const ref = (0, import_core20.refFromVariable)(name);
4232
+ if (tree.readMetaSync(ref) === void 0) {
4233
+ tree.writeMetaSync(ref, {});
4234
+ meta += 1;
4235
+ }
4236
+ }
4237
+ return {
4238
+ environment,
4239
+ source: source.type,
4240
+ localSource: false,
4241
+ values: 0,
4242
+ meta,
4243
+ refs: names.size,
4244
+ valuesUnreadable: true
4245
+ };
4246
+ }
3989
4247
  async function runPull(options) {
3990
4248
  const project = openProject(options.cwd);
3991
- const environment = targetEnvironment(project, options.environment);
3992
- const source = await sourceProviderFor(project, environment);
4249
+ const environment = targetEnvironment(project, options.environment, options.envFlags);
4250
+ const source = options.source ?? await sourceProviderFor(project, environment);
3993
4251
  if (source.type === LOCAL_TREE_TYPE) {
3994
4252
  return { environment, source: source.type, localSource: true, values: 0, meta: 0, refs: 0 };
3995
4253
  }
4254
+ if ((0, import_core20.holdsProjection)(source)) {
4255
+ return pullProjection(project, source, environment);
4256
+ }
3996
4257
  const tree = localTree(project);
3997
4258
  const files = await source.list();
3998
4259
  let values = 0;
@@ -4027,6 +4288,22 @@ function renderPull(result2) {
4027
4288
  }
4028
4289
  ]);
4029
4290
  }
4291
+ if (result2.valuesUnreadable === true) {
4292
+ return formatRows([
4293
+ {
4294
+ glyph: CHECK,
4295
+ label: "Pulled",
4296
+ subject: `${result2.refs} ${result2.refs === 1 ? "parameter name" : "parameter names"}`,
4297
+ detail: `from ${result2.source} for environment ${result2.environment}`
4298
+ },
4299
+ {
4300
+ glyph: WARN,
4301
+ label: "Values not readable",
4302
+ subject: "this destination never returns a secret's value",
4303
+ detail: "fill them locally with `penv set` or `penv fill` \u2014 `penv validate` names every gap"
4304
+ }
4305
+ ]);
4306
+ }
4030
4307
  return formatRows([
4031
4308
  {
4032
4309
  glyph: CHECK,
@@ -4048,13 +4325,17 @@ var pullCommand = (0, import_citty14.defineCommand)({
4048
4325
  description: "Materialise the local .penv tree from an environment's source-of-truth provider"
4049
4326
  },
4050
4327
  args: {
4051
- env: { type: "string", description: "The environment to pull" }
4328
+ env: {
4329
+ type: "string",
4330
+ description: "The environment to pull (or pass it as a bare flag: --production)"
4331
+ }
4052
4332
  },
4053
4333
  run({ args }) {
4054
4334
  return guard(async () => {
4055
4335
  const result2 = await runPull({
4056
4336
  cwd: process.cwd(),
4057
- ...args.env === void 0 ? {} : { environment: args.env }
4337
+ ...args.env === void 0 ? {} : { environment: args.env },
4338
+ envFlags: shorthandCandidates(args, ["env"])
4058
4339
  });
4059
4340
  write(renderPull(result2));
4060
4341
  });
@@ -4062,7 +4343,7 @@ var pullCommand = (0, import_citty14.defineCommand)({
4062
4343
  });
4063
4344
 
4064
4345
  // src/commands/remove.ts
4065
- var import_core19 = require("@penvhq/core");
4346
+ var import_core21 = require("@penvhq/core");
4066
4347
  var import_citty15 = require("citty");
4067
4348
  async function runRemove(options) {
4068
4349
  const project = openProject(options.cwd);
@@ -4080,12 +4361,12 @@ async function runRemove(options) {
4080
4361
  continue;
4081
4362
  }
4082
4363
  await project.provider.remove(file);
4083
- removed.push((0, import_core19.formatValueFile)(file));
4364
+ removed.push((0, import_core21.formatValueFile)(file));
4084
4365
  }
4085
4366
  return {
4086
4367
  parameter: options.key,
4087
4368
  removed,
4088
- considered: files.map((file) => (0, import_core19.formatValueFile)(file))
4369
+ considered: files.map((file) => (0, import_core21.formatValueFile)(file))
4089
4370
  };
4090
4371
  }
4091
4372
  function renderRemove(result2) {
@@ -4134,7 +4415,7 @@ var removeCommand = (0, import_citty15.defineCommand)({
4134
4415
  });
4135
4416
 
4136
4417
  // src/commands/rotate.ts
4137
- var import_core20 = require("@penvhq/core");
4418
+ var import_core22 = require("@penvhq/core");
4138
4419
  var import_citty16 = require("citty");
4139
4420
  function rotatingFile(ref, environment) {
4140
4421
  return {
@@ -4160,7 +4441,7 @@ async function writeRotatedValue(project, provider, ref, environment, value) {
4160
4441
  }
4161
4442
  function requireNewValue(value, phase, key) {
4162
4443
  if (value === void 0) {
4163
- throw new import_core20.PenvError(
4444
+ throw new import_core22.PenvError(
4164
4445
  "ROTATION_NO_VALUE",
4165
4446
  `A ${phase} rotation of ${key} writes a new value, and none was given`,
4166
4447
  "Pass the new value as the argument \u2014 `penv rotate <key> <value>` \u2014 or pipe it in on stdin."
@@ -4169,8 +4450,8 @@ function requireNewValue(value, phase, key) {
4169
4450
  return value;
4170
4451
  }
4171
4452
  function requireRetaining(provider, environment) {
4172
- if (!(0, import_core20.retainsPrevious)(provider)) {
4173
- throw new import_core20.PenvError(
4453
+ if (!(0, import_core22.retainsPrevious)(provider)) {
4454
+ throw new import_core22.PenvError(
4174
4455
  "ROTATION_NOT_RETAINING",
4175
4456
  `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`,
4176
4457
  "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."
@@ -4182,12 +4463,20 @@ async function runRotate(options) {
4182
4463
  const project = openProject(options.cwd);
4183
4464
  const environment = targetEnvironment(project, options.environment);
4184
4465
  const ref = refFromKey(options.key, project.config);
4185
- const provider = await sourceProviderFor(project, environment);
4466
+ const source = await sourceProviderFor(project, environment);
4467
+ if (!(0, import_core22.holdsRecords)(source)) {
4468
+ throw new import_core22.PenvError(
4469
+ "ROTATION_NOT_RECORDS",
4470
+ `Environment ${environment} is backed by \`${source.type}\`, which holds a resolved projection penv cannot rotate in place`,
4471
+ "Rotate the parameter in the environment that holds the records (the local tree, Vault, SSM), then `penv push` the result to this destination."
4472
+ );
4473
+ }
4474
+ const provider = source;
4186
4475
  const nowIso = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
4187
4476
  const before = await provider.readMeta(ref);
4188
- const { mechanism } = (0, import_core20.rotationOf)(before, environment);
4477
+ const { mechanism } = (0, import_core22.rotationOf)(before, environment);
4189
4478
  if (mechanism === void 0) {
4190
- throw new import_core20.PenvError(
4479
+ throw new import_core22.PenvError(
4191
4480
  "ROTATION_NO_MECHANISM",
4192
4481
  `Parameter ${options.key} declares no rotation mechanism for environment ${environment}, so penv does not know how to rotate it`,
4193
4482
  '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.'
@@ -4197,7 +4486,7 @@ async function runRotate(options) {
4197
4486
  const complete = options.complete === true;
4198
4487
  if (mechanism === "atomic-cutover") {
4199
4488
  if (begin || complete) {
4200
- throw new import_core20.PenvError(
4489
+ throw new import_core22.PenvError(
4201
4490
  "ROTATION_MECHANISM_MISMATCH",
4202
4491
  `Parameter ${options.key} is atomic-cutover, which flips in one step, so \`--begin\`/\`--complete\` do not apply`,
4203
4492
  "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."
@@ -4205,13 +4494,13 @@ async function runRotate(options) {
4205
4494
  }
4206
4495
  const value = requireNewValue(options.value, "cutover", options.key);
4207
4496
  await writeRotatedValue(project, provider, ref, environment, value);
4208
- const after2 = (0, import_core20.completeRotation)(before, environment, nowIso);
4497
+ const after2 = (0, import_core22.completeRotation)(before, environment, nowIso);
4209
4498
  await provider.writeMeta(ref, after2);
4210
4499
  return result(ref, environment, mechanism, "cutover", provider.type, true, after2);
4211
4500
  }
4212
4501
  const retaining = requireRetaining(provider, environment);
4213
4502
  if (begin === complete) {
4214
- throw new import_core20.PenvError(
4503
+ throw new import_core22.PenvError(
4215
4504
  "ROTATION_PHASE_REQUIRED",
4216
4505
  `A dual-valid rotation of ${options.key} needs exactly one of \`--begin\` or \`--complete\``,
4217
4506
  "`--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."
@@ -4220,16 +4509,16 @@ async function runRotate(options) {
4220
4509
  if (begin) {
4221
4510
  const value = requireNewValue(options.value, "begin", options.key);
4222
4511
  await writeRotatedValue(project, retaining, ref, environment, value);
4223
- const after2 = (0, import_core20.beginRotation)(before, environment, nowIso);
4512
+ const after2 = (0, import_core22.beginRotation)(before, environment, nowIso);
4224
4513
  await retaining.writeMeta(ref, after2);
4225
4514
  return result(ref, environment, mechanism, "begin", retaining.type, true, after2);
4226
4515
  }
4227
- const after = (0, import_core20.completeRotation)(before, environment, nowIso);
4516
+ const after = (0, import_core22.completeRotation)(before, environment, nowIso);
4228
4517
  await retaining.writeMeta(ref, after);
4229
4518
  return result(ref, environment, mechanism, "complete", retaining.type, false, after);
4230
4519
  }
4231
4520
  function result(ref, environment, mechanism, phase, source, wroteValue, after) {
4232
- const { state, rotatingSince, lastRotated } = (0, import_core20.rotationOf)(after, environment);
4521
+ const { state, rotatingSince, lastRotated } = (0, import_core22.rotationOf)(after, environment);
4233
4522
  return {
4234
4523
  parameter: [...ref.namespace, ref.name].join("/"),
4235
4524
  environment,
@@ -4316,7 +4605,7 @@ var rotateCommand = (0, import_citty16.defineCommand)({
4316
4605
  // src/commands/watch.ts
4317
4606
  var import_node_fs5 = require("fs");
4318
4607
  var import_node_path8 = require("path");
4319
- var import_core21 = require("@penvhq/core");
4608
+ var import_core23 = require("@penvhq/core");
4320
4609
  var import_citty17 = require("citty");
4321
4610
  var DEBOUNCE_MS = 100;
4322
4611
  function runWatch(options) {
@@ -4449,8 +4738,8 @@ function runWatch(options) {
4449
4738
  }
4450
4739
  addWatcher(project.penvDir, true);
4451
4740
  addWatcher((0, import_node_path8.dirname)(project.configFile), false, configFile);
4452
- if ((0, import_core21.schemaInsideTree)(project.config) === void 0) {
4453
- const schemaFile = (0, import_node_path8.resolve)(project.root, (0, import_core21.schemaFileOf)(project.config));
4741
+ if ((0, import_core23.schemaInsideTree)(project.config) === void 0) {
4742
+ const schemaFile = (0, import_node_path8.resolve)(project.root, (0, import_core23.schemaFileOf)(project.config));
4454
4743
  addWatcher((0, import_node_path8.dirname)(schemaFile), false, (0, import_node_path8.basename)(schemaFile));
4455
4744
  }
4456
4745
  void validate();
@@ -4560,7 +4849,7 @@ var main = (0, import_citty18.defineCommand)({
4560
4849
  }
4561
4850
  });
4562
4851
  function runMain() {
4563
- (0, import_core22.setKeychain)(defaultKeychain);
4852
+ (0, import_core24.setKeychain)(defaultKeychain);
4564
4853
  return (0, import_citty18.runMain)(main);
4565
4854
  }
4566
4855
  // Annotate the CommonJS export names for ESM import in node: